2026-05-23 — Workflow Lifecycle & Automation Hygiene
The Factory CI/automation surface is structurally sound but operationally undisciplined. This decision installs a lifecycle, a tiering model, and an enforcement mechanism so the surface stays healthy as it grows.Summary
Factory has 95 workflows producing ~280 runs/day, with ~5 Tier-1 workflows currently red onmain and no paging response, 8 of 27 open PRs sitting unmerged as stale bot snapshots, and 47 workflows missing concurrency: controls. The architecture is correct. The lifecycle is missing. This decision adds the lifecycle.
We are not ripping up the supervisor loop, the auto-merge triangulation, the snapshot-PR pattern, or the security-hardening workflows. We are adding the discipline that turns “fire-and-forget automation” into “fire-and-self-heal automation.”
Empirical findings (2026-05-23)
Decision
Adopt a six-pillar workflow lifecycle, implemented over four phases. Each pillar has an explicit owner artifact, an executable enforcement mechanism (where feasible), and a measurable acceptance criterion.Pillar 1 — Tiered SLO + paging
Every workflow is classified Tier-1 / Tier-2 / Tier-3 in.github/workflows/REGISTRY.md. Red-state SLO is matched to tier:
Pillar 2 — Auto-quarantine for chronic failures
A meta-workflow (workflow-health-warden.yml) runs daily and, for any scheduled workflow with ≥10 consecutive failed runs:
- Disables its schedule via
gh workflow disable - Opens an issue labeled
workflow-quarantinedandpriority/p1against the workflow’s CODEOWNER - Posts to Pushover with the offending workflow + last error step
Pillar 3 — Snapshot PR auto-merge contract
For PRs that touch ONLY paths in an allowlist (.github/snapshot-paths.yml), the opening cron must:
- Open the PR
- Approve from
factory-cross-repo[bot](already a bypass actor on ruleset 15843812) - Enable
gh pr merge --auto --squash
Pillar 4 — Workflow budget gate
A required check (workflow-budget-check) triggers when .github/workflows/**.yml changes. The PR body must contain one of:
retires: <existing-workflow.yml>(and the file must be deleted in the same PR), ORbudget-exception: <reason>(and a CODEOWNER must✅the PR)
Pillar 5 — Concurrency hygiene as a typecheck
A required check (workflow-concurrency-check) validates that every workflow with a pull_request: or push: trigger has a concurrency: block. Initial migration is a single PR fixing all 47 missing blocks.
Pillar 6 — Visible Tier-1 health dashboard
Extendfactory-status-dashboard.yml to surface Tier-1 red state at the top of docs/STATE.md. A single line: “Tier-1 red workflows: 0 (✓) | Last quarantine: 7d ago | Snapshot PR backlog: 0”. When that line is anything other than 0 (✓), the human has the signal without hunting.
Why
Why tiering, not deletion. Each workflow exists because of a documented incident or pattern (#529 for apply-sec-hardening, GitHub’s auto-merge rebase gap for auto-merge-spotter, etc.). Deleting them re-opens the gaps they close. Tiering is the right primitive: it tells the operator which alerts are pageable.
Why auto-quarantine. A scheduled workflow that has failed 10 consecutive times is producing no signal — it’s only producing noise and consuming the credibility of every other alert. Quarantining it preserves the option to re-enable while forcing the broken-window into the human’s attention queue.
Why a budget gate, not periodic cleanup. Periodic cleanup never happens; the workflow that was urgent yesterday is normal today. LIFO-with-exception is the only pattern that holds.
Why concurrency-as-typecheck. It’s mechanical, has zero design space, and every push without it leaks compute. Once added, the rule maintains itself.
Why snapshot auto-merge. Bot review of bot-generated digest data is theater. The choice is: auto-merge it (with a bounded allowlist), commit it directly (loses the auditable PR trail), or stop generating it (loses the data). Auto-merge with allowlist is the only choice that keeps both the audit trail and the operator’s time.
Implementation plan
Four phases. Each phase is a single PR (or small PR series), independently shippable, with an explicit Definition of Done. Phases land in order; we do not begin Phase N+1 until Phase N is green onmain.
Phase 0 — Stop the bleeding (this PR’s pre-work, hours)
Independent of the lifecycle work, the existing red Tier-1 state is an open exposure window.
Phase 0 is not optional. It happens before Phase 1 lands — otherwise we’re enforcing tiers against a baseline that’s already in violation.
Phase 1 — Tier registry + concurrency baseline (1 PR, ~3h)
Files created/modified:.github/workflows/REGISTRY.md(new) — table of all 95 workflows, tier assignment, CODEOWNER, retirement notes.github/workflows/_workflow-concurrency-check.yml(new) — validator that fails when apush:orpull_request:workflow lacksconcurrency:- All 47 workflows missing
concurrency:— single mechanical PR adds the standard block: .github/CODEOWNERS— add/.github/workflows/REGISTRY.md @adrper79-dotto lock the registry to CODEOWNER edits
- REGISTRY.md classifies all 95 workflows (Tier-1: ~12, Tier-2: ~25, Tier-3: ~58 by current estimate)
-
workflow-concurrency-checkpasses on a sample PR touching no workflow files -
workflow-concurrency-checkfails on a deliberately-broken test PR (manual verification) - All 47 listed workflows have concurrency blocks
- Merge does not break any existing green workflow
Phase 2 — Snapshot PR contract + backlog drain (1 PR, ~3h)
Files created/modified:.github/snapshot-paths.yml(new) — the allowlist.github/scripts/snapshot-pr-helper.mjs(new) — helper invoked by each snapshot cron after opening a PR: validates paths against allowlist, approves viafactory-cross-repo[bot], enables auto-merge. Refuses to act on PRs that touch out-of-allowlist paths.- Each snapshot-generating workflow (
generate-state.yml,cost-observability.yml,revenue-digest.yml,morning-digest.yml,completion-tracker.yml,platform-conformance.yml,launch-readiness.yml,track-kpis.yml,pr-queue-digest.yml) — add a final step callingsnapshot-pr-helper.mjsagainst the opened PR docs/runbooks/snapshot-pr-contract.md(new) — the operator-facing runbook
- Next state-snapshot PR auto-merges within 10 minutes of opening
- An adversarial test PR with a snapshot path + an out-of-allowlist path is not auto-merged (helper rejects it)
-
docs/STATE.mdupdates daily without human intervention - Phase-0 backlog drained (precondition); ongoing backlog stays at 0
Phase 3 — Workflow Health Warden + paging (1 PR, ~4h)
Files created/modified:.github/workflows/workflow-health-warden.yml(new) — daily cron at13 13 * * *(after morning-digest, before cohesion-courtesy-check).github/scripts/workflow-health-warden.mjs(new) — the logic:- Read
REGISTRY.mdfor tier assignments - For each workflow, fetch last 20 runs via
gh api - Compute consecutive-failure count
- For Tier-1 red >1h: Pushover P1 + create/update
priority/p0issue (dedup by title) - For Tier-2 red >24h: append to daily digest issue + create/update
priority/p1issue - For Tier-3 with ≥10 consecutive failures:
gh workflow disable+ createworkflow-quarantined+priority/p1issue
- Read
docs/runbooks/workflow-health-warden.md(new) — runbook including how to release a quarantined workflow
- First scheduled run completes successfully
- Quarantine logic tested against a deliberately-broken Tier-3 workflow (or unit test against fixture run-history)
- Pushover alert verified end-to-end on a Tier-1 simulated failure
- Dedup logic: re-running the warden does not create duplicate issues for the same red workflow
gh workflow disable. No state is mutated except quarantined-workflow disable, which is logged in the issue and reversible by gh workflow enable.
Phase 4 — Budget gate + dashboard surface (1 PR, ~3h)
Files created/modified:.github/workflows/workflow-budget-check.yml(new) — required check on PRs touching.github/workflows/**.yml.github/scripts/workflow-budget-check.mjs(new) — parses PR body forretires:orbudget-exception:, fails if neither is present and a new workflow file is addedfactory-status-dashboard.yml— extend to emit Tier-1 health summary intodocs/STATE.mdscripts/generate_state.py— read warden output, inject the Tier-1 status line at top- Branch protection ruleset 15843812 — add
workflow-budget-checkandworkflow-concurrency-checkas required contexts (via.github/security/main-ruleset.jsonsoapply-sec-hardeningwill self-apply)
- Adversarial PR adding a new workflow without
retires:orbudget-exception:is blocked - Legitimate retirement PR (deletes one, adds one, mentions
retires:) passes -
docs/STATE.mdline 1 shows the Tier-1 status summary - Ruleset change verified via
apply-sec-hardeningrun
generate_state.py.
Consequences
Positive:- Tier-1 workflow red state cannot remain undetected for >1h
- Snapshot PR backlog goes to zero and stays there
- Cancelled-run noise drops sharply (47 workflows × ~average 3 cancellations/day → ~140 fewer runs/day)
- New workflows cost something (a retirement or an exception); growth is bounded
- Workflow conformance sub-score (currently 33/100) moves toward 80+ across the portfolio
- 4 new workflows added (warden, budget-check, concurrency-check, snapshot-helper as called-job) — but Pillar 4’s budget gate applies to them too; each is documented and load-bearing
- Snapshot PRs lose the (theoretical) human-review eye on bot-generated data — this is deliberate (see Pillar 3 rationale)
- Quarantining a workflow could mask a real signal if its CODEOWNER doesn’t react to the issue — mitigated by Pushover paging on quarantine event
gh workflow enable.
Non-goals (explicit)
This decision does not:- Change the supervisor loop’s pickup gating or template flow
- Change the auto-merge triangulation (
auto-merge-approved-prs+auto-merge-spotter+copilot-*remain as designed) - Modify FRIDGE rules
- Touch wordis-bond, packages/, migrations/, or Stripe code paths (Red-tier per FRIDGE rule 3)
- Migrate any workflow off scheduled triggers to webhooks (separate decision)
- Flip
allowed_actions: "all"→"selected"or enablesha_pinning_required: true— these are supply-chain hardening decisions worth their own RFC; this decision is about lifecycle, not perimeter
Revisit when
- All four phases land and Tier-1 red-state SLO is observed for 30 days, OR
- A workflow-related incident occurs that the lifecycle didn’t catch (file new decision describing the gap), OR
- Workflow count crosses 110 (the budget gate should be making this near-impossible; if it happens, the gate is being bypassed)
References
docs/supervisor/FRIDGE.md— ten rules; this decision honors rules 3, 6, 8, 9docs/architecture/FACTORY_V1.md— canonical architecturedocs/STATE.md— live state; will be updated in Phase 4 to surface Tier-1 health- Incident #529 — the original branch-protection-by-hand regression that motivates
apply-sec-hardening - PR #602 — the audit that hardened
apply-sec-hardeningto its current shape .github/workflows/COORDINATION.md— existing workflow-level coordination notes (Phase 1 REGISTRY.md will supersede)