Adopted 2026-08-07 (first exercised as the v0.3.3 release). One principle
generates every rule here:
mainis the production mirror: it advances only at release time, so the code onmainand the code people install are always the same thing.
"Production" is two surfaces that update together on a main push: the npm
package (published by CI on a tag) and app.mirafold.com (Cloudflare Pages
builds every push to main as production and other branches as previews —
that branch mapping lives in the Cloudflare dashboard, not in this repo). Flow
b keeps them in lockstep by making a main push and a release the same event.
| branch | what it is | protection (GitHub rulesets) |
|---|---|---|
main |
the production mirror — every commit on it is inside some release | required checks (Tier 1, Tier 2+3, DCO), no force-push/delete; repo-admin bypass exists for emergencies but a direct push breaks the flow-b invariant — don't, except as step one of an immediate release |
next |
staging — day-to-day work accumulates here | PR-only for everyone (0 approvals, same required checks), no bypass, no force-push/delete |
feature/*, fix/*, refactor/* |
working branches, cut from next |
none — name them anything, force-push freely |
release/x.y.z |
short-lived release prep, cut from next (or from main for a hotfix) |
none — it exists for hours |
Three mechanics to know:
- Every commit headed for a PR needs a DCO sign-off — commit with
git commit -s. The DCO check is required on both protected branches. Aprepare-commit-msghook that appends the trailer is a per-machine convenience; nothing in the repository installs one for a fresh clone, so-s(orgit rebase --signoffto repair a branch) is the rule. - An open or green feature PR is not approval to merge it. Keep the PR open through the requested review and refactor passes. When it appears ready, ask Kyle explicitly whether to merge; merge only after he approves.
- Every PR gets automated review comments — read them before any merge.
The Codex reviewer (
chatgpt-codex-connector) posts inline P1/P2 findings, each with a claimed failure, minutes after the PR opens; after a push, comment@codex reviewto get the new head reviewed. Cloudflare's bot only posts the preview link. There is no CodeQL workflow in this repository (.github/workflows/holdsci.ymlandrelease.yml; checked 2026-09-15). A PR that "went green" can still be carrying findings. Before asking for merge approval on a feature PR, and before merging a release PR, pull every comment (gh api repos/mirafold/mirafold/pulls/<n>/comments, plus/issues/<n>/commentsand/pulls/<n>/reviews), verify each claim against the code — a reviewer's failure scenario is a hypothesis, not a finding, until it is reproduced — and fix the legitimate ones with a test per class. On a release PR the fixes go to afix/*branch offnextand merge intonext(so staging has them regardless of the release); then merge that fix branch — never all ofnext— into the release branch, so work that landed onnextafter the cut cannot ride into production unselected. Never commit fixes onrelease/*directly, or staging only receives them via the post-release sync. Adopted 2026-08-29 (v0.6.0: ten findings, all legitimate, first seen after the release PR was already green). - A
v*tag publishes onlymain's current tip. The release workflow's first step fails any tag pointing elsewhere (a feature branch, an oldmaincommit). The tag trigger itself is branch-blind by platform design; the guard is what makes a mis-aimed tag a red workflow run instead of a bad release.
-
Feature work: branch off
next, commit with-s, and open a PR intonext. Keep implementation follow-ups and refactors on that open PR. Once the work and required checks appear ready, read the automated review comments (mechanic above) and address the legitimate ones on the same PR; then ask Kyle explicitly for merge approval and leave the PR open until he gives it. Repeat untilnextholds the release you want. -
Cut the release branch:
git switch -c release/x.y.z origin/next. On it, regenerate the bundled-license notices and commit any change —node scripts/third-party-notices.mjs(required whenever a browser-side dependency moved; CI fails the release if the file is stale). -
Bump the version in
package.jsontox.y.zon that branch — commitrelease: vx.y.z(signed off). npm refuses to republish an existing version, so a missing bump kills the publish at the last step. -
PR
release/x.y.z→main. When the checks are green, read the automated review comments on it; a legitimate finding goes throughnextfirst (mechanic above), and the release branch takes the fix branch withgit merge origin/fix/<name>— the PR updates itself and the checks re-run. Merge when green with the findings addressed. -
Tag and push — this is the publish, and it's a human act (the signing key lives only on the release manager's machine):
git switch main && git pull npm pack # builds; prints mirafold-x.y.z.tgz sha256sum mirafold-x.y.z.tgz # goes into the tag message git tag -s vx.y.z -m "sha256 <that hash>" git push origin vx.y.zThe release workflow verifies the tag's SSH signature against
.github/allowed_signersbefore anything publishes (2026-08-26 audit) — a new signing key means a new line in that file, merged tomainfirst. The tag message carries the tarball's SHA-256 so the signed tag attests to the exact bytes. No hand-runnpm publish, ever — the tag push triggers.github/workflows/release.yml, which re-verifies (guard, tag↔version check, typecheck, tests), packs once, refuses unless its pack's SHA-256 equals the one in the tag message, then waits forkserrecto approve the protectednpm-publishenvironment. Approve that deployment from the GitHub Actions run; the workflow then publishes the exact tarball with provenance via npm trusted publishing. The environment admits onlyv*tags, so the signed tag, workflow summary, and registry bytes are one file, not three packs assumed identical. The workflow has no manual trigger. npm package Settings → Publishing access must be set to, and remain, Require two-factor authentication and disallow tokens; trusted publishing removes the CI token but does not disable traditional token publishing by itself. -
Verify the same day: the release run is green including the guard step;
npm view mirafold versionshowsx.y.z; the registry tarball's sha256 matches the signed tag message (curlit down andsha256sum— the workflow already proved tag ↔ pack, this proves pack ↔ registry); and the packaged smoke passes against the published package —node scripts/packaged-pass.mjs(a global install driven in a real browser; it has caught launch blockers the test tiers cannot see). The Mirafold Desktop repository (mirafold/mirafold-desktop) consumes the published package: its scheduled intake notices the new npm version, re-pins to it, and publishes a Desktop release on its own — nothing to trigger here, but expect that release to follow within its polling window. -
Close the loop — do not skip: PR
main→nextand merge it. The version bump and release merge commit now exist onmainonly; until this sync lands, the next cycle's release PR will conflict onpackage.json. Pushmain's tip to async/*branch first (git push origin main:refs/heads/sync/main-into-next-vx.y.z) so the PR is a fixed snapshot rather than trackingmain. -
Delete the merged work (
feature/*,fix/*,refactor/*),release/*, andsync/*branches.
Same cycle in miniature, starting from main instead of next:
cut release/x.y.(z+1) from main, commit the fix + version bump there
(signed off), PR → main, merge on green, tag, verify — then the same
main → next sync, which delivers the fix to staging too.
Don't merge it into next yet — that's the whole mechanism. The branch stays
alive and periodically merges next into itself to avoid rotting. If
something already on next must not ship: cut release/x.y.z from the last
good commit before it, or revert the unwanted merge on the release branch
only. There is no second staging branch, deliberately.