Branching & Releases
Every repository uses one long-lived branch, main. A merge to main ships to staging; a version tag promotes that same image to production. The full pipeline, what each repository must provide, and the migration rules are in conventional/ci-cd.md. The day-to-day branch rules - feature branches, merge requests, how a release is chosen when several people have work in flight, and the one case that justifies a release branch - are in conventional/branching.md.
| Ref | What happens | Who |
|---|---|---|
| Merge request | Lint, validate, test, image check. All blocking. | CI |
Merge to main | Build the image once, migrate staging, deploy staging. Automatic. | CI |
Tag vX.Y.Z | Promote the image staging ran. One approval, then migrate and deploy. | Lead approves |
Branch rules
mainis always deployable: every commit on it is already running, or has run, on staging.mainandv*tags are protected. Every commit tomainarrives through a reviewed and approved PR with a green pipeline.
Deploying to staging
- Open a PR from a feature branch into
main. - PR must be reviewed, approved, and its pipeline green.
- Merge. CI builds the image, applies migrations, and deploys staging. Nothing to press.
Build version
Every image carries a build version in the format:
#{pipeline_id}-{short_commit_ref}Example: #482-abc1234
- Pipeline ID - the
mainpipeline that built the image (monotonically increasing, easy to reference) - Short commit ref - the merged commit (7 characters)
The image is built once, so staging and production show the same build version for the same release. It must be visible to whoever verifies - expose it in the app footer or a /version endpoint. Every verification comment and bug report includes the build version so developers know exactly what code was tested.
Releasing to production
We release twice a week: Monday and Wednesday, unless it is a hotfix. The cutoff for a commit is the end of the previous working day, so every release candidate gets a night on staging.
- Pick the commit on
mainwhose work was verified and accepted on staging (its build version names it). - Tag it with the next version:
git tag v1.4.0 <commit> && git push origin v1.4.0. - The tag pipeline checks that exact image ran on staging. If it never did, it fails and nothing ships.
- The lead presses approve:prod. Migrations and the deploy follow automatically, in that order.
- If production misbehaves, press rollback:prod. It switches traffic back to the previous release without touching the schema.
- Once the 30-minute watch is clean, release the Jira version
vX.Y.Z. That moves its work items to Done.
The full runbook - the pre-release checklist, why migrate runs before deploy, how backward compatibility is checked, how the HAProxy slot switch works, when to roll back, and what to do when a migration fails - is in conventional/release.md.
Feature branches
- Always fork from
main. - Name branches using the format:
<type>/<short-description>(e.g.feat/user-auth,fix/login-bug). - Keep branches short-lived. Rebase onto
maindaily; 3 days is the target lifetime. - Merging is a commitment to ship. Merge when the business has approved the release, not when the code compiles.
- A feature the business has not approved yet stays on its branch, rebased daily, verified on a local stack. It never reaches
mainor staging until the answer is yes. Decide within 30 days: merge behind an off flag, or delete the branch. - An approved feature with a future launch date merges behind a feature flag that is off, not on a long-running branch.
- A merge leaves exactly one commit on
main- fast-forward merge with squash, never a merge commit. - Migrate is never part of the app. It runs as a one-off container from the release's image, in its own CI job before deploy. Every service's
deploy/deploy.shanswers the same commands -migrate,deploy(withallfor the whole stack),redeploy,rollback,status, reading every image from the host's.env.images- so no operation needs a code change (contract). - Re-stamp migrations before you merge. A branch that adds migrations runs
templates/migrations/restamp.shafter its final rebase, so its versions are newer than everything onmain. A lower version is skipped in silence.
The reasoning behind each of these, and what to do when they are awkward, is in conventional/branching.md.
Commit messages
Follow Conventional Commits: feat:, fix:, chore:, refactor:, hotfix:, etc.
Hotfixes
A hotfix is an urgent production fix that cannot wait for the next release day: users are affected now. It is a smaller release, not a different one - same main, same pipeline, same approval.
- If the bad change is in the live release, press rollback:prod instead. It is faster and already available.
- Otherwise branch off
mainashotfix/<short-description>, keep the diff to the fix alone, and open a PR intomain. One approval, full pipeline green - no checks are skipped. - Merge, verify the fix on staging, then tag a patch bump (e.g.
v1.4.1) and approve.
Details, including when a hotfix is the wrong tool and what to do if it needs a migration, are in conventional/release.md.