Finish publication recovery workflow

This commit is contained in:
2026-11-02 19:37:29 +00:00
parent 6915a86132
commit 599ad81bec
10 changed files with 697 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
# Command line
ReleaseKit currently provides six commands. The exit statuses below belong
ReleaseKit currently provides eight commands. The exit statuses below belong
to the native executable. `go run` examples are convenient for source execution,
but the Go driver maps a nonzero child exit status to 1. Use the built executable
or `./scripts/release` for publication when distinct statuses are needed.
@@ -75,7 +75,7 @@ Unsupported argument sequences print usage to standard error.
Publish a source tag:
```sh
go run ./cmd/releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] VERSION
go run ./cmd/releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] [--record-dir DIR] VERSION
```
`publish` reads the same product, remote, validation, and additional-check
@@ -138,13 +138,62 @@ mismatches fail during preflight before Go validation or tag creation. Git URL
rewrites are resolved first, and the push URL is used when fetch and push URLs
differ. `check` ignores this field.
The command automatically saves available publication evidence in the repository's
private attempt store. Use `--record-dir DIR` to select another directory; a
relative override is resolved from the source repository root. Standard error
reports the absolute saved record path or a recording warning separately from
the publication result. Recording failure does not change publication status.
When publication fails and a record was saved, the CLI also prints a copyable
`reconcile --record` command for that exact record. The command uses the running
executable's absolute path, so it remains usable when the executable is not on
`PATH`; if that executable is no longer available, use another supported
ReleaseKit CLI.
The command returns status 0 for confirmed publication, 1 for a known
publication failure or conflict, 2 for usage, profile, or invalid-version
errors, and 3 when the push outcome remains unknown. Unknown status must be
resolved against the authoritative remote before retrying. The
resolved with the explicit-record recovery operation below before retrying. The
[maintainer procedure](release.md) selects the repository profile and gives
the source-release workflow.
## Recover a saved publication attempt or discover local attempts
```sh
releasekit reconcile --record FILE [--json]
releasekit publication-attempts [--repo DIR] [--record-dir DIR] [--json]
```
`reconcile` requires one explicitly selected record and reads a relative `FILE`
from the invoking directory. It observes the endpoint and tag stored in that
record; it does not load a profile, inspect the current candidate or local tag,
run checks, or change repository state. `publication-attempts` lists records in
the selected store without querying remotes or choosing an attempt. An omitted
`--repo` means the current directory. Its relative `--record-dir` is resolved
from the selected repository root; an absolute directory can be listed outside
a checkout. The selected directory must already exist. Discovery reports usable
records and per-record diagnostics together when some records cannot be read.
By default, both operations print human-readable output. `--json` writes one
structured document to standard output after valid arguments, including for
reconciliation and discovery failures. Recovery does not run candidate checks
or make hosted delivery requests. It reads the record's captured endpoint, while
using current supported transport settings for its recorded remote name.
| Recovery command result | Status |
| --- | --- |
| Reconciliation observes the intended publication | 0 |
| Reconciliation observes absence or conflict | 1 |
| Reconciliation cannot read or decode the selected record, or has an invocation error | 2 |
| Reconciliation remains unknown, including missing evidence or cancellation | 3 |
| Discovery completes without record diagnostics, including an existing empty store | 0 |
| Discovery is incomplete or fails, including cancellation | 1 |
| Discovery argument or usage error | 2 |
The CLI JSON schema and safe evidence fields are documented in the
[publication record format](integrations/publication-records.md). Recovery
guidance for storage, older evidence, and retry decisions is in the
[maintainer procedure](release.md#resolve-publication-failures).
Build selected application targets:
```sh

View File

@@ -123,3 +123,53 @@ against the captured endpoint before retrying; see the [maintainer procedure](..
for the recovery steps. The result's endpoint is the exact URL and can contain
credentials; handle it accordingly. The [CLI contract](../cli.md) documents
command output and exit statuses.
## Retain and reconcile publication evidence
`Publish` automatically attempts to save one private record for each invocation.
The default store is under the repository's Git common directory, so linked
worktrees share it. `ReleaseRequest.PublicationRecordDir` can override the
directory; relative overrides resolve from the source repository root. The CLI
equivalent is `publish --record-dir DIR`. ReleaseKit creates its owned storage
directories privately and creates record files with owner-only permissions;
existing directory permissions are left as they are. The caller owns storage
retention and removal.
The saved endpoint is retained exactly for later observation and may include
credentials. Treat records, `PublicationEvidence`, and `PublicationResult` as
sensitive. Store overrides in a location with suitable access controls, avoid
printing raw endpoint values, and remove records when their recovery value has
expired. Diagnostic errors and CLI output apply credential redaction; the
private record and typed evidence preserve the raw endpoint.
Recording failure is reported in `PublicationResult.RecordError`, separately
from `Publish`'s return error and publication state. A successful push remains
successful if storage fails, but the saved handoff is unavailable. Callers can
still pass `result.Evidence` directly to `ReconcilePublication` when it contains
the required identity. For example:
```go
func observe(ctx context.Context, result releasekit.PublicationResult) (releasekit.PublicationReconciliationResult, error) {
return releasekit.ReconcilePublication(ctx, result.Evidence)
}
```
The evidence is populated as publication identity becomes available, including
partial evidence on some earlier failures. An abrupt process termination before
the save point can leave no record; removed storage also removes the handoff.
Records from older attempts may lack the tag object, endpoint, or other identity
needed for a conclusive observation.
Use `LoadPublicationEvidence(path)` to read one explicit record,
`ListPublicationAttempts(ctx, request)` to discover readable records and
per-record diagnostics in a local directory, or `ReconcilePublication(ctx,
evidence)` to observe a selected endpoint. Listing does not query remotes, and
neither listing nor reconciliation changes saved records. Recovery does not
infer the attempt from current HEAD, local tags, module files, or profiles.
`ReconcilePublication` returns a nil error for a completed `published`,
`not_published`, or `conflict` observation; unresolved `unknown` results include
a typed error that retains the underlying cause. See the
[GoDoc](../../recovery.go) for the exported fields and error kinds, the
[CLI guide](../cli.md) for command statuses, and the
[record format guide](../integrations/publication-records.md) for supported
stored and structured formats.

View File

@@ -0,0 +1,111 @@
# Publication records and recovery output
This guide describes the supported private publication-record envelope and the
versioned JSON documents emitted by the recovery CLI. The
[publication consumer guide](../consumers/publish.md) owns storage placement,
permissions, retention, and sensitive-data handling. The
[CLI reference](../cli.md) owns command behavior, path selection, and exit
statuses; the exported Go contract is documented in [GoDoc](../../recovery.go).
## Private attempt record
Each saved attempt is one complete JSON document with this versioned envelope:
```json
{
"format_version": 1,
"evidence": {
"repository_dir": "/work/project",
"module_dir": "/work/project",
"module_path": "example.com/project",
"version": "v1.2.3",
"tag": "v1.2.3",
"candidate_commit": "<full Git object ID>",
"tag_object": "<full Git object ID>",
"endpoint": "ssh://git.example/owner/project.git",
"remote": "origin",
"recorded_at": "2026-11-02T12:00:00Z"
}
}
```
The format version is an integer. Evidence fields may be absent when publication
stopped before they were captured; loading preserves that partial evidence and
does not fill fields from the current checkout. Additional fields in a
supported version are ignored. A missing or unsupported version, malformed or
multiple JSON documents, or fields with invalid structural types are reported
as record-format errors. The codec supports version 1; no migration from an
unsupported version is performed.
The `endpoint` value is stored exactly as captured and may contain credentials.
Private records are sensitive. Do not copy them into public logs or source
control. For their location, permissions, and lifecycle, see the
[publication consumer guide](../consumers/publish.md#retain-and-reconcile-publication-evidence).
## Recovery CLI JSON
After valid command arguments, `--json` emits one JSON document to standard
output for both successful and failed recovery operations. Each document has
`schema_version: 1` and a `command` name. These are CLI presentation documents,
not private attempt records or a Go serialization of public structs.
`reconcile` includes `record_path`, safe `evidence`, and `state` when a record
was read. Available `observed_tag_object` and `observed_commit` fields are
included when present. A `diagnostic` is included for unresolved observation
or record loading failure. Loading failure omits evidence, state, and observed
IDs.
`publication-attempts` includes `record_dir`, an `attempts` array with each
`record_path` and safe `evidence`, and a `diagnostics` array. A top-level
`diagnostic` describes enumeration or cancellation failure. Both arrays are
present when empty so consumers can decode the same shape for empty stores.
For example, an unresolved reconciliation can have this shape:
```json
{
"schema_version": 1,
"command": "reconcile",
"record_path": "/private/attempt-abc.json",
"evidence": {
"repository_dir": "/work/project",
"module_path": "example.com/project",
"version": "v1.2.3",
"tag": "v1.2.3",
"candidate_commit": "<full Git object ID>",
"tag_object": "<full Git object ID>",
"endpoint": "ssh://git.example/owner/project.git",
"remote": "origin"
},
"state": "unknown",
"diagnostic": {
"kind": "remote_observation",
"message": "<credential-safe diagnostic>"
}
}
```
A discovery document always contains both arrays, including for an empty store:
```json
{
"schema_version": 1,
"command": "publication-attempts",
"record_dir": "/private/publication-attempts",
"attempts": [],
"diagnostics": []
}
```
CLI evidence uses the private record's snake-case field names. Endpoint strings
in CLI output are credential-redacted; the private record and typed
`PublicationEvidence` retain the exact endpoint. A diagnostic contains `kind`
and safe `message`, with `record_path` and `missing_fields` when applicable.
Raw Go errors and private record contents are not emitted as JSON fields.
Consumers should select attempts by their returned record paths and evidence.
The listing order is deterministic by path, not a recommendation to choose the
first or newest attempt. Unknown fields may be added in later CLI schema
versions; reject unsupported `schema_version` values rather than interpreting
them as version 1. See the [CLI reference](../cli.md#recover-a-saved-publication-attempt-or-discover-local-attempts)
for invocation and status behavior.

View File

@@ -11,12 +11,14 @@ two Go facades and a command-line interface:
- `buildinfo` is the small runtime facade. `Version` reports the injected
application version, the main-module version when available, or `dev`.
- `cmd/releasekit` is the executable interface. Its `version`, `check`,
`publish`, `build`, `deliver`, and `ci-deliver` commands adapt profiles and
command output to the supported library operations.
`publish`, `reconcile`, `publication-attempts`, `build`, `deliver`, and
`ci-deliver` commands adapt profiles and command output to the supported
library operations.
See the [CLI contract](../cli.md), [configuration reference](../config.md),
[candidate check guide](../consumers/check.md),
[source publication guide](../consumers/publish.md),
[publication record format](../integrations/publication-records.md),
[build guide](../consumers/build.md),
[delivery guide](../consumers/delivery.md), and
[runtime version guide](../consumers/runtime-version.md) for task-oriented use.
@@ -49,7 +51,7 @@ reports these implemented packages:
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
| `internal/releaseidentity` | Committed-object and working-tree module selection, declaration reading, and semantic-version-to-tag derivation. |
| `internal/validation` | Shared Go validation with optional race, full-vet, and static package-compilation target selections, formatting, workspace-file and filesystem-replacement checks, per-blob size limits, and selected Markdown destination and fragment checking; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, pre-push evidence recording, saved-attempt discovery, later observation, and recovery state. |
| `internal/runtimeinfo` | Injected version selection and cached fallback metadata lookup for the runtime facade. |
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
@@ -75,6 +77,18 @@ 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.
Publication records and recovery follow a separate path from candidate
validation. `internal/publish` captures and saves attempt evidence before the
tag push, lists local records, and observes explicitly selected evidence through
`internal/git`; the root facade owns the reusable Go evidence and result types.
The CLI's `reconcile` and `publication-attempts` commands load, present, and
select records without loading a profile or choosing an attempt automatically.
These operations do not rerun candidate checks or hosted delivery and do not
change local or remote state. The [publication consumer guide](../consumers/publish.md)
owns storage and API use; the [record format guide](../integrations/publication-records.md)
owns the private and CLI JSON formats; the [CLI contract](../cli.md) owns command
semantics and statuses.
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
@@ -102,6 +116,7 @@ not a separate release matrix:
| Piped child-process exchanges and cancellation cleanup | `internal/process` | `TestStartPipedExchangesBeforeInputEOFAndPreservesSettings`, `TestStartPipedCancellationAfterReadinessReapsChild`, `TestStartPipedPreservesNonzeroExit` |
| 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` |
| Saved attempt recording, discovery, explicit later reconciliation, and read-only recovery | `releasekit`, `internal/publish`, `internal/git`, `cmd/releasekit` | `TestLostPublicationOutputCanBeRecoveredFromExplicitRecords`, `TestBuiltPublishRecoveryCommandSelectsItsRecordAmongOtherAttempts`, `TestRecoveryFacadeLoadsPartialEvidenceAndPreservesCause`, `TestRecoveryFacadeErrorUnwrapsCancellation`, `TestListPublicationAttemptsReadsExplicitExternalStore` |
| 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` |
| 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` |

View File

@@ -124,48 +124,65 @@ the complete checked-in release note. Report its result separately from the
completed source release. For failed or uncertain delivery, follow the recovery
instructions below using the same immutable version.
## Resolve failures
## Resolve publication failures
A failed push triggers a URL-resolution check against the captured endpoint,
then at most one exact authoritative tag query. If the endpoint no longer
resolves to itself, recovery reports `unknown` without querying the rewritten
destination. An exact tag-object match confirms publication even when Git
reported an error. A proven absent tag is `not_published`; a different object
is a conflict; an unreadable query is `unknown`. The command retains its local
tag and reports the state, phase,
candidate commit, tag object, and captured publication endpoint when available.
Do not retry an unknown result until you resolve the authoritative ref at that
exact endpoint. Copy the reported endpoint value into the variable below using
normal shell quoting; retain the version and tag object from the same result.
The command checks that Git URL rewriting still resolves this value to itself;
the manual check below applies the same rule before querying:
Each publication invocation attempts to save its captured identity before the
tag push. The CLI reports the saved record path separately from publication
success or failure and, after a push or immediate-reconciliation error, prints a
copyable command that selects that exact record. Run that command to make a
separate later observation:
```sh
releasekit_endpoint='COPY_CAPTURED_ENDPOINT'
releasekit_version='vX.Y.Z'
test "$(git ls-remote --get-url -- "$releasekit_endpoint")" = "$releasekit_endpoint" &&
git ls-remote --refs -- "$releasekit_endpoint" "refs/tags/$releasekit_version"
'/path/to/releasekit' reconcile --record '/absolute/path/to/attempt-record.json'
```
A failed URL-equality check or remote query leaves the outcome unresolved.
Correct transport configuration before continuing. The reported endpoint may
be different from `origin`'s fetch URL or its current push URL; querying either
of those does not establish absence at the captured endpoint.
The saved evidence fixes the original endpoint and tag identity. Later recovery
uses current supported transport settings for the recorded remote name, but it
does not substitute the current remote URL, inspect the local tag or current
HEAD, load a profile, rerun checks, or change repository state. A completed
observation reports `published`, `not_published`, or `conflict`; an incomplete
observation reports `unknown` with diagnostic context. Resolve an unknown result
by correcting the identified endpoint rewrite, transport configuration, or
missing evidence. For incomplete saved identity, select another record with the
required fields or use complete typed evidence through the Go API.
Compare the returned object ID with the tag object reported by the command.
An exact match confirms publication. If the ref is absent, the version was not
published; only then may you deliberately remove the unpublished local tag
with `git tag -d vX.Y.Z` before correcting the candidate or retrying. Do not
move, overwrite, or delete a tag that exists remotely. If a different object
uses the version, choose a new version after resolving the conflict.
If command output was lost, list local attempts and explicitly select the
matching record by its module, full tag, and saved identity:
If local tag inspection fails after creation, the reported ref may remain even
when no tag object ID was obtained. Inspect that ref before recovery.
```sh
releasekit publication-attempts --repo "$PWD"
releasekit reconcile --record '/absolute/path/from-the-list/attempt-record.json'
```
Discovery only reads the local store. It neither queries remotes nor chooses an
attempt automatically. Use a supported ReleaseKit CLI if the executable named
in the printed handoff command is no longer available. An older record may lack
the endpoint, tag-object ID, candidate commit, or another field required for a
conclusive result.
A later observation that the tag is absent describes the remote at that time;
it does not prove the version was never published. Never reuse a version known
to have been published, and preserve published tags and contents as immutable.
Keep removal of an unpublished local tag, candidate correction, and retry as
explicit operator decisions after reviewing the record and authoritative
remote state. A conflicting object requires resolving the conflict and selecting
an unused version when appropriate; do not move, overwrite, or delete a
published tag.
If record storage fails, ReleaseKit reports the recording warning separately;
the warning does not change the actual publication outcome. The saved handoff is
then unavailable. A Go caller that retained `PublicationResult.Evidence` can
still pass that typed evidence directly to `ReconcilePublication`. Process
termination before the record is written, removed storage, and older incomplete
records can leave no sufficient saved evidence for later recovery.
Later reconciliation adds no successful-push read-back, polling, CI, consumer
installation, or hosted-delivery gate. Hosted page delivery remains separate.
If validation or a guard fails before this invocation pushes, correct and
commit the candidate, publish `main`, and run the command again. A retained
local tag still requires the authoritative-state check above before removal;
a concurrent publisher may already have published the version. The prepared
local tag still requires reviewing the selected record and authoritative state
before deciding whether to remove it. The prepared
Woodpecker workflow builds the CLI from the tagged checkout, then uses
`ci-deliver` for page-only delivery. To activate it after configuring the
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`

View File

@@ -1,6 +1,6 @@
# Publication reconciliation implementation plan
Status: ready for implementation.
Status: complete.
Implement the target and resolved decisions in the
[feature roadmap](publication-reconciliation.md). The original
@@ -471,6 +471,8 @@ launchers and source bootstrap working without adding consumer-owned recording.
## Stage 8: Finish assembled coverage and canonical documentation
**Completed:** 2026-11-02
**Outcome:** The full feature works across process boundaries, with supported
contracts and a short consumer recovery workflow.