Remove completed release delivery plans and audit records

This commit is contained in:
2026-11-01 19:37:40 +00:00
parent e42cad7b1a
commit 3f00ad2754
3 changed files with 0 additions and 1215 deletions

View File

@@ -1,180 +0,0 @@
# Roadmap: forge delivery boundary and credential preflight
Status: implementation complete; review corrections are tracked in
[remediation](remediation.md).
The [implementation plan](implementation.md) owns stages, concrete design
choices, task sequencing, and verification.
## Objective and scope
Introduce an internal boundary between shared hosted-release delivery and the
Gitea implementation. The result should support adding another forge through
an adapter while reusing ReleaseKit's release and asset-delivery policy.
Add operation-specific credential preflight through that boundary so delivery
stops on credential problems before expensive preparation or builds.
ReleaseKit's `Publish` operation validates and publishes source through Git.
Its `Deliver` operation verifies tagged source and delivers release notes and
declared assets to a hosting service. This refactor concerns the latter operation
and its composition through the existing public facade and CLI.
Gitea is the concrete backend for this work. Forgejo, GitHub, GitLab, and other
backends remain future implementation work. The boundary should express the
operations used by the current delivery workflow and allow refinement when a
second backend supplies concrete requirements.
## Historical planning baseline
The original source baseline was `ac78e7d`, following the published v0.5.0
module migration and the subsequent token-file and forbidden-file changes.
The following describes that revision, before implementation of this roadmap.
- [delivery.go](../../delivery.go) exposes `GiteaDestination` through
`DeliveryRequest.Destination` and converts it into `gitea.Destination`.
- [internal/delivery](../../internal/delivery/delivery.go) resolves source
identity, checks tagged source, reads notes, coordinates builds, and directly
calls `gitea.Client` for release and asset operations. Its result conversion
and failure classification use Gitea-owned internal types.
- [release.go](../../internal/gitea/release.go) combines Gitea HTTP operations
with hosted-tag verification, matching-page acceptance, release creation,
and reconciliation of ambiguous creates.
- [assets.go](../../internal/gitea/assets.go) and
[upload.go](../../internal/gitea/upload.go) combine HTTP operations with
manifests, local-file verification, remote-content comparison, asset
observations, upload sequencing, and retry bookkeeping.
- CLI credential resolution supplies an explicit token from the configured
environment variable or token file. The profile describes a Gitea repository;
it has no forge selector.
- CLI loading checks token availability, but forge API authentication first occurs
through delivery requests after application dependency preparation and managed
builds. Library delivery also checks a blank token only at the later Gitea
request boundary. There is no dedicated authenticated credential preflight.
The [Gitea integration guide](../integrations/gitea.md) and public Go declarations
now describe the implemented contracts. Behavioral tests provide regression
coverage for the selected behavior. The
[architecture policy](../policy/architecture.md) governs facade ownership and
dependency direction.
## Targeted end state
### Neutral public destination and explicit forge selection
Replace `GiteaDestination` with a facade-owned neutral destination type and
require explicit forge selection in the public API and delivery configuration.
Gitea is the sole supported selection initially.
An omitted, blank, or unsupported selection produces a validation error; Gitea
is selected explicitly rather than inferred from a default or destination URL.
The destination represents the addressing and credentials needed for current
delivery through neutral public names. Concrete backend construction uses the
selected forge. Additional backends may extend that model when their addressing
and protocol requirements are implemented.
### Shared delivery responsibilities
Shared internal code owns:
- Automatic credential preflight coordination before expensive delivery work.
- Captured source identity, local tag/commit checks, committed release notes,
managed or prebuilt artifact coordination, and local artifact verification.
- The existing logical rules for accepting matching hosted content, reporting
conflicts, creating missing content, and reconciling uncertain writes.
- Manifest generation, content comparison, and asset observations used to decide
which declared assets require delivery.
- Aggregation into the existing public delivery outcomes and asset categories.
These operations consume backend-neutral internal values and operations. The
generic delivery workflow has no dependency on `internal/gitea`. The facade or
private composition code constructs the concrete backend and supplies it to
shared delivery.
### Credential preflight
Delivery automatically checks the credentials required for its selected backend.
Local checks establish availability and valid destination settings; a read-only
backend probe establishes authentication and access to the configured repository.
Reliable evidence of insufficient required permissions stops the operation.
Unexposed permissions remain unverified; successful reads establish no blanket
guarantee that later writes will succeed. Preflight performs no test writes and
preserves ordinary delivery authorization and recovery handling.
Library and ordinary CLI delivery perform preflight before managed builds and
full artifact verification. CI loads the verified tagged profile first, then
performs preflight before application dependency preparation. A callable facade
operation lets consumers check a destination before their own preparation,
without requiring a release tag or note to exist.
Failures distinguish missing or rejected credentials, insufficient permissions,
an inaccessible destination, and unavailable or unverifiable service responses.
Delivery reports `not_attempted` for these pre-write failures. Diagnostics retain
useful causes while excluding tokens. Each check describes access at that time;
later operations retain their normal checks.
Required credentials follow operation scope. Candidate checking and standalone
builds remain credential-free. Source publication continues using Git's native
SSH/HTTPS authentication and existing remote guards, independently of hosted
delivery credentials. This feature adds no Gitea-token requirement to `Publish`.
### Backend responsibilities
The Gitea adapter owns destination validation specific to Gitea, API routes,
authentication headers, request and response formats, pagination, multipart
uploads, attachment downloads, and HTTP-specific error interpretation.
It also owns authenticated credential probes and interpretation of available
permission evidence, using repository-scoped checks without requiring unrelated
account or administration scopes.
The internal contract supplies the logical operations needed to preflight
credentials, inspect a hosted tag, find or create a release, enumerate assets,
read asset content, and upload an asset. Release and asset references carry
backend-owned handles; shared orchestration works with logical identity and content rather than
interpreting Gitea's positive integer IDs.
The contract also carries the evidence needed for existing recovery behavior:
whether a write was attempted, whether its outcome is established or uncertain,
and actionable failure information. Generic code interprets delivery outcomes;
the adapter interprets protocol responses. A logical backend operation may
involve multiple HTTP requests.
The operations and values stay internal under the existing facade policy.
Current Gitea behavior is the concrete compatibility target. This work does
not establish compatibility guarantees for unimplemented forges or a public
plugin protocol.
## Behavioral scope and compatibility
The refactor preserves the existing Gitea delivery contracts for complete note
bodies, retained existing titles, stable release state, content conflicts,
unrelated attachments, additive retries, and source-only or binary delivery.
Read-only inspection failures and unresolved writes retain their distinct
outcomes. Recovery retains write history, including confirmed progress after
an ambiguous create. The current integration guide owns the concrete protocol
and follow-up lookup behavior.
Local verification reuse, streaming, cancellation, stream closure, temporary
build ownership, and upload cleanup remain part of the affected workflow.
Read-only credential preflight precedes expensive local artifact work; local
content verification still completes before hosted release or asset writes.
Credential environment/file selection and forbidden-file validation retain
their existing responsibilities. Backend-specific address validation stays
with that backend.
The public destination replacement, required forge selection, and earlier
credential failures are deliberate changes for the next minor release.
Migration guidance describes consumer edits under the project's direct-cutover
approach.
Current contract documents and examples change with implementation; historical
release notes retain their original contents.
Completion means shared delivery and asset policy have independent internal
ownership, Gitea implements the backend boundary, existing Gitea contracts work
through it, and the neutral public destination and explicit selection are
consistent across the API, configuration, examples, and documentation.
Credential preflight operates through the backend boundary in both library and
CLI delivery, with accurate failure distinctions and documented permission limits.
Additional forge implementations and compatibility claims belong to later work.
Release preparation and publication continue under the
[existing procedure](../release.md).

