decision
ADR-0027: GitHub repo governance (caseproof org) and GCP Workload Identity Federation (WIF) binding strategy
ADR-0027 (Proposed — governance §1 amended 2026-06-25 to a **phase-aware** posture (maintainer solo-merge accepted during internal-alpha/staging; the second-party-review gate is re-decided at prod go-live). See the Review log., 2026-06-24): GitHub repo governance (caseproof org) and GCP Workload Identity Federation (WIF) binding strategy.
WIF = Workload Identity Federation — GCP’s keyless CI auth: GitHub Actions exchanges its OIDC token for short-lived GCP credentials (no stored service-account key), gated by a provider condition that pins which repo may authenticate.
Status: Proposed — governance §1 amended 2026-06-25 to a phase-aware posture (maintainer solo-merge accepted during internal-alpha/staging; the second-party-review gate is re-decided at prod go-live). See the Review log.
Date: 2026-06-24
Deciders: Seth (Lead Architect), Blair (CEO, material-infra sign-off), Paul Carter (org owner, ruleset actions)
Context
On 2026-06-24 both MemberIntel repos moved from Seth’s personal GitHub account into the caseproof org:
- KB:
sethshoultes/memberpress-intel→caseproof/memberintel-kb(renamed) - Product:
sethshoultes/memberintel→caseproof/memberintel(same name)
Two infrastructure facts surfaced during the move that need a recorded decision, because both are currently implicit and have already caused — or will imminently cause — friction:
1. The org branch ruleset now governs main. The org ruleset “Default branch protection” (bypass_actors: []) requires, on main: a pull request (no direct pushes), 1 approving review from someone other than the last pusher, all review threads resolved, and signed commits. It cannot be bypassed by repo members. Two automation paths conflict with this:
- The KB standup bot (
daily-standup.yml) direct-pushes generated standups tomaindaily. Post-transfer this push is rejected by the ruleset — the daily build/deploy/reindex chain breaks (tracked incaseproof/memberintel-kb#18). - The product repo’s own
deploy.ymltriggers on push tomainbut deploys rather than pushing, so it’s unaffected by the branch rule — noted for completeness.
2. GCP Workload Identity Federation pins the repo by NAME and is not in Terraform. The product repo authenticates to GCP (memberintel-v1) via WIF. The provider’s attributeCondition was assertion.repository == 'sethshoultes/memberintel' — keyed on the repo name/path, which changes on transfer/rename. The WIF pool/provider and the deploy-SA workloadIdentityUser binding are manually created (bootstrap), not managed in infra/ Terraform. The transfer therefore required a hand cutover of the condition + SA binding (done 2026-06-24; validated by a green staging deploy). A name-keyed, hand-managed binding is a latent landmine: the next rename/transfer silently breaks deploys with no test to catch it.
Decision
Governance (phase-aware). The second-party-review gate is a production concern — its rationale (no un-reviewed merges to a customer-facing product) does not yet apply: as of 2026-06-25 there is no customer-facing surface; everything is internal-alpha on staging. Accordingly:
- During internal-alpha (now): a maintainer may solo-merge their own PRs. Org admin Ronald (
raymundo05) granted Seth the maintainer role on 2026-06-25, so--adminmerges succeed. The automated review loops (Copilot +/code-review) are the working quality gate — run on non-trivial / ingest-path / auth / schema PRs because they catch real bugs, but not a hard blocker on alpha velocity. - At/before prod go-live: re-decide the human gate — either re-tighten the ruleset to require second-party approval on
main, or formally accept maintainer solo-merge with the automated loops as the standing gate. This is a material call (Blair sign-off) once it concerns un-reviewed merges to a then-customer-facing product. - Automation (independent of the above): the KB standup bot still needs a narrowly-scoped bot-only bypass actor — it direct-pushes to
mainand bots can’t--admin, so it stays blocked (caseproof/memberintel-kb#18) until that bypass is added (org-owner action). Do not widen it beyond the single bot identity.
WIF binding. Harden the product repo’s WIF so it survives future renames/transfers: change the provider attributeCondition and the deploy-SA principalSet binding from the repo name to the stable repository_id (numeric, unchanged by rename/transfer), and bring the WIF pool/provider/binding into infra/ Terraform so the configuration is versioned and reviewable rather than living only in the live GCP project. Until both land, the manual cutover procedure is documented in docs/runbooks/transfer-github-repo-to-caseproof-org.md.
Consequences
Positive:
- Deploys survive future repo renames/transfers (the failure mode we just hit becomes impossible).
- Governance is explicit and auditable; automation coexistence is intentional, not accidental.
- WIF config in Terraform is reviewable and reproducible, ending the “manual, undocumented, name-keyed” trifecta.
Negative / costs:
repository_idis opaque (a number, not a readable path) in the condition — mitigated by a comment recording which repo it maps to.- Bringing WIF into Terraform has a bootstrap chicken-and-egg (the WIF that lets CI run TF would now be managed by that TF) — handled with a one-time
terraform importof the existing pool/provider/binding rather than recreating them. - Adding a bot bypass actor slightly widens the ruleset surface; scope it to the single bot identity and document it here.
Mitigations:
- Record the numeric
repository_id→ repo-name mapping in the Terraform and in this ADR when implemented. terraform importthe live WIF resources; verify with a staging deploy before considering it done.- Keep the cutover runbook current as the fallback for any out-of-band move.
Alternatives considered
- Keep name-based, manually-managed WIF. Rejected — it silently breaks deploys on any future rename/transfer, with no test or alert to catch it. We just paid this cost once; recording it as “accepted” would guarantee paying it again.
- Add a broad admin/human bypass actor to the ruleset so maintainers can solo-merge. Accepted for the internal-alpha phase; re-decided at prod (see Governance, amended 2026-06-25). The original rejection rationale — un-reviewed merges to a customer-facing product — doesn’t bite until a customer-facing product exists; it doesn’t yet (staging-only). At prod go-live this reverts to a live decision: re-tighten vs. formally accept.
- Rewrite the standup bot to open a PR instead of direct-pushing. Rejected as a standalone fix — auto-merging that PR is blocked by the same
required_approving_review_count/require_last_push_approvalrules, so it doesn’t actually unblock automation without a bypass actor anyway. - Leave WIF out of Terraform but switch to
repository_id. Partial — fixes the rename landmine but leaves the config undocumented/unreviewable in GCP. Acceptable as an interim step if the Terraform import is deferred, but the import is the durable answer.
Review log
- 2026-06-24 — Drafted (Seth), surfaced by the org transfers. Status: Proposed.
- 2026-06-25 — Governance §1 amended to phase-aware (Seth). No customer-facing surface exists yet (staging / internal-alpha only), so the second-party-review rationale doesn’t yet apply. Org admin Ronald (
raymundo05) granted Seth the maintainer role → maintainer solo-merge (--admin) is the accepted interim posture, with the automated review loops (Copilot +/code-review) as the working gate. The human-gate posture is re-decided at/before prod go-live (material — Blair sign-off). The WIF section (§2) is unchanged and still pending implementation. The KB standup-bot bypass (caseproof/memberintel-kb#18) remains required independent of the human-merge posture.