Files
ReleaseKit/docs/roadmap/audit.md

153 lines
8.8 KiB
Markdown

# Targeted concurrency audit
Status: complete; all five finding groups corrected and verified.
## Scope and baseline
Reviewed ReleaseKit at `6176971`, including the
[PromptKit report](promptkit-bug-report.md) and
[fixture synchronization roadmap](race.md). The audit covered shared-state
ownership in HTTP fixtures, background goroutines, cancellation callbacks,
subprocess-driven tests, output capture, and production operation callbacks.
Code discovery used the codebase graph, with source inspection and filesystem
searches where the graph omitted recent files. Reviewed current source and
fixture consumers, including assertion and diagnostic reads. The full offline
race-enabled baseline used Go `go1.26.8 linux/amd64`:
```sh
GOTOOLCHAIN=local GOWORK=off GOPROXY=off GOSUMDB=off \
go test -race -mod=readonly -count=1 ./...
```
The recorded baseline failed in
`TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout`. It reported four race
diagnostics involving three fields: `commit`, `remoteCommit`, and
`releaseCreates`. Other packages passed. An earlier redirected invocation
returned unsuccessfully without diagnostics and supplied no usable baseline;
the complete results above came from a direct invocation.
The additional findings below were established by source inspection. Their
absence from this baseline's diagnostics reflects its exercised schedules.
## Findings and corrections
### 1. CLI delivery fixtures share mutable state without synchronization
Status: corrected; original reported defect and related fixture accesses.
The HTTP handlers in [main_test.go](../../cmd/releasekit/main_test.go) read
configuration and update page state, counters, note content, and assets while
tests configure and inspect that state. The
[mapped workflow fixture](../../cmd/releasekit/multi_module_woodpecker_test.go)
has the same ownership pattern. Subprocess completion supplies no shared-state
synchronization with the parent process's HTTP goroutines.
**Correction:** Each fixture owns a mutex and a separate plain state value.
Handlers serialize state access; test configuration uses locked updates, and
assertions and diagnostics use locked snapshots. Snapshots copy asset maps and
byte slices so inspection retains independent values. All consumers use that
boundary, including source-only, binary, sparse-checkout, alias, failure,
ordinary Woodpecker, and mapped-module tests. Response helpers run under the
handler's lock. The CLI fixture derives response URLs from the request; mapped
fixture configuration is initialized before its server starts.
### 2. The public delivery fixture has the same unsynchronized ownership
Status: corrected; additional finding.
The fixture in [delivery_test.go](../../delivery_test.go) shares request counts,
tags, notes, release-page maps, and asset maps between handlers and test code.
The module-page test also changes the selected tag and note between deliveries.
Its consumers include [repository resolution](../../repository_test.go) and
[partial-clone inspection](../../source_inspection_test.go) tests.
**Correction:** Added a fixture-local mutex, locked updates, and independent
snapshots, including copied page and asset maps. Tag and note changes occur in
one locked update. The server URL is initialized before serving requests.
Applied the same startup ordering to the existing synchronized benchmark server
and its [artifact-read regression test](../../delivery_reads_linux_test.go).
Binary assertion groups reuse their captured snapshot to avoid repeated asset
copies.
### 3. Gitea adapter fixtures expose unsynchronized counters and flags
Status: corrected; additional finding.
[Release fixtures](../../internal/gitea/release_test.go) share creation and
lookup counts, creation flags, and cancellation observations between handlers
and assertions. Lost-acknowledgment scenarios also carry state across separate
HTTP connections. The
[external attachment fixture](../../internal/gitea/assets_test.go) shares its
download counter with the test goroutine.
**Correction:** Used standard-library atomic counters and flags for these
independent scalar observations. Handler updates and assertion/diagnostic reads
use their atomic methods. Existing exact counts, conflict handling, recovery,
and external-origin credential assertions remain in place.
### 4. HTTP assertions invoke fatal methods from request goroutines
Status: corrected; additional finding.
Gitea release and asset handlers, including their assertion helpers, used
`testing.T.Fatal` and `Fatalf`. Those methods terminate the calling goroutine
and belong to the test goroutine. In a handler, they can abandon request
processing and produce misleading transport failures.
**Correction:** Handler assertions report through `Error` or `Errorf` and
return from the affected handler or helper. Fatal assertions remain in the test
goroutine. The fixtures still mark every violated assertion as a test failure.
### 5. A release fixture omits server lifetime cleanup
Status: corrected; additional finding.
`newReleaseServer` returned an active HTTP server without registering cleanup,
and several callers omitted closure. This left listeners and serving goroutines
alive after their owning test finished.
**Correction:** The helper registers `server.Close` with `t.Cleanup`, giving
server shutdown one owner for all its consumers.
## Production and other reviewed paths
The audit found no confirmed production concurrency defect in these paths:
| Boundary | Ownership and synchronization evidence |
| --- | --- |
| [Git blob reader](../../internal/git/blob_reader.go) | A mutex serializes reads, close, and terminal state. A cancellation callback closes process pipes, and the read operation joins a started callback before continuing. Process diagnostics are inspected after child wait. |
| [Process adapter](../../internal/process/process.go) | Process handles are initialized before publication; cancellation uses a context. `sync.Once` protects child wait and its result. Ordinary command completion joins output-copy workers before returning captures. |
| [Go output capture](../../internal/gotool/gotool.go) | One mutex protects combined output, forwarding, and writer errors. Capture ownership transfers after the process finishes writing. |
| [Gitea upload](../../internal/gitea/upload.go) | The multipart worker owns the writer and file reader. An error channel transfers completion; request failure closes the pipe and joins the worker. |
| [Runtime metadata](../../internal/runtimeinfo/version.go) | The injected value is set at link time; fallback initialization uses `sync.OnceValue`. Existing concurrent accessor coverage uses atomics, channels, and a wait group. |
| [Go settings owner](../../internal/gotool/owner.go), candidate validation, and publication callbacks | Mutable owners and validation caches belong to one operation. Validation, product callbacks, and subsequent builds execute sequentially. Internal mutable owners remain behind the facade. |
| Background test workers | Reviewed build/process/dependency cancellation, Git-reader lifecycle, publication watchers, and destination-resolution workers. Results transfer through channels; the publication watcher has an explicit completion join. |
| Other HTTP fixtures | Immutable asset payloads remain read-only. The upload fixture already locks files and observations and returns copied POST names. The module-proxy fixture uses an atomic request counter. |
| Process environment | Tests changing environment settings run sequentially within their package; subprocess overlays belong to individual invocations. |
These conclusions apply to the reviewed ownership paths. Race-enabled execution
and inspection supply evidence for this audit, rather than a guarantee about
every possible caller or schedule. Production files and public API contracts
were unchanged by the corrections.
## Verification and completion
The affected source-delivery, ordinary Woodpecker, and mapped-module workflows
passed with the race detector after fixture synchronization. The Gitea release
and asset checks also passed during focused verification. Existing assertions
continue to cover tag mismatch rejection, source preservation, complete notes,
idempotent delivery, asset counts, conflicts, and recovery.
**Completed:** The final full offline race-enabled suite passed across all
packages with Go 1.26.8, including the original failing workflow and every
fixture consumer. The compiled public API/dependency check passed. The corrected
handlers, configuration updates, snapshots, diagnostics, counters, and server
startup/cleanup were reviewed against their ownership boundaries. Local
documentation links and fragments, Go formatting, and diff whitespace passed.
The linked roadmap and bug report record the completed remediation.
The audit and corrections are local development work. Future publication follows
the existing [release procedure](../release.md), with the published v0.4.0
contents preserved.