Skip to content

release: rt release app <name> for single-app patch releases - #464

Merged
m4ttheweric merged 16 commits into
mainfrom
rt-release-app
Sep 25, 2026
Merged

m4ttheweric merged 16 commits into
mainfrom
rt-release-app

Conversation

@m4ttheweric

@m4ttheweric m4ttheweric commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds rt release app <name>, a single served-app patch release as one resumable verb. It runs the whole pin-only fast path (bump, bundle, merge, notes, tag, verify) for one app, so shipping a board fix no longer takes about 25 hand-run commands.

What it does

  • Qualify
    • <name> must be an apps-monorepo row whose pin keeps the fast path (board, chat, console, boxscore).
    • The last tag is read from origin (git ls-remote --tags --refs origin 'v*'), never from local tags.
    • Since that tag, origin/main may carry only RELEASE_NOTES.md and website/. Once deps.lock is in the diff, preflight's checkGate runs, then a stricter check: every row stays identical except version, url and sha256 on serve-only rows, and schema and arch are unchanged.
    • Anything else is refused with a pointer to /rt:release, and so is an existing tag for the next app version.
    • If the newest tag's publish has not verified (run, notes, assets, state), the verb re-verifies that tag instead of stacking a new release on it.
  • Bump:
    • Sets apps/<name>/package.json and the app's "apps/<name>" workspace version in the apps repo's root bun.lock to the next patch, in one fast-forward-only commit through the git data API on top of the apps head that qualify read.
    • Refuses if bun.lock doesn't record the current pin. That is checked in qualify, so a dry run reports it too.
    • A failed ref update is diagnosed the same way as the notes commit (apps main moved, not allowed, or network).
    • If a human already bumped past the pin, that version is used and the bump is skipped.
  • What counts as a change: "changes since the pin" covers apps/<name> plus every workspace package it depends on, transitively. Any dependency whose name is a workspace package counts, whatever its specifier. So do root catalog entries the app or those packages use (catalog: and named catalogs) that changed between the pin tag and main, and the notes name them.
  • Bundle:
    • Dispatches bundle-apps.yml (apps=<name>, dry_run=false), finds its run, and watches it with verify's tolerant poll, never gh run watch --exit-status.
    • bundle-apps.yml now has a run-name carrying its inputs. A run already building the app is adopted by that title, never a dry run or an all run.
    • The in-flight check comes before the app-tag check, so a run whose pr job is still going is waited out rather than reported as a missing PR.
  • Bot PR:
    • The PR must be the workflow's own: bundle-ci/<run>, not cross-repository, and authored by the release-token account.
    • Its diff must move only <name>'s row. That row may differ from its base only in version, url and sha256, plus update-lock.ts's fixed status, archive and extract.
    • It downloads the published asset and checks its sha256 against the row.
    • It runs codesign --verify --strict and checks Identifier=com.mattstack.helper.<name> under a Developer ID authority, with TeamIdentifier equal to the DEVELOPMENT_TEAM in rt-tray/project.yml. The binary is never executed.
    • It waits for CI green, ignoring CodeRabbit entirely. A gh pr checks error is an error after three in a row, never pending.
    • It squash-merges with --match-head-commit. The PR search covers every state (filtered to the workflow's PRs for the target version), so a closed bot PR is refused with the reopen command rather than reported missing.
  • Notes:
    • RELEASE_NOTES.md for last-tag..main gets one section per moved app, built from git log of the app and its workspace packages between the old and new app tags. Subjects only; PR numbers become m4ttstack/apps#N so they don't link to rt PRs.
    • A "Held pins" section records every other app whose code moved since its pin, and the plan lists the same apps.
    • A Full Changelog link closes the notes, and there are no em or en dashes.
    • Approval is a y/N prompt on a TTY. Off a TTY or with --json, the run stops with the notes, their notesHash and a resume command. --yes-notes takes only that 12-character hash. The tag form is a usage error, and a hash for other or changed notes is refused with the new notes and hash.
    • Existing notes on main are reused only when their subject is chore(release): notes for <tag> and they landed after the last pin move.
  • Commit, tag, verify:
    • The notes commit goes through the git data API with a fast-forward-only ref update. A failed update is diagnosed as main moved, not allowed (auth or protection), or a network error.
    • Just before tagging, the gate runs again on the commit being tagged. Then git tag -a vX.Y.(Z+1) goes on exactly that commit and is pushed; a local-only tag is accepted only when it points at that commit.
    • runVerify runs, the same checks as rt release verify. A releases/latest that is still propagating ends as pending with rt release verify <tag> to recheck, not failed.

Every step detects whether it already happened, so rerunning after a failure resumes: bump already on main, run in flight or already published, PR already merged, notes already committed, tag already pushed. Each failure names the step and the resume command.

With no name on a TTY, rt release app opens the rt-ui picker over the eligible apps with their pinned versions (e.g. board 0.1.7). The picker is gated on isTTY && !json && !RT_BATCH, and the tree declares omitBehavior: "picker". Off a TTY, or with --json or RT_BATCH, it gives the usual usage error.

rt release verify also changes for everyone: it now looks for the tag-push run for about three minutes before calling it missing, and it takes a skipLatest option.

Decisions worth a look

  • deck is refused, confirmed by Matt. Preflight's gate classifies a deck pin as full-gate (the walkthrough gates it), so the verb refuses deck and points at /rt:release.
  • No release.yml rehearsal before the tag, per the approved design. The rt:release skill's step 8 now records that ruling. The gate only admits serve-only pins, notes and website/, so the tag runs the same pipeline the last tag already ran. The tag run still builds, notarizes and clean-rooms.
  • Empty releases are refused. With no commits under the app or its workspace packages since its pinned tag, and no pending release of it, the verb says there's nothing to release.
  • Exit codes: released, planned and awaiting-approval exit 0; pending, declined and failed exit 1.
  • Bot PR author: BUNDLE_PR_AUTHOR is the release-token account (m4ttheweric), the only login the workflow opens PRs as.
  • The apps history comes from a blobless bare clone in a temp dir, never the shared mattstack-apps checkout.

Also:

  • checkGate takes an optional ref.
  • keepsFastPath and pollRunCompletion are exported.
  • DepsRow gains sha256.
  • The e2e harness gains a cwd option.
  • docs/release-and-distribution.md describes the verb and the run-name contract.
  • The rt:release skill gets a "Fast path: one served-app fix" section: the --yes-notes <hash> resume, the pending and refusal outcomes, and exactly which preflight checks the verb runs. Step 2b's bump now says a direct commit to apps main, no PR.

Follow-ups (from the second review, not in this PR)

  • Held apps go stale: the held list is computed in qualify and can be out of date by the notes step; recompute it there.
  • Slow publish reads as failed: a release.yml run still going past the 60-minute watch is reported failed; return pending while the run itself is pending.
  • Pin reverts: a pin moving backwards reads as a normal release with "version bump only"; refuse when the pin is lower than the shipped version.
  • Bot PR identity: the PR author is hardcoded (m4ttheweric); optionally also require the head commit author bundle-apps workflow and that the run is a bundle-apps run on main.
  • Nit: --json does not stop at the notes when they are already committed (intended resume behavior), but the tree hint and the skill say it always stops.
  • Nit: after a --yes-notes refusal the resume field starts with prose, while the skill says to run the new resume.

How to use

rt release app board --dry-run                          # print the plan, change nothing
rt release app board                                    # on a terminal: runs, asks y/N on the notes
rt release app board --json                             # agents: stops at the notes (awaiting-approval, notesHash, resume)
rt release app board --json --yes-notes <notesHash>     # after the notes are approved (the hash only)
rt release app                                          # on a terminal: pick the app

Verification

  • Unit (TDD): lib/release/__tests__/release-app.test.ts covers the pure pieces: qualification, versions, the pin-only lock gate, the bot-PR row rules, codesign team and identity, the notes hash, run-name parsing, held pins, workspace deps and notes rendering.
  • Orchestrator: lib/release/__tests__/release-app-run.test.ts (55) drives the verb against a stateful fake of git and GitHub. That covers every review fix (origin tags, the stricter gate, bound approval, re-verifying an unverified tag, pending verify, website-only main, a non-pin merge mid-run, the tag-time gate, stale notes, notes-subject reuse, run-name adoption, the PR author and fork check, strict codesign and team, gh pr checks errors, a closed PR and ref-update diagnosis) as well as the original flows. Round 3 adds the combined package.json and bun.lock bump commit, a stale bun.lock refusal, apps main moving under the bump, a catalog-only change, a closed bot PR found by the scan, and the tag form or a stale hash refused. Mutation-checked: removing each of 23 fixes turns its test red, including the notes-time gate, the stale-notes ancestor check and the bun.lock half of the bump.
  • verify: new tests for waiting on a late tag-push run and for skipLatest.
  • Command: commands/__tests__/release-app.test.ts (15) covers the picker path (fake picker seam), usage errors, --yes-notes taking only a 12-hex hash (the tag form is a usage error, and the value is never taken for the app name), and exit codes including pending.
  • e2e: e2e/tests/release-app.test.ts runs rt release app board --dry-run --json with the compiled binary under an isolated HOME. It uses real git against local bare repos and a journaling gh shim, with no network. It asserts the plan, the exact read-only gh calls, and that both bare remotes' refs are byte-identical before and after.
  • Skill, tested with fresh agents on paper scenarios, with a baseline before each round of edits. Round 1 took a board fix from about 25 hand-run commands to about 6. In round 2, the baseline missed the hash, the mismatch handling, the step 8 contradiction and the scope of the checks; after the edit, all six probes were answered from the skill text.
  • Round 3 ran targeted tests only (the release lib suite, the command test and the release-app e2e), plus bunx tsc --noEmit, bun run picker:check, bun run docs:check (reference regenerated); CI runs the suites. No real bundle-apps run, tag or release was run.

🤖 Generated with Claude Code

m4ttheweric and others added 7 commits September 25, 2026 10:14
…RunCompletion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o remote calls

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-app fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 57 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 85 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5c9a6d41-19a3-46c0-b390-8a6341692d87

📥 Commits

Reviewing files that changed from the base of the PR and between 3b687ae and 61a9b53.

📒 Files selected for processing (17)
  • .github/workflows/bundle-apps.yml
  • commands/__tests__/release-app.test.ts
  • commands/release.ts
  • docs/release-and-distribution.md
  • e2e/harness.ts
  • e2e/tests/release-app.test.ts
  • lib/command-tree-def.ts
  • lib/release/__tests__/preflight.test.ts
  • lib/release/__tests__/release-app-run.test.ts
  • lib/release/__tests__/release-app.test.ts
  • lib/release/__tests__/verify.test.ts
  • lib/release/preflight.ts
  • lib/release/release-app.ts
  • lib/release/verify.ts
  • skills/rt-release/SKILL.md
  • website/docs/reference/release/app.mdx
  • website/docs/reference/release/index.mdx

Comment @coderabbitai help to get the list of available commands.

m4ttheweric and others added 9 commits September 25, 2026 11:07
…ers skip releases/latest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…val, and review fixes

Last tag from origin, not local tags; the gate refuses any deps.lock change
beyond a serve-only pin and runs again on the commit being tagged; an
unverified newest tag is re-verified before a new release; --yes-notes binds
to the notes hash; bundle-apps runs carry a run-name so in-flight runs are
matched by title; the bot PR must be the workflow's own and its binary must
pass codesign --verify --strict under the release team; workspace packages
count toward an app's changes; held pins are listed in the plan and notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blish exits 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the verb checks, step 8 ruling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, hash-only approval, catalog and name-linked deps, all-state bot PR scan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r; --yes-notes takes only the hash

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@m4ttheweric
m4ttheweric merged commit abef596 into main Sep 25, 2026
8 checks passed
@m4ttheweric
m4ttheweric deleted the rt-release-app branch September 25, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant