Skip to content

ADR-0010 — Branch strategy + environments + code review policy

ADR-0010 — Branch strategy + environments + code review policy

Status: Accepted (CEO ratifying 2026-05-22) Date: 2026-05-22 Owner: tech-architect Paired implementation: tech-architect (this ADR + infra files + CLAUDE.md edits) + CEO (GitLab branch protection rule application, see §10 below) Supersedes: none Amends scope: ADR-0009 §3.1, §7 — replaces the single-env “master = prod” implicit model; the same VPS now hosts two stacks Related canon: ADR-0009 (VPS topology + LE SAN cert + GitLab CI shape), ADR-0006 (mock-auth posture preserved on both envs), D-015 (paused-indefinitely production migration — staging here is NOT that production)

1. Context

ADR-0009 stood up Phase 14 on a single Hetzner CX22 with one environment, one branch (master), and zero code-review gate. Every commit landed on master, every push triggered the deploy pipeline straight into the public-facing closed-beta endpoint. That topology was correct for the bootstrap window — minimal moving parts, fast iteration, no CI overhead, no review ceremony when the cohort was the CEO solo.

Pre-launch maturity changes the cost-benefit:

  • The cohort is widening. Asynchronous tester traffic is starting to land. Every direct-to-prod push is now a potential cohort-visible regression — a TypeError in /walker/profile reaches actual walkers within ~5 minutes of git push.
  • The codebase is past the “trivial to read in one sitting” stage. Backend modules have cross-imports (combat + quest + walker), Prisma schema has multi-table foreign keys, the data package’s content layer is large enough that an unfamiliar diff is non-trivial to vet at a glance. Single-reviewer-of-record (CEO) without a structured CR step means rapid changes ship without a second pair of eyes.
  • The orchestrator review pattern is now mature. Earlier ADRs and PR-style work showed that orchestrator-dispatched cross-checks (system-reviewer for canon, lead-level review for cross-domain conflicts) catch real issues — naming collisions (D-027 reviewer block on class.pisarz), schema drift, language-policy violations. There is no longer a “we don’t know what good CR looks like for this codebase” excuse.
  • Hotfix discipline is needed. If the production endpoint serves a bad commit, the current rollback path is “git checkout previous SHA on VPS, docker compose up”. Workable, but it routes the recovery through the same direct-push channel as the original mistake, with no second eyes between mistake and fix. A protected main branch with a dedicated hotfix workflow tightens the loop.

What we do NOT need: a remote dev environment for ad-hoc developer experimentation. The CEO works on a local VM (pnpm dev), and that is sufficient for the dev tier. The VPS budget pays for two stacks (prod + staging); a third would push us into a different VPS tier with no observable benefit at cohort scale.

