Merging a release-labeled PR should BE the release — auto-tag + publish on merge, version transition as the interlock #96

Closed
opened 2026-07-19 15:13:33 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 15:13:33 +00:00 (Migrated from github.com)

Anchor issue for the three repos (rig and cast twins linked below), from the 0.7.0 ceremony retro.

The gap, as lived

box#95 merged with four approvals — and nothing happened, correctly: release.yml fires on a tag push that is a separate, manual, silent-when-forgotten step. The operator expected a release; the system delivered an absence. A forgotten tag produces no error and no red X — the worst failure shape. Meanwhile automation's failure shape (a red workflow run on main) is loud and retryable. When choosing between two unreliabilities, pick the loud one.

The ship decision doesn't need the tag to stay human: it already lives in the release PR — the one PR whose whole diff is "the version leaves -dev", carrying three bot reviews and the maintainer's. Merging it IS "hell yeah lfg". After that, tagging is transcription, and transcription is exactly where humans err and machines don't.

Design

The label is the intent; the version is the interlock. The agent opens the ceremony PR labeled release (already the hand-set convention in all three repos) with the bumped version and stamped changelog. On merge, the release happens.

  • Trigger: pull_request: closed targeting main, gated on merged == true AND the release label. Closed-unmerged never fires.
  • Asserts (in order, each fail-loud, creating nothing):
    1. the version at the merge commit is non--dev;
    2. the version changed in this PR (base vs merge) — the -dev transition as a safety check, so a mislabeled ordinary PR fails here loudly;
    3. the changelog's top section names exactly that version and extracts non-empty via the existing release-notes.sh;
    4. no tag and no release exist for it yet (idempotent re-runs).
  • Act, in the same job: create the tag at the merge commit via the API, then publish the GitHub release with the extracted notes (and build/attach assets where the repo has them — cast's cast-X.Y.Z.tgz). Same-job matters: a GITHUB_TOKEN-created tag does not trigger other workflows, which both forces the one-job shape and guarantees the legacy tag-push workflow cannot double-publish.
  • The existing tag-push path stays as the documented manual fallback and backfill (it served 0.7.0 itself).

Non-goals

No release-please, no auto-generated notes, no schedule — the curated-changelog culture and the PR-shaped decision stay exactly as #83 built them.

Twins: heavy-duty/rig (VERSION file, same shape) and heavy-duty/cast (version in package.json, plus the prebuilt asset step).

🤖 From the release-ceremony retro with the operator.

Anchor issue for the three repos (rig and cast twins linked below), from the 0.7.0 ceremony retro. ## The gap, as lived box#95 merged with four approvals — and nothing happened, correctly: `release.yml` fires on a tag push that is a separate, manual, *silent-when-forgotten* step. The operator expected a release; the system delivered an absence. A forgotten tag produces no error and no red X — the worst failure shape. Meanwhile automation's failure shape (a red workflow run on main) is loud and retryable. When choosing between two unreliabilities, pick the loud one. The ship decision doesn't need the tag to stay human: it already lives in the release PR — the one PR whose whole diff is "the version leaves `-dev`", carrying three bot reviews and the maintainer's. Merging it IS "hell yeah lfg". After that, tagging is transcription, and transcription is exactly where humans err and machines don't. ## Design **The label is the intent; the version is the interlock.** The agent opens the ceremony PR labeled `release` (already the hand-set convention in all three repos) with the bumped version and stamped changelog. On merge, the release happens. - **Trigger:** `pull_request: closed` targeting main, gated on `merged == true` AND the `release` label. Closed-unmerged never fires. - **Asserts (in order, each fail-loud, creating nothing):** 1. the version at the merge commit is non-`-dev`; 2. the version **changed in this PR** (base vs merge) — the `-dev` transition as a safety check, so a mislabeled ordinary PR fails here loudly; 3. the changelog's top section names exactly that version and extracts non-empty via the existing `release-notes.sh`; 4. no tag and no release exist for it yet (idempotent re-runs). - **Act, in the same job:** create the tag at the merge commit via the API, then publish the GitHub release with the extracted notes (and build/attach assets where the repo has them — cast's `cast-X.Y.Z.tgz`). Same-job matters: a `GITHUB_TOKEN`-created tag does not trigger other workflows, which both forces the one-job shape and guarantees the legacy tag-push workflow cannot double-publish. - **The existing tag-push path stays** as the documented manual fallback and backfill (it served 0.7.0 itself). ## Non-goals No release-please, no auto-generated notes, no schedule — the curated-changelog culture and the PR-shaped decision stay exactly as #83 built them. Twins: heavy-duty/rig (VERSION file, same shape) and heavy-duty/cast (version in `package.json`, plus the prebuilt asset step). 🤖 From the release-ceremony retro with the operator.
Sign in to join this conversation.
No milestone
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: heavy-duty/box#96
No description provided.