Document neutral forge delivery and preflight

This commit is contained in:
2026-11-01 18:38:03 +00:00
parent c3f6eeb6df
commit cbdba90014
12 changed files with 370 additions and 82 deletions

View File

@@ -11,6 +11,8 @@ delivery are documented. ReleaseKit does not declare binary deliverables.
The Go module is `go.maximumdirect.net/releasekit`. The
[adoption guide](docs/consumers/adoption.md#migrating-from-the-original-module-path)
explains the v0.5.0 migration from the original Gitea module path.
The [v0.6.0 release note](docs/releases/v0.6.0.md) describes the delivery API
and profile migration.
Release operations can select one Go module within a repository. The repository
root remains the default; a selected module's declared import path and physical
@@ -79,6 +81,8 @@ failure recovery.
components and their responsibilities.
- [Architecture policy](docs/policy/architecture.md): responsibility and dependency
boundaries for development.
- [ADR 0006](docs/adr/0006-neutral-forge-delivery-and-preflight.md): neutral
delivery policy and credential preflight.
- [ADR 0001](docs/adr/0001-public-facade.md): the accepted public facade decision.
- [Policy provenance](docs/policy/meta.md): adopted policy sources and update
guidance.

View File

@@ -29,7 +29,8 @@ const (
DeliveryUnknown DeliveryState = "unknown"
)
// Forge selects the hosted delivery backend.
// Forge selects the hosted delivery backend. A delivery request must select a
// supported value explicitly; the destination URL does not select a backend.
type Forge string
const (
@@ -39,6 +40,7 @@ const (
// DeliveryDestination supplies an explicitly selected hosted destination.
type DeliveryDestination struct {
// Forge selects the adapter that interprets the destination fields.
Forge Forge
BaseURL string
Owner string
@@ -51,11 +53,16 @@ type DeliveryDestination struct {
type DeliveryPreflightFailureKind string
const (
DeliveryPreflightMissingCredential DeliveryPreflightFailureKind = "missing_credential"
DeliveryPreflightInvalidCredential DeliveryPreflightFailureKind = "invalid_credential"
// DeliveryPreflightMissingCredential means no nonblank credential was supplied.
DeliveryPreflightMissingCredential DeliveryPreflightFailureKind = "missing_credential"
// DeliveryPreflightInvalidCredential means the selected service rejected the credential.
DeliveryPreflightInvalidCredential DeliveryPreflightFailureKind = "invalid_credential"
// DeliveryPreflightInsufficientPermissions means a required delivery permission is known to be absent.
DeliveryPreflightInsufficientPermissions DeliveryPreflightFailureKind = "insufficient_permissions"
// DeliveryPreflightInaccessibleDestination means the credential cannot access the selected repository.
DeliveryPreflightInaccessibleDestination DeliveryPreflightFailureKind = "inaccessible_destination"
DeliveryPreflightUnavailable DeliveryPreflightFailureKind = "unavailable"
// DeliveryPreflightUnavailable means the service response could not verify access.
DeliveryPreflightUnavailable DeliveryPreflightFailureKind = "unavailable"
)
// DeliveryPreflightError reports a classified failure to verify a destination.
@@ -128,14 +135,17 @@ type DeliveryResult struct {
Uncertain []DeliveryAsset
}
// Deliver verifies the selected local tag and hosted source, then creates or
// finds its release page and adds any missing declared build assets. Build
// requests are compiled into temporary storage that Deliver removes on return.
// Prebuilt artifacts remain caller-owned and are not removed. Both paths
// validate source identity and artifact contents before hosted writes.
// Deliver requires an explicit supported destination forge. It checks the local
// request and source, then performs read-only credential preflight before a
// managed build or full artifact verification. It verifies the selected local
// tag and hosted source, then creates or finds its release page and adds any
// missing declared build assets. Prebuilt artifacts remain caller-owned. Local
// artifact contents are verified after preflight and before hosted writes.
// Module selection and declaration links resolve from the captured commit.
// Page-only delivery can use a sparse checkout with available committed objects;
// managed builds require their selected source files in the working tree.
// Preflight failures return a [DeliveryPreflightError] and leave State at
// [DeliveryNotAttempted].
func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, error) {
result := DeliveryResult{Version: request.Version, State: DeliveryNotAttempted}
backend, err := deliveryBackend(request.Destination)
@@ -174,6 +184,10 @@ func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, erro
// PreflightDelivery checks destination credentials and repository access using
// read-only backend operations. It does not require a checkout or published tag.
// The result verifies only the access and permission evidence exposed by the
// selected backend; it does not guarantee that a later write will succeed.
// Failures are returned as [DeliveryPreflightError] values when classified by
// the backend, and retain their underlying cause for errors.Is and errors.As.
func PreflightDelivery(ctx context.Context, destination DeliveryDestination) error {
backend, err := deliveryBackend(destination)
if err != nil {

View File

@@ -0,0 +1,92 @@
# ADR 0006: Separate delivery policy from forge protocols
## Status
Accepted.
## Date
2026-11-01
## Context
ReleaseKit's delivery workflow handles release-page matching and creation
recovery, remote asset comparison, upload ordering, retries, and partial or
uncertain outcomes. Those policies are useful independently of the Gitea HTTP
protocol, but the earlier client-owned flow coupled them to Gitea types and
operations.
Delivery also benefits from checking credentials and repository access before
managed builds, dependency preparation, or complete artifact hashing. Gitea
exposes authenticated repository reads, but its `permissions.push` metadata and
basic token evidence do not establish permission to write release pages or
assets. A read-only check must represent that evidence limit without claiming
that later writes are guaranteed.
The root `releasekit` facade owns public types, while implementation packages
remain internal. The CLI owns credential loading and process exit status.
Source publication has separate Git authentication and completion semantics.
## Decision
The root facade owns `Forge`, `DeliveryDestination`, explicit backend selection,
and public preflight errors. `internal/forge` defines the internal backend
contract and neutral release, asset, failure, and write-evidence values.
`internal/delivery` owns shared delivery coordination and policy.
`internal/gitea` owns Gitea destination rules, authenticated preflight requests,
protocol operations, codecs, handles, streaming, and conversion to neutral
failures. `internal/artifact` owns local asset declarations and verification.
Shared delivery, forge, and artifact packages have no production dependency
path to the Gitea adapter.
Delivery selection is explicit. The supported value is `gitea`; neither a URL
nor an omitted selection chooses a backend. There is no compatibility alias for
the removed public Gitea destination type.
The facade exposes `PreflightDelivery` and runs the same read-only backend
preflight automatically during `Deliver`. Gitea preflight authenticates through
repository-scoped reads and classifies missing or rejected credentials,
inaccessible destinations, and unavailable verification. It does not issue a
test release or upload, require extra user/admin token scopes, or treat
unexposed write permission evidence as a failure. Success confirms only the
authentication, repository access, and permission evidence available through
that check; ordinary writes remain authoritative. Preflight failures preserve
their causes and leave delivery `not_attempted`.
The CLI requires `delivery.forge` only for `deliver` and `ci-deliver`. Binary
CI delivery performs a standalone preflight after checkout/profile/token
preparation and before application dependency downloads, then `Deliver`
rechecks. Page-only CI relies on automatic delivery preflight. `check`,
`build`, and source `publish` do not require delivery configuration or hosted
credentials. Local artifact verification follows preflight but completes before
hosted writes.
## Alternatives considered
- Keep delivery policy in the Gitea client. This retains protocol coupling and
makes shared recovery behavior harder to exercise without the adapter.
- Select Gitea from the URL or preserve the old Gitea-specific facade type.
This hides backend selection and keeps the public contract tied to one
adapter.
- Probe permissions with a test release or asset, or require additional user
scopes. Those operations can create remote state or demand evidence unrelated
to release delivery; the repository read does not expose the required
release-write proof.
- Skip preflight and let the first write discover credential problems. That
delays a known failure until after costly local work and provides no
standalone check for callers preparing their own delivery.
## Consequences
- Shared policy can be tested with controlled backends, while local HTTP tests
focus on Gitea protocol behavior and assembled integration flows.
- Consumers must replace the removed Gitea-specific destination type, set
`ForgeGitea` in API calls, and add `"forge": "gitea"` to delivery profiles.
- Delivery may make additional authenticated read requests before local
artifact hashing. It still verifies full local content before any hosted
write.
- Credential, access, and availability failures have distinct public error
kinds and CLI classifications. Successful preflight does not certify future
release or asset writes.
- Source publication retains native Git authentication and is not gated by
hosted delivery preflight.

View File

@@ -168,15 +168,17 @@ tree and does not perform candidate validation or remote publication. `check`
and `publish` run the same configured build after their one candidate
validation, using temporary output outside the worktree.
Deliver a tagged source release to Gitea:
Deliver a tagged source release:
```sh
go run ./cmd/releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION
```
`deliver` requires a `delivery` profile object and a token loaded from its
configured environment variable or token file. Credential selection and errors
are defined in the [configuration reference](config.md). It verifies
`deliver` requires a `delivery` profile object with an explicit
`"forge": "gitea"` selection and a token loaded from its configured environment
variable or token file. The destination URL does not select a forge. Selection
errors are reported before token access or delivery work. Credential selection
and errors are defined in the [configuration reference](config.md). It verifies
the local tag, clean tagged checkout, hosted tag, and committed release note,
then creates or finds the release page and reports delivery state separately
from source publication. Historical lightweight tags and detached checkouts
@@ -195,16 +197,23 @@ Profiles without targets use page-only delivery
and need no receipt, binary build, or manifest. `deliver` never runs candidate
checks or publishes a tag.
After cheap local checks, delivery performs authenticated, read-only repository
preflight before a managed build or full artifact size and digest verification.
An unavailable preflight leaves delivery `not_attempted`; after preflight
succeeds, local artifact verification still finishes before any hosted write.
Use one `deliver` invocation for the normal workflow. To build without delivery
credentials, or to retain build output across retries, run `build --output DIR`
first and then run `deliver --assets DIR` with the token available. The ordinary
`deliver` command does not download application dependencies or change the
checkout.
The command returns 0 for complete delivery, 1 for a known failure, conflict,
or partial delivery, 2 for usage, profile, or missing-credential errors, and 3
when a remote write remains uncertain. The result lists matching, uploaded,
pending, conflicting, and uncertain assets as disjoint current observations;
The command returns 0 for complete delivery, 1 for an unavailable preflight,
cancellation, known delivery failure, conflict, or partial delivery, 2 for
usage, profile, forge-selection, credential, known permission deficiency, or
inaccessible-destination errors, and 3 when a remote write remains uncertain.
The result lists matching, uploaded, pending, conflicting, and uncertain assets
as disjoint current observations;
early failures may leave some declared assets unreported. On failure, the CLI
prints state-based recovery guidance. Fix local or preflight failures before
retrying; fix the reported cause before retrying partial delivery; resolve an
@@ -231,10 +240,11 @@ verifies the tag, HEAD, and clean worktree. With `--prepared`, it performs those
checks read-only against the existing checkout and does not fetch or check out.
In either mode, it loads the profile from the verified source. `--expected-module`
optionally checks that the profile selects the module chosen by a launcher;
module selection itself remains profile-owned. The command validates delivery
settings and the configured token, then downloads application dependencies only
when the profile selects build targets. Binary profiles are built and delivered
through the same facade path as `deliver`; source-only profiles skip dependency
module selection itself remains profile-owned. The command validates explicit
forge selection and resolves the configured token. For binary profiles, it
performs a read-only repository preflight before downloading application
dependencies; delivery performs a fresh check after dependency preparation.
Source-only profiles rely on delivery's automatic preflight and skip dependency
downloads and binary builds.
`--config` follows the existing profile path rules: relative paths are resolved
from `--repo`, and absolute paths are used as-is. The default mode prepares and
@@ -248,10 +258,11 @@ tag arguments. The event ID may identify an annotated tag object; see the
[delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for resolution
and workflow preparation. The CLI does not read
provider-specific environment variable names. Checkout and dependency
preparation failures return status 1; usage, profile, credential, and invalid
version errors return status 2. Final delivery uses the same statuses as
`deliver`: 0 for complete, 1 for known failure, conflict, or partial delivery,
and 3 for uncertain remote state.
preparation failures, unavailable preflight, and cancellation return status 1;
usage, profile, forge-selection, credential, known permission deficiency,
inaccessible-destination, and invalid-version errors return status 2. Final
delivery uses the same statuses as `deliver`: 0 for complete, 1 for known
failure, conflict, or partial delivery, and 3 for uncertain remote state.
The maintained module-based tag workflow verifies and checks out the event tag
before resolving the consumer module. It builds the ReleaseKit version already

View File

@@ -16,7 +16,7 @@ The current profile fields are:
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
| `validation` | No | Optional shared checks selected by ReleaseKit. |
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
| `delivery` | No | Optional Gitea `base_url`, `owner`, `repository`, `token_env`, and `token_file` settings for tagged-source delivery. |
| `delivery` | No | Optional selected forge and destination fields for tagged-source delivery. |
`module_dir` names the selected module's physical location in the repository;
ReleaseKit reads its declared import path from that module's own `go.mod`. The
@@ -153,20 +153,39 @@ local files to exist, or inspect Git history. Existing clean-index/worktree
requirements still apply to candidate operations. An ignore rule alone does
not remove a previously committed file from the candidate.
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
`token_env` and `token_file`. `token_env` names the environment variable
containing the Gitea token and defaults to `RELEASEKIT_GITEA_TOKEN`. A nonblank
environment value takes precedence. When it is unset or blank, `token_file`
selects a plain token-containing file; relative paths resolve from the Git
repository root, and absolute paths are used as supplied. The CLI trims leading
and trailing whitespace from file contents. An unreadable or blank file, or
the absence of both credential sources, returns status 2. Omission of
`token_file` retains environment-only loading.
The `delivery` object contains `forge`, `base_url`, `owner`, `repository`, and
optional `token_env` and `token_file`. A delivery request must set `forge` to
`"gitea"`; omission, an empty or whitespace-only value, and unsupported values
are errors when `deliver` or `ci-deliver` is requested. The URL does not select
the forge. These semantic checks run only for delivery, so `check`, `publish`,
and `build` do not require a delivery object, selection, or credentials.
`token_env` names the environment variable containing the Gitea token and
defaults to `RELEASEKIT_GITEA_TOKEN`. A nonblank environment value takes
precedence. When it is unset or blank, `token_file` selects a plain
token-containing file; relative paths resolve from the Git repository root,
and absolute paths are used as supplied. The CLI trims leading and trailing
whitespace from file contents. An unreadable or blank file, or the absence of
both credential sources, returns status 2. Omission of `token_file` retains
environment-only loading.
After resolving the token, delivery performs an authenticated, read-only
repository access check. Missing or rejected credentials, known insufficient
permissions, and inaccessible destinations are configuration/credential
failures. An unavailable service or canceled check is an operational failure.
See the [CLI contract](cli.md) for exact exit statuses. Successful preflight
confirms authentication and repository access and accounts for any exposed
delivery-permission deficiency. It does not guarantee later release or asset
writes. Local artifact size and digest checks follow preflight and still finish
before hosted writes.
Credential loading runs only for `deliver` and `ci-deliver`. `check`, `publish`,
and `build` do not read the token file or require a token. The CLI passes the
resolved value explicitly to the library operation. Selecting a token file and
selecting the optional forbidden-file validator are independent settings.
resolved value explicitly to the library operation. `ci-deliver` checks the
destination before downloading application dependencies for a binary build;
the later delivery performs a fresh check. Page-only CI relies on delivery's
automatic check. Selecting a token file and selecting the optional
forbidden-file validator are independent settings.
`expected_push_url`, when nonempty, is compared exactly with Git's resolved
effective push URL during `publish`, before candidate validation. It applies

View File

@@ -1,6 +1,8 @@
# Deliver a tagged source release
Use `releasekit.Deliver` or the `deliver` command after a source tag exists.
Select the delivery backend explicitly in the profile or
`DeliveryDestination`; the destination URL does not infer a backend.
Delivery status is separate from source publication: a delivery problem does
not move or remove the tag, and a retry can use the same immutable version.
The operation accepts historical lightweight and annotated tags, including a
@@ -62,6 +64,7 @@ Add a Gitea destination to the profile:
{
"product": "Example",
"delivery": {
"forge": "gitea",
"base_url": "https://gitea.example",
"owner": "example",
"repository": "example",
@@ -93,6 +96,7 @@ as `.releasekit/gitea-token` and configure:
"forbidden_files": [".releasekit/gitea-token"]
},
"delivery": {
"forge": "gitea",
"base_url": "https://gitea.example",
"owner": "example",
"repository": "example",
@@ -129,8 +133,9 @@ directory must contain the private `.releasekit-build.json` receipt created by
`build`. ReleaseKit checks its schema, repository, commit, version, resolved
module directory and path, full tag, normalized build settings, and artifact
declarations. Old receipts must be regenerated with the updated `build`
command. Delivery verifies the artifact bytes and sizes before making a hosted
request; caller-owned files remain available for retries.
command. Delivery verifies the artifact bytes and sizes after read-only
preflight and before any hosted write; caller-owned files remain available for
retries.
Ordinary `deliver` does not download application dependencies or change the
checkout.
@@ -150,13 +155,57 @@ published content needs correction. If rebuilding could change bytes already
on the server, retain the original build output and retry with `--assets DIR`.
The command does not run candidate checks or publish tags.
## Credential preflight
Both `Deliver` and the CLI perform a read-only destination preflight after
cheap local request and source checks. It happens before managed builds and
full artifact size and digest verification. If it succeeds, delivery verifies
the local artifact contents before making any hosted write. A failed preflight
leaves delivery `not_attempted`, including when the service is unavailable.
Call `PreflightDelivery` directly when another caller-controlled operation
should follow credential verification. The check needs no checkout or published
tag and performs no test release or upload. For Gitea it verifies authenticated
repository access; exposed metadata can identify known deficiencies, but
successful preflight does not guarantee later release or asset writes. A
`DeliveryPreflightError` identifies missing credentials, rejected credentials,
known insufficient permissions, an inaccessible destination, or unavailable
verification. Its `Unwrap` method preserves the cause for `errors.Is` and
`errors.As`. A 403 means access was denied, not necessarily that a token
expired; a 404 can mean a missing repository or hidden access.
```go
err := releasekit.PreflightDelivery(ctx, releasekit.DeliveryDestination{
Forge: releasekit.ForgeGitea,
BaseURL: "https://gitea.example",
Owner: "example",
Repository: "example",
Token: token,
})
if err != nil {
var preflight *releasekit.DeliveryPreflightError
if errors.As(err, &preflight) {
log.Printf("delivery preflight %s: %v", preflight.Kind, preflight.Err)
}
return err
}
```
The CLI performs the same check automatically. For binary `ci-deliver`, it
checks access before application dependency downloads, then `Deliver` checks
again after preparation. Page-only CI needs no separate preflight call.
`check`, `build`, and source `publish` remain independent of hosted delivery
credentials. Calling preflight before source publication is optional and does
not add a publication gate.
## Go API
Pass the Gitea credential explicitly. Set `Build` to a `BuildSpec` to compile
selected targets as part of delivery. The facade verifies the local tag, clean
checkout, and committed release note before compiling. Managed output is stored
outside the checkout and removed when delivery returns. `Output` receives build
progress. With no build, or with an empty target set, delivery is page-only.
Pass the Gitea credential explicitly and select `ForgeGitea`. Set `Build` to a
`BuildSpec` to compile selected targets as part of delivery. The facade verifies
the local tag, clean checkout, and committed release note before compiling.
Managed output is stored outside the checkout and removed when delivery
returns. `Output` receives build progress. With no build, or with an empty
target set, delivery is page-only.
To deliver binaries built separately, set `Prebuilt` to the `BuildResult`.
ReleaseKit checks its repository, module directory, declared module path, full
@@ -176,7 +225,8 @@ result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
Product: "Example",
Version: "v1.2.3",
ExpectedTag: "integrations/example/v1.2.3", // optional CI event check
Destination: releasekit.GiteaDestination{
Destination: releasekit.DeliveryDestination{
Forge: releasekit.ForgeGitea,
BaseURL: "https://gitea.example",
Owner: "example",
Repository: "example",

View File

@@ -1,9 +1,10 @@
# Gitea release pages
ReleaseKit's public `Deliver` operation and `deliver` command verify tagged
source, find or create the selected module's release page, inspect existing assets, and
upload missing assets through the internal Gitea client. Page-only and
binary-producing profiles use the same delivery path. The
ReleaseKit's public `Deliver` operation and `deliver` command select Gitea
explicitly, verify tagged source, find or create the selected module's release
page, inspect existing assets, and upload missing assets. Shared delivery code
owns content comparison and recovery; the Gitea adapter owns the HTTP protocol.
Page-only and binary-producing profiles use the same delivery path. The
[consumer delivery guide](../consumers/delivery.md) covers caller workflows;
this guide describes the Gitea protocol and its failure behavior.
@@ -13,6 +14,23 @@ the `Authorization: token …` request header and is not included in errors or
results. The base URL must use HTTP or HTTPS and cannot contain URL credentials,
a query, or a fragment.
Before a build or full local artifact verification, Gitea preflight makes two
authenticated, read-only repository requests: `GET /api/v1/repos/{owner}/{repo}`
resolves the repository, then `GET /api/v1/repositories/{id}` confirms access
through the authenticated repository route. The response identity must match
the configured owner and repository. This check also works without a checkout
or a published tag and makes no release or asset writes.
A 401 response identifies a rejected credential. A 403 identifies denied
access but does not establish that the credential is invalid. A 404 can mean a
missing repository or access hidden by the service. Network failures, service
errors, and malformed responses that prevent verification are reported as
unavailable. Repository `permissions.push` describes Git code-push access; it
does not establish permission to create release pages or upload assets. Gitea
does not expose enough evidence through this probe to verify those write scopes,
so they remain unverified and later writes remain authoritative. Preflight does
not request unrelated user scopes or create a test release or asset.
The selected module derives a complete tag from its semantic version, declared
module path, and physical repository-relative directory. For example, version
`v1.2.3` in `integrations/ckconfig` uses
@@ -27,34 +45,37 @@ The default shared validation convention uses repository-root `COPYRIGHT`,
layout and release-note links. Use the complete derived tag as the immutable
source ref and the actual repository-relative file paths in those links.
Shared-file selection controls validation inputs; delivery preserves the
committed note and its footer links. The client first reads
committed note and its footer links. After successful preflight, the Gitea
adapter reads
`/api/v1/repos/{owner}/{repo}/tags/{tag}` and requires the hosted tag's commit
SHA to equal the captured source commit. It then reads
`/api/v1/repos/{owner}/{repo}/releases/tags/{tag}`. A matching existing page
must have that tag, the complete committed release note as its body, and stable
is accepted by shared delivery policy only when it has that tag, the complete
committed release note as its body, and stable
non-draft state. Its existing title is retained. ReleaseKit does not edit a
conflicting page. Slash-containing tags are escaped as one URL path segment.
If the page is absent, the client posts to `/api/v1/repos/{owner}/{repo}/releases`
with the selected full tag and commit, the release note's H1 as title, the complete
note as body, and draft and prerelease disabled. A create conflict, interrupted
response, or malformed create response (including inconsistent decoded fields)
triggers one lookup. An exact page found
there confirms the existing content; a differing page is a conflict. If that
lookup cannot determine the result after a possibly successful write, the
client returns an unknown outcome. Other HTTP failures retain their structured
failure kind and status code. A recovered create retains its write history;
a later asset-inspection failure reports partial page progress.
If the page is absent, the adapter posts to
`/api/v1/repos/{owner}/{repo}/releases` with the selected full tag and commit,
the release note's H1 as title, the complete note as body, and draft and
prerelease disabled. A create conflict, interrupted response, or malformed
create response (including inconsistent decoded fields) triggers one lookup by
the shared workflow. An exact page found there confirms the existing content;
a differing page is a conflict. If that lookup cannot determine the result
after a possibly successful write, delivery returns an unknown outcome. Other
HTTP failures retain their structured failure kind and status code. A recovered
create retains its write history; a later asset-inspection failure reports
partial page progress.
For a binary-producing release, the client can build a deterministic
For a binary-producing release, shared delivery builds a deterministic
`SHA256SUMS` file from the declared artifact names and SHA-256 digests. Entries
are sorted by filename and use the standard two-space separator. A source-only
release produces no manifest.
Asset inspection lists the release attachments, streams each declared existing
file, and compares its SHA-256 digest. It reports each asset as matching,
missing, conflicting, or unverifiable. Same-name attachments with different
bytes are conflicts even when their reported size matches. An existing
The adapter lists release attachments and opens their download streams. Shared
delivery compares each declared existing file by SHA-256 digest and reports it
as matching, missing, conflicting, or unverifiable. Same-name attachments with
different bytes are conflicts even when their reported size matches. An existing
`SHA256SUMS` must exactly match the complete expected manifest; a subset or
different manifest is a conflict. An unreadable attachment is unverifiable,
not missing. A failed read-only inspection reports `not_attempted`; if this
@@ -65,10 +86,10 @@ Attachment downloads and redirects carry the Gitea authorization token only
when their scheme, host, and port match the configured service. Cross-origin
content is downloaded without that token and verified by its digest.
The upload operation accepts only an inspection result without conflicts or
Shared delivery uploads only after inspection finds no conflicts or
unverifiable assets. It uploads missing binaries in filename order and then the
missing `SHA256SUMS`, using multipart field `attachment` and the filename query
parameter `name`. Matching attachments are retained; unrelated and existing
missing `SHA256SUMS`, using the adapter's multipart field `attachment` and
filename query parameter `name`. Matching attachments are retained; unrelated and existing
attachments are never overwritten or deleted. Results use disjoint current
observations: matching, uploaded, pending, conflicting, and uncertain. An asset
moved from known missing work to another category is removed from `pending`;

View File

@@ -39,9 +39,10 @@ reports these implemented packages:
| `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. |
| `internal/build` | Resolved-identity builds, normalized settings, selected-module compilation, version injection, external native smoke output, and artifact digests. |
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
| `internal/delivery` | Captured-commit module identity and note selection, literal-tag checkout checks, prebuilt identity binding, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
| `internal/delivery` | Read-only backend preflight, captured-commit module identity and note selection, literal-tag checkout checks, prebuilt identity binding, managed builds in disposable storage outside the checkout, artifact declaration, manifest assembly, shared page and asset policy, streamed comparisons, write recovery, and delivery outcomes. |
| `internal/forge` | Backend contract, neutral observations and failures, opaque handles, preflight classifications, and write evidence shared by delivery policy and adapters. |
| `internal/git` | Git command boundary, committed-tree reads and shared reference traversal, checkout-root and commit capture, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
| `internal/gitea` | Gitea API transport, full-tag hosted release pages, asset inspection, manifests, and uploads. |
| `internal/gitea` | Gitea destination rules, authenticated repository preflight, HTTP operations and codecs, opaque handle interpretation, response validation, redirects, multipart streaming, and protocol-to-neutral failure conversion. |
| `internal/gotool` | Operation-local Go configuration discovery and reuse, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
| `internal/process` | Cancellable child-process execution, including owned stdin/stdout pipes for serialized exchanges, with per-command environment overlays and removals. |
@@ -74,11 +75,16 @@ it does not consult neighboring files or traverse submodule checkouts.
See the [shared validation guide](validation.md) for checker, Go command, and
candidate-file integration mechanics.
Delivery verifies all local binary sizes and digests before hosted requests.
Its Gitea upload path reuses that verification and streams missing files once
for transport. Independent calls to the internal client's `UploadMissing`
verify missing local files before writing. Both paths share remote inspection,
conflict handling, and reconciliation.
The root facade selects the backend explicitly and passes a configured
`internal/forge.Backend` into `internal/delivery`. Delivery performs cheap
request and source checks, then read-only backend preflight before managed
builds or full local artifact verification. Artifact size and digest
verification completes before hosted writes and is reused for upload streams.
Shared delivery code owns release-page matching and creation recovery, remote
asset comparison, upload sequencing, and partial or uncertain outcomes.
`internal/gitea` implements only the Gitea protocol operations behind the
neutral contract. `internal/artifact` and `internal/forge` do not depend on the
adapter.
## Capability owners and behavioral evidence
@@ -97,10 +103,11 @@ not a separate release matrix:
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
| Cross-builds, version smoke checks, module paths, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `internal/artifact`, `internal/tempdir`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput`, `TestBuildResolvesDirtyWorkingTreeDeclaration`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure`, `TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets`, `TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree` |
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverUsesCommittedModuleIdentityForSeparatePages`, `TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites`, `TestEnsureReleaseEscapesSlashContainingTag` |
| Explicit destination selection, standalone and automatic preflight, and delivery ordering | `releasekit`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliveryForgeSelectionIsExplicit`, `TestPreflightDeliveryChecksAuthenticatedRepositoryWithoutCheckout`, `TestPreflightDeliveryPreservesPublicFailureKindsAndCauses`, `TestAutomaticPreflightFailurePrecedesPrebuiltArtifactHashing`, `TestDeliverUnavailablePreflightStopsBeforeManagedBuild`, `TestCIDeliverUnavailablePreflightStopsBeforeDependencyPreparation`, `TestCIDeliverPageOnlyReportsUnavailablePreflightWithStatusOne` |
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverUsesCommittedModuleIdentityForSeparatePages`, `TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesUsesSelectedModuleDirectory`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` |
| Shared module identity and tag derivation | `internal/releaseidentity`, `internal/candidate`, `internal/build`, `internal/delivery` | `TestDeriveUsesModuleDeclarationAndPhysicalLayout`, `TestCommittedAndWorkingTreeDeclarationsUseTheirOwnSources`, `TestSelectedNestedModuleDoesNotRequireRootDeclaration`, `TestBuildResolvesDirtyWorkingTreeDeclaration`, `TestDeliverUsesCommittedModuleIdentityForSeparatePages` |
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
| Shared page, manifest, asset comparison, and additive retry policy | `internal/delivery` | `TestCoordinateReleaseCreatesWithFullNoteAndVerifiesRequestedTitle`, `TestCoordinateReleaseReconcilesCreateOutcomes`, `TestInspectRemoteAssetsComparesContentAndPreservesUnrelatedAssets`, `TestUploadMissingAssetsReconcilesOnceAndKeepsLaterAssetsPending` |
| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/`, `examples/multi-module/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerTagDeliveryTemplate`, `TestWoodpeckerTagDeliveryUsesTaggedModuleVersion`, `TestWoodpeckerSourceDeliveryTemplateSelfBootstraps`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout`, `TestMultiModuleSelectorMapsSelectorsAndTags`, `TestMultiModuleLauncherUsesSelectedModuleAndProfile`, `TestMultiModuleWoodpeckerDelivery` |
Run the complete suite with `go test ./...`. The public API and import boundary

View File

@@ -77,6 +77,17 @@ and orchestration bookkeeping belong behind that boundary. Additional public
packages and extension interfaces require a deliberate decision grounded in a
durable consumer need.
Hosted delivery follows a neutral internal contract. The root facade selects a
backend explicitly and constructs it; `internal/forge` defines the backend
operations and neutral observations, failures, and write evidence;
`internal/delivery` owns shared page and asset policy; and `internal/gitea`
adapts the Gitea protocol. `internal/delivery`, `internal/forge`, and
`internal/artifact` must not depend on the Gitea adapter. Artifact declaration
and local content verification remain independent of delivery policy and forge
protocols. The compiled dependency check protects this transitive boundary.
See [ADR 0006](../adr/0006-neutral-forge-delivery-and-preflight.md) for the
selected delivery and preflight design.
Go declarations and GoDoc own exact API contracts. Compiled-export checks,
dependency checks, and external-package behavioral tests protect the selected
facade boundaries.

59
docs/releases/v0.6.0.md Normal file
View File

@@ -0,0 +1,59 @@
# ReleaseKit v0.6.0
## Summary
ReleaseKit adds neutral delivery coordination behind an explicit Gitea backend
selection and introduces authenticated, read-only credential preflight.
## Changes
- `releasekit.DeliveryRequest.Destination` now uses the facade-owned
`releasekit.DeliveryDestination`. Set `Forge: releasekit.ForgeGitea` for
Gitea delivery. The removed `GiteaDestination` type has no compatibility
alias.
- Delivery profiles must include `"forge": "gitea"`. The selection is
required only when `deliver` or `ci-deliver` is requested; the URL does not
infer a backend. `check`, `build`, and source `publish` remain independent of
delivery selection and credentials.
- `releasekit.PreflightDelivery` allows callers to check a destination without
a checkout or tag. `Deliver` performs the same read-only preflight
automatically. For Gitea, the check authenticates through repository-scoped
reads and makes no test release or asset writes.
- `DeliveryPreflightError` exposes classified failures and retains the
underlying cause. Successful preflight confirms authentication and repository
access plus available permission evidence; it does not guarantee future
release or asset writes. Gitea's code-push permission metadata does not
establish release-write or asset-write permission.
- The CLI keeps `RELEASEKIT_GITEA_TOKEN` as its default token environment
setting and preserves configured environment/file precedence. Selection,
credential, and inaccessible-destination errors return status 2; unavailable
verification and cancellation return status 1.
- Binary `ci-deliver` checks credentials before application dependency
downloads and checks again during delivery. The automatic check now precedes
full local artifact size and digest verification; those checks still finish
before hosted writes.
## Upgrade guidance
1. Replace `releasekit.GiteaDestination` with
`releasekit.DeliveryDestination` and set
`Forge: releasekit.ForgeGitea` in each API request.
2. Add `"forge": "gitea"` to every profile used for `deliver` or
`ci-deliver`, including profiles loaded from a tagged CI checkout. Keep the
existing `base_url`, owner, repository, token environment, and token file
settings.
3. If handling preflight failures in Go, use `errors.As` to inspect
`*releasekit.DeliveryPreflightError` and use `errors.Is` or `errors.As` on
its unwrapped cause as needed.
4. Continue to treat successful preflight as evidence of repository access,
not a guarantee that a later write will be accepted. Source publication
continues to use Git authentication and does not require hosted delivery
credentials.
See the [delivery guide](../consumers/delivery.md), [configuration reference](../config.md),
[CLI contract](../cli.md), and [Gitea integration guide](../integrations/gitea.md)
for current behavior.
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.6.0/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.6.0/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.6.0/docs/oss/attribution.md)

View File

@@ -1,7 +1,6 @@
# Implementation plan: neutral forge delivery and credential preflight
Status: implementation in progress; Stages 1–8 are complete and Stages 9–10
are pending.
Status: implementation complete; Stages 1–10 are complete.
## Purpose and execution
@@ -515,7 +514,7 @@ Run relevant package tests once assembled; Stage 10 owns the final full checks.
## Stage 10: Document the cutover and verify the sprint
Status: pending.
Status: complete.
1. Add the next sequential ADR (currently `0006`) using the documentation
policy's lightweight Nygard format. Record the shared-policy/backend boundary,

View File

@@ -90,7 +90,8 @@ outside the checkout, and builds the ReleaseKit version selected there. It then
uses `ci-deliver --prepared` with the mapped profile and expected physical module.
The `core-v2` profile demonstrates binary delivery; the other profiles are
page-only. Before enabling the workflow, configure each selected profile's
`delivery` destination and add the Woodpecker repository secret
`delivery` object with `"forge": "gitea"`, its `base_url`, owner, and repository,
then add the Woodpecker repository secret
`releasekit_gitea_token`; the workflow exposes it as
`RELEASEKIT_GITEA_TOKEN` only in the delivery step.