Skip to content

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 .prettierignore excluding .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:

  1. Main now has the full release's content.
  2. post-main-merge.yml syncs main → staging.
  3. Staging and main are now identical.
  4. Next cycle starts with release/beta recreated 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 — merge origin/main/origin/release/beta if it's already shipping, merge the one dependency branch, or use a throwaway integration branch. See GIT_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-cases for the exact commands.