Skip to content

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.

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)
  • 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 staging at 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 touching apps//packages/ cannot merge without one — the Changeset required check enforces it (a gate, not a habit).
  • Open the PR against staging (never main); get the 1 review.
  • That’s it. Versioning, the CHANGELOG, and the Version-PR merge are automated — don’t run pnpm changeset version/tag or scripts/release.sh locally. 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.

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 merge uses GraphQL, which does not exercise ruleset bypass for App tokens — it fails with “the base branch policy prohibits the merge”, and an --auto fallback 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, check RELEASE_APP_ID (repo var) + RELEASE_APP_PRIVATE_KEY (secret) and the App installation.
  • App-token pushes trigger on: push workflows (only GITHUB_TOKEN pushes are suppressed by GitHub’s anti-recursion rule), so the FF push itself fires the gcp-deploy.yml prod deploy — auto-deploy-main watches that push-triggered run instead of dispatching a second one.
  • release.yml no longer triggers on push (2026-08, #2818): it runs on an hourly cron + workflow_dispatch instead, because the Version-PR create/auto-merge cycle used to fire on every single merge to staging — 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.yml triggers on staging only” guard against a spurious main-push Version PR is moot: schedule and workflow_dispatch don’t carry a ref from the triggering push at all, so the workflow explicitly pins ref: staging on both checkouts instead.
  • The Version-PR branch must be pushed as the App, not just managed via its API token. changesets/action pushes with the checkout-persisted credentials and only uses its GITHUB_TOKEN env for API calls — so the mint step runs before checkout and the checkout gets token: <app token>. A github.token push makes GitHub hold every pull_request run on the Version PR as action_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.

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.

  • ⏳ Release record: wire pnpm changeset tag + gh release create into auto-deploy-main.yml after the FF push (step 5 — the dated git tag / GitHub Release per release).
  • ⏳ Python version sync: changeset version only bumps package.json; automate the apps/{temporal-worker,dagster}/pyproject.toml bump 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.
  • ✅ Changeset gate enforced as a required CI check (Changeset required).
  • ✅ Review-gated staging + restricted main (branch protection).
  • ✅ Release-bot App token — review-gating without breaking the auto-merge.