153 lines
8.8 KiB
Markdown
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.
|