408 lines
21 KiB
Markdown
408 lines
21 KiB
Markdown
# 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](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
|
||
|
||
Status: Resolved.
|
||
|
||
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.
|
||
|
||
**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](../../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.
|
||
|
||
**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](../../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.
|
||
|
||
**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](../../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.
|
||
|
||
**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](../../internal/build/candidate.go) implements temporary
|
||
storage allocation, containment checking, cleanup, and candidate-build
|
||
coordination in the public package. [delivery_assets.go](../../internal/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
|
||
|
||
Status: Resolved.
|
||
|
||
Category: Inadvertently strengthened filesystem prerequisite.
|
||
|
||
**Evidence:** [Candidate builds](../../internal/build/candidate.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.
|
||
|
||
**Resolution:** Candidate and CLI delivery builds use shared system temporary
|
||
storage allocated by `internal/tempdir`, with worktree containment checks and
|
||
caller-owned cleanup. Regressions execute both operations with read-only
|
||
repository parents and confirm clean worktrees. Additional helper regressions
|
||
cover relative system temporary paths and cleanup after rejecting in-worktree
|
||
storage. Affected suites pass.
|
||
|
||
### R7 — Medium: Attachment origin restrictions exceed credential isolation
|
||
|
||
Status: Resolved.
|
||
|
||
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.
|
||
|
||
**Resolution:** Attachment URLs and redirects may cross origins; the client
|
||
sets the Gitea token only on requests to its configured origin. HTTP tests
|
||
verify direct and redirected external matches, conflicting external bytes,
|
||
absence of authorization on external requests, and continued same-origin
|
||
authentication. The Gitea suite passes.
|
||
|
||
### R8 — Medium: The release wrapper collapses documented exit codes
|
||
|
||
Status: Resolved.
|
||
|
||
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.
|
||
|
||
**Resolution:** The source wrapper builds its native executable into ignored
|
||
`.releasekit/bin` and invokes it directly. Documentation distinguishes native
|
||
exit codes from `go run` driver behavior. Local-repository tests execute the
|
||
actual wrapper and verify success 0, rejected publication 1, invalid input 2,
|
||
and unresolved publication 3. The focused wrapper test passes.
|
||
|
||
### R9 — High: Manual recovery inspects the fetch endpoint
|
||
|
||
Status: Resolved.
|
||
|
||
Category: Release recovery correctness and documentation gap.
|
||
|
||
**Evidence:** [The runbook](../release.md), 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.
|
||
|
||
**Resolution:** Failed publication output includes the captured endpoint. The
|
||
runbook checks URL resolution and queries that exact endpoint, retaining tags
|
||
on failed or unresolved checks. Local wrapper fixtures use separate fetch and
|
||
push repositories; a lost acknowledgment changes the configured push URL, and
|
||
recovery still finds the intended object at the captured endpoint while the
|
||
fetch endpoint reports absence. The guarded runbook commands are exercised
|
||
verbatim, including refusing a query after URL rewriting changes the endpoint.
|
||
Focused publication/wrapper tests pass.
|
||
|
||
### R10 — Medium: The boundary checker bans valid command-internal composition
|
||
|
||
Status: Resolved.
|
||
|
||
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.
|
||
|
||
**Resolution:** The import checker identifies each executable’s internal helper
|
||
subtree and permits collaboration inside it. Library-to-command imports,
|
||
helper-to-controller imports, and internal-to-facade imports remain rejected.
|
||
Positive and negative dependency fixtures and the actual compiled repository
|
||
boundary checks pass.
|
||
|
||
## 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.
|