Branching and Git

The branch model, the commit message format with its Jira key, and the everyday branch, PR and merge loop.

2026-11-07

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 work
  • fix/ELBWEB-446-widget-links — bug fixes
  • chore/*, 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 by release-build.yml. v0.1.0-rc.1 to v0.4.0-rc.2 exist.
  • vX.Y.Z — the final tag on main, with a GitHub Release, written by release.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

  1. 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-thing
    
  2. Do the work, and verify before you push. pnpm verify runs 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 test minus @v4/e2e and @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 verify
    

    verify is a local tool: CI does not run it, but runs the same checks in its own steps. If you touched the content model, also run pnpm --filter @v4/model drift — it hits the network, so verify leaves it out.

  3. Commit. Conventional-commit subject (see Commit conventions). The pre-commit hook (scripts/hooks/pre-commit) runs two advisory steps: format-staged.mjs runs Prettier over the staged files and re-stages what it changed, then docs-drift.mjs --staged warns if a code change makes a mapped doc stale. Neither blocks a commit by default; set DOCS_DRIFT=block to make staged docs drift fail the commit, or DOCS_DRIFT=off to skip it.

  4. Push and open a PR against develop. CI (ci.yml) and the pr ticket check run on every PR; the model PR gate runs too if you touched packages/model/migrations/**, packages/model/apps/** or packages/model/baseline/**.

  5. Get it green and approved, then merge. develop requires the verify, lint and report checks and one approving review. Merge with a merge commit; see Merging.

  6. Delete the branch.

Rules that don't bend

  • Never commit to main directly. main only moves when the release workflow releases a ref. Note that main has 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 to develop.
  • Rebase or merge develop into 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.