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
mainwith 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 dedicateddevandqabranches, there was no structural barrier preventing contributors from targetingmaindirectly. - Default branch mismatch: GitHub's
default_branchpointed tomain, 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-protectionandcode-qa-protection, targeting allsvc-*andapp-*repos by name pattern (established by ADR-0011). - PR #68: added rulesets
exception-dev-protectionandexception-qa-protection, targeting repos that carry thebranch_strategy=dev-qa-mainGitHub 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
| Branch | Role | Who targets it |
|---|---|---|
dev | Integration branch; default branch for active repos | All contributors (feature PRs) |
qa | Staging branch; mirrors pre-production state | Maintainers (promotion PRs from dev) |
main | Production branch; always deployable | Maintainers (promotion PRs from qa) |
Promotion flow
- Feature branches are cut from
dev; PRs targetdev. dev → qais a maintainer-only promotion PR. CI enforces that the source branch isdevviapromotion-gate-qa.yml.qa → mainis a maintainer-only promotion PR. CI enforces that the source branch isqaviapromotion-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 value | Default branch | Applicable rulesets | Promotion flow |
|---|---|---|---|
dev-qa-main | dev | exception-dev-protection, exception-qa-protection | feature → dev → qa → main |
main-only | main | (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
| Ruleset | Targets | Mechanism | Introduced in |
|---|---|---|---|
code-dev-protection | all svc-*, app-* repos (name pattern) | PR #67 | 2026-04-27 |
code-qa-protection | all svc-*, app-* repos (name pattern) | PR #67 | 2026-04-27 |
exception-dev-protection | repos with branch_strategy=dev-qa-main | PR #68 | 2026-04-27 |
exception-qa-protection | repos with branch_strategy=dev-qa-main | PR #68 | 2026-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
- Set
branch_strategy: dev-qa-mainin the repo's YAML file inplatform-settings. - Declare
default_branch: devin the same YAML file (or its suborg config). - Create
devandqabranches before the safe-settings sync runs. - Add
promotion-gate-qa.ymlandpromotion-gate-main.ymlCI 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
devandqabefore reachingmain, reducing the risk of broken code reaching production. - No accidental direct pushes to
main:default_branchpointing todevdirects all standard clone-and-PR workflows to the integration branch; theexception-dev-protectionandcode-dev-protectionrulesets enforce PR requirements ondevandqa. - 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_strategycustom 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, notmain. The default GitHub UI (star, fork, clone) still surfacesmainas the visible default untildefault_branchis flipped. Mitigated by: CONTRIBUTING.md update, branching ops guide, anddefault_branch: devdeclared inplatform-settings. - Manual promotion PRs:
dev → qaandqa → mainpromotions 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
mainmust be back-merged intoqaanddevto 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 themain-onlystrategy 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 managingrelease/*andhotfix/*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
- platform-settings PR #67 — org-level
rulesets
code-dev-protectionandcode-qa-protection(name-pattern-based,svc-*/app-*) - platform-settings PR #68 — org-level
rulesets
exception-dev-protectionandexception-qa-protection(custom-property-based,branch_strategy=dev-qa-main) eigenoid/eigenoid—promotion-gate-qa.yml— CI workflow enforcing thedev → qasource-branch gateeigenoid/eigenoid—promotion-gate-main.yml— CI workflow enforcing theqa → mainsource-branch gate- ADR-0003 — Adopt ADR format — listed "branching strategy" as a pending architectural decision; this ADR resolves it
- ADR-0006 — safe-settings for GitHub governance — org-level ruleset management and safe-settings as the source of truth for GitHub org config
- ADR-0007 — Declarative repo lifecycle governance — repo creation flow; new repos adopting this strategy follow the lifecycle process defined here
- ADR-0011 — Repository naming convention —
svc-*andapp-*prefixes establish the glob patterns used by thecode-*rulesets - eigenoid/platform-settings — source of truth
for all org rulesets, repo properties, and
branch_strategycustom property values