Development Lifecycle — From Ticket to Production¶
A visual, end-to-end walkthrough of how a Linear ticket moves from "just created" to "shipped to prod". For the reference rules (branching, merge strategies, ruleset config), see GIT_WORKFLOW.md. This doc focuses on the flow.
The three long-lived branches¶
| Branch | Purpose |
|---|---|
main |
Production. Protected. Merge-commit-only. Append-only history. |
release/beta |
Aggregates UAT-passed tickets for the next release. Created fresh each cycle from main. Deleted after the release ships. |
staging |
UAT environment. Squash-only. Periodically reset via reset-staging.yml. |
Full flow diagram¶
Day 0 — new ticket DEV-XXX created in Linear
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 1. Developer creates branch │
│ │
│ if release/beta exists: base = release/beta (cycle in progress) │
│ else: base = main (first of new cycle) │
│ │
│ $ git checkout -b feature/pablo/dev-XXX-description │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 2. Developer builds + pushes + opens PR → staging │
│ │
│ /build DEV-XXX → plan, build, run checks │
│ /pr → squash PR into staging │
│ │
│ Merge strategy: Squash (enforced by staging ruleset) │
└──────────────────────────────────────────────────────────────────────────┘
│ PR merges
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 3. deploy-staging.yml runs automatically │
│ │
│ - Deploys app/aihub/lambdas/migrations to staging (as needed) │
│ - Moves Linear ticket → "Testing" │
│ - Assigns QA │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 4. QA tests on staging.farmcove.co.uk │
│ │
│ FAIL → dev fixes on same branch, pushes; staging redeploys; re-test │
│ PASS → QA moves Linear ticket to "UAT Passed" │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 5. linear-uat-passed-create-pr.yml runs automatically │
│ │
│ IF Linear label = "Hotfix": │
│ → creates PR: bug/dev-XXX → main (skip to STEP 8) │
│ │
│ ELSE (normal release flow): │
│ → ensures release/beta exists (creates from main if missing) │
│ → git merge --no-ff feature/dev-XXX into release/beta │
│ → creates "Integrate feature/dev-XXX into release/beta" commit │
│ → moves Linear → "Ready for Deployment" │
│ → comments Linear with release/beta branch URL │
│ │
│ CONFLICT → comment on Linear, dev pulls release/beta, rebases, │
│ resolves, pushes; moves ticket back to "UAT Passed" to retry│
└──────────────────────────────────────────────────────────────────────────┘
│
│ (developer's role ends here for
│ release tickets — wait for batch)
│
▼
◆─────────────────── release/beta accumulates tickets ───────────────◆
│ │
│ DEV-504 integrated ──┐ │
│ DEV-506 integrated ──┼──▶ release/beta │
│ DEV-510 integrated ──┤ │
│ DEV-511 integrated ──┘ │
│ │
◆────────────────────────────────────────────────────────────────────◆
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 6. Release manager decides to ship the batch │
│ │
│ Opens PR manually: release/beta → main │
│ Requirements: │
│ - head = release/beta (magic trigger for deploy-production.yml) │
│ - base = main │
│ - Merge strategy: Merge commit (enforced by main ruleset) │
└──────────────────────────────────────────────────────────────────────────┘
│ PR reviewed + merged
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 7. deploy-production.yml runs automatically │
│ │
│ - Walks "Integrate X into release/beta" merge commits to find tickets │
│ - Deploys app/aihub/lambdas/migrations to production │
│ - Runs smoke E2E tests │
│ - Generates release notes aggregating all tickets │
│ - Creates version tag (v1.23.0) │
│ - Creates GitHub release with notes │
│ - Updates every Linear ticket in the batch → "Done" │
│ - Sends email to team │
│ - Deletes every feature branch referenced in the integrate commits │
│ - Deletes release/beta itself │
└──────────────────────────────────────────────────────────────────────────┘
│ simultaneously
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 7b. post-main-merge.yml runs automatically │
│ │
│ - Syncs main → staging (so staging catches up with prod) │
│ - Deletes the hotfix branch itself (for hotfix PRs) │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
cycle complete. new tickets base on main again
(release/beta doesn't exist until the first ticket
of the next cycle triggers its recreation in STEP 5)
═══════════════════════════════════════════════════════════════════════════
HOTFIX BYPASS PATH (STEP 8) — only for tickets labelled "Hotfix"
┌──────────────────────────────────────────────────────────────────────────┐
│ bug/dev-XXX → main (PR created by STEP 5 automation OR manually) │
│ - Merge strategy: Merge commit (enforced by main ruleset) │
│ - On merge: │
│ · deploy-production.yml deploys straight to prod │
│ · Linear ticket → "Done" │
│ · post-main-merge.yml syncs main → staging │
│ · Hotfix branch auto-deleted │
└──────────────────────────────────────────────────────────────────────────┘
Who does what¶
| Actor | Responsibilities |
|---|---|
| Developer | Creates branch → opens PR to staging → fixes UAT bugs on same branch → done (no second PR for features) |
| QA | Tests on staging → moves Linear ticket to "UAT Passed" |
linear-uat-passed-create-pr.yml automation |
Integrates UAT-passed feature into release/beta, or creates hotfix PR for tickets labelled Hotfix |
| Release manager | Decides when to cut a release → opens release/beta → main PR → gets it reviewed → merges |
deploy-production.yml automation |
Everything after the release/main merge: tickets, tags, release notes, Linear updates, branch cleanup |
Ruleset enforcement at each merge¶
| Merge | Base branch ruleset enforces |
|---|---|
| Feature/bug → staging | Squash only, 0 approvals, all-checks-passed must pass |
| release/beta → main | Merge commit only, 1 approval, all-checks-passed must pass |
| Hotfix → main | Merge commit only, 1 approval, all-checks-passed must pass |
reset-staging.yml force-push |
Staging's "Block force pushes" — DEPLOY_BOT App bypass (only identity allowed to force-push) |
Typical timeline¶
Mon AM DEV-401 ticket created
Mon PM PR to staging, merged, UAT testing
Tue AM UAT passed → integrated into release/beta
(repeat for DEV-402, DEV-403, DEV-404 over the week)
Fri AM Release manager opens release/beta → main PR
Fri PM Reviewed, merged → prod deploys → all 4 tickets close
release/beta deleted, staging synced with main
Mon AM New cycle starts; DEV-410 bases on main (release/beta gone)
DEV-410 integrated → release/beta gets recreated from main
"Will I hit conflicts merging my PR into staging?"¶
Usually no, but here's when you can and why:
Case 1 — Your branch based on release/beta, staging already has earlier tickets¶
You branched off release/beta (which contains, say, DEV-504 + DEV-506 integrated via Integrate X into release/beta merge commits). Your PR targets staging, which has the same two tickets as their squashed PR merges (different SHAs, same tree content).
Git's merge algorithm is smart enough to handle this most of the time — it compares tree content, not commit SHAs. When staging's squash and release/beta's integrate-commit produce the same file contents, git sees no conflict.
You only hit conflicts when the content differs. That happens when:
- YAML or other files got reformatted during manual conflict resolution on one merge path but not the other (the classic "editor auto-formatted on save" issue — mitigated by our
.prettierignoreexcluding.github/workflows/*.yml). - Someone edited a file on release/beta (via a conflict-resolution merge commit) in a way that differs from the squashed version on staging.
- A release/beta integrate pulled in a branch that had been rebased or amended between the staging PR merge and the release/beta integration.
Case 2 — Feature A and Feature B both touch the same lines¶
Classic conflict — happens in any git flow. Solution: whoever merges second rebases their branch against staging (or release/beta, wherever they're based) and resolves.
Case 3 — Your branch based on main, staging has ahead-commits¶
If the release cycle hasn't started yet (no release/beta exists), you branch from main. Your PR to staging will merge cleanly unless someone else's staging PR already touched the same lines (Case 2).
How conflicts self-heal each cycle¶
When the release/beta → main PR merges:
- Main now has the full release's content.
post-main-merge.ymlsyncsmain → staging.- Staging and main are now identical.
- Next cycle starts with
release/betarecreated from main → all three branches converge.
So even if divergence builds up within a single release cycle, it gets wiped on every shipped release.
What protects against recurring conflict storms¶
| Protection | Where |
|---|---|
Branch from release/beta when it exists |
.claude/skills/build/SKILL.md — stops stale-base conflicts |
Prettier ignores .github/workflows/*.yml |
.prettierignore — stops auto-formatter drift between branches |
| Staging ruleset enforces squash-only | GitHub ruleset — keeps staging history clean and predictable |
| Main ruleset enforces merge-commit-only | GitHub ruleset — release flow depends on merge-commit structure |
reset-staging.yml periodically resets staging |
Cleans up abandoned / stale features; run after each release |
When you will have to manually resolve conflicts¶
- Your feature's PR to staging if another developer merged a touching change first → never merge or rebase staging into your branch (your branch later merges into
release/beta, so absorbed staging content would ship unreviewed). Resolve based on where the conflicting change came from — mergeorigin/main/origin/release/betaif it's already shipping, merge the one dependency branch, or use a throwaway integration branch. SeeGIT_WORKFLOW.md§ "The One-Branch Rule" and § "Conflict on your staging PR is your collision detector". - Your feature's auto-integration into release/beta fails because an earlier cycle ticket already changed the same lines → the Linear automation comments on your ticket telling you to merge release/beta into your branch, resolve, push, and move the ticket back to "UAT Passed" to retry. See
GIT_WORKFLOW.md#edge-casesfor the exact commands.
Related docs¶
GIT_WORKFLOW.md— the reference rules, ruleset configurations, edge casesCODING_GUIDELINES.md— code standards.claude/skills/build/SKILL.md— the/buildskill used in Step 2.claude/skills/pr/SKILL.md— the/prskill used in Step 2