From cbdba900142432f34cb3bc3ed00154ee957c9f7f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 1 Nov 2026 18:38:03 +0000 Subject: [PATCH] Document neutral forge delivery and preflight --- README.md | 4 + delivery.go | 32 +++++-- ...06-neutral-forge-delivery-and-preflight.md | 92 +++++++++++++++++++ docs/cli.md | 43 +++++---- docs/config.md | 43 ++++++--- docs/consumers/delivery.md | 66 +++++++++++-- docs/integrations/gitea.md | 69 +++++++++----- docs/internal/overview.md | 25 +++-- docs/policy/architecture.md | 11 +++ docs/releases/v0.6.0.md | 59 ++++++++++++ docs/roadmap/implementation.md | 5 +- examples/multi-module/README.md | 3 +- 12 files changed, 370 insertions(+), 82 deletions(-) create mode 100644 docs/adr/0006-neutral-forge-delivery-and-preflight.md create mode 100644 docs/releases/v0.6.0.md diff --git a/README.md b/README.md index 4e944bb..6abda0c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/delivery.go b/delivery.go index 98dfbb9..059590f 100644 --- a/delivery.go +++ b/delivery.go @@ -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 { diff --git a/docs/adr/0006-neutral-forge-delivery-and-preflight.md b/docs/adr/0006-neutral-forge-delivery-and-preflight.md new file mode 100644 index 0000000..801d4b8 --- /dev/null +++ b/docs/adr/0006-neutral-forge-delivery-and-preflight.md @@ -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. diff --git a/docs/cli.md b/docs/cli.md index dffced8..b42c6e4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 diff --git a/docs/config.md b/docs/config.md index b05e658..2420ccf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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 diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index 51f06d0..f104078 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -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", diff --git a/docs/integrations/gitea.md b/docs/integrations/gitea.md index da5cb78..aeab738 100644 --- a/docs/integrations/gitea.md +++ b/docs/integrations/gitea.md @@ -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`; diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 7a638a5..ab20713 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -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 diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index e9d77b6..693ffe5 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -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. diff --git a/docs/releases/v0.6.0.md b/docs/releases/v0.6.0.md new file mode 100644 index 0000000..e2efd4e --- /dev/null +++ b/docs/releases/v0.6.0.md @@ -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) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 5950ffa..6cfc91a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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, diff --git a/examples/multi-module/README.md b/examples/multi-module/README.md index ee9531a..df4980e 100644 --- a/examples/multi-module/README.md +++ b/examples/multi-module/README.md @@ -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.