Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 218 additions & 9 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Build one Git-distributed plugin payload for Claude Code and Codex. Keep shared
- Fresh native completion: follow `docs/native-capability-qualification.md`; every bounded cell needs a human-recorded result bound to one candidate.
- Hosted or PR completion: follow `docs/pull-requests-and-ci.md`; every required check must pass on the current head. Revalidate canaries, safeguards, and release state before release claims.
- Raw native receipts: follow the private XDG location, permissions, and promotion boundary in `docs/native-capability-qualification.md`.
- Never let a failed check read as a satisfied one. In verification code, distinguish "proved absent" from "could not prove", and fail closed on the second. This is the recurring defect in the release and readiness paths: `|| true` turning an API outage into an empty result, a `gh` exit code standing in for a 404, an unrelated object answering for the one that was asked about, a discarded malformed page reporting a value as missing. Each reported healthy while the thing it guarded was broken. Prefer an explicit status check over an exit code, and give the unprovable case its own outcome rather than folding it into the negative one.
- Preserve the consumer contract: no user-managed Bun, Node.js, Python, npm, or setup command.

Initialization, development, installation, replacement, qualification, or release: start at `README.md`'s **Choose a path** index.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Contributors need Git and [Bun](https://bun.sh/docs/installation). Plugin consum
| Configure GitHub release automation | Read [Configure release automation](docs/release-setup.md). |
| Qualify fresh native capabilities | Read [Qualify fresh native capabilities](docs/native-capability-qualification.md). |
| Publish a release | Read [Publish a release](docs/publishing.md). |
| Maintain or repair release state | Read [Maintain or repair release state](docs/release-repair.md). |
| Maintain, resume, or repair release state | Read [Maintain, resume, or repair release state](docs/release-repair.md). |
| Qualify publishing-system changes with public/private canaries | Read [Qualify public and private canaries](docs/canary-qualification.md). |
| Change the distribution architecture or its language | Read [`CONTEXT.md`](CONTEXT.md), then the relevant [ADR](docs/adr/). |

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0003-reviewed-versioned-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Packaging emits a deterministic `tar.gz` and `*.checksums.json`. The JSON binds

## Repair contract

Manual dispatch accepts `operation=maintenance` or `operation=repair`. Maintenance is the default and only updates the standing release PR. Repair requires `operation=repair` plus `release_tag` naming an existing `vX.Y.Z` tag. Repair begins from the immutable tag, rederives candidate topology and fresh PR authorship from GitHub, and checks identity against repository configuration rather than trusting the tag-carried record. It repeats the complete proof and validates any existing GitHub Release target. It compares each asset before writing: matching assets remain untouched, missing assets are added, and mismatches fail closed. A mismatched asset may be replaced only when `replace_mismatched_assets=true` is approved through the protected `release` environment. Repair never moves the tag and never represents a new release.
Manual dispatch accepts `operation=maintenance`, `operation=resume`, or `operation=repair`. Maintenance is the default and only updates the standing release PR; it fails closed and names the exact resume command when it detects a merged release PR whose tag was never created, so a stranded candidate cannot silently stop later releases. Resume requires `operation=resume` plus `candidate_sha` naming a merged, proven, untagged candidate. Resume recovers the publication-candidate record persisted before proof, revalidates PR identity, base branch, merge binding, topology, projection, and version against that record, requires the target tag to be absent, and repeats the complete proof before entering the protected `release` environment. It never mints a fresh admission, never reconstructs an expired record, and never creates an ad hoc tag. Repair requires `operation=repair` plus `release_tag` naming an existing `vX.Y.Z` tag. Repair begins from the immutable tag, rederives candidate topology and fresh PR authorship from GitHub, and checks identity against repository configuration rather than trusting the tag-carried record. It repeats the complete proof and validates any existing GitHub Release target. It compares each asset before writing: matching assets remain untouched, missing assets are added, and mismatches fail closed. A mismatched asset may be replaced only when `replace_mismatched_assets=true` is approved through the protected `release` environment. Repair never moves the tag and never represents a new release.

## Human-owned safeguards

Expand Down
35 changes: 32 additions & 3 deletions docs/release-repair.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Maintain or repair release state
# Maintain, resume, or repair release state

Use this runbook to update the standing release PR or repair an incomplete publication at an existing immutable tag.
Use this runbook to update the standing release PR, resume a proven candidate stranded before its tag, or repair an incomplete publication at an existing immutable tag.

Manual dispatch accepts two operation values. `maintenance` is the default; it only updates the standing release PR and never publishes. `repair` requires `release_tag` set to the exact existing `vX.Y.Z` tag. This repairs an incomplete publication; it does not create a new release.
Manual dispatch accepts three operation values. `maintenance` is the default; it only updates the standing release PR and never publishes. `resume` requires `candidate_sha` and publishes a merged, proven candidate whose tag was never created. `repair` requires `release_tag` set to the exact existing `vX.Y.Z` tag; it repairs an incomplete publication and does not create a new release.