View File

@@ -1,570 +0,0 @@
# Implementation plan: neutral forge delivery and credential preflight
Status: implementation complete; Stages 1–10 are complete.
Follow-up review corrections are tracked in [remediation](remediation.md).
## Purpose and execution
Implement the target in the [feature roadmap](forge-boundary.md). That document
owns scope and user choices; this plan owns concrete design and ordered work.
The historical planning baseline was `44823ce`, whose source implementation
was unchanged from `ac78e7d`. It included Gitea delivery, token files, and
`forbidden_files`. The neutral destination and credential preflight were the
planned additions. The sections below retain the original implementation plan;
the completed stages record its execution.
Execute Stage 1 through Stage 10 in order as one end-to-end sprint. Each stage
is a focused Luna implementation task. Intermediate stages may leave callers
or tests uncompilable while responsibilities move. Continue without an
intermediate release, pilot, external action, or human review. Run focused
checks when the affected packages compile; Stage 10 owns final assembled
verification. Record completion here and follow the
[agent policy](../policy/agents.md) for commits and validation reporting.
Read the [development guide](../development.md) and applicable policies. Use
the [architecture policy](../policy/architecture.md),
[testing policy](../policy/testing.md), and
[documentation policy](../policy/documentation.md) throughout. Existing GoDoc,
[configuration](../config.md), [CLI](../cli.md), and
[Gitea integration](../integrations/gitea.md) documents define current contracts.
Tests supply regression evidence; assess incidental implementation assertions
against those contracts before preserving them.
This sprint implements the Gitea backend behind a neutral boundary. Git source
publication, module/tag derivation, build targets, buildinfo, receipts, token
precedence, and optional candidate validators retain their responsibilities.
Additional backends, public backend registration, new transport retry policies,
and release publication are outside this sprint.
## Selected design
### Public API and configuration
Replace the root facade's `GiteaDestination` with these facade-owned declarations:
```go
type Forge string
const ForgeGitea Forge = "gitea"
type DeliveryDestination struct {
Forge Forge
BaseURL string
Owner string
Repository string
Token string
}
```
`DeliveryRequest.Destination` becomes `DeliveryDestination`; its other fields,
the public result shape, state values, and asset categories remain as defined
today. Use explicit conversions into internal values. The facade owns backend
selection and construction through a private helper in `delivery.go`.
Add `delivery.forge` to the existing flat JSON delivery object. Its supported
value is `"gitea"`, corresponding to `ForgeGitea`. Omitted, empty, whitespace-only,
and unsupported selections are errors when delivery is requested. Selection is
explicit in both interfaces; neither URL inspection nor an omitted field selects
a backend. Keep `base_url`, `owner`, `repository`, `token_env`, and `token_file`
in their current locations. The owner/repository fields express the current
backend's addressing needs; future adapters can extend the neutral model when
needed without promising every forge the same address structure now.
Keep semantic delivery validation at the delivery entry points. Strict JSON
decoding still applies to all commands, but `check`, `publish`, and `build` do
not acquire a new requirement for a delivery object, valid forge selection, or
credentials. `ci-deliver` first prepares/verifies the tagged checkout and loads
its profile as today, then checks the forge before token loading and application
dependency preparation. Ordinary `deliver` checks it before loading receipts,
building, or making hosted requests. Library selection failure returns
`DeliveryNotAttempted` with the requested version and an error before invoking
the generic workflow. Credential preflight failures use the classification
described below.
After explicit Gitea selection, retain `RELEASEKIT_GITEA_TOKEN` as the CLI's
existing default token environment name. Preserve configured names, environment
over file precedence, trimming and file path resolution, and status 2 for
configuration/credential errors. The library receives `Token` explicitly.
Gitea's remaining destination validation stays in the adapter. Consumer
migration replaces the public type, supplies `Forge: releasekit.ForgeGitea`,
and adds `"forge": "gitea"` to each delivery profile. Use a direct API cutover
without a legacy `GiteaDestination` alias or implicit-selection compatibility
path.
### Credential preflight contract and ordering
Add `PreflightDelivery(ctx context.Context, destination DeliveryDestination) error`
to the root facade. It checks the selected destination without requiring source,
version, tag, note, or artifact inputs. `Deliver` automatically uses the same
internal preflight implementation; callers do not need to invoke the standalone
operation for ordinary delivery.
Use a facade-owned `DeliveryPreflightError` with `Kind` and `Err` fields,
`Error`, and `Unwrap`. Its named string kind, `DeliveryPreflightFailureKind`,
has these constants:
| Constant | Value |
| --- | --- |
| `DeliveryPreflightMissingCredential` | `missing_credential` |
| `DeliveryPreflightInvalidCredential` | `invalid_credential` |
| `DeliveryPreflightInsufficientPermissions` | `insufficient_permissions` |
| `DeliveryPreflightInaccessibleDestination` | `inaccessible_destination` |
| `DeliveryPreflightUnavailable` | `unavailable` |
Convert internal classifications explicitly and retain causes and cancellation
identity without exposing internal types in
exported signatures. The unavailable category includes network/service failures
and malformed responses that prevent verification. A 404 can mean hidden or
missing repository access; diagnostics must preserve that ambiguity. A 403
establishes denied access, not necessarily an expired or invalid token.
Preflight checks local destination rules and nonblank credentials, then invokes
the backend's read-only authenticated repository probe. It fails on rejected
credentials, inaccessible target access, and reliably established deficiencies
in permissions needed for delivery. Scope checks to release/asset delivery;
code-push permission alone is not an equivalent release permission. Unexposed
token scopes or write permissions remain unverified and do not independently
fail preflight. A nil error establishes authentication and repository access
plus no known delivery-permission deficiency; it does not certify every future
write. Preserve ordinary authorization and write recovery. Do not create test
releases, upload test assets, or request extra user/admin token scopes.
Library delivery performs cheap request/source/note checks, then credential
preflight before managed builds or full artifact size/digest verification.
Ordinary CLI delivery resolves credentials as today and relies on this automatic
library preflight. Reading a prebuilt receipt can remain a cheap earlier step.
After preflight succeeds, full local content verification still precedes every
hosted release/asset write. This deliberately permits the earlier read-only
credential probe before local artifact verification.
For binary `ci-deliver`, load the verified tagged profile and resolve credentials,
invoke the standalone facade preflight, then prepare application dependencies.
Subsequent `Deliver` performs its normal preflight before building; that later
check is fresh after network-capable dependency preparation. Page-only CI has
no application dependency preparation and can rely on `Deliver` alone. No
long-lived validation cache or public preflight-bypass setting is needed.
Automatic preflight failure leaves delivery `not_attempted`, including service
unavailability; unknown-write outcomes remain reserved for unresolved writes.
CLI missing/invalid credentials, established insufficient permissions, and
inaccessible destinations return status 2. Unavailable verification and
cancellation return status 1 with their actual causes. Existing local profile
errors keep status 2. Document these earlier failures and classifications as
part of the next minor release's operational compatibility impact.
`check` and standalone `build` acquire no credentials or online preflight.
`publish` retains Git's native SSH/HTTPS authentication and remote guards; it
does not invoke hosted delivery preflight or require a forge token. Consumers
can explicitly call `PreflightDelivery` before their own preparation or source
publication without making that a new mandatory publication gate. Application
dependency credentials stay with the existing Go/Git preparation mechanisms.
### Internal ownership and dependencies
| Owner | Responsibility after the sprint |
| --- | --- |
| Root facade | Public destination and callable preflight, explicit selection, concrete backend construction, request/result/error conversion. |
| `internal/forge` (new) | Backend interface, preflight classifications, observed release/asset values, opaque handles, neutral failures, and write evidence. |
| `internal/artifact` | Existing build identity helpers plus expected local assets, manifests, declaration validation, and local size/digest verification. |
| `internal/delivery` | Automatic preflight coordination, local source/build coordination, hosted tag comparison, page matching/creation recovery, remote content comparison, upload sequencing/recovery, observations, and final outcomes. |
| `internal/gitea` | Destination rules, authenticated preflight probes and permission evidence, HTTP operations, codecs, protocol acknowledgments, authentication and redirects, multipart streaming, and protocol-to-neutral failure conversion. |
| CLI | Profile adaptation, token loading, receipts, preparation, presentation, and exit statuses. |
Both delivery and Gitea depend on `internal/forge`; that contract imports neither
of them. Artifact mechanics stay independent of Gitea and delivery. The generic
workflow receives a configured backend rather than destination credentials.
The facade can import Gitea to assemble the operation. The runtime facade keeps
its existing small dependency graph.
Use one internal `Backend` interface with these logical operations. The names
below define the intended seam; small private naming adjustments are routine
implementation choices.
| Operation | Inputs and outputs |
| --- | --- |
| `Preflight` | Context; checks the backend's configured credentials/repository through read-only requests and returns a classified preflight error or nil. |
| `TagCommit` | Context and full tag; returns observed commit or a classified error. |
| `FindRelease` | Context and full tag; returns logical release data or a classified not-found/error. |
| `CreateRelease` | Context and requested tag, commit, title, body, draft/prerelease values; returns observed release, write evidence, and error. |
| `ListAssets` | Context and opaque release handle; returns logical remote assets or error. |
| `OpenAsset` | Context and opaque asset handle; returns `io.ReadCloser` or error. |
| `UploadAsset` | Context, opaque release handle, filename, and `io.Reader`; returns write evidence and error. |
Logical release data contains the URL, full tag, title, body, draft/prerelease
flags, and an opaque handle. Remote asset data contains filename, reported size,
and an opaque handle. Use internal `any` handle fields with private concrete
Gitea handle values. Only the creating adapter interprets them. Keep numeric IDs
and download URLs in those private values; the shared workflow performs no
integer-ID checks or URL construction.
Write evidence carries an `Attempted` flag and a disposition: not attempted,
accepted, rejected, conflict, or uncertain. Accepted means the adapter received
and decoded the required protocol acknowledgment; shared release-content checks
still apply. A known rejection differs from an uncertain outcome. Carry this
evidence even with an error, so successful reconciliation can retain earlier
write history. Record an attempted write when it is handed to the transport,
consistent with the existing possibly-successful-request handling; this is not
a claim that the server received it.
Neutral failure kinds cover the existing invalid, not-found, unauthorized,
conflict, malformed, unverifiable, transport, rejected, and unknown distinctions.
Retain underlying causes through wrapping/`Unwrap` or `errors.Join`. Gitea HTTP
status and operation details belong to the adapter's diagnostic cause. Shared
recovery uses the neutral kind and write evidence, with no HTTP status branches
or Gitea error assertions. Credentials stay out of errors and results.
The shared caller closes streams returned by `OpenAsset`, including on read
failure. For uploads it opens/closes the local file or supplies an inline
manifest reader; the adapter borrows that reader for the call and finishes its
multipart writer/pipe work before returning. Retain cancellation propagation
and cleanup rather than introducing detached recovery contexts, timeouts, or
buffering of complete binaries.
## Stage 1: Establish the internal backend contract
Status: complete.
1. Add `internal/forge` with the interface and logical values specified above.
Define preflight classifications, neutral failures, and write-evidence types
with comments about not-found, accepted acknowledgments, uncertain writes,
opaque handles, and
stream ownership. Use ordinary Go values and error wrapping.
2. Keep this package concerned with the delivery seam; production backend
selection belongs to the facade. Avoid a registry, capability negotiation,
public interfaces, or additional forge implementations in this work.
3. Identify the existing Gitea policy types replaced by later stages:
`Request`, `Result`, `ExpectedAsset`, `Inspection`, asset identities/states,
upload results, and failure assertions in `internal/delivery`. Assign their
replacement ownership as above rather than reproducing the whole Gitea
model under new names.
Result: an internal contract implemented independently of the public facade and
delivery policy. Type-check the new package when available. Test nontrivial
error behavior where useful; recovery is tested through the shared workflow.
## Stage 2: Move local asset and manifest mechanics
Status: complete.
1. Move `ExpectedAsset` and the generic work in `BuildManifest`,
`validateExpectedAssets`, and local verification from `internal/gitea` to
`internal/artifact`. Keep the filename, SHA-256, size, path, and inline-content
representation and existing accepted inputs.
2. Retain `artifact.ManifestFilename`, sorted complete manifest generation, its
byte format, duplicate/name/digest validation, and the empty-set result that
produces no manifest.
3. Adapt `internal/delivery/assets.go` and build validation to shared types.
Keep target/name correspondence, captured module/tag/commit binding,
`SourceVerified`, regular-file checks, and prebuilt output ownership. Keep
build results and the private receipt format unchanged.
4. Move/consolidate manifest and local-content tests with these responsibilities.
Preserve rejection of same-size local tampering.
Result: local assets are declared and verified independently of Gitea. Retain
one full local size/digest preflight before hosted writes and reuse it for
uploads; subsequent transport reads stream content without a second full
preflight. This preserves existing efficiency/integrity choices rather than
adding a guarantee against concurrent file replacement.
## Stage 3: Extract Gitea read operations
Status: complete.
1. Adapt the Gitea client into a backend configured with its private destination
and optional HTTP client. Implement `TagCommit`, `FindRelease`, `ListAssets`,
and `OpenAsset` using the existing HTTP helpers and routes.
2. Keep Gitea address/token rules, full-tag escaping, JSON handling, attachment
metadata validation, and HTTP status interpretation here. Map a release
lookup's 404 to neutral not-found; a missing hosted tag remains a failure to
verify source. Preserve operation-specific checks rather than applying
create-response requirements to every read.
3. Split `downloadHash` so the adapter opens the download stream and generic
delivery hashes it. Keep service-origin token forwarding, cross-origin
downloads without that token, redirect traversal, and response cleanup with
Gitea. Preserve current listing behavior; extracting the boundary does not
introduce pagination or a new response-completeness guarantee.
4. Retain local HTTP tests for routes, authentication, escaped tags, metadata
decoding, malformed lists, redirects, and unreadable assets. Adapt tests to
the read operations; shared comparison tests move in Stage 7.
Result: backend reads expose observations and streams. Content equality, tag
comparison, page acceptance, and delivery outcomes belong to shared policy.
## Stage 4: Implement read-only Gitea credential preflight
Status: complete.
1. Implement backend `Preflight` with local destination/token validation and
authenticated repository-scoped reads. Perform no source/tag checks here so
the same probe works before a release tag exists.
2. Resolve the configured repository with `GET /repos/{owner}/{repo}`, then
use its ID with the authenticated `GET /repositories/{id}` route. Validate
the required repository identity and successful response shape. The second
route requires authentication and repository scope, so public anonymous
repository access alone cannot satisfy preflight. Keep headers, IDs, and
route details within Gitea. See the upstream
[route definitions](https://github.com/go-gitea/gitea/blob/release/v1.26/routers/api/v1/api.go).
3. Interpret reliable release/asset permission evidence when available. Gitea's
basic `permissions.push` describes code pushes, and a successful repository
GET does not establish `write:repository` token scope. Treat unexposed
release-write/token-scope evidence as unverified. Do not equate absent
metadata with denial or add `/user` probes requiring unrelated user scope.
The upstream [repository model](https://github.com/go-gitea/gitea/blob/release/v1.26/modules/structs/repo.go)
and [scope documentation](https://docs.gitea.com/development/api-usage/)
describe those distinctions. Keep later write authorization authoritative.
4. Classify missing/blank credentials locally, server-rejected authentication,
permission denial, inaccessible/hidden destinations, and unavailable or
malformed verification responses. Preserve cancellation and useful HTTP
causes without including the token. Unsupported/malformed destination
settings retain actionable local configuration errors.
5. Use a local HTTP fixture to cover accepted repository-scoped credentials,
invalid tokens, denied/missing repositories, service/decoding failure, and
cancellation. Demonstrate that anonymous public access cannot pass the
authenticated step and that unexposed write scopes are not claimed as
verified or treated as a new failure. Assert read-only behavior at this seam.
Result: the backend checks authentication and repository access before costly
delivery work and accurately represents the limits of available permission
evidence. It uses no extra account/admin credentials or destructive probe.
## Stage 5: Extract Gitea write operations and evidence
Status: complete.
1. Implement `CreateRelease` from the current release POST and `UploadAsset`
from multipart upload. Keep explicit target commit, stable flags, full note,
query encoding, attachment field, and token header behavior. Protocol codecs
and private IDs remain here.
2. Populate attempted-write evidence on each path. Translate the existing
`shouldReconcileWrite` triggers into neutral dispositions: HTTP conflict is
conflict; server errors, transport interruption, cancellation/deadline, and
malformed acknowledgments are uncertain; definitive other rejections remain
rejected. Preserve causes and HTTP details. Local failures before transport
are not attempted. Shared policy decides how to recover.
3. Validate Gitea acknowledgments here, including positive IDs and the uploaded
asset's selected filename. Leave requested release tag/title/body/stability
comparison to Stage 6. A decoded acknowledgment that fails shared comparison
can still require reconciliation.
4. Accept a borrowed content reader for upload. Move file/inline-reader selection
to shared code and preserve multipart streaming, pipe closure, writer
completion, response cleanup, and context cancellation without an extra
content pass.
5. Keep HTTP tests for request bodies, uploads, malformed acknowledgments, known
rejections, ambiguous writes, and cancellation at this boundary. Recovery
orchestration tests move with policy in Stages 6 and 7.
Result: individual writes return evidence. Follow-up lookups, content comparison,
and continuation decisions belong to shared delivery. Remove the adapter's old
orchestration as its callers move rather than retaining a parallel policy path.
## Stage 6: Move hosted page policy and recovery
Status: complete.
1. Add shared page coordination in `internal/delivery` using `forge.Backend`.
After credential preflight and local source/build verification, obtain the
hosted full tag's commit and compare it with the captured commit before
page writes. Find the
release, accepting the same full tag/body/stable non-draft content and
retaining an existing title. Only a missing release triggers create.
2. Supply the full note and H1 title to `CreateRelease`. Apply shared comparison
to a successful create response, including its requested title. Reconcile
an inconsistent response as the existing client does.
3. Move create recovery out of `EnsureRelease`. Preserve the current single
follow-up lookup for a conflict or uncertain create:
| Follow-up observation | Result |
| --- | --- |
| Matching existing page | Accept; retain the earlier write attempt. |
| Different page | Conflict. |
| Missing after conflict | Conflict. |
| Missing after uncertain write | Unknown. |
| Result cannot be established | Unknown; retain write and inspection causes. |
4. Preserve known/read-only failures as distinct from unknown writes. Retain
resolved source identity, confirmed URL, and write history. A later asset
inspection failure after confirmed page-write progress reports partial
progress. Page-only delivery succeeds without listing/uploading assets or
creating a manifest.
5. Move page policy cases into shared tests using a small controlled backend:
matching content, conflicts, missing/wrong tag, ambiguous create recovery,
and recovered-create history. Keep wire assertions in adapter tests.
Result: page behavior runs through the seam. Current Gitea recovery remains
unchanged; this does not promise a fixed HTTP request count for future logical
backend methods.
## Stage 7: Move remote asset comparison and upload recovery
Status: complete.
1. Move inspections, asset states/identities, upload aggregation, ordering,
and recovery into `internal/delivery`. Use `ListAssets` and `OpenAsset` for
streamed SHA-256 comparison. Retain filename grouping, duplicate-name
conflicts, digest comparison, unrelated asset preservation, and unverifiable
content classification. Reported size alone remains insufficient for equality.
2. Retain complete-manifest comparison, acceptance of a matching manifest when
binaries are missing, and rejection of a subset/different manifest. Add no
independent remote-size acceptance rule.
3. Preflight declarations before uploads. Matching assets stay matching;
conflicts stop with conflict; unverifiable reads stop with not-attempted
asset work, upgraded to partial on confirmed page-write progress. Whole-list
read failures retain existing uncertain asset observations without becoming
unknown-write outcomes.
4. Upload only missing assets in filename order with `SHA256SUMS` last. Open
content through the artifact representation and close it after the borrowed
upload call. Reuse Stage 2 verification. Acknowledged uploads move from pending
to uploaded.
5. On conflict or uncertain upload, perform the existing single list/content
reconciliation for that asset. Matching bytes become matching; differing
bytes become conflicting; missing after conflict remains conflict; missing
after uncertain write or an unreadable result becomes unknown/uncertain.
Stop on unresolved/conflicting outcomes and retain later known-missing assets
as pending. Keep reconciliation free of repeated POSTs.
6. Preserve disjoint current observations, earlier successful assets, and public
states. Definitive upload failure retains partial progress; early failures
can leave declarations unreported. A new invocation re-inspects remote state
and resumes only missing work.
7. Move comparison, manifest, partial retry, and category tests here. Use a small
backend double for controlled failures, with representative assembled HTTP
coverage instead of duplicating every policy case at both layers.
Result: shared code owns asset policy/recovery. Remove the former Gitea
`InspectAssets`, `UploadMissing`, and `UploadVerifiedMissing` entry points when
their callers/tests move; keep transport helpers needed by the backend. These
internal entry points have no public compatibility obligation.
## Stage 8: Connect the neutral facade and CLI
Status: complete.
1. Implement the public declarations/conversions in Selected design. Replace
current `GiteaDestination` uses. The private facade factory switches on the
explicit forge and constructs Gitea; invalid selection returns the described
early result.
2. Implement `PreflightDelivery` and `DeliveryPreflightError` conversions using
that same backend factory and preflight implementation. Support destination
checking without a repository checkout or published tag. Preserve causes
with `Unwrap` and keep all exported named types facade-owned.
3. Change the internal delivery request to receive the backend. Replace Gitea
asset/error references in request/result conversion and build validation.
Pass delivery's single captured module/source identity through its existing
workflow. Retain local source rechecks, temporary output cleanup, and
prebuilt output ownership. Invoke backend preflight after cheap local checks
and before managed builds/full artifact hashing. Convert its failures to
the facade error and retain `not_attempted` delivery state.
4. Add `Forge` to CLI delivery settings with JSON key `forge`. Validate selection
during delivery adaptation before credentials or subsequent delivery work,
returning status 2 for invalid selection. Map the facade's supported value;
preserve strict decoding and non-delivery behavior.
5. Update `deliveryDestination`, `deliverWithDestination`, ordinary delivery,
and CI delivery. Retain tagged profile loading, module/tag checks, dependency
preparation, receipt binding, and output/exit classification. Keep token
resolution in the CLI with current precedence and Gitea environment default.
For binary CI, invoke `PreflightDelivery` after tagged profile/credential
loading and before dependency preparation. Ordinary and page-only delivery
rely on automatic library preflight; subsequent binary CI delivery rechecks
after dependency preparation. Apply the preflight exit classifications above.
6. Add `"forge": "gitea"` to ReleaseKit's profile, maintained delivery profiles,
examples, and active test fixtures. Check/build-only profiles can keep
omitting delivery. Keep token names, file paths, and Woodpecker secret mappings.
Result: both interfaces select Gitea explicitly through a neutral destination
and share generic orchestration and automatic credential preflight. CI checkout
preparation retains its earlier position because it establishes which tagged
profile is authoritative; credential verification precedes application downloads.
## Stage 9: Complete regression coverage and dependency checks
Status: complete.
1. Finish adapting facade, CLI, HTTP fixture, and benchmark callers. Retain
root/nested/major-module identities, page-only delivery, managed/prebuilt
binaries, tagged retries, source checks, partial/conflict/unknown reporting,
and cleanup coverage.
2. Add focused API/CLI selection cases: explicit Gitea succeeds; omitted, blank,
and unsupported values fail; the URL does not infer a backend. For CLI errors,
assert status/semantic diagnostics and that token access, dependency
preparation, and hosted work were not reached. Reuse existing non-delivery
tests to show those commands still need neither selection nor credentials.
3. Cover automatic and standalone preflight with controlled failures: library
and ordinary CLI failures precede builds/full artifact hashing and writes;
binary CI failures precede dependency downloads; page-only CI still checks
credentials; standalone checks need no checkout or tag. Assert typed error
kinds, causes, CLI status, and `not_attempted` even on unavailable service.
Verify unavailable permission metadata alone is not failure and no preflight
path performs test writes. Update affected "before hosted requests" fixtures
to permit credential reads while retaining assertions that local verification
precedes writes. Keep `check`, `build`, and source `publish` independent of
hosted credential preflight and preserve the existing native Git guards.
4. Use controlled backend tests to demonstrate shared page/asset policy without
Gitea imports. Consolidate obsolete client-policy tests rather than retaining
wrappers. Keep representative real-Gitea HTTP assembly tests for page
creation and binary delivery/retry, exercising conversions and opaque handles.
5. Extend `internal/apicheck` with a focused check that shared delivery, forge
contracts, and artifact mechanics have no production dependency path to
`internal/gitea`. Preserve export ownership/runtime checks, keeping new
delivery-related packages out of the runtime dependency graph through the
existing checking mechanism. Add a small violation fixture following current
apicheck tests.
6. Review stream/error paths for cancellation identity, reader closure,
multipart worker completion, and causes. Reuse cancellation tests; add a
narrow regression for a gap introduced by moved ownership. Keep the default
suite offline and credential-free.
Result: selected requirements have meaningful coverage and clear test owners.
Run relevant package tests once assembled; Stage 10 owns the final full checks.
## Stage 10: Document the cutover and verify the sprint
Status: complete.
1. Add the next sequential ADR (currently `0006`) using the documentation
policy's lightweight Nygard format. Record the shared-policy/backend boundary,
neutral facade-owned destination, explicit selection, credential preflight
ownership and evidence limits, alternatives, and consequences. Keep progress
in these roadmaps.
2. Update GoDoc, `docs/config.md`, `docs/cli.md`, and
`docs/consumers/delivery.md` for the neutral destination, selection, automatic
preflight, callable preflight, typed failures, and CLI exit classifications.
Update current README/runbook/example snippets where affected. Keep Gitea
wire/authentication details in `docs/integrations/gitea.md`; describe shared
coordination in internal documentation linked from `docs/internal/overview.md`.
Align architecture policy wording with the implemented boundary while
preserving its broader constraints.
Document the authenticated repository probe and unverified write scopes in
the Gitea guide. Describe standalone preflight as a caller option; preserve
existing source-publication completion and credential requirements.
3. Write `docs/releases/v0.6.0.md`, the next minor release note, explaining the
features, compatibility impact, and public type/profile migration. Follow
the [release policy](../policy/release.md) and
[runbook](../release.md) for note/footer requirements. Explain the unchanged
Gitea token default and profile updates before tagging: CI reads the tagged profile.
Include fail-fast credential checks, additional read-only requests, failure
classifications, and the limited meaning of successful preflight. Explain
that local artifact verification now follows credential reads and still
precedes hosted writes.
Preserve historical notes. Prepare documentation here; tagging, pushing,
and publication remain separate release work.
4. Check maintained JSON/Go examples, profile-generating fixtures, and Woodpecker
tests for explicit selection. Search active code/docs for obsolete public
type uses and implicit-selection instructions, distinguishing history and
migration examples. Verify documentation links, fragments, and whitespace.
5. Run `go test ./...` and `go run ./internal/apicheck` from the repository root
with prepared local dependencies and offline module/toolchain access. These
include representative CLI/consumer workflows. Reuse successful checks for
unchanged content; fix failures and rerun affected checks. No live forge,
real token, CI run, or consumer pilot is required for sprint completion.
6. Review the final diff and dependency flow against every feature target and
stage. Verify generic policy has no Gitea dependency; only the adapter
interprets its handles/statuses; the assembled workflow retains recovery
outcomes; current delivery profiles explicitly select Gitea; migration
instructions match the API/configuration; credential failures stop before
costly delivery work; source publication remains independent; local content
verification is reused before writes; and comparison/transport remain streamed.
7. Mark stages complete as verified. Commit completed work under the agent
policy, report validation and the final commit, and leave a clean workspace.
Retain accurate completed plans for later release-preparation cleanup.
Result: the initial refactor, migration guidance, and offline verification are
complete. All stages proceed to this end state within the sprint.

View File

@@ -1,465 +0,0 @@
# Remediation: forge boundary and credential preflight review
Status: complete; all eight issues are addressed, with completion recorded below.
## Review baseline and scope
Reviewed `cbdba90`, including the implementation commits after `9d6952a`,
against the [feature roadmap](forge-boundary.md),
[implementation plan](implementation.md), and the user's selected scope.
This document records review findings and their remediation. Findings describe
the reviewed revisions; completion notes record their corrections.
A follow-up integration audit reviewed `72de187`. Its production source is
unchanged from `cbdba90`. That audit traced the new boundary and preflight
through existing CI preparation, module/tag selection, build receipts, token
loading, source publication, and retry/error handling. It added finding 5 and
expanded finding 3 with a later authorization-failure reproduction.
An algorithmic audit reviewed `370fe3a`, whose production source is still
unchanged from `cbdba90`. It added findings 6–8. These opportunities and defects
were retained when policy moved from the former Gitea implementation into
shared delivery.
The intended production boundary is present. The root facade owns neutral
destination selection and public preflight errors; `internal/forge` supplies
the backend contract; `internal/delivery` owns page/asset policy and recovery;
`internal/gitea` owns protocol operations; and `internal/artifact` owns local
declarations and content verification. Gitea remains the implemented backend.
Automatic and standalone preflight use read-only repository requests, and binary
CI checks access before dependency preparation. Source publication retains its
separate Git authentication and completion semantics.
## 1. Complete local destination preflight error handling — P2
Status: complete.
**Completed:** Added the facade-owned `DeliveryPreflightInvalidDestination`
classification, preserved the adapter's validation cause, and mapped it to CLI
status 2 for ordinary and CI delivery. Updated consumer/configuration guidance.
Focused standalone, automatic, and CLI regressions cover invalid URLs, owners,
and repository names, early termination, and absence of hosted/build work.
Focused preflight tests and the compiled API/dependency check passed.
**Evidence:** [Gitea preflight](../../internal/gitea/backend.go),
[public error conversion](../../delivery.go), and
[CLI error handling](../../cmd/releasekit/main.go).
`Backend.Preflight` returns `destinationAPI` validation errors directly. Those
errors are neutral `forge.Failure` values with `FailureInvalid`, rather than
classified preflight failures. `publicPreflightError` consequently passes them
through. Both CLI delivery paths treat the resulting unclassified error as
status 1.
A temporary consumer with an otherwise valid tagged source and a nonblank
synthetic token reproduced this with `base_url: "ssh://forge.example"`:
`PreflightDelivery` returned `*forge.Failure`, `errors.As` could not obtain a
`*DeliveryPreflightError`, and `deliver` exited 1. A malformed owner/repository
segment follows the same local validation path. No server request is needed to
identify these configuration problems.
This leaves the public preflight error description incomplete and conflicts
with the documented status 2 for profile/configuration failures. Operators
cannot reliably distinguish a locally invalid destination from an operational
verification failure through the new boundary.
**Required work:**
- Make deterministic destination-setting failures recognizable through the
facade and classify them as CLI configuration failures with status 2 in both
ordinary and CI delivery. Keep backend-specific validation in its adapter;
share its classification instead of duplicating Gitea rules in the CLI.
- Make GoDoc and the selected public error representation agree about local
configuration failures. Preserve useful causes and identify the invalid
setting; avoid labeling a malformed URL as a rejected token or an unavailable
service. Keep real service/transport failures at status 1.
- Add focused coverage for an invalid URL and invalid repository addressing,
including standalone preflight and the CLI mapping. Verify early failure,
`not_attempted` delivery state, and absence of hosted requests/build work.
This correction concerns existing destination rules and failure classification;
it adds no token-format restriction, remote permission requirement, or probe.
## 2. Protect the runtime boundary from the new forge contract — P2
Status: complete.
**Completed:** Added `forge` to the runtime dependency exclusions and covered
both direct and transitive forbidden dependencies with focused graph fixtures.
The complete checker test package and the actual compiled API/dependency check
passed; the current runtime dependency graph remains accepted.
**Evidence:** [dependency checker](../../internal/apicheck/main.go), especially
`forbiddenRuntimeDependency`, and the runtime boundary in
[architecture policy](../policy/architecture.md#runtime-version-boundary).
Implementation Stage 9 also calls for keeping the new delivery packages out of
the runtime dependency graph through the existing checking mechanism.
The checker excludes named delivery implementations such as `gitea` and
`delivery`, but its runtime exclusion list omits the newly added `forge`
package. A temporary test overlay supplied this package graph:
```text
buildinfo -> internal/forge
```
`checkImportDirections` returned nil. The graph would make the runtime facade
depend on the release backend contract while the advertised boundary check
continued to pass. The actual current `buildinfo` graph remains appropriately
small; the gap is in the planned regression protection.
**Required work:**
- Extend the existing runtime check to exclude the new forge delivery contract.
Retain the existing policy for legitimate runtime dependencies.
- Add a focused fixture showing rejection of a runtime dependency on
`internal/forge`, including traversal through a small intermediary where
needed to protect the existing transitive rule. Retain the passing actual
repository check.
This enforces the already selected separation from orchestration. It does not
require a new public package restriction or a broader dependency policy.
## 3. Qualify phase-specific CLI exit-status documentation — P3
Status: complete.
**Completed:** Qualified the CLI reference and unreleased v0.6.0 note by phase:
local loading/classified preflight errors use status 2; later known failures
use 1; unresolved writes retain 3 even when canceled. Added a narrow CLI
regression for preflight versus later authorization failure and cancellation
after an attempted page write. Focused CLI preflight/recovery tests passed.
**Evidence:** [CLI exit statuses](../cli.md),
[v0.6.0 release note](../releases/v0.6.0.md),
[write evidence](../../internal/gitea/backend.go),
[asset reconciliation](../../internal/delivery/remote_assets.go), and
[CLI delivery mapping](../../cmd/releasekit/main.go).
The new CLI prose and release note broadly state that cancellation returns
status 1. That is correct for preflight and preparation cancellation. After a
possibly successful write, cancellation can prevent reconciliation and leave
delivery unknown; the CLI correctly returns status 3 in that case.
A temporary shared-delivery test supplied an attempted uncertain upload with
`context.Canceled` and an unavailable canceled reconciliation. It returned
`State == Unknown`, retained the cancellation cause, and placed the asset in
`Uncertain`. The CLI's unknown-state branch maps that result to 3.
The broad documentation introduces a stronger cancellation guarantee than the
requested preflight behavior and conflicts with preserved write recovery.
The same integration issue affects the unqualified statement that credential
failures return status 2. With valid tagged source, a synthetic 401 on the first
repository preflight read returned status 2. When both repository preflight
reads succeeded and the hosted-tag read then returned 401, ordinary delivery
returned status 1. Later provider failures retain the existing delivery error
path and are not `DeliveryPreflightError` values. Successful preflight cannot
guarantee that authorization will remain valid during later operations.
**Required work:**
- Scope status 1 wording to preflight/preparation cancellation and cancellation
that leaves no unresolved write. State that an unresolved write retains
status 3, including when cancellation prevented verification.
- Align the CLI reference and unreleased v0.6.0 note. Keep implementation
behavior and unknown-write recovery intact.
- Qualify credential/access status 2 as applying to local credential loading
and classified preflight failures. Describe later authorization failures
through the existing delivery outcomes: known failure can return 1, while an
unresolved write retains 3. Do not reclassify later failures as preflight
failures or erase partial progress to satisfy overbroad prose.
- Reuse existing cancellation/recovery tests. If CLI coverage does not already
distinguish these cases, add a narrow case protecting unknown-write status
after cancellation; avoid an expanded cancellation matrix.
## 4. Align roadmap completion records — P3
Status: complete.
**Completed:** Marked the feature roadmap's implementation complete, labeled
both planning baselines as historical, and linked both plans to this
remediation record. Reviewed the status wording against the completed stages
and retained the plans for release-preparation cleanup.
**Evidence:** [feature roadmap](forge-boundary.md) and
[implementation plan](implementation.md).
The implementation plan marks all ten stages complete, while the feature
roadmap still says implementation has not started. The plan's introductory
baseline text also uses present-tense descriptions of the old implementation
and says the neutral destination and preflight are planned.
**Required work:** Mark the feature roadmap's implementation status accurately
and clearly label the implementation plan's original baseline as historical.
Keep these records consistent with the completed stages and remaining
remediation. Roadmap retirement can follow the existing release-preparation
cleanup; this finding does not require removing plans now.
## 5. Bind CI release identity before online preflight and preparation — P2
Status: complete.
**Completed:** CI now derives the selected module identity from the verified
event commit and checks the complete tag before credential loading, forge
requests, or dependency preparation. The optional launcher-module guard and
delivery's later source/preflight verification retain their responsibilities.
A focused page-only/binary regression covers a matching launcher selection
with a mismatched derived tag and verifies no forge or Go-tool work. That test
and existing valid CI/preflight/managed-delivery tests passed.
**Evidence:** [CI adapter](../../cmd/releasekit/main.go), especially
`runCIDelivery` and `checkExpectedModule`, and the existing identity/tag checks
in [shared delivery](../../internal/delivery/delivery.go).
Binary `ci-deliver` invokes standalone credential preflight and application
dependency preparation before `Deliver` derives the selected module's tag and
checks it against the CI event tag. `PrepareTaggedCheckout` proves that the
supplied Git tag and commit agree; it does not prove that this is the release
tag derived from the profile's module. The optional expected-module guard
compares selected directories and also does not establish that relationship.
A temporary CLI fixture had a clean tagged checkout at `v1.2.3`, a profile
selecting committed module `child`, and valid repository credentials. The
selected module's derived tag was `child/v1.2.3`. With `--prepared` and
`--expected-module child`, both profile variants eventually returned the same
tag-mismatch error, but their earlier effects differed:
| Profile | Forge requests | Dependency preparation |
| --- | --- | --- |
| Page-only | None | Skipped |
| Binary | Two credential-preflight reads | Invoked before the mismatch was reported |
Neither variant made a hosted release write; the existing late guard still
prevents delivery under the wrong module tag.
The dependency command was intercepted by a temporary local Go wrapper;
no application dependencies were fetched for the reproduction. A real binary
job can perform avoidable downloads or let a preparation failure obscure the
already determinable identity mismatch. A preflight credential failure can
likewise hide the source-selection error.
Dependency preparation already preceded this guard before the feature work.
The new standalone preflight adds hosted credential requests to that existing
ordering gap. Page-only delivery continues to reach the shared identity check
before any hosted requests, so the integration now has inconsistent early
validation behavior across the two supported CI paths.
**Required work:**
- After verifying the event checkout and loading its profile, derive the
committed selected-module release identity and apply the existing full-tag
comparison before the first forge probe or application dependency download.
Reuse the existing identity derivation and keep the optional launcher-module
comparison's responsibility separate.
- Preserve credential preflight before expensive dependency/build work, with
normal delivery verification after preparation. This moves an existing
acceptance check earlier; it adds no module map, source-publication gate,
candidate test suite, or extra requirement to standalone `PreflightDelivery`.
- Add a focused CI regression for a valid Git tag/commit and matching
`--expected-module` whose derived module tag differs. Cover page-only and
binary profiles, asserting the identity diagnostic and absence of forge
requests or dependency preparation. Retain the valid binary path's early
credential check and subsequent delivery behavior.
## 6. Make pending-asset bookkeeping linear — P3
Status: complete.
**Completed:** Replaced repeated slice removals with pending-map updates and a
single deferred compaction pass on every return path. Category bookkeeping is
now expected O(`n`) with O(`n`) metadata, preserving pending observation order.
Added an early-return regression with out-of-order observations for definitive
and uncertain failures. Delivery/Gitea tests and focused facade/CLI recovery
tests passed.
**Evidence:** `uploadMissingAssets`, `reconcileAssetUpload`, and
`removeIdentity` in [shared asset delivery](../../internal/delivery/remote_assets.go).
Every acknowledged upload removes its filename from `result.Pending` through
`removeIdentity`. That helper searches the slice and shifts its remaining tail.
For `k` initially missing assets uploaded successfully in their pending order,
the searches find the first element immediately, but the deletions still move
`(k-1) + (k-2) + ... + 1 = k(k-1)/2` identities. Bookkeeping alone is therefore
Theta(`k²`), independently of network and content processing. A different
pending order can also incur repeated linear searches. Reconciled uploads use
the same removal helper.
**Refactoring opportunity:** Track pending membership or per-filename state in
a map, update it in expected constant time per completed/reconciled upload,
and materialize `Pending` with one pass through the initial pending order when
returning. For `n` declarations, this permits O(`n`) total category bookkeeping
and O(`n`) metadata space. Keep the current category ordering during this
refactor; a final pass avoids repeated shifts without requiring swap deletion.
Apply finalization to every return path so partial, conflicting, and unknown
outcomes retain later missing assets and disjoint categories.
Existing progress/reconciliation tests should remain useful. Add a focused
case only if needed to protect pending results after early termination. The
usual small target sets limit practical impact; this finding establishes an
avoidable asymptotic cost without claiming measured release-time savings.
## 7. Order the missing upload set once — P3
Status: complete.
**Completed:** Declaration validation now receives an unsorted collection.
Classification collects only missing declarations, and that owned worklist is
sorted in place once after the no-upload exits. Scheduling now takes
O(`n + k log k`) metadata work while retaining filename order and the manifest
last. Existing delivery/artifact tests passed, including manifest matching,
upload ordering, conflict/unverifiable stops, and partial recovery.
**Evidence:** `orderedExpected`, `orderMissing`, and `uploadMissingAssets` in
[shared asset delivery](../../internal/delivery/remote_assets.go).
`orderedExpected` copies and sorts all `n` declarations before validation and
classification. This happens even when every asset matches, a conflict stops
delivery, or an unverifiable asset prevents uploads. If uploads proceed, the
coordinator copies and sorts the full list again, then skips entries outside
the missing set. The first full sort requires O(`n log n`) comparisons in the
worst case. The second sort receives already ordered input, so its actual cost
can be lower, but its full-list copy and ordering pass are still redundant.
**Refactoring opportunity:** Collect declarations without ordering, validate
and classify them in a linear pass, and return immediately when classification
prevents or eliminates uploads. Collect only the `k` missing declarations and
sort that worklist once, retaining filename order and `SHA256SUMS` last.
Declaration/classification/ordering work becomes O(`n + k log k`), with O(`n`)
metadata space, and O(`n`) work when no upload is needed. This also removes
redundant full-list copies. These bounds treat filename operations as unit
cost and exclude inline-content hashing, which remains linear in its bytes.
Keep complete sorted manifest generation and all validation. The plan selected
the upload ordering and manifest format; this opportunity concerns the size
and duplication of the scheduling work, not either selected behavior. The
existing ordering, matching-manifest, and no-write-on-conflict tests cover the
main behavior that the refactor should preserve. Combined with finding 6,
successful upload coordination can avoid its quadratic bookkeeping term.
## 8. Preserve asset-inspection failure causes during aggregation — P2
Status: complete.
**Completed:** Joined per-asset inspection causes with filename context in
unverifiable and conflict summaries, retaining conflict precedence and existing
states/categories. Added focused multiple-cause coverage and public API
regressions for cancellation and truncated downloads. Both regressions detect
the original defect and pass with the fix. Full offline tests, focused race
tests, and the API/dependency check passed; final verification is recorded below.
**Evidence:** `inspectRemoteAssets` and `uploadMissingAssets` in
[shared asset delivery](../../internal/delivery/remote_assets.go), and the
cause-preservation requirement in the
[implementation plan](implementation.md#internal-ownership-and-dependencies).
An asset-open or content-read failure is retained in `AssetInspection.Err` and
classified as `AssetUnverifiable`. `uploadMissingAssets` then discards those
errors and returns a fresh generic `FailureUnverifiable` message. `Deliver`
returns that aggregate error, so callers lose the underlying diagnostic and
cannot use `errors.Is` or `errors.As` to recognize its cause.
A temporary test overlay supplied one existing asset whose `OpenAsset` returned
`context.Canceled`, then repeated the case with a distinct read-failure
sentinel. Inspection retained each cause. Aggregation correctly reported
`NotAttempted`, one uncertain asset, and no uploads, but `errors.Is` could find
neither cause in its returned error. Whole-list failures already return their
original cause directly; this finding concerns per-asset failures folded into
an otherwise successful inspection.
**Required work:** Collect relevant non-nil observation errors during the
existing classification pass and wrap or join them with the aggregate summary.
Preserve actionable asset context, cancellation identity, and the existing
conflict precedence, asset categories, and delivery states. Read-only failures
remain distinct from unresolved writes. This takes O(`n`) traversal and O(`u`)
error references for `u` unverifiable observations, without a new public error
type or protocol operation. Retain a focused regression asserting cause
identity through the returned delivery error, including cancellation.
## Verification performed
Using Go 1.26.10 on linux/amd64 with module/toolchain downloads disabled:
- `go test ./...` passed.
- `go run ./internal/apicheck` passed for the current repository.
- `go test -race ./internal/delivery ./internal/gitea` passed.
- Temporary review fixtures reproduced findings 1 and 2, and confirmed the
cancellation outcome underlying finding 3. The runtime-boundary reproduction
intentionally failed its assertion because the prohibited graph was accepted.
- An additional temporary HTTP/1.1 and HTTP/2 check confirmed that a large upload
rejected before its body was consumed returns promptly as a known rejection;
that investigated path produced no finding.
Temporary fixtures and test overlays were kept outside the checkout. No
implementation files or permanent tests were changed during this review.
### Follow-up integration audit
The same Go 1.26.10 toolchain and offline environment were used. Existing full
suite, API-boundary, and focused race results above remain applicable because
the production source was unchanged. Additional checks comprised:
- A fresh CLI integration run covering tagged CI preparation, managed/prebuilt
delivery and stale receipts, module-relative output, multi-module dispatch,
Woodpecker workflows, and source-tool bootstrap; all selected tests passed.
- Focused token-file precedence, repository-root token-file loading,
forbidden-file validation, and check/publication integration tests passed.
- Temporary CLI fixtures demonstrating finding 5 and the initial-versus-later
401 exit-status difference in finding 3, using a local HTTP server and an
intercepted dependency command.
The build, receipt, publication, candidate-validation, dependency-preparation,
and Go-tool implementation owners were also compared with the pre-feature
baseline. Their production code is unchanged; the confirmed new interaction
occurs in the CI adapter's composition and the phase-specific error contract.
### Algorithmic audit
The audit covered the implemented plan's facade/CLI adaptation, backend
contract and preflight, page coordination/recovery, local artifact mechanics,
remote inspection/upload bookkeeping, Gitea read/write transport, and dependency
checks. It compared the moved code with the pre-feature baseline. Bounds below
describe collection/content work with ordinary expected constant-time map
operations and unit-cost metadata comparisons; variable-length names and URLs
add their corresponding byte-processing costs.
| Reviewed operation | Assessment |
| --- | --- |
| Remote asset inspection | Expected O(`n + r + B`) time for `n` declarations, `r` listed assets, and `B` downloaded bytes; filename maps avoid nested list searches, and content hashing is streamed. |
| Artifact declaration and local verification | Map-based identity/duplicate checks are linear; hashing is linear in content bytes. Complete manifest sorting remains the selected O(`n log n`) algorithm. |
| Page matching and recovery | Note comparisons are linear in note bytes; shared coordination uses the selected bounded follow-up lookup. |
| Gitea adaptation and transport | Metadata conversion is linear in listed entries; uploads stream bytes. Redirect tracking uses a visited map rather than repeated scans. |
| Facade result conversion and dependency checks | Result conversion is linear in category entries. Dependency traversal uses visited sets from a fixed number of roots, O(`V + E`) in the reachable graph. |
| Upload scheduling and pending state | Findings 6 and 7 identify avoidable work beyond the selected ordering and recovery behavior. |
The audit preserved explicitly selected algorithms: complete sorted manifests,
filename upload ordering with the manifest last, full local verification before
writes, streamed remote comparisons, one follow-up reconciliation per ambiguous
write, current asset listing behavior, and fresh credential checks around CI
preparation. It proposes no batching, pagination, concurrency, validation cache,
or altered recovery policy.
Fresh offline tests for `internal/delivery`, `internal/artifact`, and
`internal/gitea` and the API/dependency-boundary check passed. A temporary
overlay intentionally failed two cause-preservation assertions to reproduce
finding 8; it was removed after the check. Findings 6 and 7 follow from
source-level operation counts. No benchmarks
or real-world performance measurements were run, and no production code or
permanent tests changed.
### Remediation verification
All eight issues were addressed sequentially and marked complete with their
implementation or documentation changes. Final checks used Go 1.26.10 on
linux/amd64 with `GOTOOLCHAIN=local`, `GOPROXY=off`, and `GOSUMDB=off`:
- `go test ./...` passed.
- `go run ./internal/apicheck` passed.
- `go test -race ./internal/delivery ./internal/gitea` passed.
- Focused public API and CLI regressions also passed with the race detector:
invalid destinations, CI module/tag mismatch, phase-specific statuses, and
asset-inspection cause preservation.
- A temporary source overlay verified that the new aggregation/public API
cause-preservation regressions fail against the previous implementation.
- Local links and heading fragments across changed Markdown files resolved;
whitespace checks passed. No benchmark measurements were needed.