From ff677d820c2d5ef6e46fef00a241b17eac6b2ce0 Mon Sep 17 00:00:00 2001 From: atuin-bot <152089506+atuin-bot@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:52:04 -0700 Subject: [PATCH] chore(release-skill): time label-gated PR merges around the release (#3654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teaches the `/release` skill about the `merge-immediately-before-release` and `merge-immediately-after-release` labels. ## Why Both labels exist on the repo and are in active use — #3651 is open right now carrying `merge-immediately-after-release`, with `[DO NOT MERGE UNTIL RELEASE]` in the title. The title prefix is the tell: the skill has no idea these labels exist, so maintainers have been hand-signalling the timing instead. I checked what an agent actually does today. Given four open PRs and no hint about the labels, it produces a complete nine-step release plan and never runs `gh pr list` even once. The labeled PR is simply invisible to it and gets stranded. ## What the labels mean These mark PRs — typically ones touching `docs/` or `install.sh` — that deploy on merge to `main` (via `trigger-docs-deploy.yml`) rather than shipping inside the release artifact. So their merge has to be timed around the release rather than left to land whenever. | Label | Merged at | In the tag & artifact? | |-------|-----------|------------------------| | `merge-immediately-before-release` | Step 7.5 — after the prep PR merges, before the tag | Yes | | `merge-immediately-after-release` | Step 8.5 — after the release workflow uploads assets | No | An `after-release` PR being absent from the tag and artifact is the intended outcome, not a problem to fix. An author who needed the change in the artifact would have used the before-release label. ## Changes - **Step 2.5** surveys both labels up front and asks which of the PRs found should in fact be merged at their labeled time. Declined PRs are dropped for the rest of the run. - **Step 7.5** merges before-release PRs after the prep PR lands and before the tag. They're absent from the changelog, which was cut earlier — that's expected, and the step says so to stop anyone regenerating it. - **Step 8.5** merges after-release PRs only once the release workflow has finished and the assets exist. `install.sh` fetches `releases/latest/download/...`, so merging on tag-push alone would put an installer live against a release that's still building. Aborts and merges nothing if the workflow failed or assets are missing. Each merge takes a second confirmation at the point of merging: the survey approves the *timing*, the checkpoint approves merging *now*. ## Drive-by fix Step 7's `gh pr create` was missing a line continuation after `--repo atuinsh/atuin`, so `--draft` parsed as a separate command — the PR got created **non-draft** and the only symptom was a `--draft: command not found` that's easy to miss in scrollback. It's valid bash syntax (`--draft` is just a command name that doesn't exist), so nothing catches it at parse time. Scanned every other shell block in the file for the same defect class; this was the only instance. ## Testing Behaviour was verified with paper dry-runs against the skill text — no real release was run. - **Baseline, labels hidden:** never checked the labels at all. Confirms the gap is real. - **Baseline, labels handed over:** still diverged — merged the before-release PR before the clone, invented its own asset-waiting rule, and said it was "inferring policy from label names." - **With the new steps:** placement matches spec exactly, both confirmations fire, unlabeled PRs aren't surfaced as candidates, changelog left alone. - **Pressure test** (flight deadline + "that label was probably a mistake" + a plausible technical argument for merging early): held the line, and did it by reasoning rather than rule-citing — it worked out that `install.sh` never ships inside the artifact anyway, so merging early wouldn't achieve what was asked and *would* open the broken-installer window. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .atuin/skills/release/SKILL.md | 144 ++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/.atuin/skills/release/SKILL.md b/.atuin/skills/release/SKILL.md index a3c8f30e..c84fe511 100644 --- a/.atuin/skills/release/SKILL.md +++ b/.atuin/skills/release/SKILL.md @@ -48,6 +48,60 @@ After determining the version: --- +## Step 2.5 — Survey Release-Labeled PRs + +Two labels mark PRs whose merge has to be timed around the release. They are +typically PRs touching `docs/` or `install.sh` — changes that deploy on merge to +`main` rather than shipping inside the release artifact. + +| Label | Merged at | In the tag & artifact? | +|-------|-----------|------------------------| +| `merge-immediately-before-release` | Step 7.5 — after the prep PR merges, before the tag | Yes | +| `merge-immediately-after-release` | Step 8.5 — after the release workflow uploads assets | No | + +Check both: + +```bash +gh pr list --repo atuinsh/atuin --state open \ + --label merge-immediately-before-release \ + --json number,title,url +gh pr list --repo atuinsh/atuin --state open \ + --label merge-immediately-after-release \ + --json number,title,url +``` + +If both come back empty, say so and continue to Step 3. No checkpoint needed. + +The two labels are mutually exclusive: they name opposite sides of the tag. If a +PR number comes back in **both** lists, that is a labelling mistake — the two +queries are independent, so it would otherwise be approved twice, merged at Step +7.5, then merged again at Step 8.5 against an already-closed PR. **Stop** and ask +the user which single timing applies, then treat it as carrying only that label +for the rest of the release. Do not guess, and do not carry it into both steps. + +If either is non-empty: + +1. Show the user every PR found, grouped by label, with its number, title, and + the merge point from the table above. +2. **Checkpoint:** Use AskUserQuestion (multiSelect) to ask which of these PRs + should in fact be merged at their labeled time. +3. Record the approved set — it carries to Step 7.5 and Step 8.5. A PR the user + declines is left alone for the remainder of the release: don't merge it, + don't raise it again. + +Take each label at face value. It records the PR author's decision about timing; +it is not a suggestion for you to re-evaluate: + +- An `after-release` PR is **not** in the tag and **not** in the release + artifact. That is the intended outcome, not a problem to solve. The label is + for changes that deploy the moment they merge, so artifact inclusion is + irrelevant to them. An author who needed the change in the artifact would have + labeled it `merge-immediately-before-release`. +- Never merge an `after-release` PR early to get it into the release, and never + reclassify a PR from one label to the other. + +--- + ## Step 3 — Set Up Working Directory Clone a fresh copy into a temp directory: @@ -149,7 +203,7 @@ Create the PR: gh pr create \ --title "chore(release): prepare for release " \ --body "" \ - --repo atuinsh/atuin + --repo atuinsh/atuin \ --draft ``` @@ -159,6 +213,37 @@ When the user reports the PR is merged, proceed to the next step. --- +## Step 7.5 — Merge `merge-immediately-before-release` PRs + +Skip to Step 8 unless Step 2.5 recorded approved PRs with this label. + +The prep PR has merged and the tag does not exist yet. This is the only window +in which these PRs land in `v` and its artifacts. + +For each approved PR: + +1. Check it is still mergeable and its checks are green: + ```bash + gh pr view --repo atuinsh/atuin \ + --json mergeable,mergeStateStatus,statusCheckRollup + ``` + If it is not mergeable, or checks are red, report that and ask the user how + to proceed. Do not force it through. + +2. **Checkpoint:** Show the PR number and title and ask the user to confirm this + specific merge. Step 2.5 approved the *timing*; this confirms merging *now*. + +3. Merge: + ```bash + gh pr merge --repo atuinsh/atuin --squash + ``` + +These commits land in the tag but are absent from the changelog generated in +Step 5, which was cut before they merged. That is expected — do not regenerate +the changelog to include them. + +--- + ## Step 8 — Tag Release Back in the working directory: @@ -175,6 +260,62 @@ triggered. --- +## Step 8.5 — Merge `merge-immediately-after-release` PRs + +Skip to Step 9 unless Step 2.5 recorded approved PRs with this label. + +"After the release" means **after the release workflow has finished and the +assets exist** — not merely after the tag was pushed. These PRs deploy the +instant they merge, so a docs change announcing the new version must not go +live while the release it points at is still building. + +1. Wait for the release workflow the tag just triggered: + ```bash + gh run list --repo atuinsh/atuin --workflow release.yml \ + --branch "v" --limit 1 + gh run watch --repo atuinsh/atuin --exit-status + ``` + `--exit-status` is what makes a failed run exit non-zero. Without it + `gh run watch` prints the failure but still exits `0`, and a broken release + sails through into the merges below. If it exits non-zero, **stop** here — + do not go on to the release lookup. + +2. Confirm the release exists and has assets attached: + ```bash + gh release view "v" --repo atuinsh/atuin --json assets,isDraft + ``` + If the release is missing, is still a draft, or has no assets, **stop**. + Report it and merge nothing — these PRs keep until the release is actually + out. A release object alone is not evidence: it may predate this run or have + been left half-built by a failed workflow. What clears this gate is a + non-draft release with assets attached. + +3. Re-check that each approved PR is still mergeable and its checks are green: + ```bash + gh pr view --repo atuinsh/atuin \ + --json mergeable,mergeStateStatus,statusCheckRollup + ``` + Step 2.5 surveyed these PRs before the release was even built, so its + findings are now many minutes stale — long enough for a PR to pick up a + conflict or a red check. If it is not mergeable, or checks are red, report + that and ask the user how to proceed. Do not force it through. These PRs + deploy the instant they merge, so there is no CI gate downstream to catch a + bad one. + +4. **Checkpoint:** For each approved PR, show its number and title and ask the + user to confirm this specific merge. Step 2.5 approved the *timing*; this + confirms merging *now*. + +5. Merge each PR the user confirms: + ```bash + gh pr merge --repo atuinsh/atuin --squash + ``` + +Merging a `docs/` change here fires `trigger-docs-deploy.yml` and the docs go +out immediately. That is the whole point of the label. + +--- + ## Step 9 — Publish to crates.io **If this is a prerelease**, skip this step entirely and tell the user. @@ -211,5 +352,6 @@ Summarize what was done: - Version released - PR URL - Tag name +- Which label-timed PRs were merged, and which the user declined (if any) - Which crates were published (if any) - Working directory path and how to clean it up (`rm -rf`) -- knotmirror 0.0.1