19 KiB
ReleaseKit implementation review and remediation
Status: In progress. Resolution notes record completed fixes and verification.
Date: 2026-10-06
Review scope and evidence
Reviewed the initial implementation from cc7ca47 through 539877e, including
the public facades, internal publication/build/delivery paths, executable and
profile/receipt adapters, boundary checker, tests, maintained workflows, and
current documentation. The worktree was clean at the start of the review.
The feature roadmap, implementation plan,
ADR 0001, and applicable adopted policies define
the intended scope. An agent-authored implementation or assertion does not
independently authorize an additional restriction.
The uncached offline suite passed with go test -count=1 ./..., using
GOWORK=off, GOPROXY=off, GONOPROXY=none, GOSUMDB=off, and
GOTOOLCHAIN=local. The compiled-export and dependency checker passed with
go run ./internal/apicheck. Passing these checks does not resolve the findings
below: controlled reproductions exposed behavior outside their current coverage.
Reproductions used temporary consumer modules, local Git repositories and bare
remotes, local HTTP servers, and one temporary Go test overlay. They changed no
tracked implementation files and used no real credentials or publication service.
The prepared workflow syntax was also compared with the official Woodpecker matrix and environment references. Live CI, live Gitea delivery, consumer migrations, and measured consumer code reduction remain intentionally deferred; their absence is not a finding.
Architecture assessment
One Go module exposes two supported packages. The root releasekit package
owns Check, Publish, Build, Deliver, and their public request/result
values. buildinfo.Version forwards into a small standard-library runtime
implementation. The builder and runtime share a private injection-symbol
contract. Public signatures own their named types and expose no internal
implementation types.
cmd/releasekit calls the supported facades; command-owned internal helpers
parse profiles, execute configured checks, and handle private receipts. Internal
packages own candidate inspection, Git endpoint/ref operations, publication
reconciliation, cross-builds, subprocess execution, and Gitea transport/delivery.
Source publication and asynchronous delivery have separate outcomes. Page-only
delivery and the three initial binary targets are implemented. The default
suite uses local collaborators and is independent of consuming checkouts.
This structure substantially matches the decision. R5 identifies implementation mechanics still placed in the public package. The more consequential gaps are source/artifact binding and accurate recovery state, rather than exposed type ownership or runtime dependency weight.
Findings
R1 — High: Delivery accepts binaries built from uncommitted source
Status: Resolved.
Category: Correctness and divergence from tagged-source delivery.
Evidence: The builder records HEAD in
gitCommit but compiles the current worktree without establishing that it is
that committed source. Delivery checks
checkout cleanliness and tag equality at delivery time, then accepts matching
commit metadata and artifact hashes. The receipt adapter
adds hashes and build settings, but no evidence of source cleanliness during
compilation.
A temporary application was committed and tagged, changed to print
UNCOMMITTED, and built successfully. Its result recorded the original tagged
commit. After restoring the original clean source, Deliver accepted the
result and uploaded the uncommitted binary and manifest with state complete.
No caller forged metadata or edited a receipt.
Required remediation: Bind delivery-eligible build results and receipts to
the committed source actually compiled. Observe the relevant source state
before and after compilation, or build the committed source in a controlled
checkout. Verify eligibility when consuming a result or receipt. Keep ordinary
local build behavior explicit: this finding does not authorize a universal
clean-main restriction, an attestation system, or a concurrency-proof source
snapshot guarantee. Delivery still accepts detached historical tagged source.
Focused verification: An ordinary build from modified source must not become eligible for tagged delivery merely by cleaning the worktree afterward. Cover source changes during a build and unchanged clean tagged builds through the supported API and CLI receipt path.
Resolution: Build results and private receipts now carry SourceVerified,
set only after completed builds observe clean source and unchanged HEAD. Local
dirty builds remain supported; binary delivery rejects their results and
receipts even after checkout cleanup. API tests cover clean, initially dirty,
and changed-during-build source; a CLI regression covers receipt delivery after
cleanup. The offline suite and public boundary checks pass.
R2 — High: Some successful page writes are reported as not attempted
Status: Resolved.
Category: Failure handling and recovery-state correctness.
Evidence: EnsureRelease, at the
postErr == nil branch around line 152, returns acceptCreated directly.
Semantic verification failures therefore bypass the reconciliation used for
JSON decoding or transport failures. Delivery
then calls stateForError(err, false) and reports not_attempted.
A fixture stored the requested page, returned HTTP 201 with a different title,
and exposed the correct page on subsequent GET. Delivery returned
not_attempted and malformed_response after one POST and only the initial
page lookup. It did not perform the selected reconciliation lookup, even though
the page existed. A reconciled successful create also loses write history when
acceptExisting returns Created: false, affecting subsequent error reporting.
Required remediation: Route both syntactic and semantic create-response failures through the one-lookup recovery path. Preserve sufficient write and confirmed-page progress to classify later failures accurately. A confirmed page match resolves delivery of that page; unresolved write outcomes remain unknown. Retain the selected bounded reconciliation and immutable-content rules.
Focused verification: Cover HTTP 201 with an inconsistent decoded response, its matching and conflicting follow-up lookups, and a recovered create followed by asset-inspection failure. Assert both request counts and returned state.
Resolution: Semantic create-response failures now use one reconciliation lookup. Recovered page results retain create-request history, preserving partial progress on later asset-inspection failure. Local HTTP tests cover matching, conflicting, and unavailable lookups; a CLI fixture verifies recovered creation followed by failed asset inspection. Both affected suites pass.
R3 — Medium: Read-only asset failures are classified as unknown writes
Status: Resolved.
Category: Divergence from the selected outcome contract.
Evidence: UploadMissing, around line 102,
sets DeliveryUnknown when preflight contains an unverifiable attachment.
This occurs before any upload. The CLI maps that
state to exit 3, documented as an unresolved remote mutation.
With an existing matching page and an attachment download returning HTTP 503,
a local reproduction returned unknown with zero POST requests. Uncertainty
about existing content was promoted into uncertainty about a write that never
occurred.
Required remediation: Keep unverifiable-content facts separate from uncertain mutation outcomes. Report a known inspection failure when no write was attempted; preserve partial page progress if this invocation already created a page. Reserve unknown-write state and exit 3 for unresolved writes, as selected in the plan. Adjust tests and documentation consistently.
Focused verification: An unreadable existing attachment on an otherwise read-only invocation must not return unknown-write state or exit 3. A genuinely ambiguous upload must continue to do so.
Resolution: Unverifiable preflight assets now return an inspection failure
with not_attempted, retaining partial page progress when creation preceded
the read failure. CLI tests cover existing and newly created pages, no asset
writes, and exit 1. Existing ambiguous-upload regressions still verify unknown
write outcomes. Both affected suites pass.
R4 — Medium: Local artifact regeneration is prohibited
Status: Resolved.
Category: Inadvertently strengthened prohibition.
Evidence: Build, around line 114, refuses
any existing selected artifact filename, including its own prior output. A
second identical successful-build request to the same directory fails with
already exists; preserving caller-owned file. The
build guide and CLI contract turn this
implementation choice into a stated restriction.
The plan selects regeneration of owned local filenames while preserving unrelated files. Published remote content remains immutable; that rule does not prohibit rebuilding local output. This refusal also obstructs recovery from a partial local build without manual deletion.
Required remediation: Regenerate the selected tool-owned local outputs and preserve unrelated files. Keep receipt validity consistent with complete build and smoke success when outputs are regenerated. Update tests and the current documents to remove the blanket existing-output prohibition. Use ordinary filesystem operations without adding broad directory cleanup or durability machinery.
Focused verification: Repeating a build in its output directory succeeds; a changed local build can regenerate its owned names; unrelated files survive; a partial rebuild does not authorize incomplete assets through a stale receipt.
Resolution: Builds regenerate selected local artifact names and preserve unrelated output files. The CLI invalidates prior receipts once a rebuild has reached its output directory, including failed rebuilds. API regressions cover repeated and changed-source builds; CLI coverage proves a failed smoke check removes the previous receipt. Affected suites pass.
R5 — Medium: The root facade still owns release mechanics
Status: Resolved.
Category: Architectural ownership gap and maintainability.
Evidence: candidate_build.go implements temporary storage allocation, containment checking, cleanup, and candidate-build coordination in the public package. delivery_assets.go implements artifact inventory validation, file opening/stat/hash checks, filename derivation, and manifest assembly there. These are substantial implementation responsibilities beyond forwarding and type conversion.
No internal type leak was found. The gap is the placement of mechanics that ADR 0001 and the architecture policy assign behind the internal boundary.
Required remediation: Move these responsibilities into cohesive internal implementation, leaving the root package to own supported declarations, explicit conversion, callback adaptation, forwarding, and error identity. Choose package placement around existing responsibilities; add no public assembly API or generic framework.
Focused verification: Existing external-package behavior and boundary checks remain valid after the move. Confirm filesystem and artifact algorithms are owned internally without introducing inward imports of public facades.
Resolution: Candidate-build coordination now belongs to internal/build;
artifact inventory, hashing, and manifest assembly belong to internal/delivery.
The root facade performs conversions and forwarding only. Existing artifact
tests moved with their implementation; external API and CLI tests, the complete
offline suite, and compiled-export/import checks pass.
R6 — Medium: Builds require a writable parent of the repository
Category: Inadvertently strengthened filesystem prerequisite.
Evidence: Candidate builds, around line 21,
and CLI delivery builds, around line 194,
allocate temporary storage with os.MkdirTemp(filepath.Dir(repositoryDir), ...).
The requirement is disposable output outside the worktree, not sibling output.
A clean candidate in a writable repository under a read-only parent failed at
candidate temporary-directory creation with permission denied, despite an
available writable system temporary directory.
Required remediation: Allocate ordinary temporary build storage outside the worktree without requiring write access to its parent. Share the mechanism where appropriate. Preserve cleanup of tool-owned temporary paths and avoid adding broad output-directory restrictions.
Focused verification: Candidate and temporary delivery builds work with a read-only repository parent and a usable system temporary directory. Generated candidate output still leaves the worktree clean.
R7 — Medium: Attachment origin restrictions exceed credential isolation
Category: Inadvertently strengthened prohibition.
Evidence: downloadHash, around lines 164 and 188, rejects cross-origin attachment URLs and redirects before making a request. The integration guide and tests state this as a prohibition. A matching asset hosted by a second local server was marked unverifiable; that server received zero download requests.
The selected requirement keeps authorization on the configured Gitea origin. It does not prohibit downloading an attachment elsewhere without that token. The implementation adds a deployment restriction for external asset hosting.
Required remediation: Support content verification across attachment URLs and redirects while sending the Gitea token only to the configured origin. Keep credential supply explicit and retain digest comparison. Remove the stronger request-origin ban from tests and documentation; introduce no new network-policy framework.
Focused verification: Matching cross-origin bytes can be verified and conflicting bytes rejected. Direct and redirected external requests never carry the Gitea token; same-origin authenticated downloads continue to work.
R8 — Medium: The release wrapper collapses documented exit codes
Category: Executable integration and documentation correctness.
Evidence: scripts/release, line 9, executes
go run ./cmd/releasekit publish. The Go driver exits 1 when its child exits
2 or 3. The runbook attributes the native CLI's distinct
statuses to that wrapper.
Both go run ./cmd/releasekit check invalid and ./scripts/release invalid
returned shell status 1; the native CLI classifies invalid versions as 2.
The same driver behavior loses the unknown-publication status 3.
Required remediation: Keep source bootstrap, but build and invoke the native
executable with the prepared local toolchain so the maintainer entry point
preserves its status. Keep generated tool output ignored or outside the
worktree. Clarify the distinction for retained go run examples, and make
runbook status claims match the actual entry point. This adds no publication,
distribution, or external preparation gate.
Focused verification: The real wrapper preserves invalid-input status 2 and unknown-publication status 3, as well as ordinary success and known failure. Use local fixtures rather than a real publication endpoint.
R9 — High: Manual recovery inspects the fetch endpoint
Category: Release recovery correctness and documentation gap.
Evidence: The runbook, around line 82, prescribes
git ls-remote --refs origin refs/tags/<version> and permits local-tag removal
when that query returns absence. This query uses the normal fetch URL, whereas
the selected publisher deliberately supports a separate push URL.
A local origin with different bare fetch and push repositories returned no
tag through the documented query after that tag was successfully published to
the push repository. The actual publication endpoint still contained the tag.
The CLI's failure summary currently omits the captured endpoint present in
PublicationResult.Endpoint.
Required remediation: Make recovery inspect the exact captured publication endpoint and intended tag object. Provide the needed endpoint information in CLI recovery output and accurate copyable instructions. Do not infer that a published version or local tag is safe to remove from absence at the fetch repository. Preserve the existing rule that unresolved publication retains the local tag.
Focused verification: Exercise the documented recovery path with separate fetch/push repositories and confirm that a published tag is recognized. Include endpoint changes after an uncertain push when verifying retained recovery facts.
R10 — Medium: The boundary checker bans valid command-internal composition
Category: Inadvertently strengthened architectural prohibition.
Evidence: checkImportDirections, around
line 115, applies its blanket rejection of every cmd/ import to command-owned
internal packages too. A temporary overlay fixture supplied a dependency from
cmd/releasekit/internal/cli/parser to
cmd/releasekit/internal/cli/settings; the checker rejected it as importing a
public or command package.
Both sides are internal helpers owned by the same executable. This composition respects the agreed separation of command parsing from facade calls. The checker adds a prohibition that would constrain ordinary internal refactoring.
Required remediation: Distinguish library internal packages, executable controllers, and command-owned internal helpers. Keep library dependencies inward and keep facade calls in the controller, while permitting coherent command-internal collaborators to use their own internal contracts. Avoid turning current package inventory into a fixed future architecture.
Focused verification: A command-internal helper dependency passes. Library internal imports of command code and internal-helper imports of public facades remain rejected.
Remediation boundaries
Address these findings before treating the initial implementation as complete for the maintainer's next-step decisions. The work should repair the selected behavior and ownership boundaries, with focused regression checks for the reproduced defects. It should preserve the two facades, selected publication standard, immutable hosted contents, lightweight runtime dependency graph, source-only hosted pages, and separate publication/delivery status.
The findings require no consumer migration, intermediate release, live service probe, or new human approval gate. After remediation, the maintainer can choose the broader testing, adoption, and deployment steps.