feat(recovery): a reusable workflow for the (B) cut-recovery, so adopters need no local rt #1135

Closed
opened 2026-09-04 16:23:23 +02:00 by bosun · 0 comments
Owner

An adopter whose cut was interrupted can only recover by cloning the repo, obtaining rt through fetch-rt.sh, running rt recover-pending-cut, committing and pushing. That is the local-tooling burden the toolkit exists to remove, and it lands on them at their worst moment.

What to build

reusable-recover-pending-cut.yml — a consumer-callable workflow that runs the verb (#1130) and opens a PR with the folded CHANGELOG.

Shape, and the constraints are the substance

  • workflow_dispatch only. Never push, never schedule. release-cut.yml's own comment states the reason: a push trigger claims the concurrency group before any decision is made.
  • Opens a PR; does not push to main. reusable-release.yml's rt manifest-pr step is the pattern.
  • Takes the version as an explicit input the operator types — a confirmation, not a lookup. Firing on the wrong version is the one way this does damage.
  • No new state checking. rt recover-pending-cut already refuses unless the tree is in exactly the state recovery (B) repairs; the workflow inherits that.

🔴 It must never run automatically

docs/integration.md § Cut-cancellation recovery (#417) states that release-toolkit fails closed rather than auto-heals, because the choice between (A) and (B) is destructive in one direction: (B) applied to a published release orphans it. An automatic recovery always picks (B).

The tool reports which recovery applies. A human still chooses. This workflow automates the execution of (B), never the decision to use it. (A) stays entirely manual.

AC

  • a consumer can recover from pending_cut without a local checkout or a local rtscope, stated because this is a state claim: verified STRUCTURALLY. The workflow now bootstraps rt on the runner (composite/bootstrap-rt, the same artifact the four PR-CI gates use), so nothing local is required. It has NOT been exercised end-to-end against the live stuck cut — that run is the operator's per the dispatch, and no arm here substitutes for it.
  • workflow_dispatch only — an arm reddens if any other trigger is added
  • it opens a PR; main is not written directly
  • a version input that does not match the pending version refuses, and the refusal names both
  • the workflow is in the .gitea twin set and passes the drift gate
  • an arm asserting (A) is not reachable from this workflow

🔴 AC1 was FALSE when this PR was first pushed, and every arm was green

Dispositioning these boxes is what caught it. The reusable called rt bare with no acquisition step, so on an adopter's runner every invocation is a command-not-found — the exact condition AC1 denies. It survived four mutation-verified arms, gitea-twin --check, register-check and 98 bats because every arm greps the run block's TEXT. A gate that grades the form of a step cannot grade whether its program exists.

Fixed in 63f3337 with the three-step bootstrap, plus two arms mutated separately (drop the bootstrap → one red; git add -u back to -A → a different red).

📌 The staging was a second defect the first would have hidden: the toolkit checkout lands in .release-toolkit/ inside the consumer's tree, and git add -A would have committed it into their repository. It is now git add -u, which also makes the staging read the same population as the git diff --quiet gate above it — tracked-only.

Depends on

#1094 — the split credential pair. This writes to the consumer's repo and opens a PR there, so it needs the git credential and the API token, tiered as reusable-release.yml already does (RELEASE_TOOLKIT_GIT_TOKEN / RELEASE_TOOLKIT_API_TOKEN / RELEASE_TOKEN_OVERRIDE).

  • #1130 — the verb this wraps, on main at 38a2b73
  • #1128 — after the fold, decide routes to update and re-prepares
  • #1101 — the trigger that produces this state; unrepaired
An adopter whose cut was interrupted can only recover by cloning the repo, obtaining `rt` through `fetch-rt.sh`, running `rt recover-pending-cut`, committing and pushing. That is the local-tooling burden the toolkit exists to remove, and it lands on them at their worst moment. ## What to build `reusable-recover-pending-cut.yml` — a consumer-callable workflow that runs the verb (#1130) and opens a PR with the folded CHANGELOG. ## Shape, and the constraints are the substance - **`workflow_dispatch` only.** Never `push`, never `schedule`. `release-cut.yml`'s own comment states the reason: a push trigger claims the concurrency group before any decision is made. - **Opens a PR; does not push to `main`.** `reusable-release.yml`'s `rt manifest-pr` step is the pattern. - **Takes the version as an explicit input** the operator types — a confirmation, not a lookup. Firing on the wrong version is the one way this does damage. - **No new state checking.** `rt recover-pending-cut` already refuses unless the tree is in exactly the state recovery (B) repairs; the workflow inherits that. ## 🔴 It must never run automatically `docs/integration.md § Cut-cancellation recovery (#417)` states that release-toolkit fails closed rather than auto-heals, because the choice between (A) and (B) is destructive in one direction: **(B) applied to a published release orphans it**. An automatic recovery always picks (B). The tool reports which recovery applies. A human still chooses. This workflow automates the *execution* of (B), never the *decision* to use it. **(A) stays entirely manual.** ## AC - [x] a consumer can recover from `pending_cut` without a local checkout or a local `rt` — **scope, stated because this is a state claim:** verified STRUCTURALLY. The workflow now bootstraps `rt` on the runner (`composite/bootstrap-rt`, the same artifact the four PR-CI gates use), so nothing local is required. It has NOT been exercised end-to-end against the live stuck cut — that run is the operator's per the dispatch, and no arm here substitutes for it. - [x] `workflow_dispatch` only — an arm reddens if any other trigger is added - [x] it opens a PR; `main` is not written directly - [x] a version input that does not match the pending version refuses, and the refusal names both - [x] the workflow is in the `.gitea` twin set and passes the drift gate - [x] an arm asserting (A) is not reachable from this workflow ### 🔴 AC1 was FALSE when this PR was first pushed, and every arm was green Dispositioning these boxes is what caught it. The reusable called `rt` bare with **no acquisition step**, so on an adopter's runner every invocation is a command-not-found — the exact condition AC1 denies. It survived four mutation-verified arms, `gitea-twin --check`, `register-check` and 98 bats because **every arm greps the run block's TEXT**. A gate that grades the form of a step cannot grade whether its program exists. Fixed in `63f3337` with the three-step bootstrap, plus two arms mutated separately (drop the bootstrap → one red; `git add -u` back to `-A` → a different red). 📌 The staging was a second defect the first would have hidden: the toolkit checkout lands in `.release-toolkit/` inside the consumer's tree, and `git add -A` would have committed it into **their** repository. It is now `git add -u`, which also makes the staging read the same population as the `git diff --quiet` gate above it — tracked-only. ## Depends on **#1094** — the split credential pair. This writes to the consumer's repo and opens a PR there, so it needs the git credential and the API token, tiered as `reusable-release.yml` already does (`RELEASE_TOOLKIT_GIT_TOKEN` / `RELEASE_TOOLKIT_API_TOKEN` / `RELEASE_TOKEN_OVERRIDE`). ## Related - #1130 — the verb this wraps, on main at `38a2b73` - #1128 — after the fold, `decide` routes to `update` and re-prepares - #1101 — the trigger that produces this state; unrepaired
bosun closed this issue 2026-09-04 16:54:37 +02:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1135
No description provided.