Skip to content

ADR-0015: GitFlow-lite branch model with develop → main promotion for multi-env repos

  • Status: Accepted — implemented 2026-06-06 (develop branches live on mgh-website + quantmods-website + mgh-admin-ui + mgh-admin-api; default branch stays main for every repo)
  • Date: 2026-06-06
  • Deciders: cloud-architect, cloudflare-expert, oci-expert, finops-agent, secops-agent, marnissi.investments

Context

With two environments (dev and prod, per ADR-0013) and two brands now in scope, the CI/CD pipeline must have a clear model for how code moves from development to production. Today every application repo uses a single main branch with no formal dev-to-prod promotion step. For mgh-website, which is the only repo with a live CI deploy, every merge to main goes straight to the production bucket. There is no stable branch whose state deterministically maps to the dev environment.

The workspace CLAUDE.md and existing CI setup do not specify what branch triggers a dev deploy versus a prod deploy. Without this decision recorded, each repo agent makes an independent choice, leading to drift: one repo uses main for dev, another uses release, a third uses environment-specific tags. The multi-brand expansion amplifies this risk because quantmods-website will join the CI pipeline immediately and needs a clear model to implement against.

Constraints on the branch model decision: (1) GitHub Free plan provides no branch protection rules or rulesets on private repos (documented in workspace CLAUDE.md §Free-plan compensating controls). Any enforcement is social and tooling-based. (2) The existing Conventional Commits convention (workspace CLAUDE.md §Commits) must apply on all branches. (3) infra/ is managed via tofu apply (operator-authorised, not CI-triggered); it has no deploy step and needs no env promotion concept. (4) mgh-docs deploys from main only via Cloudflare Pages (ADR-0014); PR previews replace the dev branch concept for that repo. (5) labs/ is R&D with no production deployment; no env promotion needed.

Decision

Adopt a GitFlow-lite model for repos that deploy to both dev and prod environments. Two long-lived branches per repo:

  • develop — the integration branch. Feature branches are merged here. CI deploys to the dev environment on every push to develop.
  • main — the production branch. Receives changes exclusively via a promotion PR from develop → main. CI deploys to the prod environment on every push to main.

Scope of this model. Applies to: mgh-website, quantmods-website, mgh-admin-ui, mgh-admin-api. These are the four repos with both a dev and a prod deployment target (per ADR-0013 hostname matrix).

Excluded repos and their branch model. The following repos remain main-only and are not subject to this ADR's promotion flow:

Repo Rationale
infra/ tofu apply is operator-authorised and not triggered by CI on any branch. The branch is a code review surface, not a deploy trigger.
devops/ Reusable workflows are consumed by other repos via uses: @main. No deploy step; main is the stable reference that callers pin to.
mgh-docs/ CF Pages deploys from main only. PR previews serve as the dev iteration surface (ADR-0014). A develop branch would trigger a second prod-equivalent Pages deployment with no hostname to distinguish it from prod.
labs/ R&D workloads; no production deployment path, no env promotion concept.

Default branch: main for all repos. The default branch is NOT changed to develop. GitHub's default branch determines: (a) the base for new PRs when not explicitly specified; (b) what external forks and clones check out. Keeping main as the default means that an accidentally misdirected PR (feature branch opened with no explicit base) targets main, which requires manual retargeting to develop. This friction is the correct safety boundary — a developer who means to open a PR to develop must do so intentionally. Additionally, auditors, new hires, and security reviewers who clone a repo without reading branch documentation get the prod-shaped state by default, which is the safer starting point for review.

Feature branch workflow. Feature branches follow the existing workspace convention: <type>/<issue-number>-<slug> (e.g., feat/42-add-portfolio-chart). Feature branches target develop, not main. PRs are opened feature → develop. The /workflow:ship command (workspace CLAUDE.md §/workflow) is the enforcement point: it opens PRs against develop for in-flight feature work and refuses to merge on red CI.

Promotion PR shape. When the operator is ready to promote develop to prod:

Title: release: promote <repo> to prod (YYYY-MM-DD)
Base:  main
Head:  develop

The PR body is auto-generated with the commits since the last promotion:

gh pr create \
  --base main \
  --head develop \
  --title "release: promote <repo> to prod ($(date +%Y-%m-%d))" \
  --body "$(git log main..develop --oneline)"

The merge strategy for promotion PRs is merge commit (not squash, not rebase). Squashing a promotion PR would compress the entire sprint's work into one commit on main, discarding individual commit context and making git blame/git bisect useless on the prod branch. The merge commit on main records the promotion event with a clear timestamp; develop history remains intact and visible.

Single approver for promotion: an owners team member. Since GitHub Free does not enforce required reviews on private repos, this is a social control: the /workflow:ship command checks for a passing CI run before merging; the operator does not self-merge without a second pair of eyes on promotion PRs except in declared emergency hotfix scenarios (see below).

Emergency hotfix path. If a critical production bug requires a fix faster than the develop → main cycle, the hotfix branch targets main directly (hotfix/<issue>-<slug>), is reviewed by the operator, merged to main (triggering prod deploy), and then immediately back-merged to develop (git merge main on develop) to prevent divergence. This is the only sanctioned direct-to-main path. The post-fact back-merge is required — skipping it causes develop to diverge from main and makes the next promotion PR large and conflict-prone.

