Branching and Git
The branch model, the commit message format with its Jira key, and the everyday branch, PR and merge loop.
The branch model
This repo runs a lightweight gitflow: one integration trunk (develop), one
release branch per release (release/X.Y.Z), and main as the mirror of what
has been released. Everything else is a short-lived topic branch that merges into
develop (or, for a fix to a release, into its release branch) and is deleted.
The long-lived branches
| Branch | Role | How it moves |
|---|---|---|
develop |
Integration trunk | Every PR merges here behind the required checks. This is where work lands day to day. It is the repository's default branch. |
release/X.Y.Z |
One per release | Cut from develop when a release is ready for staging. Fixes for that release are PRs into it. Every push is built, tagged vX.Y.Z-rc.N and deployed to Staging. |
main |
Mirror of what is released | Only ever moves when the release workflow releases a ref, and only to that ref. |
develop is the trunk: CI runs on every push to it, so it holds a green
baseline per merge, and each merge primes the Turborepo remote cache so that
later PR runs replay from the cache instead of rebuilding from cold. Every push
to develop also deploys the Flightcontrol Development environment (the web
app, these docs and Storybook).
A release branch is what QA tests on Staging and what is eventually
released. Flightcontrol's Staging environment deploys one release branch at a
time (flightcontrol.json, currently release/0.4.0); see
Staging. Release branches so far are
release/0.1.0 to release/0.4.0.
main is not a working branch. Nobody commits to it directly and no PR targets
it in normal work — the release workflow moves it. It has not moved yet in this
repo: release.yml has never been run, so main still sits at the commit from
21 September 2026, and there is no production environment to deploy it to (see
Environments).
Topic branches
Work happens on a short-lived branch cut from develop. The name carries the
conventional-commit type and, for ticketed work, the Jira key:
feat/ELBWEB-464-sponsored-by— new workfix/ELBWEB-446-widget-links— bug fixeschore/*,ci/*,docs/*,build/*,deps/*— housekeeping with no ticket
The key is not decoration. Release tracking reads a PR's ticket from its branch
name and title only, and the pr ticket check fails a PR into develop or a
release branch that names none — unless it is housekeeping (a chore, ci,
docs, build, deps or release branch, or the same type in the title) or
carries the no-ticket label. A fix/ branch is not exempt. See
Jira release tracking.
Once the PR is merged, delete the branch.
Tags and releases
Two kinds of tag, both written by workflows, never by hand:
vX.Y.Z-rc.N— one per push to a release branch, published as a GitHub pre-release byrelease-build.yml.v0.1.0-rc.1tov0.4.0-rc.2exist.vX.Y.Z— the final tag onmain, with a GitHub Release, written byrelease.yml. None has been cut yet.
Cutting a release is covered in Releasing.
Fixes to a release
A fix for something already on a release branch is a PR into
release/X.Y.Z (CI and the pr ticket check both run on PRs into release/**),
and the same fix must reach develop too, so it is not lost at the next cut.
A hotfix flow — branch from main, PR into both main and develop, cut a
patch release — has never been exercised here, because nothing has been
released to main yet.
Commit conventions
Commits and PR titles follow Conventional Commits. This is not cosmetic: release notes are generated mechanically from merged PR titles, so the title you write on a PR is the changelog line a reader sees.
The format
<type>(<scope>): <summary>
type is the change class, scope is the affected area (often the package or
feature), and the summary says briefly what is now true. Real examples from
the history:
fix(video): a collection tab keeps the reader's place (ELBWEB-619)
feat(coaches): the coach page, with the coach header (ELBWEB-434, ELBWEB-500)
fix(articles): the header widgets overlap the image by one column, as the design draws it (ELBWEB-446)
fix(motion): the schedule's cards no longer vanish after a round switch (ELBWEB-526)
chore(hosting): Staging deploys release/0.4.0, and its build runs the ELBWEB-577 data scripts
Common types in this repo: feat, fix, perf, ci, docs, style,
chore, refactor, test. The scope is free-form but should name something a
reader recognises (video, articles, model, caching, hosting).
Name the ticket
Ticketed work names its Jira key (ELBWEB-123) in the PR title, the branch name,
or both. Release tracking reads the key from those two places only — never from
commit bodies or the PR description — so a PR that names it nowhere else reaches
Staging with no Fix Version. The pr ticket check enforces it; see
Jira release tracking.
How release notes are generated
release.yml publishes the GitHub Release with gh release create --generate-notes,
which builds the note body from the titles of the pull requests merged since
the previous release, whatever merge method was used. Write the PR title as the
changelog entry: it will be read by people who never see the diff.
Each rc pre-release (release-build.yml) carries a different body: the build's
ticket report from scripts/ci/release-jira.mjs.
Trailers
Commits authored with Claude Code carry a Co-Authored-By trailer naming the
assisting model, added automatically:
Co-Authored-By: Claude <model> <noreply@anthropic.com>
Pull request bodies opened from a session carry the matching
🤖 Generated with Claude Code footer. Trailers sit in the message body, below a
blank line after the summary, and do not affect release notes.
Day-to-day git
The everyday loop is: branch off develop, push, open a PR, get it green, merge
back into develop. main is never touched by hand.
The loop
Branch from
develop. Name it for the work and include the Jira key —feat/ELBWEB-123-…,fix/ELBWEB-123-…— or use a housekeeping type (chore/*,ci/*,docs/*) for work with no ticket.git switch develop && git pull git switch -c feat/ELBWEB-123-my-thingDo the work, and verify before you push.
pnpm verifyruns the cheap CI gates locally (scripts/verify.mjs), stopping at the first failure: Prettier, ESLint at zero warnings, type-check, the package unit suites (turbo testminus@v4/e2eand@v4/storybook), the Figma bridge check (check-bridges.mjs, offline), composition drift (pnpm compose -- --check), and the web, content-app and video-app codegens with their drift diff.pnpm verifyverifyis a local tool: CI does not run it, but runs the same checks in its own steps. If you touched the content model, also runpnpm --filter @v4/model drift— it hits the network, soverifyleaves it out.Commit. Conventional-commit subject (see Commit conventions). The pre-commit hook (
scripts/hooks/pre-commit) runs two advisory steps:format-staged.mjsruns Prettier over the staged files and re-stages what it changed, thendocs-drift.mjs --stagedwarns if a code change makes a mapped doc stale. Neither blocks a commit by default; setDOCS_DRIFT=blockto make staged docs drift fail the commit, orDOCS_DRIFT=offto skip it.Push and open a PR against
develop. CI (ci.yml) and thepr ticketcheck run on every PR; the model PR gate runs too if you touchedpackages/model/migrations/**,packages/model/apps/**orpackages/model/baseline/**.Get it green and approved, then merge.
developrequires theverify,lintandreportchecks and one approving review. Merge with a merge commit; see Merging.Delete the branch.
Rules that don't bend
- Never commit to
maindirectly.mainonly moves when thereleaseworkflow releases a ref. Note thatmainhas no required checks or reviews configured, so nothing but this rule stops a hand push. - Never squash a PR in a stack. When one PR is based on another, squashing the first rewrites the commits the rest are built on and re-conflicts every PR above it. Use a merge commit.
- Fix a release on its release branch. A fix to something on Staging is a PR
into
release/X.Y.Z, and the same fix goes todevelop. - Rebase or merge
developinto your branch to stay current, not the other way round.
Working in a shared worktree
More than one agent or session can be writing to the same checkout. Before you
stage, list what changed outside your own paths and stage explicit paths — never
git add -A on a shared tree — so you don't sweep up someone else's in-flight
work.