396 lines
21 KiB
Markdown
396 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. 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. Focused publication/wrapper tests pass.
|
|
|
|
### 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.
|