This project follows the fairyfox system's git-flow standard (the branching model, not the
git flow CLI — plain git carries it). These are procedures the AI upholds by judgement, not
automation. Canonical source: hub/standards/git-workflow.md in the system clone.
Branch model
git-flow runs two long-lived branches and three kinds of short-lived support branch.
main— production. Every commit onmainis a tagged release, reached only by a merge commit (the--no-ffequivalent). Never commit directly — as of 2026-07-02mainis branch-protected and only accepts changes through a pull request (see "mainis branch-protected" below), so the release path runs through a PR rather than a local push. It advances at a release: a PATCH goes fromdev; a MINOR/MAJOR goes through arelease/*branch; an urgent fix goes through ahotfix/*branch.mainis the repo's stable branch and default branch. (masterwas renamed tomainon 2026-06-25 to adopt the system standard —masteris no longer used.)dev— the integration branch (git-flow'sdeveloprole, kept under the shorter name). All finished work lands here first. This is the branch the hub tracks for sync (the registrybranch:field).feature/<name>— the normal unit of work. Branch fromdev, build, merge back intodevwith--no-ff, then delete. Never branch a feature offmain.release/<x.y.0>— the mechanism for a MINOR or MAJOR release. Branch fromdevto bake the release (final polish,VERSIONbump, changelog), merge intomain+ tag and back intodev(--no-ffboth), then delete.hotfix/<x.y.z>— an urgent production fix. Branch frommain, fix, merge intomain+ tag and back intodev(--no-ffboth), then delete.
Support-branch names use a type/ prefix plus a short kebab-case description.
Developing a feature
git checkout dev
git checkout -b feature/<name>
# … commit work on the feature branch, push it to back it up …
git checkout dev
git merge --no-ff feature/<name> # keeps the feature as one revertible unit
git branch -d feature/<name>
git push origin dev
The --no-ff merge commit groups the feature's commits under one parent — legible in history and
revertible in one move (git revert -m 1 <merge>).
Solo / small-project latitude: a genuinely trivial change (a typo, a one-line doc fix) may be
committed directly on dev rather than via a feature/* branch. Anything that is really "a feature,"
or is large/risky, still gets its own branch. The release path below is not latitude — it is fixed
by the SemVer level.
main is branch-protected (as of 2026-07-02)
main carries a GitHub branch-protection rule, so releases run through a pull request, not a local
git push origin main. The rule:
- Require a pull request before merging, with 0 required approvals — this is a solo project, so there's no second reviewer; you open the PR and merge it yourself once checks pass. (GitHub won't let you approve your own PR, so requiring ≥1 approval would deadlock — hence 0.)
- Require status checks to pass, strict (branch up to date):
Lint, format, smoke & unit testsandBuild & unit-test the SPA(the two core CI jobs; the heavier E2E/perf jobs are intentionally not gating, so a flake there never blocks a release). - Enforce for administrators — the owner is not exempt (a rule you can bypass isn't protection).
- Block force-pushes and branch deletion; require conversation resolution.
- Linear history is OFF on purpose, so the
--no-ff-style merge commits each release creates are allowed.
Managed via gh api PUT /repos/1fairyfox/random-ai-prompt/branches/main/protection; fully
reversible from Settings → Branches. dev is unprotected (day-to-day work + the release
back-merge push land there directly). This is a local divergence from the hub git-workflow standard,
proposed back in notes/fairyfox-reports/2026-07-02-propose-scorecard-hardening.md.
Cutting a release
The release path is set by the SemVer level (see versioning.md). Because main is
branch-protected, each path lands on main via a PR merge (gh pr merge … --merge, which creates
the release merge commit) instead of a direct push.
⚠ Local correction (pending hub adoption). The upstream (hub) form of these flows merged into
mainbut never broughtmainback todev, so every release left a merge commit onmainthatdevnever received —devdrifts behindmain, one commit per release. Observed live:devwas 32 commits behindmain. Invariant to hold: after every release,devmust CONTAINmain— so the release ends by fast-forwardingdevup tomain. The corrected commands below do this. Proposed back to the hub innotes/fairyfox-reports/2026-07-01-propose-git-workflow-backmerge.md; a scheduledbranch-syncworkflow guards the invariant.
-
PATCH (the default — fixes, docs, ordinary changes): release
dev → mainvia a PR.gh pr create --base main --head dev --title "Release vX.Y.Z" --fill gh pr checks <#> --watch # the required CI checks must go green gh pr merge <#> --merge # merge commit == the release act; CI (release.yml) tags vX.Y.Z — do NOT tag by hand git fetch origin git switch dev git merge --ff-only origin/main # REQUIRED: catch dev up to main so dev ⊇ main git push origin dev # dev is unprotected — this push is fine -
MINOR / MAJOR (a milestone): go through a
release/X.Y.0branch. (MAJOR →1.0.0etc. is the owner's call only.)git switch dev git switch -c release/X.Y.0 # … finalize: bump VERSION, finish the changelog entry, last polish … git push origin release/X.Y.0 gh pr create --base main --head release/X.Y.0 --title "Release vX.Y.0" --fill gh pr checks <#> --watch gh pr merge <#> --merge --delete-branch # merge commit tags vX.Y.0; deletes the release branch git fetch origin git switch dev git merge --ff-only origin/main # REQUIRED: fast-forward dev up to main (== the release merge) git push origin dev(Fast-forwarding
devtomaingives one shared merge commit anddev == mainafter the release — never merge the release branch intodevseparately.)
Merging the release PR into main after a VERSION bump cuts a GitHub Release (release.yml,
tag-gated) and refreshes the Pages docs (pages.yml). See deployment.md.
Who creates the tag — CI, not by hand (this repo)
The branch commands above push main without tagging. This project's
release.yml derives v<VERSION> and creates the tag itself on the main push, and
gates the release on that tag not already existing. So a tag pushed by hand makes the gated workflow
find the tag present and skip itself — a silent no-op release. The rule for this repo is therefore:
- The merge to
mainis the release act; CI applies the tag. Never rungit tag/git push --tagsfor a release here. - The invariant still holds — every commit on
mainends up carrying its matchingvX.Y.Ztag — the question is only which actor applies it, and here it's CI.
(This is a deliberate, recorded divergence from the hub's hand-tag example commands, which assume a
project whose release.yml does not tag. The hub documents exactly this CI-vs-hand fork.)
Hotfixes
git fetch origin
git switch -c hotfix/X.Y.Z origin/main # branch from main's tip
# … fix, bump VERSION (patch), changelog …
git push origin hotfix/X.Y.Z
gh pr create --base main --head hotfix/X.Y.Z --title "Hotfix vX.Y.Z" --fill
gh pr checks <#> --watch
gh pr merge <#> --merge --delete-branch # merge commit tags vX.Y.Z — do NOT tag by hand
git fetch origin
git switch dev
git merge --ff-only origin/main # catch dev up to main (carries the hotfix) so dev ⊇ main
git push origin dev
Commit style
- Focused, present-tense
type: summarymessages (e.g.build: migrate to ES modules + Node 24,chore: add eslint + prettier,docs: add notes system). - Stage specific files (
git add <paths>), nevergit add -A/git add .. - The changelog entry for a substantive change rides inside the same commit (top of
notes/version/YYYY-MM.md) — see../version.md. - Keep
VERSIONandpackage.jsoncurrent as part of the release — bumped ondevfor a PATCH, or on therelease/*/hotfix/*branch for a milestone/hotfix; the release tag onmainmatches it. Seeversioning.md.
Merging — --no-ff, never rewrite
git-flow merges create a merge commit (the --no-ff effect): features merge --no-ff into dev
locally; every landing on main (a PATCH dev → main, or a release//hotfix/ branch) goes through a
PR merged with gh pr merge --merge, then dev fast-forwards up to main. Each keeps the grouping
legible and revertible. Every merge into main is a tagged release. This is all additive — it
never rewrites history (and force-pushes to main are now blocked by branch protection).
Hard safety rules (absolute)
- Never
git push --force/--force-with-lease/ rewrite pushed history. (git-flow's--no-ffmerge commits are additive and allowed — they are not a rewrite.) - Never
git reset --hard,rebase,clean -fd, or delete a long-lived branch (main/dev) without an explicit request. Spentfeature//release//hotfix/branches are deleted as the normal end of their merge. - Inspect
git statusbefore and after every git operation. - Don't commit user data / runtime artifacts:
user-settings.json,results.json,output/,node_modules/(all gitignored).
Pre-release manifest gate (hub 0.21.0)
Before a release to main, the adoption manifest must be current: any
"standards adopted" claim in the release/changelog is backed by a manifest row, and full CI is green
and platform-enforced (every required job in main's branch-protection required-status-checks,
not just "ran once"). copied-only rows may ship; they may not be reported as implemented.
Verify (is it being followed?)
The check that catches a violation — run on request, report done/partial/missing
(the per-standard slice the compliance audit aggregates):
| Passes only when… | How to check |
|---|---|
Stable branch is main, not master |
git branch -a |
Every commit on main is a --no-ff release merge carrying a matching vX.Y.Z tag — no direct commits |
git log --first-parent --oneline main; git tag |
| Pushed history is intact — no force-push / rebase / reset of published commits | history stable across fetches; no --force in reflog |
Spent feature//release//hotfix/ branches deleted; main/dev intact |
git branch -a |
Each release to main rode a green build/test checkpoint |
release followed a green CI run |