Skip to content

chore(release): collapse release flow to single human action#377

Merged
brianmhunt merged 3 commits into
mainfrom
chore/release-on-main-trigger
May 4, 2026
Merged

chore(release): collapse release flow to single human action#377
brianmhunt merged 3 commits into
mainfrom
chore/release-on-main-trigger

Conversation

@brianmhunt
Copy link
Copy Markdown
Member

@brianmhunt brianmhunt commented May 4, 2026

Summary

Implements the plan in #373 — switch release.yml from tag-push trigger to push-to-main trigger, collapse three jobs into two, and make merging the version PR the only human action in a release.

Changes

.github/workflows/release.yml

  • Trigger flips from push: tags: v* to push: branches: [main].
  • Two jobs (prepare / publish-and-tag) preserve least-privilege isolation:
    • prepare holds contents:write + pull-requests:write (no OIDC).
    • publish-and-tag holds contents:write + id-token:write (no PR write).
  • prepare uses persist-credentials: false paired with commitMode: github-api, so changesets/action commits the version bump via the API (verified, github-actions[bot]-authored).
  • publish-and-tag is gated on hasChangesets == 'false', so feature PRs with changesets do not trigger the publish path — their changesets accumulate in the version PR.
  • Build + test run inside publish-and-tag, gated by the outer job condition, so no-changeset paths skip them.
  • Publish uses changesets/action's publish input. Its published output is the authoritative "did anything go to npm" signal — closing the trap where a plan-only / doc-only main push would otherwise hit gh release create for an already-existing tag.
  • Per-package git tags suppressed via --no-git-tag; the repo-wide vX.Y.Z is the single tag.
  • gh release create creates tag ref + release in one API call. No more git tag && git push orphan-tag failure mode.
  • Concurrency block (workflow-ref) serializes against itself so two near-simultaneous main pushes can't race.
  • Prerelease detection anchored on canonical suffixes (*-alpha, *-alpha.*, etc.) — no false positives like 1.0.0-alphabet.

AGENTS.md

  • § Release Process rewritten around the single-action flow. Drops force-push-tag instructions.
  • Workflows table row for release.yml updated (trigger column: "Push to main").

