From 1d6611601cbd515399f9e25605e06c73bc321a05 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 6 Oct 2026 00:14:00 +0000 Subject: [PATCH] Record implementation review and remediation findings --- docs/roadmap/remediation.md | 323 ++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/roadmap/remediation.md diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md new file mode 100644 index 0000000..a8650f6 --- /dev/null +++ b/docs/roadmap/remediation.md @@ -0,0 +1,323 @@ +# ReleaseKit implementation review and remediation + +Status: Open. These findings describe work to address; this review changes no +implementation or supported contract. + +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](releasekit.md), [implementation plan](implementation.md), +[ADR 0001](../adr/0001-public-facade.md), 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](https://woodpecker-ci.org/docs/usage/matrix-workflows) and +[environment](https://woodpecker-ci.org/docs/usage/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 + +Category: Correctness and divergence from tagged-source delivery. + +**Evidence:** [The builder](../../internal/build/build.go) records `HEAD` in +`gitCommit` but compiles the current worktree without establishing that it is +that committed source. [Delivery](../../internal/delivery/delivery.go) checks +checkout cleanliness and tag equality at delivery time, then accepts matching +commit metadata and artifact hashes. The [receipt adapter](../../cmd/releasekit/internal/cli/receipt.go) +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. + +### R2 — High: Some successful page writes are reported as not attempted + +Category: Failure handling and recovery-state correctness. + +**Evidence:** [EnsureRelease](../../internal/gitea/release.go), 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](../../internal/delivery/delivery.go) +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. + +### R3 — Medium: Read-only asset failures are classified as unknown writes + +Category: Divergence from the selected outcome contract. + +**Evidence:** [UploadMissing](../../internal/gitea/upload.go), around line 102, +sets `DeliveryUnknown` when preflight contains an unverifiable attachment. +This occurs before any upload. [The CLI](../../cmd/releasekit/main.go) 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. + +### R4 — Medium: Local artifact regeneration is prohibited + +Category: Inadvertently strengthened prohibition. + +**Evidence:** [Build](../../internal/build/build.go), 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](../consumers/build.md) and [CLI contract](../cli.md) 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. + +### R5 — Medium: The root facade still owns release mechanics + +Category: Architectural ownership gap and maintainability. + +**Evidence:** [candidate_build.go](../../candidate_build.go) implements temporary +storage allocation, containment checking, cleanup, and candidate-build +coordination in the public package. [delivery_assets.go](../../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. + +### R6 — Medium: Builds require a writable parent of the repository + +Category: Inadvertently strengthened filesystem prerequisite. + +**Evidence:** [Candidate builds](../../candidate_build.go), around line 21, +and [CLI delivery builds](../../cmd/releasekit/main.go), 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](../../internal/gitea/assets.go), around lines 164 +and 188, rejects cross-origin attachment URLs and redirects before making a +request. The [integration guide](../integrations/gitea.md) 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](../../scripts/release), line 9, executes +`go run ./cmd/releasekit publish`. The Go driver exits 1 when its child exits +2 or 3. [The runbook](../release.md) 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](../release.md), around line 82, prescribes +`git ls-remote --refs origin refs/tags/` 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](../../internal/apicheck/main.go), 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.