Free-plan compensating controls. Branch protection rules (required reviewers, required status checks, restrict pushes) are unavailable on private repos on the GitHub Free plan (workspace CLAUDE.md §Free-plan compensating controls). Enforcement relies on: (a) CI checks run on both develop and main; (b) /workflow:ship refusing to merge on red CI; (c) operator discipline not to push directly to main outside declared exceptions; (d) CODEOWNERS (advisory only on Free) listing the owners team on all repos. These compensating controls are not as strong as branch rulesets — they are the best available on the current plan. Upgrading to GitHub Team ($4/user/month) would enable branch rulesets; finops-agent tracks this as an upgrade trigger when team size or release cadence warrants it.

CI branch → env mapping. CI workflow files in each app repo use the branch name as the signal for deploy target. A workflow_dispatch with env input supplements push triggers for manual promotions:

Trigger Branch Deploy target
push develop dev env (dev.<brand>.com or <service>.dev.marnissi-holdings.com)
push main prod env (<brand>.com or <service>.marnissi-holdings.com)
push feature/* no deploy; CI lint/test only

This mapping is implemented in the reusable workflow (devops/, ADR-0016) via the env input parameter. App repo caller workflows pass env: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}.

develop branch creation. infra-agent does not create develop branches; github-agent does this as part of the multi-brand setup plan. A develop branch is created from main at the same HEAD SHA, ensuring dev and prod start in sync. CI is configured on develop from day one.

Consequences

  • Cost (delta vs free tier): $0 delta on GitHub. Two long-lived branches per repo are free. CI minutes for develop branch builds are additive — roughly 2x the current per-repo CI minute usage. For the repos in scope (Next.js builds, React builds, Python API), each build is under 5 minutes; doubling it stays well within GitHub Actions Free tier (2,000 minutes/month for private repos on Free plan). finops-agent monitors cumulative CI minute usage across all repos.

  • Operational surface:

  • develop branches must be created in mgh-website, quantmods-website, mgh-admin-ui, mgh-admin-api. github-agent creates these from main HEAD.
  • CI workflows in each app repo gain a branch condition: deploy steps run only on develop (→ dev) and main (→ prod); lint/test steps run on all branches including feature branches.
  • Promotion PR cadence is operator-driven. Recommended: at minimum once per sprint (two-week cadence). Infrequent promotion causes large, hard-to-review promotion PRs and increases the risk of a develop → main conflict.
  • Back-merge from main to develop after hotfixes must not be forgotten. A missed back-merge is the most common failure mode of GitFlow-lite. secops-agent is not the right reviewer for this; it is a branching hygiene check in the /workflow:ship checklist.
  • The workspace CLAUDE.md §Branches section references this ADR for the GitFlow-lite model. docs-agent updates it.

  • Security posture:

  • Prod deployments are triggered only by merges to main. A compromised feature branch cannot trigger a prod deploy directly; it must be merged to develop (CI runs), then promoted to main via PR (human review + CI). This two-gate structure reduces the blast radius of a compromised branch.
  • CI secrets scoped to the prod deploy (OCI Customer Secret Key for prod bucket, CF API token for prod Workers deploy) should be separated from dev deploy secrets at the GitHub Actions secret level. secops-agent verifies that prod-scoped secrets are not available to workflows triggered by pushes to develop or feature branches. Environment-scoped secrets in GitHub Actions (repository environments) achieve this — but GitHub Free does not support environment protection rules (approval gates). Dev and prod deploy secrets can still be declared in separate GitHub Environments without protection rules; the push branch condition in the workflow prevents misuse.
  • secops-agent checklist: on each promotion PR, confirm that the diff between develop and main matches expectations (no unreviewed commits, no orphaned feature commits). The git log main..develop --oneline output in the PR body is the audit trail.

  • Migration path if we revisit: Removing the develop branch is a one-PR change per repo: merge develop to main, delete develop, update CI workflow conditions to remove branch gating. Feature branches return to targeting main directly. Estimated effort: one hour per repo. No application code changes required.

Alternatives considered

Option Why rejected
main-only with environment differentiation by tag Tags are mutable (force-push) on GitHub Free, provide no automatic CI trigger without tag-push event wiring, and require tagging discipline that is easy to skip under deadline pressure. A permanent develop branch is a more durable signal than a tag convention.
Full GitFlow (develop + release + hotfix + main) Full GitFlow adds a release branch for release candidate stabilization and a separate hotfix branch namespace. For a two-developer team at current scale, the release branch is overhead without benefit — develop → main promotion serves the same stabilisation purpose. Hotfix branches targeting main are handled as a named exception in this ADR. Full GitFlow is the correct upgrade path when the team reaches >5 developers or when release candidate freeze periods are needed.
Trunk-based development (single main, short-lived feature branches, feature flags for isolation) Trunk-based development minimises merge conflicts and maximises integration frequency. It is the right model for teams with strong feature-flag infrastructure and fast CI. MGH does not yet have a feature flag system (no ADR exists for it); deploying unfinished features to prod is not acceptable without flags. Trunk-based development is the preferred long-term direction and should be revisited when a feature flag service is added to the stack.
Environment differentiation by repo (separate prod and dev repos) Multiple repos per service (e.g., mgh-website-prod, mgh-website-dev) fragment code review across repos, break cross-linking, and violate the workspace repo structure. Ruled out on operational grounds.
Branch per environment per brand (e.g., develop-mgh, develop-quantmods) Overcomplicates the branch model for what is a clean separation already achieved at the Tofu and CI level by env and brand input parameters. Both brands' websites can share the same repo (their own repos) and the same develop → main model; brand differentiation happens at CI deploy time, not at branch level.