Test plan

  • Land plan: collapse release to a single human action #373 first (the plan PR) so plans/single-action-release.md is on main when this PR merges and the workflow comment chain stays consistent. (Plan PR was deliberately not bundled here so the design discussion stayed on the plan.)
  • No-op patch release as the first run after this lands: one trivial changeset (e.g. typo), watch the version PR appear, merge it, watch publish run, confirm vX.Y.Z tag + GH release on the version-PR-merge commit.
  • Verify a doc-only push to main (e.g. README edit) runs only prepare, exits with should_publish=true, runs publish-and-tag, sees outputs.published=false, skips the tag step. (This is the "wasteful but safe" path described in the plan's Doc-only main pushes tradeoff.)
  • Verify a feature-PR-with-changeset merge runs only prepare, exits with should_publish=false, skips publish-and-tag entirely, and updates the open version PR.

Notes

This PR depends on #373 conceptually but does not require it to merge first to function — the workflow stands on its own. Recommend landing #373 first for review-trail clarity.

Adversarial review

Performed in commit message of 78d156f9. Fresh subagent flagged the gating-logic bug (conflating "no pending changesets" with "version PR just merged"); fixed by switching to changesets/action's published output. Remaining items accepted as documented tradeoffs.

Summary by CodeRabbit

  • Chores

    • Modularized CI by extracting build-and-test into a reusable workflow and updated main pipeline to delegate to it.
    • Simplified release pipeline to a single prepare flow that gates publish steps and runs the reusable build-and-test.
    • Added workflow concurrency control to avoid overlapping runs.
  • Documentation

    • Updated release process docs to reflect the new automated release flow and maintainers' guidance.

Per the plan in #373: trigger release.yml on push to main instead of
tag push, collapse three jobs into two, and make "merge the version
PR" the only human step.

release.yml:
- Trigger flips from `push: tags: v*` to `push: branches: [main]`.
- Two jobs (`prepare` / `publish-and-tag`) preserve least-privilege:
  prepare holds contents+pull-requests (no OIDC); publish-and-tag
  holds contents+id-token (no PR-write).
- prepare uses `persist-credentials: false` paired with
  `commitMode: github-api` so changesets/action commits the version
  bump via the GitHub API (verified, github-actions[bot]-authored).
- publish-and-tag is gated on `hasChangesets == 'false'` so feature
  PRs with changesets do not trigger the publish path; their
  changesets accumulate in the version PR.
- Build + test run inside publish-and-tag, gated by the outer job
  condition, so no-changeset paths skip them.
- Publish uses changesets/action's publish input so its `published`
  output is the authoritative "did anything go to npm" signal,
  closing the trap where every doc-only / plan-only main push would
  otherwise hit `gh release create` for an existing tag.
- Per-package git tags suppressed via `--no-git-tag`; the repo-wide
  `vX.Y.Z` is the single tag.
- `gh release create` creates tag ref + release in one API call.
- Concurrency block (`workflow-ref`) serializes against itself.
- Prerelease detection anchored on canonical suffixes (`*-alpha`,
  `*-alpha.*`, etc.) — no false positives like `1.0.0-alphabet`.

AGENTS.md:
- § Release Process rewritten around the single-action flow. Drops
  force-push tag instructions.
- Workflows table row for release.yml updated (trigger column:
  "Push to main").

Adversarial review: fresh subagent against this branch's diff
flagged 11 issues. Critical (gating logic conflated "no pending
changesets" with "version PR just merged") fixed by switching to
`changesets/action`'s `published` output. Plan-file reference in
workflow header dropped (plan lives on PR #373, not yet on main).
AGENTS.md "re-runs" wording sharpened. Remaining items accepted as
documented tradeoffs (concurrency latency on doc pushes, broader
credential scope on publish-and-tag, fetch-depth default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 13:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Extracts build/test into a reusable GitHub Actions workflow, updates main-build to call it, reworks release.yml to run on pushes to main and drive publishing via Changesets (gating build/test and publish/tag steps), and updates AGENTS.md to document the new main-branch release flow.

Changes

Release & CI workflow refactor (single DAG)

Layer / File(s) Summary
Reusable Workflow
.github/workflows/build-and-test.yml
Adds a new reusable workflow Build and Test triggered by workflow_call with a test job that runs in Playwright container: installs Bun, installs deps, builds, verifies ESM, runs Vitest (browser matrix + CLI), and uploads builds/knockout/dist as artifact tko.
Main Workflow Delegation
.github/workflows/main-build.yml
Replaces inline build/test job with a single job calling the local reusable workflow (uses: ./.github/workflows/build-and-test.yml), adds concurrency to serialize runs, preserves triggers (push to main, workflow_dispatch).
Release Workflow Restructure
.github/workflows/release.yml
Changes trigger from tag pushes to pushes on main; replaces multi-job tag-driven release flow with prepare job running changesets/action (outputs should_publish); conditionally invokes build-and-test; adds publish-and-tag job that builds, determines VERSION via tools/release-version.cjs, runs Changesets publish (publish path), and creates repo tag + GitHub Release only when publish reports success (prerelease detection by version suffix).
Documentation
AGENTS.md
Updates CI/CD table entry to show release.yml now triggers on Push to main and rewrites maintainer instructions to instruct merging the Changesets version PR as the single human action; documents no manual tag-push entry point and refers to github-release.yml for retries.

Sequence Diagram

sequenceDiagram
    actor Maintainer
    participant Git as Repository (main)
    participant GH as GitHub Actions
    participant Prepare as changesets prepare
    participant BuildTest as build-and-test
    participant Publish as publish-and-tag
    participant Registry as Package Registry
    participant Release as GitHub Release

    Maintainer->>Git: Merge Changesets version PR
    Git->>GH: Trigger release.yml (push to main)
    GH->>Prepare: Run changesets/action (prepare)
    Prepare-->>GH: Emit should_publish=true
    GH->>BuildTest: Invoke reusable build-and-test (when should_publish)
    BuildTest->>BuildTest: Install, build, run tests
    BuildTest-->>GH: Upload artifact (tko)
    GH->>Publish: Run publish-and-tag (when should_publish)
    Publish->>Publish: Determine VERSION (tools/release-version.cjs)
    Publish->>Registry: changesets publish (publish path)
    Registry-->>Publish: published=true
    Publish->>Release: Create tag & GitHub Release
    Release-->>Git: Push vX.Y.Z tag
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A workflow hopped from burrow to tree,
Build, test, and publish — all calling on me.
Merge the version PR, then watch the dance,
Artifacts collected, tags born by chance.
Hooray for automation — a carrot for CI! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'chore(release): collapse release flow to single human action' clearly and directly summarizes the main objective of the changeset—consolidating the release workflow so that merging the version PR is the only required human action, eliminating prior multi-step manual tagging requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/release-on-main-trigger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78d156f9e5

ℹ️ 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".

Comment thread .github/workflows/release.yml Outdated
id: publish
uses: changesets/action@v1
with:
publish: npx changeset publish --no-git-tag
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don’t rely on published when using --no-git-tag

This workflow now uses changesets/action output steps.publish.outputs.published to decide whether to create the repo-wide vX.Y.Z release tag, but the publish command is changeset publish --no-git-tag. In changesets/action, published is inferred from New tag: lines in publish stdout, and those lines are only emitted when git tags are created; with --no-git-tag, npm publish can succeed while published remains false. On version-PR merge commits, that causes the Create GitHub release step to be skipped, so packages can be published without creating the expected repo tag/release.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR rewrites the release pipeline around the standard Changesets “push to main” flow so that merging the version PR becomes the only manual release step, and updates contributor/maintainer docs to match.

Changes:

  • Switches release.yml from a tag-push trigger to push on main, collapsing the flow into prepare and publish-and-tag.
  • Moves publish, repo tag creation, and GitHub release creation into the post-version-PR-merge path, using changesets/action outputs to avoid spurious releases on no-op publishes.
  • Rewrites AGENTS.md release guidance and workflow table to document the new single-action release process.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
AGENTS.md Updates maintainer release documentation and the workflow trigger/purpose table.
.github/workflows/release.yml Replaces the tag-driven release workflow with a push-to-main Changesets flow and combines publish/tag/release logic into one job.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +111 to +118
# Build is gated on the publish path so doc-only / plan-only main
# pushes do not pay the cost.
- name: Build all packages
run: bun run build

# Tests run before publish so a regression caught only in the
# browser matrix cannot ship to npm. main-build.yml runs in
# parallel and is not a gate; this is the gate.
Comment thread .github/workflows/release.yml Outdated
Comment on lines +111 to +112
# Build is gated on the publish path so doc-only / plan-only main
# pushes do not pay the cost.
Comment thread .github/workflows/release.yml Outdated
Comment on lines +9 to +12
# - publish-and-tag: when no pending changesets remain (i.e. the
# version PR was just merged), build, test, npm-publish via OIDC,
# then create the repo-wide vX.Y.Z tag + GitHub release in one
# gh release create call.
Comment thread .github/workflows/release.yml Outdated
id: changesets
uses: changesets/action@v1
with:
version: npx changeset version
Comment thread .github/workflows/release.yml Outdated
id: publish
uses: changesets/action@v1
with:
publish: npx changeset publish --no-git-tag
Comment thread .github/workflows/release.yml Outdated
# Tests run before publish so a regression caught only in the
# browser matrix cannot ship to npm. main-build.yml runs in
# parallel and is not a gate; this is the gate.
- name: Run tests
Comment thread .github/workflows/release.yml Outdated
Comment on lines +93 to +94
# persist-credentials defaults to true — required for the
# post-publish tag/release step to authenticate.
Replace the in-job `bun run test` (which only ran headless because
publish-and-tag's runner has no Playwright browsers) with a real
browser-matrix gate.

- New `.github/workflows/build-and-test.yml` reusable workflow
  (`on: workflow_call`) holds the Playwright-container build + browser
  matrix + cli-happy-dom + ESM-extension verification + artifact
  upload.
- `main-build.yml` becomes a thin shim that calls the reusable on push
  to main / workflow_dispatch.
- `release.yml` adds a `build-and-test` job that calls the same
  reusable, gated on `should_publish == 'true'`. `publish-and-tag` now
  `needs: [prepare, build-and-test]`, so a red browser matrix blocks
  publish through standard GitHub Actions semantics — no polling.

Both callers pass `secrets: inherit` so future secret-using steps in
the reusable don't silently get nothing. main-build.yml gains a
concurrency block matching release.yml's so two rapid main pushes
cannot run two parallel Playwright runners against the same SHA.

publish-and-tag still runs `bun run build` locally to produce the
publishable dists in its worktree. The reusable is the gate, not an
artifact source.

Adversarial review: fresh subagent against this diff flagged 10 items.
Two actionable (secrets: inherit, main-build concurrency) — fixed.
Remaining items accepted as documented tradeoffs (double build per
push, cosmetic name on shim).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
AGENTS.md (1)

133-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the main-build.yml row while you're here.

This PR makes main-build.yml a wrapper over the reusable browser-matrix workflow, but the CI/CD table still describes it as Build + audit + headless test. Tightening that row now will keep the docs aligned with the workflow change described below.

Based on learnings: If PR replaces a test runner, environment, or matrix target, say so explicitly in the PR and justify the coverage delta; a test failing in a new environment is usually signal worth investigating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 133 - 159, Update the CI/CD table entry for
main-build.yml to reflect that it is now a wrapper around the reusable
browser-matrix workflow rather than the old "Build + audit + headless test"
description: find the table row for `main-build.yml` and replace the short
description with a concise, accurate summary such as "Wrapper over reusable
browser-matrix workflow — build + audit; run browser-matrix tests" (or
equivalent wording that calls out the reusable browser-matrix workflow and the
actual steps run).
🧹 Nitpick comments (2)
.github/workflows/release.yml (1)

63-75: ⚡ Quick win

Use Bun for the Changesets commands in the release path.

npx adds a second package-manager execution path to the workflow that cuts releases. Switching both invocations to bunx changeset ... keeps this pinned to the repo-managed toolchain and avoids an npm fallback in the critical path.

Suggested diff
-          version: npx changeset version
+          version: bunx changeset version
...
-          publish: npx changeset publish --no-git-tag
+          publish: bunx changeset publish --no-git-tag

Based on learnings: Use bun install instead of npm install, and bunx instead of npx; Bun is the package manager and script runner.

Also applies to: 151-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 63 - 75, Replace any npx
invocations used to run Changesets in the release workflow with bunx to pin
execution to the repo-managed Bun toolchain: specifically update the
changesets/action step where the input "version: npx changeset version" is set
(and the analogous occurrences later in the file) to use "bunx changeset ..."
instead of "npx changeset ..."; ensure any other release-related commands that
currently call npm/npx (e.g., install or run commands referenced near the
changesets usage) are switched to bun install / bunx equivalents so the workflow
uses Bun end-to-end.
.github/workflows/build-and-test.yml (1)

15-20: ⚡ Quick win

Lock this reusable workflow to read-only permissions.

This job only needs repository read access, but the shared workflow leaves GITHUB_TOKEN permissions implicit. Adding an explicit read-only boundary here keeps future callers from accidentally giving a pure build/test job write scopes.

Suggested diff
 on:
   workflow_call:
 
+permissions:
+  contents: read
+
 jobs:
   test:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-and-test.yml around lines 15 - 20, The test job
currently runs with implicit GITHUB_TOKEN scopes; add an explicit read-only
permission block to lock it down by adding a permissions mapping under the test
job (jobs -> test) such as permissions: contents: read (or other minimal
read-only scopes you need) so the job/container (container image
mcr.microsoft.com/playwright:v1.59.1-noble) only has repository read access;
ensure you place the permissions key at the same level as runs-on/container in
the test job.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@AGENTS.md`:
- Around line 133-159: Update the CI/CD table entry for main-build.yml to
reflect that it is now a wrapper around the reusable browser-matrix workflow
rather than the old "Build + audit + headless test" description: find the table
row for `main-build.yml` and replace the short description with a concise,
accurate summary such as "Wrapper over reusable browser-matrix workflow — build
+ audit; run browser-matrix tests" (or equivalent wording that calls out the
reusable browser-matrix workflow and the actual steps run).

---

Nitpick comments:
In @.github/workflows/build-and-test.yml:
- Around line 15-20: The test job currently runs with implicit GITHUB_TOKEN
scopes; add an explicit read-only permission block to lock it down by adding a
permissions mapping under the test job (jobs -> test) such as permissions:
contents: read (or other minimal read-only scopes you need) so the job/container
(container image mcr.microsoft.com/playwright:v1.59.1-noble) only has repository
read access; ensure you place the permissions key at the same level as
runs-on/container in the test job.

In @.github/workflows/release.yml:
- Around line 63-75: Replace any npx invocations used to run Changesets in the
release workflow with bunx to pin execution to the repo-managed Bun toolchain:
specifically update the changesets/action step where the input "version: npx
changeset version" is set (and the analogous occurrences later in the file) to
use "bunx changeset ..." instead of "npx changeset ..."; ensure any other
release-related commands that currently call npm/npx (e.g., install or run
commands referenced near the changesets usage) are switched to bun install /
bunx equivalents so the workflow uses Bun end-to-end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7949279f-5a11-447b-8ddb-80496f39a45b

📥 Commits

Reviewing files that changed from the base of the PR and between 7840419 and 7aefbc4.

📒 Files selected for processing (4)
  • .github/workflows/build-and-test.yml
  • .github/workflows/main-build.yml
  • .github/workflows/release.yml
  • AGENTS.md

Codex P1: changesets/action infers `outputs.published` by parsing
`New tag:` lines from `changeset publish` stdout. Those lines are only
emitted when changeset publish creates per-package git tags. With
`--no-git-tag`, `published` always reports false → the
post-publish tag step never fires → release tag never created.

Drop the flag and let changeset publish create its 27 per-package
tags. Noisy but harmless; the canonical release marker remains the
repo-wide vX.Y.Z tag created by `gh release create` after publish.

Also addresses Copilot review:
- Standardize on `bunx` for CLI invocations (was `npx`); matches
  the rest of the repo's tooling per AGENTS.md.
- Reword the workflow header to acknowledge that publish-and-tag
  runs on any no-pending-changeset main push (docs/plans included),
  not only after a version-PR merge — the published-output gate is
  what makes the path safe.
- Sharpen the persist-credentials note: the post-publish gh release
  create authenticates via GH_TOKEN; persist-credentials: true is
  needed for `changeset publish`'s per-package tag pushes via git
  remote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brianmhunt
Copy link
Copy Markdown
Member Author

Addressed in 29d8dec

Codex P1 (blocker): dropped --no-git-tag. Confirmed changesets/action parses New tag: lines from changeset publish stdout to set outputs.published; suppressing those lines made the gate dead. Per-package tags now ship (27 per release) — noise but harmless. Canonical release marker remains the repo-wide vX.Y.Z tag created by gh release create.

Copilot:

  • npxbunx for changeset version and changeset publish. Matches AGENTS.md tooling.
  • ✅ Header comment reworded — publish-and-tag runs on any no-pending-changeset main push (docs/plans included), not only after version-PR merge. The published gate is what makes the path safe.
  • ✅ persist-credentials note sharpened: gh release create uses GH_TOKEN; the credential persistence is for changeset publish's per-package tag pushes via git remote.

Not addressed (by design)

  • Double bun run build (Copilot): yes — once in build-and-test (reusable, in Playwright container, gates publish via needs:) and once in publish-and-tag (in the runner that will execute changeset publish). Reusable workflows do not share artifacts unless actions/upload-artifact + download-artifact is wired through, and our existing artifact (tko, builds/knockout/dist) covers only one build's output, not all 27 packages' dists. Future optimization: expand the upload to cover every packages/*/dist and builds/*/dist, then download in publish-and-tag and skip the local build. Out of scope for this PR.

  • bun run test step inheriting VITEST_BROWSERS=chromium: the step was already removed when build-and-test became the gate (commit 7aefbc45). The reusable workflow runs the full browser matrix in the Playwright container, and publish-and-tag needs: build-and-test. No tests run locally in publish-and-tag anymore.

@brianmhunt brianmhunt merged commit c4c1ae1 into main May 4, 2026
8 of 9 checks passed
@brianmhunt brianmhunt deleted the chore/release-on-main-trigger branch May 4, 2026 15:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

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.

2 participants