Release Workflow (AtomiqFlow)¶
This is the complete reference for how code reaches production at Shootify across backend, webapp, and compute-server. New here? Read Getting started then Shipping a feature — that's everything you need for day one. The rest is reference for when releases and edge cases come up.
Why this workflow exists¶
The old dev → staging → main flow had five recurring pains. AtomiqFlow targets each:
- 🩹
dev/stagingpoisoning — one bad commit blocked everyone. → Previews validate before merge; the trunk stays clean. - 🪟 QA tested a moving target — staging shifted under their feet. → The release PR freezes staging on demand (DRAFT → READY).
- 🤷 Release managers flew blind — "what am I about to ship?" → The release PR body lists exactly what's queued.
- 🔎 Prod was hard to audit — "what was running when X broke?" → Every prod deploy gets a
prod-…timestamp tag. - 🧪 No real env to validate against except by merging. →
cliqpreviews give every feature a full-stack environment.
The model at a glance¶
There is always one release PR — base main, head release/next. It lives in two states:
- DRAFT — accumulating. Every merge to
devfast-forwardsrelease/next, so staging tracks the trunk. The PR body grows with each change. - READY — frozen.
release/nextstops moving; staging holds a stable candidate for QA. Merging the PR ships it to prod.
The release manager flips DRAFT ↔ READY. That toggle is the whole control surface.
Environments & branches¶
| Branch | Deploys to | Who writes |
|---|---|---|
feature/* |
Its own preview | 👤 The author |
dev |
— (no env) | 🤖 PR merges only |
release/next |
Staging | 🤖 Automation (while DRAFT) · 👤 Release manager (the merge) |
main |
Prod | 🤖 Release-PR merge only |
Two deployed environments — staging and prod. There is no shared dev environment; previews cover per-feature testing. Humans only ever push to feature/*; everything else moves through PRs, automation, and the draft toggle.
Getting started¶
Before your first feature:
- Access — membership in the
shootify-ioGitHub org, an SSH key on your account, andgh auth login. - Install cliq (drives the preview environments):
- Learn the two conventions the automation enforces:
- Branch name:
<ISSUE-CODE>/<short-slug>— e.g.SAR-1234/forgot-password. cliq finds your preview by the<ISSUE-CODE>prefix, so the prefix must match across every repo you touch. - PR title:
type(scope): subject—type∈feat | fix | docs | style | refactor | test | chore;scoperequired and lowercase;subjectlowercase first char, imperative, no trailing period, whole line ≤ 72 chars. A CI check (check-title) enforces this on every PR intodev.
Shipping a feature¶
Concrete example: "add a forgot-password button," tracked as SAR-1234.
- Branch from
dev:git checkout dev && git pull && git checkout -b SAR-1234/forgot-password. - Write the code. Iterate locally.
- Push, then spin up a preview: push your branch on every repo you touched, then
cliq preview create SAR-1234. cliq stands up a 3-repo stack (backend · webapp · compute-server) with its own DB, bucket, and URLs. Repos without a matching branch fall back todev. (Un-pushed commits aren't in the preview — commit and push first, always.) A preview is a fullcliq previewenvironment with its own flags, timers, and lifecycle — skim that reference once before your first one. In particular, if your feature needs a new feature-tier env var, set it before youcreate— a preview pins an immutable env snapshot at create time, so a variable added afterwards won't reach the running stack until you rebuild. Set it withcliq preview env set MY_FLAG_URL https://… --service backendfirst; details in per-preview env values. - Validate on the preview. This is where you test against a real environment — there's no shared
devenv anymore. Push more commits, thencliq preview rebuild SAR-1234to fold them in (the DB and bucket survive the rebuild). If you only changed a feature-tier env var rather than code,cliq preview recreate SAR-1234re-snapshots env without a full rebuild. The complete validate-loop toolkit —rebuild,recreate,logs, andextend/holdfor keeping a preview alive through a demo — is in Preview Environments → Lifecycle. - Open a PR targeting
dev, mark it ready, request review.ci-checks(lint/format/types) andcheck-titlerun automatically. - Get review approval. The preview URL lets reviewers poke at the running feature.
- Squash-merge to
dev. One commit per PR keepsdeva clean linear trunk and revert mechanics simple.cliq preview destroy SAR-1234when done. - Verify on staging. While the release PR is DRAFT — the normal, default state — every merge to
devauto-mirrors torelease/nextand redeploys staging within minutes. Outside of a frozen release, what lands ondevis on staging almost immediately. Go exercise your feature there at least once: staging is the first place it runs alongside everyone else's merged work, against staging's prod-cloned data, and some bugs only surface in that combination (data shape, migration ordering, cross-feature interaction). Fix anything you find the normal way — another PR ontodev, or a revert PR (see Edge cases) if it's serious enough to pull back — before the release freezes for QA.
"Done" = merged to dev and validated on staging. The release plumbing from dev to prod is the system's job, not yours — you don't babysit the freeze, the merge commit, or the prod deploy, and if something downstream traces back to your PR you'll be looped in. But confirming your own feature actually works on staging is the one part that stays yours. A feature that merged but was never tried on staging isn't done — it's just queued, and the first person to discover it's broken will be QA on a frozen candidate, which is the expensive place to find out.
Reviewing a feature¶
Most of the time the author hands you a preview URL on the PR and you just click it. Two cases make you stand one up yourself:
- No preview attached — the author already ran
destroy, or never created one. - The branch doesn't follow
<ISSUE-CODE>/<slug>— most often afeature/<something>branch from a non-dev who vibe-coded a change and opened a PR. cliq resolves a preview by matching the code prefix (SAR-1234*) against branch names onorigin, so a branch with no issue-code prefix can't be found by code alone — you have to point cliq at it.
For a conforming branch, the code is the branch prefix, so cliq preview create <code> is enough:
For a non-conforming branch (e.g. feature/instant-checkout on PR #157), invent a canonical code for the preview yourself and name the branch explicitly:
- The code (
review-157) is just the preview's handle — it only has to be canonical ([a-z0-9-], leading letter). Deriving it from the PR number keeps it memorable and collision-free. --branchoverrides branch resolution for all three repos. If the change spans repos on differently-named branches, use--branch-<repo>per repo; repos with no override fall back todev.
From there it's identical to the author's flow — same URL shape, same cliq preview logs / rebuild, same teardown — so you can poke at the running feature, not just read the diff. Destroy it when you're done (cliq preview destroy review-157): review previews are easy to forget and each one holds a host lease. Full command and flag reference: Preview Environments.
The release lifecycle¶
While DRAFT — staging is the live integration trunk¶
The release PR sits in DRAFT by default. On every merge to dev, automation:
- Fast-forwards
release/nextto matchdev. - Updates the DRAFT release PR's body (built with
git log --no-merges, so it lists only real changes — never previous-release or back-merge commits), e.g.- feat(auth): add forgot-password (#142) — SAR-1234. - Redeploys staging.
So staging continuously reflects the trunk, and the release PR always shows what's queued. No human action needed.
READY — freeze, QA, ship¶
When the release manager decides to ship, they mark the release PR Ready for review:
release/nextfreezes — automation stops mirroringdev, so staging holds a stable candidate. Newdevmerges accumulate for the next cycle; they don't disturb QA.- QA validates the frozen staging.
- The release manager merges with "Create a merge commit" (message like
release 2026-05-28). It must be a merge commit — not squash, not rebase: a merge commit makesmain's content identical torelease/nextwhile preserving its commits at their original SHAs (onlymain's tip is the newrelease …commit). Squash/rebase would rewrite history and break the hotfix reconciliation. - Automation deploys prod and creates a
prod-YYYY-MM-DD-HHMMSStag on themaincommit. (Today prod rebuilds frommain, whose content equals the tested candidate; promoting the exact tested image digest is a planned hardening, since thepython:3.12base image floats.) - A fresh DRAFT release PR opens for the next cycle (it fills on the next merge to
dev).
Who is the release manager? A role, not a person — rotating, on-call, or whoever ships first that day. Only requirement: merge rights on main and accountability for the release PR's contents.
Cadence & the 48h rule. Ship as often as you like — small batches keep QA fast. But a release that's been frozen (READY) for more than ~2 days should either ship or be flipped back to DRAFT. Don't let a frozen candidate linger and starve the trunk.
Edge cases¶
The flow above is the 95% path. The rest is what makes or breaks the workflow in practice.
One scenario runs through every example below. Three features have merged to
dev, in this order, and are queued on the current release PR:
#142—feat(auth): add forgot-password— theSAR-1234feature from Shipping a feature above. (This is the same#142you saw in the DRAFT release-PR body earlier.)#143—feat(account): add password-strength meter— merged just after it, and it reuses a validation helper that#142introduced.#144—feat(account): show last-login time— independent of both.The release manager has marked the PR READY, so
release/nextis frozen and staging holds these three for QA. Then QA hits a problem in#142. Every edge case below traces that one thread — so when you see#143/#144, they're these two PRs.
QA finds a bad commit while frozen¶
The PR is READY and #142 (forgot-password) turns out to be broken. The move is: flip the PR back to DRAFT. That re-enables auto-mirror, so a correction landing on dev flows to staging.
You then have two choices.
Choice A — hold the whole release¶
Wait for the author to fix #142 via a normal PR. When the fix lands on dev, auto-mirror propagates it to release/next, staging redeploys, QA re-tests, you re-mark READY and ship. Simplest when the rest isn't urgent.
Choice B — exclude the bad commit, ship the rest¶
The author opens a normal PR with a single commit: git revert <142-sha>. It follows the normal flow — preview, review, merge to dev. Auto-mirror propagates it; staging redeploys without #142's effects. Re-mark READY, QA re-verifies, ship. In prod, #142 and revert(#142) cancel out. The author opens a separate fix PR later, which ships in a future release.
Why this works: nobody ever touches release/next directly. Every change — including the revert — starts on dev. The classic "we fixed staging but forgot to backport to dev, so the bug came back" failure mode is impossible, because dev is the only writable trunk. The draft toggle does the rest.
When the revert isn't clean¶
Whether git revert applies cleanly depends on whether later PRs depend on the reverted change (not merely that they came after it):
- Case 1 — no overlap.
#144(last-login time) never touched the forgot-password code, so reverting#142leaves it untouched. The revert applies clean; the release ships#143+#144without#142. - Case 2 — textual overlap, no dependency.
#143happened to edit the sameauth/file as#142, but a different block.git revert <142-sha>throws a conflict that's purely textual — resolve it on the revert branch and move on; nothing actually depended on the reverted lines. - Case 3 — real dependency.
#143's password-strength meter imports the validation helper that#142introduced. Reverting#142deletes that helper, so#143no longer builds. A plain revert is off the table — pick a strategy from the table below.
The revert PR's preview tells you which case you're in: 🟢 green = Case 1 / resolved Case 2; 🔴 red = Case 3. For Case 3:
| Strategy | Use when | Tradeoff |
|---|---|---|
| Cascade revert | Authors agree to redo #143/#144 later |
Revert #144→#143→#142 together; ships nothing for those today |
| Fix-forward | Bug is small, local, well-understood | Ship #142 + fix; "patch and hope" — high confidence only |
| Hold release | Fix is non-trivial, the rest isn't urgent | Choice A above |
| Retro-flag | Bug only triggers when the feature is used | Wrap #142 in a flag, ship flag-off |
Why we can't just "skip #142" from release/next¶
Force-pushing release/next or cherry-picking a fresh branch both look tempting and both break the model:
- Force-push rewrites shared history; the next auto-mirror (once DRAFT) brings
#142right back fromdev. - Cherry-pick works once, but
#142still lurks ondevto ambush the next release — a hidden divergence between what shipped and the trunk.
Revert-on-dev is the only approach where dev and release/next agree. That consistency is what keeps the model sane across many releases.
Bug found in prod after release¶
- Branch
hotfix/<short-description>frommain. - Write the fix; validate it with
cliq preview create <code>. - Open a PR targeting
maindirectly, review, merge. Prod deploys. - Merge
mainback intodev(git checkout dev && git merge main). Because it's a merge — not a cherry-pick — the hotfix keeps its original SHA ondev, andrelease/nextabsorbs it via auto-mirror (while DRAFT).
Why merge-back is mandatory. Until the hotfix reaches dev/release/next, the branches diverge: main has a fix the candidate lacks. Shipping the next release in that state would merge into a brand-new tree (features + hotfix) that was tested nowhere, straight to prod. Merging main → dev first means release/next picks up the hotfix, staging redeploys, and QA re-validates the combined state — so the next release ships exactly what was tested. And since the hotfix is now the same SHA on both sides, the next release/next → main merge is conflict-free. (If a release is currently READY/frozen when the hotfix happens, the merge-back lands on dev and waits for the next cycle; ship or re-draft the frozen release so it picks the hotfix up.)
The merge-back leaves a small merge bubble on dev; git log --first-parent dev hides it.
Code that should not ship yet¶
The model assumes on dev ⇒ shippable. Code that lands on dev but mustn't ship for two weeks (marketing-gated launches, partner integrations, half-built collaborative features) fights that.
- The right answer is feature flags — code propagates, the runtime switch stays off until launch day. "Shipped" the day it merges; visible the day the flag flips.
- When no flag is possible (DB migrations, large refactors): hold the PR open until you actually want it. Suboptimal — long-running branches drift — but honest. Use it sparingly, not as a habit.
Cross-repo coordinated releases¶
Each repo runs its own AtomiqFlow cycle; they don't auto-coordinate.
- ✅ Backward compatibility — repos deployable independently; new endpoints land before the webapp uses them, old ones deprecate for a cycle before removal.
- ✅ Feature flags — ship both sides behind a flag, flip in lockstep.
- ⚠️ When neither works — sequence the merges (ship backend first, then the webapp release that depends on it) and note the order in both PRs.
Quick reference¶
| You want to... | You do... |
|---|---|
| Start a feature | Branch <ISSUE>/<slug> from dev, push, cliq preview create <code> |
| Set an env var before a preview boots | cliq preview env set KEY val --service <svc> before create (env values) |
Review a PR with no preview / a feature/* branch |
cliq preview create review-<pr> --branch <branch> (Reviewing a feature) |
| Get your feature to staging | Merge your approved PR to dev (release PR is DRAFT → staging redeploys) |
| Confirm your merged feature | Exercise it on staging while the release PR is DRAFT — required before "done" |
| Ship to prod | Release manager marks the release PR READY, QA signs off, merges (merge commit) |
| Drop a bad commit from a frozen release | Flip the release PR to DRAFT, open a revert PR against dev |
| Fix a prod bug | PR to main, then merge main back into dev |
| Hide unfinished code | Feature flag, off by default |
| Branch | Deploys to | Who pushes |
|---|---|---|
feature/* |
Preview | 👤 You |
dev |
— | 🤖 PR merge only |
release/next |
Staging | 🤖 Automation (while DRAFT) · 👤 Release manager (merge) |
main |
Prod | 🤖 Release-PR merge only |