Releasing ERP-Unlocked
How we release — and why it satisfies SOC 2 change management (CC8). The pipeline
reuses what’s installed (changesets, release.yml, the staging→main promotion).
The review gate, the changeset gate, and the release-bot App token went live
2026-06-27; remaining hardening is in the last section.
The flow
Section titled “The flow”| Step | Mechanism | Control / evidence (CC8) |
|---|---|---|
| 1. Describe the change | feature branch + one pnpm changeset per PR |
the change is described before it merges |
| 2. Review | PR → staging; branch protection requires 1 approval + Verify pnpm supply-chain controls + Changeset required |
reviewed — every change is reviewed |
| 3. Version + notes | release.yml runs hourly (cron, not push — see below), runs changeset version, opens/updates a “Version PR”, and the release-bot App auto-merges it |
the release notes, auto-generated from the changesets |
| 4. Release to prod | manual auto-deploy-main dispatch fast-forwards main (the daily 2 AM cron only checks divergence) |
approved — a named human triggers the prod release |
| 5. Release record | git tag + GitHub Release from the CHANGELOG | the dated, versioned record (⏳ not wired yet) |
Day-to-day: what you actually do
Section titled “Day-to-day: what you actually do”- Open the PR as a draft while you iterate. Branch Protection, Deploy
Branch Gate, and Auto Changeset all skip draft PRs (2026-08, #2818) — every
push used to pay the full CI gate, which stopped scaling once several
agent-authored PRs were iterating against
stagingat once. CodeRabbit still reviews drafts (.coderabbit.yaml), so you get review feedback for free while iterating; the full gate starts running again — same as any other open PR — once you mark it ready for review, on that transition and every push after it. - Every PR:
pnpm changeset→ pick package(s) + bump → write one user-facing line. A PR touchingapps//packages/cannot merge without one — theChangeset requiredcheck enforces it (a gate, not a habit). - Open the PR against
staging(nevermain); get the 1 review. - That’s it. Versioning, the CHANGELOG, and the Version-PR merge are automated —
don’t run
pnpm changeset version/tagorscripts/release.shlocally. The Version PR updates on the hourly cron, not instantly on your merge — if you need it sooner (e.g. to promote right after merging), dispatch it:gh workflow run release.yml --ref staging.
Writing a good changeset (the release-notes standard)
Section titled “Writing a good changeset (the release-notes standard)”pnpm changeset → package(s) + bump (patch / minor / major) → one user-facing line:
- ✅
fix(webapp): orders with >50 line items no longer time out at submit - ❌
fixed bug,updated code,wip
The changelog is only as good as these lines. Write them for the person reading the release, not for git.
Promoting to production
Section titled “Promoting to production”main builds prod and is updated only by the manual auto-deploy-main
fast-forward: gh workflow run auto-deploy-main.yml --ref staging. That manual
trigger is the production-release approval. promotion-guard.yml blocks any
non-staging branch from reaching main. Two rulesets guard main: the base
one blocks force-push/deletion, and “main - require approval (quarantine)”
(id 18225657, added 2026-06-28 during the access quarantine) requires a PR with
1 approval. The fast-forward push survives because it runs as the
release-bot App, which is a bypass actor on the quarantine ruleset — humans
cannot push to main at all. Never fast-forward main by hand.
How the bot merges and pushes under review-gating
Section titled “How the bot merges and pushes under review-gating”staging requires a review and main carries the quarantine ruleset, but the
default GITHUB_TOKEN can’t be a ruleset bypass actor — so both automations
run on a token minted from the ordermatic-release-bot GitHub App (App id
4160177), which is on the bypass lists: the Version-PR auto-merge in
release.yml, and the main fast-forward push in auto-deploy-main.yml.
Gotchas, all learned the hard way when the Version PR sat open 2026-06-27 →
07-04:
- The merge must go through REST (
PUT /repos/…/pulls/{n}/merge).gh pr mergeuses GraphQL, which does not exercise ruleset bypass for App tokens — it fails with “the base branch policy prohibits the merge”, and an--autofallback never fires because nobody reviews the bot’s PR. - The jobs hard-fail when the App token is missing rather than falling
back to
github.token(which can’t bypass, so the merge would stall silently). If they fail, checkRELEASE_APP_ID(repo var) +RELEASE_APP_PRIVATE_KEY(secret) and the App installation. - App-token pushes trigger
on: pushworkflows (onlyGITHUB_TOKENpushes are suppressed by GitHub’s anti-recursion rule), so the FF push itself fires thegcp-deploy.ymlprod deploy —auto-deploy-mainwatches that push-triggered run instead of dispatching a second one. release.ymlno longer triggers onpush(2026-08, #2818): it runs on an hourly cron +workflow_dispatchinstead, because the Version-PR create/auto-merge cycle used to fire on every single merge tostaging— each cycle re-running the full CI suite on the bot’s own PR — which was ~18% of all CI runs in a day of agent-driven merge volume with zero human involved. This also means the old “release.ymltriggers onstagingonly” guard against a spurious main-push Version PR is moot: schedule andworkflow_dispatchdon’t carry a ref from the triggering push at all, so the workflow explicitly pinsref: stagingon both checkouts instead.- The Version-PR branch must be pushed as the App, not just managed via
its API token.
changesets/actionpushes with the checkout-persisted credentials and only uses itsGITHUB_TOKENenv for API calls — so the mint step runs before checkout and the checkout getstoken: <app token>. Agithub.tokenpush makes GitHub hold everypull_requestrun on the Version PR asaction_required, the gates never report, and the auto-merge (correctly) refuses to merge.
The auto-merge no longer posts validate / build-and-push commit statuses —
those contexts were required by long-gone branch protection; nothing requires
them now and the App token got 403s creating them.
Compliance note (SOC 2 CC8 / Change Management Policy §3): the automated
gates are enforced server-side on the bot merge — the App is a bypass
actor on the review ruleset only; the “staging - automated gates”
ruleset has an empty bypass list (changed 2026-07-05), so GitHub rejects the
REST merge until every required check passes on the Version-PR head. The
auto-merge just retries within a 25-minute budget and fails loudly if the
gates never go green. No client-side check polling (App tokens can’t reliably
read Actions check runs), no fabricated control evidence, and “the gates are
non-bypassable — no one, including admins” is literally true in the ruleset
config. RELEASE_PAT is dead (never mapped from secrets); don’t rely on it.
Versioning
Section titled “Versioning”Semver, monorepo-wide via changesets. patch = fixes, minor =
backward-compatible features, major = breaking. The first real release moves
off the placeholder 0.1.0.
Compliance mapping (CC8 — change management)
Section titled “Compliance mapping (CC8 — change management)”Every prod release leaves an audit trail with no extra work: the changeset (the what and why), the PR review (reviewed), the CHANGELOG and version bump (what shipped), the manual promotion run (approved, by whom, when), and the git tag / GitHub Release (the dated record). The control is stated in the Change Management Policy; this file is the procedure that operates it.
Remaining hardening
Section titled “Remaining hardening”- ⏳ Release record: wire
pnpm changeset tag+gh release createintoauto-deploy-main.ymlafter the FF push (step 5 — the dated git tag / GitHub Release per release). - ⏳ Python version sync:
changeset versiononly bumpspackage.json; automate theapps/{temporal-worker,dagster}/pyproject.tomlbump so releases don’t ship stale Python versions. - ⏳ Retire
scripts/release.sh(the Coolify-era interactive script) — the CI flow above is the one release path.
Done (live 2026-06-27)
Section titled “Done (live 2026-06-27)”- ✅ Changeset gate enforced as a required CI check (
Changeset required). - ✅ Review-gated
staging+ restrictedmain(branch protection). - ✅ Release-bot App token — review-gating without breaking the auto-merge.