Choose by tag state: no tag yet means `resume`; an existing tag means `repair`.

## Maintain the release PR

Expand All @@ -15,6 +17,33 @@ gh workflow run Release \

Maintenance completes when the standing release PR reflects the current releasable commits and its generated version projection.

## Resume a candidate stranded before its tag

A release PR can merge and pass full proof, then lose its publication job to a cancellation before the immutable tag exists. The candidate is proven but untagged, so Release Please refuses to open the next release PR and reports `There are untagged, merged release PRs outstanding - aborting`. Repair cannot help, because repair requires an existing tag.

Maintenance detects this state and fails with the exact command, including the candidate SHA. Run it:

```sh
gh workflow run Release \
--repo OWNER/REPOSITORY \
--ref main \
-f operation=resume \
-f candidate_sha=SHA
```

Resume never mints a fresh admission. It recovers the publication-candidate record the original run persisted before proof, revalidates Release Please PR identity, base branch, merge-commit binding, commit topology, projection digest, and manifest version against that record, and requires the target tag to be absent. It then repeats packaging, four-platform compatibility, and candidate proof before entering the same protected `release` environment as a normal publication.

Resume fails closed and creates nothing when:

- The persisted candidate record is missing or its 90-day retention expired.
- The target tag already exists — use repair instead.
- The record is rebound to another candidate, version, or pull request.
- The pull request is not a merged Release Please PR on the release base branch.

An expired record cannot be reconstructed, because rebuilding it would admit a candidate no run ever proved. Recover by rerunning the original release run while its candidate artifact is still retained.

Resume completes when the immutable tag targets the admitted candidate, the GitHub Release and assets match the proven package, the attestation exists, and the release PR carries `autorelease: tagged` so the next maintenance run creates the following release PR.

## Repair an incomplete publication

Start with compare-before-write repair and mismatched replacement disabled:
Expand Down
2 changes: 1 addition & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Use the runbook that matches the current job:
| Configure GitHub release automation | [Configure release automation](release-setup.md) |
| Record candidate-bound evidence from fresh clients | [Qualify fresh native capabilities](native-capability-qualification.md) |
| Publish an admitted candidate | [Publish a release](publishing.md) |
| Maintain the release PR or repair an incomplete publication | [Maintain or repair release state](release-repair.md) |
| Maintain the release PR, resume a stranded candidate, or repair an incomplete publication | [Maintain, resume, or repair release state](release-repair.md) |
| Qualify publishing-system changes in public and private repositories | [Qualify public and private canaries](canary-qualification.md) |

The root [README](../README.md) owns plugin creation and extension. The [installation guide](installing.md) owns production installation, upgrade, replacement, and rollback.
Expand Down
12 changes: 9 additions & 3 deletions scripts/readme-release-pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test("README routes each branch directly to its owner", async () => {
"[Configure release automation](docs/release-setup.md)",
"[Qualify fresh native capabilities](docs/native-capability-qualification.md)",
"[Publish a release](docs/publishing.md)",
"[Maintain or repair release state](docs/release-repair.md)",
"[Maintain, resume, or repair release state](docs/release-repair.md)",
"[Qualify public and private canaries](docs/canary-qualification.md)",
]) {
expect(readme).toContain(pointer)
Expand All @@ -44,7 +44,7 @@ test("maintainer index preserves one pointer per operator branch", async () => {
"[Configure release automation](release-setup.md)",
"[Qualify fresh native capabilities](native-capability-qualification.md)",
"[Publish a release](publishing.md)",
"[Maintain or repair release state](release-repair.md)",
"[Maintain, resume, or repair release state](release-repair.md)",
"[Qualify public and private canaries](canary-qualification.md)",
]) {
expect(index).toContain(pointer)
Expand Down Expand Up @@ -163,8 +163,14 @@ test("release repair preserves exact operations and a complete terminal bound",
expect(repair).toContain("it only updates the standing release PR and never publishes")
expect(repair).toContain("`repair` requires `release_tag`")
expect(repair).toContain("exact existing `vX.Y.Z` tag")
expect(repair).toContain("This repairs an incomplete publication; it does not create a new release.")
expect(repair).toContain("it repairs an incomplete publication and does not create a new release")
expect(repair).toContain("`resume` requires `candidate_sha`")
expect(repair).toContain("no tag yet means `resume`; an existing tag means `repair`")
expect(repair).toContain("Resume never mints a fresh admission")
expect(repair).toContain("requires the target tag to be absent")
expect(repair).toContain("Resume completes when the immutable tag targets the admitted candidate")
expect(repair.match(/-f operation=maintenance/g)).toHaveLength(1)
expect(repair.match(/-f operation=resume/g)).toHaveLength(1)
expect(repair.match(/-f operation=repair/g)).toHaveLength(2)
expect(repair).toContain("Repair completes when the immutable tag still targets the admitted candidate")
expect(repair).toContain("the GitHub Release targets that commit")
Expand Down
Loading