Skip to main content

Context

The eigenoid org started with a main-only branching model: feature branches were cut from main and merged directly back into it. This was adequate in the early-org phase when the contributor surface was small and all repos were either infrastructure or exploratory.

As the org matured — with active product repos (svc-*, app-*) receiving concurrent contributions — the single-branch model introduced risk:

  • No staged validation gate: changes moved straight to main with no enforced intermediary environment. A broken merge could affect production before CI had a meaningful chance to catch environment-specific failures.
  • Direct pushes to main: without dedicated dev and qa branches, there was no structural barrier preventing contributors from targeting main directly.
  • Default branch mismatch: GitHub's default_branch pointed to main, so the standard clone-and-PR workflow naturally targeted production, bypassing any intended promotion flow.

ADR-0003 explicitly listed "branching strategy" as a pending architectural decision at the time of the org's early phase. Two promotion-gate CI workflows already encoded the intent of a three-branch model inside eigenoid/eigenoid (promotion-gate-qa.yml and promotion-gate-main.yml) before any ADR was written — demonstrating operational readiness but leaving the strategy undocumented and unenforced at the org level.

The operational rollout happened on 2026-04-27 via two platform-settings PRs:

  • PR #67: added rulesets code-dev-protection and code-qa-protection, targeting all svc-* and app-* repos by name pattern (established by ADR-0011).
  • PR #68: added rulesets exception-dev-protection and exception-qa-protection, targeting repos that carry the branch_strategy=dev-qa-main GitHub custom property.

This ADR retroactively formalizes that rollout as the canonical decision record.

Decision

We adopt a three-branch promotion model (dev → qa → main) as the standard branching strategy for all product and service repositories in the eigenoid org.

Branch responsibilities

BranchRoleWho targets it
devIntegration branch; default branch for active reposAll contributors (feature PRs)
qaStaging branch; mirrors pre-production stateMaintainers (promotion PRs from dev)
mainProduction branch; always deployableMaintainers (promotion PRs from qa)

Promotion flow

  1. Feature branches are cut from dev; PRs target dev.
  2. dev → qa is a maintainer-only promotion PR. CI enforces that the source branch is dev via promotion-gate-qa.yml.
  3. qa → main is a maintainer-only promotion PR. CI enforces that the source branch is qa via promotion-gate-main.yml.

The default_branch for all repos adopting this model is set to dev so that the standard clone-and-PR workflow lands contributors on the correct integration branch automatically.

Opt-in mechanism: branch_strategy custom property

Repos opt in via the branch_strategy GitHub custom property, managed declaratively in eigenoid/platform-settings. All repos in scope carry exactly one of the following values (no value is treated as main-only):

branch_strategy valueDefault branchApplicable rulesetsPromotion flow
dev-qa-maindevexception-dev-protection, exception-qa-protectionfeature → devqamain
main-onlymain(none via custom property; org-level rules still apply to main)feature → main directly

dev-qa-main applies to repos that require staged validation: active product and service repos with multiple contributors and environment-specific CI gates. The exception-* rulesets (PR #68) enforce branch protection on dev and qa for these repos. Current repos carrying this value: eigenoid/eigenoid, eigenoid/eigenoid-sample.

main-only is the default for repos not explicitly opted in — typically IaC repos, documentation repos, utility repos, and tooling repos where a single promotion tier is sufficient. Org-level rulesets continue to protect main on these repos.

Org-level ruleset mapping

RulesetTargetsMechanismIntroduced in
code-dev-protectionall svc-*, app-* repos (name pattern)PR #672026-04-27
code-qa-protectionall svc-*, app-* repos (name pattern)PR #672026-04-27
exception-dev-protectionrepos with branch_strategy=dev-qa-mainPR #682026-04-27
exception-qa-protectionrepos with branch_strategy=dev-qa-mainPR #682026-04-27

Rulesets are enforced at the org level through safe-settings (ADR-0006), requiring no per-repo configuration. A repo that receives the branch_strategy=dev-qa-main custom property is automatically governed by the exception-* rulesets on the next safe-settings sync.

How new repos adopt this strategy

  1. Set branch_strategy: dev-qa-main in the repo's YAML file in platform-settings.
  2. Declare default_branch: dev in the same YAML file (or its suborg config).
  3. Create dev and qa branches before the safe-settings sync runs.
  4. Add promotion-gate-qa.yml and promotion-gate-main.yml CI workflows to the repo.

For repos using main-only, no additional action is required beyond the default repo creation flow (ADR-0007).

Consequences

Positive:

  • Staged validation: changes must pass through dev and qa before reaching main, reducing the risk of broken code reaching production.
  • No accidental direct pushes to main: default_branch pointing to dev directs all standard clone-and-PR workflows to the integration branch; the exception-dev-protection and code-dev-protection rulesets enforce PR requirements on dev and qa.
  • CI encodes policy: the promotion-gate workflows enforce source-branch constraints at the PR level, making the promotion rules machine-checked rather than convention-only.
  • Org-level enforcement without per-repo config: rulesets applied through safe-settings govern all in-scope repos from a single source of truth in platform-settings.
  • Explicit opt-in semantics: the branch_strategy custom property makes each repo's intended model visible in the GitHub UI and queryable via the API.

Negative / trade-offs:

  • Contributor onboarding friction: contributors must know to branch from dev, not main. The default GitHub UI (star, fork, clone) still surfaces main as the visible default until default_branch is flipped. Mitigated by: CONTRIBUTING.md update, branching ops guide, and default_branch: dev declared in platform-settings.
  • Manual promotion PRs: dev → qa and qa → main promotions are explicit, manual steps. There is no automatic promotion trigger. Mitigated by: the promotion-gate workflows fail loudly on incorrect source branches, and maintainers have clear, documented runbooks.
  • Hotfix back-merge requirement: hotfixes that land directly on main must be back-merged into qa and dev to keep branches in sync. Teams must manage this manually.

Alternatives considered

  • main-only (status quo for most repos): adequate for IaC, docs, and utility repos. Insufficient for active product repos with multiple concurrent contributors and environment-specific CI gates. Retained as the main-only strategy value for repos where it remains appropriate.
  • GitFlow (feature/develop/release/hotfix/main): a richer branching model with dedicated release branches and hotfix lanes. Rejected as over-engineered for the current org size; the overhead of managing release/* and hotfix/* branches is not warranted with a small maintainer group.
  • Trunk-based development with feature flags: a single long-lived trunk with short-lived feature branches and runtime flags to gate incomplete features. Considered; rejected as premature complexity given the current team size and the absence of a feature-flag infrastructure in the org.
  • Per-repo Terraform branch protection rules: managing branch protection per repo in Terraform (superseded approach). Rejected per ADR-0006 — safe-settings with org-level rulesets provides real-time drift correction and a natural inheritance hierarchy that Terraform cannot match for GitHub-specific configuration.

References