Skip to content

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.

RefWhat happensWho
Merge requestLint, validate, test, image check. All blocking.CI
Merge to mainBuild the image once, migrate staging, deploy staging. Automatic.CI
Tag vX.Y.ZPromote the image staging ran. One approval, then migrate and deploy.Lead approves

Branch rules ​

  • main is always deployable: every commit on it is already running, or has run, on staging.
  • main and v* tags are protected. Every commit to main arrives through a reviewed and approved PR with a green pipeline.

Deploying to staging ​

  1. Open a PR from a feature branch into main.
  2. PR must be reviewed, approved, and its pipeline green.
  3. 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 main pipeline 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.

  1. Pick the commit on main whose work was verified and accepted on staging (its build version names it).
  2. Tag it with the next version: git tag v1.4.0 <commit> && git push origin v1.4.0.
  3. The tag pipeline checks that exact image ran on staging. If it never did, it fails and nothing ships.
  4. The lead presses approve:prod. Migrations and the deploy follow automatically, in that order.
  5. If production misbehaves, press rollback:prod. It switches traffic back to the previous release without touching the schema.
  6. 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 main daily; 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 main or 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.sh answers the same commands - migrate, deploy (with all for 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.sh after its final rebase, so its versions are newer than everything on main. 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.

  1. If the bad change is in the live release, press rollback:prod instead. It is faster and already available.
  2. Otherwise branch off main as hotfix/<short-description>, keep the diff to the fix alone, and open a PR into main. One approval, full pipeline green - no checks are skipped.
  3. 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.