What we do need:

  1. Three branchesmain (prod-deployable), dev (staging-deployable, integration), feature/* (ephemeral per-feature).
  2. Two environmentsprod (real testers, real data, main branch) and staging (internal smoke + exit-scenario matrix, dev branch). The local VM remains the dev tier; no remote dev env.
  3. Merge-request gating — direct push to main and dev is denied. Every change goes through an MR with at least one automated review.
  4. Orchestrator-driven auto-review — every MR open / update triggers a claude-orchestrator review pass. CEO is the human approver.
  5. Branch protection in GitLab — enforced at the platform level, not just convention.

This ADR is the spec for that move.

2. Decision

WalkRPG adopts a 3-branch model (main + dev + feature/*) paired with a 2-environment topology (prod + staging) on the existing Hetzner CX22, orchestrator auto-review per MR open / update, and GitLab branch protection rules enforcing no-direct-push on the two protected branches.

Squash-merge keeps the commit log on protected branches linear and one-commit-per-feature.

The dev tier (local VM, pnpm dev) is unchanged. No remote dev environment is provisioned — this is a deliberate cost decision per §6.

3. Branch model

BranchLifecycleProtectionSourceTargetMerge mode
mainPermanent, always-deployableProtected: no direct push, no force-push, no deleteMR from dev (release flow) or hotfix/* (emergency)Auto-deploys to prodSquash
devPermanent, integrationProtected: no direct push, no force-push, no deleteMR from feature/*Auto-deploys to stagingSquash
feature/<short-name>Ephemeral, deleted after mergeNone — force-push OK during MR iterationBranch from devMR to dev(merged via squash into dev)
hotfix/<short-name>Ephemeral, deleted after mergeNone — force-push OK during MR iterationBranch from mainMR to main AND follow-up MR to dev (sync)(merged via squash into main; cherry-pick or follow-up MR carries the same fix into dev)

Naming: feature/<kebab-case-short-description> — e.g., feature/walker-profile-pagination, feature/cluster-mastery-recipe-import. No Jira IDs (flat backlog).

Hotfix naming: hotfix/<kebab-case-short-description> — e.g., hotfix/auth-callback-500, hotfix/migrations-deadlock.

4. Environment topology

EnvBranchAuto-deploy triggerHostname(s)DatabaseVolumePurpose
prodmainPush to main (post-merge)api.walkrpg.morrisassert.dev, morrisassert.dev, www.morrisassert.dev, wiki.morrisassert.dev (CF-Access-gated), walkrpg.morrisassert.dev (503)walkrpg_prod (compose stack name walkrpg)./pgdata-prod/ on VPSReal testers, real data, public closed beta
stagingdevPush to dev (post-merge)api-staging.walkrpg.morrisassert.devwalkrpg_staging (compose stack name walkrpg-staging)./pgdata-staging/ on VPSInternal smoke testing, exit-scenario matrix runs, pre-prod verification
dev (local)(any)Manual (pnpm dev)localhost:3000Local Postgres (compose dev or container)localDeveloper-local iteration. No remote endpoint.

Wiki + portfolio are deployed once, on prod env only. The wiki is static canon; a staging variant would not surface enough signal to justify the cost. If we ever need a “preview wiki” for major canon edits, we revisit (B-level swap when triggered).

Why no remote dev env:

  • The CEO’s local VM and pnpm dev cover the dev workflow.
  • A third remote env would push the VPS past its memory budget (see §6).
  • The MR-to-staging flow gives the same “shared deployment for verification” benefit at lower cost.

5. Workflow

5.1 Feature flow (normal path)

  1. CEO checks out dev, pulls latest, branches: git checkout -b feature/<name>.
  2. Work happens. Auto-commit at /work boundaries lands commits on the feature/<name> branch (per CLAUDE.md Git conventions, unchanged for feature work).
  3. git push origin feature/<name> — first push sets upstream. CI runs lint + test stages on every push (no build/deploy on feature branches).
  4. When ready, CEO opens MR feature/<name> → dev in GitLab.
  5. On MR open / update, the orchestrator auto-review fires (§6 below). Verdict: APPROVE / NEEDS_CHANGES / FLAG_LEAD.
  6. CEO reads orchestrator’s summary + inline comments. Iterates if needed (force-push to the feature branch is fine — protected branches are unaffected).
  7. CEO approves + clicks Merge. Squash merge. dev advances by one commit.
  8. dev auto-deploys to staging. Smoke test in staging (https://api-staging.walkrpg.morrisassert.dev/).
  9. Feature branch deleted post-merge (manual at first; GitLab can be configured to auto-delete on merge — recommended).

5.2 Release flow (devmain)

  1. CEO inspects staging stability (recent staging runs, no open bugs of consequence).
  2. CEO opens MR dev → main in GitLab.
  3. Orchestrator auto-review fires (same hook as feature MRs — only the source/target combo differs).
  4. CEO reviews + approves + clicks Merge. Squash merge.
  5. main advances by one commit (representing the batch of features that landed on dev since the last release).
  6. main auto-deploys to prod.
  7. Post-deploy smoke test against prod (H1 from the runbook).

5.3 Hotfix flow (emergency on prod)

  1. CEO checks out main, branches: git checkout -b hotfix/<name>.
  2. Fix + commit + push.
  3. MR hotfix/<name> → main. Orchestrator auto-review (expedited — orchestrator flags emergency in the summary if the diff is small + scoped).
  4. CEO approves + merges. Squash. main advances by one commit.
  5. main auto-deploys to prod.
  6. Sync dev: open follow-up MR hotfix/<name> → dev from the same branch (or cherry-pick the merged-squash commit onto a new feature/sync-hotfix-<name> and MR that to dev). Both paths work; the cherry-pick onto a feature branch is cleaner because dev may have diverged.
  7. dev advances; auto-deploys to staging. Staging tracks prod.

5.4 What stays unchanged from the current workflow

  • Commit style type(scope): description — same conventions.
  • Auto-commit at /work boundaries — but commits land on feature/<name>, NOT directly on main or dev. The orchestrator running /work from a feature branch commits to the same feature branch.
  • Co-Authored-By trailer policy — unchanged where applicable.
  • Lint + test gates — unchanged. They now run on feature branches AND on MR pipelines.

6. Resource budget

The Hetzner CX22 has 4GB RAM (per ADR-0009 §4 — corrected from the brief’s “8GB” figure; the spec sheet is 4GB and that is what we provisioned). The two-stack budget:

ComponentApprox memory
prod stack (api + db + nginx + certbot + web + wiki-builder)~560 MB
staging stack (api + db only — nginx/wiki/portfolio shared with prod)~300 MB
OS + Docker daemon + fail2ban + sshd~400 MB
Buffer (kernel page cache, brief spikes during deploy)~600 MB
Total used~1.86 GB
Headroom~2.14 GB

Comfortable on CX22 (4GB). Vertical-bump to CX32 (8GB) is the cost-controlled escape hatch if either stack grows past the headroom; that bump is a B-level autonomous swap within the same VPS class.

No third remote env because adding ~300 MB for a remote dev stack pushes the buffer below 600 MB, narrowing the deploy-time spike margin to uncomfortable. The local VM dev tier is the alternative, and it costs €0.

7. Code review policy

7.1 Orchestrator auto-review per MR

Every MR open or update event triggers an orchestrator review pass:

  1. Gather context: orchestrator reads the MR diff, related ADRs, CLAUDE.md conventions, and any failing CI jobs.
  2. Sub-agent dispatch: orchestrator dispatches the relevant leads inline depending on diff scope (tech-architect for schema/infra changes, narrative-designer for content changes, system-reviewer for canon-touching commits, etc.).
  3. Inline comments: orchestrator posts file-line-level comments via the GitLab API where it has specific concerns.
  4. Summary verdict: APPROVE, NEEDS_CHANGES, or FLAG_LEAD (the last routes to a specific lead for cross-domain conflict mediation per CLAUDE.md decisions policy).

For night 1, the orchestrator is invoked by CEO via claude /review <MR-URL> rather than a GitLab webhook. The webhook integration (GitLab webhook → claude trigger) is a follow-up — see §11.

7.2 CEO self-review as second check

After orchestrator returns its summary, CEO re-reads the diff with the summary in hand. CEO is the final approver — orchestrator’s APPROVE verdict counts as “one approval” toward the protected-branch rule, but only CEO can click Merge.

7.3 Squash merge

Squash merges keep the protected-branch commit log linear: one merge = one commit on dev/main, with the MR title as the commit message. The feature branch’s own commit history is preserved in the MR’s discussion (visible in GitLab UI), so granular /work-boundary commits are not lost — they just don’t pollute the protected log.

8. Branch protection rules (GitLab UI)

CEO applies these in GitLab → positive-walkers/walkrpg → Settings → Repository → Protected branches.

8.1 main branch

FieldValue
Branchmain
Allowed to mergeMaintainers
Allowed to push and mergeNo one (force-merge disabled, direct push disabled)
Allowed to force pushOFF
Code owner approval requiredOFF (no CODEOWNERS file yet — follow-up)
Allow deletionOFF
Require approval from a code ownerOFF
Require approval from anyone (free tier)1 approval required for merge

8.2 dev branch

FieldValue
Branchdev
Allowed to mergeMaintainers
Allowed to push and mergeNo one
Allowed to force pushOFF
Code owner approval requiredOFF
Allow deletionOFF
Require approval from anyone (free tier)1 approval required for merge

8.3 feature/* and hotfix/*

FieldValue
Wildcard patternfeature/*
Allowed to pushDevelopers + Maintainers
Allowed to force pushON (rebase iteration is normal)
Allow deletionON

Same shape for hotfix/*.

Note on the “1 approval required”: GitLab free-tier projects allow Required Approvals only as a hard count, not as a code-owner check. Orchestrator counts as an approval if/when the GitLab API integration lands (§11); until then, CEO is both the approver and the merger (which GitLab does permit on the same MR — the approver-equals-merger restriction is a premium feature).

For night 1, the protection rule that bites is “no direct push” — that alone forces every change through an MR, which is the maturity step we want.

9. CI/CD shape (multi-env)

The pipeline shape extends ADR-0009 §7 with two deploy jobs:

lint (every push, every MR)
test (every push, every MR)
build (push to dev or main only)
deploy-staging (push to dev only)
deploy-prod (push to main only)

Build produces a single image tagged <short-sha> + branch name (dev or main). Both deploy jobs use the same SSH_DEPLOY_KEY / SSH_DEPLOY_HOST CI variables. Branch-conditioned rules: decide which deploy fires.

resource_group: is per-env (resource_group: production for deploy-prod, resource_group: staging for deploy-staging) so concurrent pushes to one env don’t race, but the two envs CAN deploy in parallel.

10. Migration from current master direct-push

CEO executes after this ADR ratifies. Steps are non-destructive — the master history is preserved verbatim under the new name.

10.1 Rename mastermain (GitLab UI)

GitLab → positive-walkers/walkrpg → Settings → Repository → Default branch → change to main. GitLab supports the rename without rewriting history; existing commits stay attached, the default branch label moves.

If a literal main branch does not yet exist when you make the change: GitLab UI offers a one-click rename of the existing default branch. Use that — it renames master to main in place. Confirm the rename in the Branches list.

Local clones update their default with:

Terminal window
git fetch origin
git branch -m master main
git branch -u origin/main main
git remote set-head origin -a

GitLab is the source of truth; local rename is a follow-up housekeeping step.

10.2 Branch dev from main

From the new main:

Terminal window
git checkout main
git pull origin main
git checkout -b dev
git push -u origin dev

10.3 Apply branch protection rules

See §8.1 + §8.2 above. CEO applies in GitLab UI.

10.4 Update .gitlab-ci.yml

build:image rule $CI_COMMIT_BRANCH == "master"$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "dev".

deploy: rule splits into deploy-prod (main) and deploy-staging (dev).

workflow: master references → main.

This update is part of the same commit set as the ADR.

10.5 Open MRs at migration time

Any open MRs targeted at master need to be retargeted at dev (for feature work) or main (for emergency-only). GitLab UI supports retarget without losing the MR thread.

For night 1 (this ADR), there are no open MRs — the migration window is clean.

11. Known limitations / follow-ups

11.1 No ephemeral preview env per MR

GitLab CI supports per-MR preview environments (review apps). We skip this for night 1 because each preview env would consume ~300 MB of VPS memory and the CX22 budget does not have a per-MR slot. For now, “preview” = pnpm dev against the staging API.

Re-evaluate when cohort growth or feature density makes the MR-preview-env benefit obvious. B-level swap.

11.2 No automatic database migration on staging deploy at first

The deploy-staging job CAN run prisma migrate deploy against walkrpg_staging as part of the deploy — and the night-1 implementation in .gitlab-ci.yml does invoke it. The “limitation” is that we have no automated check that the staging migration plan is forward-compatible with prod’s current state. Manual review during MR is the current gate.

Re-evaluate when the schema has a destructive migration that requires a two-phase prod deploy. Tracked under ops follow-up.

11.3 No automatic rollback

Rollback is manual via runbook §K6 (now extended to multi-env). Automated rollback (CI-driven revert + redeploy) is a follow-up — value low at cohort scale.

11.4 Orchestrator review is CEO-invoked, not webhook-driven

For night 1, CEO runs claude /review <MR-URL> after opening the MR. GitLab Webhook → claude trigger requires either a self-hosted webhook receiver (claude-runner-as-a-service) or a GitLab Action equivalent (GitLab has CI-based integrations but no native “trigger external HTTP on MR open” without a custom webhook target).

Follow-up: stand up a minimal webhook receiver (~50 LOC Node) on the VPS that POSTs to a claude-orchestrator entry point. B-level when triggered.

11.5 No CODEOWNERS file yet

GitLab supports CODEOWNERS-based approval routing on paid tiers; we’re on free tier, so the value is limited. We may still author a CODEOWNERS file to give orchestrator + manual reviewers a routing hint per path — B-level when narrative-designer / mechanics-designer want path-scoped review attention.

11.6 Verify-no-direct-push CI job

A pipeline job that fails if a commit lands directly on main or dev (i.e., the branch protection was bypassed via an admin override) is documented as a follow-up. Not strictly required because GitLab branch protection IS the gate; the CI job would be belt-and-braces.

11.7 Staging cert is the same SAN

The Let’s Encrypt SAN cert (per ADR-0009 §5) now covers six hostnames instead of five — api-staging.walkrpg.morrisassert.dev joins the list. Renewal is unchanged; certbot pulls the same fullchain.

If api-staging is ever removed from the SAN list (e.g., staging moves to a different host), the cert rotates on next renewal automatically.

12. Consequences

  • Prod stability tightens. Every commit reaching prod has been through orchestrator review, staging deploy, and CEO sign-off. The cohort sees fewer regressions per unit time.
  • Iteration latency increases. A feature now takes minimum two MRs (feature → dev, then dev → main) to reach prod, plus one orchestrator review pass. At cohort scale this is the right tradeoff; pre-CR it was not.
  • GitLab CI minutes consumed roughly double. Each push to a feature branch still runs lint + test; each MR pipeline runs lint + test; each merge to dev runs build + deploy-staging; each merge to main runs build + deploy-prod. Free tier (400 min/mo) remains comfortable at the current change cadence; monitor at ~300 min/mo for upgrade trigger.
  • VPS memory budget holds. Two stacks fit; the headroom is real.
  • Hotfix discipline lands. A bad prod deploy has a defined recovery channel — branch from main, MR, deploy, sync dev — rather than the ad-hoc “checkout previous SHA” pattern.
  • The “every commit ships” workflow ends. Pre-launch maturity comes with the cost of slower cycle time per individual change. This is intentional and reversible if cohort grows in unexpected directions (e.g., a tight pre-public-launch sprint may temporarily re-enable direct-push to dev; protected branches can be relaxed via GitLab UI as a B-level swap, then reapplied).
  • One ADR-0009 amendment lands: §3.1 stack table is now per-env (prod + staging); §1 context is annotated that ADR-0010 supersedes the implicit single-env assumption.

13. When this ADR retires

ADR-0010 stays in effect through Phase 14 and into the production migration (paused per D-015). The 3-branch + 2-env shape is portable to a production-target host swap — only the deploy endpoints change.

The ADR’s protection-rule + workflow content survives a host migration. The infra-specific bits (compose file names, nginx server blocks, LE SAN list) update in the production-migration ADR at that time.

14. Open questions

  1. Should master rename be coordinated with portfolio + wiki sub-monorepo branches? All branches live in the same repo (monorepo), so the rename is one operation. No coordination needed.
  2. Does CEO want auto-delete-source-branch enabled in GitLab settings? Recommend ON for feature/* and hotfix/*. CEO confirms during §10.3 protection-rule application.
  3. Does CEO want squash-only or also merge-commit allowed? Recommend squash-only (cleaner protected log). CEO confirms; squash-only is the default the §10.4 CI update assumes.