feat(boxel-cli): auto-publish unstable per merge, repurpose manual workflow as stable promoter#4804
Conversation
…rkflow as stable promoter Closes CS-11112. PR-time enforcement shrinks to "conventional PR title" — no more local regen, no more hand-bumping plugin.json, no more four-gate CI block. On every merge to main that touches packages/boxel-cli/**, a new on-main workflow regenerates plugin/skills/, computes per-surface bumps from the merged PR title (fetched via gh api), commits back to main, and publishes @cardstack/boxel-cli@<v>-unstable.<n> under npm dist-tag `unstable`. Stable releases stay deliberate — the renamed manual workflow promotes the latest unstable to `latest` rather than cutting fresh. - .github/workflows/boxel-cli-pr-title.yml — new; amannn/action-semantic-pull-request, path-scoped to boxel-cli - .github/workflows/boxel-cli-on-main.yml — new; regen + compute-release + bump + tag + publish, concurrency-serialized - .github/workflows/manual-boxel-cli-publish.yml — repurposed: strip -unstable.N, publish under latest - .github/workflows/ci-lint.yaml — drop the four boxel-cli verification gates (lines 128-224); plain pnpm run lint stays - packages/boxel-cli/scripts/compute-release.ts — new; pure function + I/O wrapper. Handles prerelease base escalation. - packages/boxel-cli/tests/scripts/compute-release.test.ts — 29 vitest cases - packages/boxel-cli/plugin/README.md — rewrite Versioning + Releasing for the new flow - AGENTS.md — add "boxel-cli commit prefixes" subsection - packages/boxel-cli/package.json — seed 0.1.4 → 0.1.5-unstable.0 - docs/cs-11112-auto-publish-boxel-cli-unstable-plan.md — implementation plan doc
The seed bump to 0.1.5-unstable.0 (and subsequent auto-unstable versions like 0.2.0-unstable.7) broke the strict /^\d+\.\d+\.\d+$/ regex. Accept optional semver prerelease tail.
Move the `merge_group:` caveat out of the `on:` block (indent 2, which matched neither neighbor) into the file-header comment block at column 0, where the rest of the workflow's design rationale lives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the prefix → bump-level map to release-prefixes.json so the pre-merge PR title check (amannn/action-semantic-pull-request) and the post-merge classifier (compute-release.ts → classifyBumpFromTitle) read from one file. Previously the prefix list was duplicated, and a new prefix added in one place would silently drift from the other. - New: packages/boxel-cli/scripts/release-prefixes.json - compute-release.ts: import the JSON instead of inlining BUMP_BY_PREFIX - tsconfig.json: enable resolveJsonModule for the TS import - boxel-cli-pr-title.yml: sparse-checkout the JSON, extract keys via `node -e`, feed them to amannn's `types:` input Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63d5c176de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| ref: main |
There was a problem hiding this comment.
Pin release job to triggering SHA
The checkout step hard-codes ref: main, so queued or re-run jobs can execute against a newer branch tip than the push event they are supposed to release. In practice, if two packages/boxel-cli/** merges happen close together, the earlier run can classify and version the later commit (or include prior bot bump commits), which leads to incorrect bump decisions and duplicate/shifted unstable versions. Check out ${{ github.sha }} (the triggering commit) to keep release computation deterministic per event.
Useful? React with 👍 / 👎.
| if: steps.pr.outputs.skip != 'true' | ||
| env: | ||
| NEXT_NPM: ${{ steps.release.outputs.nextNpm }} | ||
| NEXT_PLUGIN: ${{ steps.release.outputs.nextPlugin }} | ||
| run: | | ||
| set -euo pipefail | ||
| git add packages/boxel-cli/package.json \ | ||
| packages/boxel-cli/plugin/.claude-plugin/plugin.json \ | ||
| packages/boxel-cli/plugin/skills |
There was a problem hiding this comment.
Block plugin skill commits when bump resolves to none
This commit/push step runs even when pluginBump is none, and it always stages packages/boxel-cli/plugin/skills. If a PR title maps to a no-release prefix (for example chore:) but regenerated skill content changes, the workflow will commit those skill updates without bumping plugin.json version, so marketplace clients keyed by version will not pick up the new content. Gate this step on bump outputs or fail when generated plugin artifacts changed without a version bump.
Useful? React with 👍 / 👎.
Summary
Closes CS-11112.
PR-time enforcement for boxel-cli shrinks to "write a conventional PR title." Every merge to
maintouchingpackages/boxel-cli/**auto-publishes@cardstack/boxel-cli@<v>-unstable.<n>to npm under dist-tagunstable(Ember canary pattern). Stable releases stay deliberate via the repurposed manual workflow, which promotes the latest unstable tolatest.What changes
.github/workflows/boxel-cli-pr-title.yml(new) validates the PR title viaamannn/action-semantic-pull-request, path-scoped topackages/boxel-cli/**. Four old CI gates inci-lint.yaml(lines 128-224) are removed — no more local regen, no more hand-bumpingplugin.json..github/workflows/boxel-cli-on-main.yml(new) regeneratesplugin/skills/, fetches the merged PR's title viagh api repos/.../commits/<sha>/pulls, classifies the bump, applies per-surface version updates, commits back with[skip ci], tags, and publishes to npm. Concurrency-serialized to prevent prerelease-counter collisions..github/workflows/manual-boxel-cli-publish.ymlrepurposed — strips-unstable.N, publishes underlatest, tags, creates a non-prerelease GitHub Release. Input is now aconfirm: 'promote'typed string.packages/boxel-cli/scripts/compute-release.ts(new) is the pure decision engine. Maps PR-title prefix → bump level, checks per-surface (src/vsplugin/), handles prerelease base escalation (e.g.0.1.5-unstable.X+ breaking change →1.0.0-unstable.<n>). 29 vitest cases.plugin/README.mdVersioning + Releasing rewritten for the new flow, including theBOXEL_SKILLS_VERSIONfix(skills):caveat.AGENTS.mdgains a "boxel-cli commit prefixes" subsection.package.jsonbumped0.1.4→0.1.5-unstable.0so the first auto-publish after this merges has a clean starting state.Test plan
pnpm test:unitcoverscompute-releaselogic — 29 / 29 passing.pnpm lintclean.PR_TITLE='feat: test' pnpm exec ts-node --transpileOnly scripts/compute-release.tsemits valid JSON.boxel-cli-on-main.yml. Expected behavior:feat(boxel-cli): …→ minor bump.scripts/compute-release.ts,tests/scripts/,package.json) →0.1.5-unstable.0escalates to0.2.0-unstable.<n>, publishes to npm underunstable.plugin/README.md) →plugin.json0.1.4→0.2.0.fix:PR touchingsrc/once this lands; verify it publishes0.2.0-unstable.<n+1>.manual-boxel-cli-publish.ymlwithconfirm: promote; verify it strips the suffix and publisheslatest.Notes for reviewer
main. Required path-filtered checks would leave non-boxel-cli PRs pending forever — discussed in the plan doc.[skip ci]AND the workflow job guardsif: github.actor != 'github-actions[bot]'.boxel-cli-releaseis shared with the manual workflow so a stable promotion can't race an auto unstable publish.