Appearance
Version on merge
Automate package versioning and publishing on every merge to main by deriving the SemVer bump from the public surface, then releasing in one workflow run.
What this workflow does
version-on-merge.yaml runs on pushes to main that touch package-facing paths. It performs this sequence:
- Installs dependencies.
- Computes the baseline bump from the surface diff, then raises it if the merge commit carries a
Release-Level:trailer. - Bumps
package.json, advancesscripts/surface.json, and writes theCHANGELOG.mdentry, then ships the release: publishes the package and then pushes thev<version>tag. Finally it lands the same bump onmainthrough an auto-merging release PR (see below).
The structural diff sets the baseline level. Behavioral breaks it cannot prove, such as a check getting stricter or a guideline reversed, are raised by a Release-Level: major trailer on the merge commit and carried by migrations/<version>.md. The trailer may only raise the level, never lower it, and an unrecognized value fails the release. No agent runs in this workflow.
Why diff against the previous snapshot
The workflow compares the live surface against the prior release baseline HEAD^:scripts/surface.json, not the already-updated snapshot in HEAD, so release bump detection stays correct when a PR updates scripts/surface.json before merge.
Trigger coverage
Release automation should run for any packaged contract change. Keep these paths in scope:
bin/**lib/**skills/**templates/**migrations/**scripts/**package.json
If a packaged path changes but is excluded from workflow triggers, consumers will not receive that release until an unrelated change re-triggers publishing.
How the release commit reaches main
Branch protection on main requires the Tests check to pass for a commit before it can land. The release bot cannot satisfy that with a direct push: a required check only reports on a commit that actually runs CI, and a pushed release commit never gets that chance before protection rejects it. (The original workflow tried to sidestep this by pushing the commit with [skip ci], which made Tests skip entirely, so the required check could never report and the push was permanently rejected.)
The workflow avoids this by shipping the release without touching main, then landing the bookkeeping through the normal CI gate:
- Publish and tag first. The package is published and the
v<version>tag is pushed (tags are not branches, so branch protection does not apply). This is the real release, and it does not depend onmain. - The release commit goes through a PR. The commit is pushed to a temporary
releases/v<version>branch. The workflow opens a PR from that branch and enables auto-merge (squash).Testsruns against it, and once green the PR squash-merges automatically and the branch is deleted.
Because publish and tag happen first, a failure to merge the bookkeeping PR can never block or undo a release that already shipped; main simply catches up when the PR merges.
This preserves branch protection for all contributors while letting the release bot land CHANGELOG.md, package.json, and the surface snapshot on main through the same CI gate that guards all other changes.
Why a GitHub App token: the PR must be opened with a GitHub App token, not the built-in GITHUB_TOKEN. GitHub's recursion guard means a PR opened by GITHUB_TOKEN never triggers the required Tests run, so auto-merge would wait forever on a check that cannot report. A PR opened with an App token runs Tests normally. See Publish to GitHub Packages for the one-time App setup (the RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY secrets).
Loop prevention: the squashed release commit keeps its chore(release) subject (the repo's squash-merge setting preserves commit messages), so the job's if: !contains(github.event.head_commit.message, 'chore(release)') guard skips it and the release does not re-trigger itself.
Required repo setting: the repository must have "Allow auto-merge" enabled (allow_auto_merge: true). Enable it once under Settings → General → Pull Requests.
Relationship to tag-publish workflow
version-on-merge publishes inline. The tag-triggered publish-to-github-packages.yaml remains for manual and initial publishes. Bot-created tag pushes from GITHUB_TOKEN do not trigger downstream workflows, so do not rely on tag chaining for regular merge releases.