This is the canonical definition of the merge-blocking pipeline and the GitHub Actions security posture every repo in this portfolio ships. It owns the shape of CI (stage order, gates, token model, branch protection, deploy/release safety); it does not own the content of individual gates — those live in their own standards and are referenced here, not repeated:
| This doc references | For |
|---|---|
SECURITY-AND-SUPPLY-CHAIN-STANDARD.md |
SHA pinning, SBOM, cosign/SLSA, Scorecard thresholds, SAST/SCA/secret/container scanning |
CODE-QUALITY-STANDARD.md |
ruff/mypy/pytest floors, coverage thresholds, uv.lock, src layout |
QUALITY-AND-METRICS-STANDARD.md |
the quality-attribute taxonomy and the per-repo Metrics ledger shape |
ACCESSIBILITY-STANDARD.md |
axe/Lighthouse/pa11y gates, target-size, ACR |
AI-EVALUATION-STANDARD.md |
faithfulness/red-team/calibration gates on prompt/retrieval PRs |
OBSERVABILITY-STANDARD.md |
structured-log schema, OTel, /livez//readyz |
INTERNATIONALIZATION-STANDARD.md |
catalog format, key-parity, pseudolocale |
Enforcement is binary. A control is AUTO-GATE (mechanically checkable, merge-blocking in CI) or REVIEW-GATE (accountable human judgment, paired with a checklist item and a dated durable artifact—committed by default, authenticated current-head PR/deploy metadata only where explicitly required below). There is no "aspirational" third category. If a row below cannot be made one of these two, it is a defect in this document.
The threat model is active, not theoretical: the March 2026 trivy-action force-push (secrets exfiltrated from 75 tags), the tj-actions compromise, and the February 2026 AI-automated cache-poisoning campaign against Microsoft/DataDog/CNCF repos are why every control below exists. Repository-specific adoption gaps stay in the private remediation registry.
Every repo ships one CI workflow (.github/workflows/ci.yml) whose jobs run these stages in this order. A merge to main requires every applicable stage green. make verify (Python) / npm run verify (TS) runs the same gates locally — see §9.
1. format ruff format --check / prettier --check → fail on any deviation
2. lint ruff check (+ zizmor on workflow PRs) → fail on any finding
3. type mypy --strict / tsc --noEmit → zero errors
4. test pytest --cov (branch>=85%, libs>=90%) / vitest → fail under threshold
5. security semgrep + gitleaks + pip-audit/osv + trivy → fail HIGH/CRITICAL (see SEC std)
6. a11y axe-core + pa11y-ci + Lighthouse (UI repos) → see ACCESSIBILITY std
7. perf k6 / Lighthouse CI budgets → regression >10% fails
8. responsible eval/citation/consent/no-outing gates → see RESPONSIBLE-TECH + AI-EVAL
9. build build artifact + container + SBOM + provenance → see SECURITY std
Stages 1–5 are mandatory for every repository. A not-yet-implemented tool
scaffolds them before feature code, and a nested project exposes the gates from
the repository root. Stages 6–8 apply by repository shape and are declared
applicable or N/A-with-reason in the repo's ROADMAP.md Metrics ledger —
silently skipping a stage is a defect.
| Stage | Applies to | N/A-with-reason permitted when |
|---|---|---|
| 1–5 | all repos | never |
| 6 a11y | any repo emitting HTML/UI (frontends, eval harnesses whose reports are user-facing) | no human-facing HTML output, declared in ledger |
| 7 perf | hosted services, frontends, LLM routes | pure library/CLI with no latency contract |
| 8 responsible | AI/RAG/eval, privacy-first, civic repos | repo's Responsible-Tech audit marks the gate N/A |
Default is write; this is wrong. Org-level default is set to read-only (Settings → Actions → General → "Read repository contents and packages permissions"), and every workflow declares a top-level permissions block. Write is granted per-job, never top-level.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
Top-level permissions: present [CICD-02] |
every workflow | zizmor + Scorecard Token-Permissions | AUTO-GATE |
| Token-Permissions score [CICD-03] | 10/10 | ossf/scorecard-action on default branch |
AUTO-GATE (fail < 8) |
| Write scopes [CICD-04] | job-level only, minimal set | zizmor excessive-permissions rule |
AUTO-GATE |
Every workflow carries this required top-level block:
# top of every workflow — deny by default
permissions:
contents: read
jobs:
verify:
permissions:
contents: read # explicit even when same as top-level
# ...
publish:
permissions:
contents: read
id-token: write # OIDC, see §3
attestations: write # SLSA provenance, see SECURITY std
packages: write # only this job, only because it pushesNo secrets: inherit in reusable-workflow calls — pass each secret explicitly. persist-credentials: false on every actions/checkout. No untrusted github.* context interpolated into run: blocks — assign to an intermediate env: var first (zizmor template-injection, merge-blocking).
Every workflow that touches AWS/GCP/Azure authenticates via OIDC; long-lived cloud keys in Actions secrets are prohibited. The trust policy is scoped to the exact repository, branch or Environment, and workflow purpose.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Cloud creds via OIDC [CICD-05] | 100% of cloud-touching jobs | grep for aws-access-key/static creds in workflows; zizmor |
AUTO-GATE |
| OIDC trust subject scope [CICD-06] | repo:org/repo:environment:<env> (never :*, never org-wide) |
review of cloud trust policy artifact | REVIEW-GATE |
| New long-lived cloud secret added [CICD-07] | alert | org audit-log rule on org.update_actions_secret |
REVIEW-GATE |
deploy:
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@<40-char-sha> # v4.x — see SEC std for pinning
with:
role-to-assume: arn:aws:iam::ACCT:role/example-frontend-deploy
aws-region: us-west-2
# NO aws-access-key-id / aws-secret-access-key anywhereThe IAM trust policy sub must pin to the specific repo + environment claim, e.g. repo:owner/example-frontend:environment:production. A wildcard subject is a REVIEW-GATE failure.
The full pinning/SBOM/signing posture lives in SECURITY-AND-SUPPLY-CHAIN-STANDARD.md. CI restates only the merge-blocking surface that this pipeline enforces:
| Metric | Target | Measured by | Gate |
|---|---|---|---|
Every uses: pinned to 40-char SHA + # vX.Y.Z comment [CICD-08] |
100% (incl. reusable workflows and the deploy path) | zizmor unpinned-uses + Scorecard Pinned-Dependencies |
AUTO-GATE |
| Pinned-Dependencies score [CICD-09] | ≥ 9/10 | ossf/scorecard-action |
AUTO-GATE |
| SHA freshness [CICD-10] | Renovate helpers:pinGitHubActionDigestsToSemver, minimumReleaseAge: 72h |
committed renovate.json / dependabot.yml |
AUTO-GATE (config present) |
Every action reference, including preview and deployment workflows, must be
pinned. One straggler tag fails the gate. Migrate with pin-github-action or
StepSecurity Action-Advisor:
# pins every uses: to its current SHA + version comment, repo-wide
npx pin-github-action .github/workflows/*.yml
# verify nothing references a tag/branch
! grep -rEn 'uses:.*@(v?[0-9]|main|master|latest)\b' .github/workflows/Branch protection is enforced through a repository-owned GitHub Ruleset named protect-main and
committed as a per-repo artifact so the posture is reviewable in-tree. An
organization may add inherited rulesets as stricter, separately named defense
in depth, but inherited state does not replace the unique repository-owned
profile that the read-only validator resolves.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Branch-Protection score [CICD-11] | ≥ 8/10 | ossf/scorecard-action |
AUTO-GATE |
Required reviewers on main [CICD-12] |
≥ 1 (≥ 2 for civic/PII repos); exactly 0 only while the bounded solo-maintainer mode below is current | ruleset artifact .github/rulesets/main.json + check_solo_governance.py --hosted |
AUTO+REVIEW |
| Required status checks [CICD-13] | format,lint,type,test,security + zizmor + codeql-actions + applicable a11y/perf/responsible |
ruleset required_status_checks |
AUTO-GATE |
| Dismiss stale reviews on push [CICD-14] | on | ruleset | AUTO-GATE |
Emergency bypass on main [CICD-15] |
one designated maintainer, PR-only (bypass_mode: pull_request); direct admin pushes remain blocked |
committed ruleset actor + bypassed PR attestation | AUTO+REVIEW-GATE |
Force-push to main [CICD-16] |
blocked | ruleset | AUTO-GATE |
Repositories that actually maintain release/* branches create a second committed protect-release
profile with the same deletion, non-fast-forward, signature, linear-history, status, and review
floors. The canonical protect-main profile intentionally matches only refs/heads/main, so its live
parity check is unambiguous.
The committed ruleset doubles as evidence and feeds SLSA Source Track L2 (attest-build-provenance populates sourceLevels only when branch protection with required reviews is active — see SECURITY std).
The bypass is a break-glass path, not a second merge policy. It may be used only when an authorized human explicitly directs the merge and a required external gate cannot produce a result (for example, hosted CI cannot allocate a runner). The change must still be carried by a pull request; direct pushes, force-pushes, and branch deletion remain blocked. Before bypassing, run every available local equivalent, record the blocked check and the explicit authorization in the PR, and merge with the platform's PR bypass so the PR timeline and merge commit remain the audit trail. Never disable or delete the ruleset to force a merge. Routine red tests, missing review, or convenience are not bypass conditions.
required_approving_review_count: 1 is impossible when the author is the only active maintainer;
GitHub cannot count self-approval. An eligible project following CODE-QUALITY-STANDARD.md §7.1 may
set that count to 0 and require_code_owner_review to false, while retaining the PR requirement,
strict required checks, stale-head invalidation, signed commits, deletion/force-push protection, and
empty bypass actors. Its current solo-maintainer declaration names the owner, declaration/expiry
dates, reporting channel, and the automatic return-to-independent-review triggers. The PR carries the
authenticated current-head decision required by CQ-37/CQ-43.
The solo-governance required status runs the repository's pinned standards validator against the
current dated declaration. Hosted mode requires GH_TOKEN/GITHUB_TOKEN and GITHUB_REPOSITORY,
looks up the current PR head and comments through gh api, authenticates the declared human's login,
account type and repository association, and requires the API's push-capable collaborator set to be
exactly that login. It also uses the ordinary read-only workflow token (Metadata:read) to compare the
active hosted protect-main rules against the committed profile. GitHub withholds
bypass_actors from callers lacking ruleset write access; PR code is never given such a credential.
The authenticated current-head owner comment therefore binds the fetched ruleset ID and
updated_at and explicitly declares the hosted bypass list empty. Omitted bypass data is accepted
only after that exact comment passes; a visible non-empty list always fails. Token, API, permission,
ruleset, event/head, comment, or collaborator ambiguity fails closed. Post the final-head comment,
then rerun the same head's failed required check; do not push a record containing a comment URL or
SHA to make it pass.
The status evaluates both the proposed committed profile and the live hosted profile on every PR; branching on the proposed approval count alone is forbidden. The only valid states are:
| Proposed committed profile | Live hosted profile | Result |
|---|---|---|
| normal | normal | API-visible fields must match; platform independent approval remains authoritative |
| normal | solo | fail — hosted count zero must never enter a no-attestation CI branch |
| solo | normal | serialized activation only: exact current-head solo attestation plus equality of every visible field except the still-stricter hosted approval count/code-owner requirement |
| solo | solo | exact visible parity plus the current-head attestation bound to hosted ruleset ID/updated_at and an owner declaration that redacted bypass actors are empty |
Activate solo mode in that order: land the declaration and proposed solo profile in one PR; while the
hosted profile is still normal, post the generated current-head comment and let CI validate the
stricter transition state. The platform still blocks self-merge. Then change only the live approval
count to 0 and code-owner requirement to false. That ruleset update changes updated_at, so the
old comment is stale: post a fresh generated comment and rerun the same head before merging. During
the brief hosted-solo interval, any concurrent PR whose committed profile is still normal fails the
matrix above. Serialize the transition; do not have another mergeable PR in flight.
GitHub's read-only response may omit bypass_actors. In normal/normal mode CI can prove only visible
parity and must say exactly that; omission is never reported as proof of an empty list. Before solo
activation and before a solo deployment, the accountable owner inspects the live settings and makes
the exact ruleset-ID/updated_at-bound empty-bypass declaration. A visible non-empty bypass list
fails in every mode. No ruleset-write credential is exposed to pull-request code.
This profile changes only unavailable human-approval mechanics. It never turns a synthetic review into an approval, never permits direct push, and never relaxes an AUTO-GATE. The normal profile is restored before merging work outside the eligibility boundaries in CQ §7.1 or as soon as a second maintainer joins.
Required status contexts are repository-specific: the generic validator accepts a non-empty unique
set and compares hosted state exactly to the committed set. Each repository's CI passes its own exact
expected set explicitly; it does not inherit this standards repo's job names. The solo profile always
includes solo-governance. Dated governance, synthetic-evidence, and solo-deployment records are
append-only after reaching the base branch; renewal adds a new dated file.
A committed CODEOWNERS routes .github/workflows/, rulesets, IaC, and security-critical files to a required reviewer. Projects with an independent reviewer set require_code_owner_review; an eligible one-person project keeps the routing file but records require_code_owner_review: false under §5.1 because a self-review is not code-owner approval. The normal setting returns with a second maintainer or for an ineligible change.
# .github/CODEOWNERS
* @owner
/.github/workflows/ @owner # workflow edits = poisoned-pipeline risk
/.github/rulesets/ @owner
/infra/ /terraform/ @owner
/SECURITY.md /.github/dependabot.yml /renovate.json @owner
# repo-specific responsible-tech guards (no-outing, consent gate, citation):
/tests/test_no_outing.py @owner
/tests/test_no_identity_inference.py @owner
| Metric | Target | Gate |
|---|---|---|
CODEOWNERS exists and covers .github/workflows/ [CICD-17] |
yes | AUTO-GATE (file presence + path coverage check) |
| Code-owner review required by ruleset [CICD-18] | on; reasoned solo-mode false only under §5.1 | REVIEW-GATE (ruleset artifact + solo declaration) |
The workflows themselves are scanned; zizmor is required.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
zizmor on any PR touching .github/workflows/** [CICD-19] |
zero High/Critical findings; SARIF to Code Scanning | required status check zizmor |
AUTO-GATE |
CodeQL language: actions [CICD-20] |
nightly + on workflow PRs | required status check codeql-actions |
AUTO-GATE |
| Dangerous-Workflow score [CICD-21] | 10/10 | Scorecard (no pull_request_target checkout of untrusted code; no script injection) |
AUTO-GATE (fail < 10) |
# .github/workflows/zizmor.yml
name: zizmor
on:
pull_request: { paths: ['.github/workflows/**', '.github/actions/**'] }
permissions:
contents: read
security-events: write # SARIF upload only
jobs:
zizmor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha> # v4.x; persist-credentials: false
with: { persist-credentials: false }
- uses: zizmorcore/zizmor-action@<sha> # vX.Y.Zpull_request_target / workflow_run are prohibited unless the workflow verifies actor membership and never checks out PR code; the only approved cross-privilege pattern is unprivileged pull_request uploads artifact → privileged workflow_run verifies then acts (zizmor + Scorecard enforce this).
Production/staging deploys run only through a named GitHub Environment with ≥ 1 required reviewer and "Protected branches only" deployment restriction. An eligible one-person portfolio/open-source project cannot use prevent-self-review truthfully; it instead uses protected branches plus the authenticated, current-artifact pre-deploy decision below. Client, regulated, essential, PII-expanding, payment, auth, or safety-critical deployments still require an independent environment reviewer.
| Metric | Target | Gate |
|---|---|---|
| Prod/staging deploy gated by named Environment + required reviewer + prevent-self-review [CICD-22] | yes; eligible solo project uses named environment + protected branch + artifact-bound owner decision, with 0 independent reviewers explicitly recorded | REVIEW-GATE (committed environments-audit-YYYY-QN.json, quarterly) |
| Exact solo deployment decision verified immediately before promotion [CICD-30] | signed provenance plus exact artifact SHA-256, tested source, optional bound evidence record, destination/environment/workflow/promotion command, rollback artifact/evidence, current hosted ruleset, evidence producers/versions, still-open issue-backed experiential gates, excluded-use flags, accountable human, and ≤90-day expiry all match | AUTO+REVIEW (check_solo_deploy_decision.py --artifact --attestation-bundle; authenticated merged-PR comments + committed decision record) |
The solo path uses templates/solo-maintainer-deploy-decision.md at
docs/governance/solo-deploy-decision-YYYY-MM-DD[-release-id].md. Build and test product source
commit P, then create the immutable candidate and record its SHA-256. There are exactly zero or
one post-test evidence commits: optional commit E adds one bound canonical synthetic
accessibility record and nothing else; otherwise E=P and evidence_record is null. The E
binding records the canonical path and material SHA-256. E (or P) becomes
pre_decision_head_sha. The next and final commit D adds only the
deploy-decision record. Product, configuration, policy, runbook, or other evidence changes after
P are forbidden; rebuild and retest from a new P instead.
After every deployment check passes, the accountable maintainer posts the validator-generated exact
decision body from their authenticated GitHub account. It states that no independent human approval
occurred and binds the immutable artifact SHA-256 and digest scope, P, optional E, destination
URL, named GitHub Environment, workflow job, exact audited repository-specific promotion command,
rollback reference/prior digest/test evidence, signed provenance, evidence producers with exact
versions, durable evidence, every open experiential gate, finding disposition, residual risks,
excluded-use flags, current hosted protect-main ruleset ID/updated_at/empty bypass declaration,
human identity, acceptance date, and expiry. The decision date must be on or after governance takes
effect; its expiry is at most 90 days and cannot outlive that governance declaration. It authorizes
only promotion of those exact bytes—never a rebuild or future release.
Immediately before promotion, from a clean full-history checkout at D, run:
GH_TOKEN="$GITHUB_TOKEN" GITHUB_REPOSITORY=owner/repo \
python3 STANDARDS/automation/check_solo_deploy_decision.py \
--artifact <exact-immutable-candidate> \
--attestation-bundle <signed-provenance-bundle> \
docs/governance/solo-deploy-decision-YYYY-MM-DD[-release-id].mdThe full gate recomputes the artifact digest; verifies signed provenance; verifies the tracked
P→E→D graph and every local file/anchor at P; imports the canonical solo-governance and
synthetic-evidence validators; proves the decision and optional evidence comments belong to merged
same-repository PRs at D and E, respectively; and proves GitHub main is D. Live API
checks require exact committed/hosted protect-main parity and the attested ruleset identity/empty
bypass state, a well-formed sole push-capable collaborator result, still-open same-repository issues
for issue-backed open gates, and the protected-branch Environment. The workflow/job named in the
record must exist at P, declare that Environment at job scope, execute the full validator and an
artifact SHA-256 recheck in real run: bodies, then execute the exact named promotion command.
Comments or nested metadata cannot satisfy those checks. Missing network, token, permission,
history, provenance, workflow, or mismatched data fails closed.
--structure-only is drafting assistance and cannot authorize promotion.
This path is unavailable for client/contractual or procurement work, formal-conformance or legally required deliverables, regulated or essential services, changes that create or expand an excluded auth/payment/PII/safety/employment/healthcare/emergency/public-benefit boundary, or remediation of a verified user-impact incident. Those deployments wait for independent human environment approval.
Deploy/release workflows set a concurrency group to prevent racing deploys; called reusable workflows declare their own group independent of the caller.
concurrency:
group: deploy-${{ github.ref }} # deploy/release: serialize
cancel-in-progress: false # never cancel an in-flight deploy
# CI/lint jobs may use cancel-in-progress: true| Metric | Target | Measured by | Gate |
|---|---|---|---|
concurrency: on every deploy/release job [CICD-23] |
present, cancel-in-progress: false |
workflow-lint check / zizmor | AUTO-GATE |
Cache (actions/cache, cache: true on setup-*) is prohibited in any job that deploys to prod, publishes a package, generates SLSA provenance, or holds id-token: write — cache poisoning violates SLSA L3 isolation. Cache is allowed only in pull_request-triggered build/test jobs.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
No caching in release/publish/provenance/id-token jobs [CICD-24] |
enforced | zizmor cache-poisoning rule + workflow-lint | AUTO-GATE |
Reusable workflows. Build/test/security-scan logic must live once in the
central standards repository. The reference contract uses
.github/workflows/python-verify.yml for stages 1–5 and composite actions such
as .github/actions/{a11y-scan,i18n-gates,eval-gates,release-pipeline} for
repo-shape-dependent stages 6–8. Product repos call these components rather
than copy-pasting them. Callers
must not override security-critical inputs (permissions, environment
names); they pass values (paths, thresholds, python versions), never
structure. This kills the per-repo drift that lets a green build in one
repo fail in another.
jobs:
ci:
uses: ChelseaKR/portfolio-standards/.github/workflows/python-verify.yml@<sha> # vX.Y.Z
permissions:
contents: read
# caller may pass project values (coverage floor, package path) but NOT permissions/secrets blanket
with: { package-path: "src tests", cov-fail-under: 90 } # libraries; default 85| Metric | Target | Gate |
|---|---|---|
| Build/deploy logic via central reusable workflow [CICD-25] | yes (product repos) | REVIEW-GATE (quarterly reusable-workflow security review, committed) |
Caller does not override permissions/secrets [CICD-26] |
enforced | AUTO-GATE (zizmor on caller) |
make verify ≡ CI. The single command runs the same gate set CI runs, eliminating local/remote drift. CI calls the same Makefile target it asks contributors to run; the two cannot diverge.
# Makefile — CI invokes `make verify`; nothing in CI runs gates the Makefile doesn't
verify: format-check lint type test security
format-check: ; ruff format --check .
lint: ; ruff check .
type: ; mypy --strict src
test: ; pytest --cov=src --cov-branch --cov-fail-under=85
security: ; pip-audit && gitleaks detect --no-banner # NO `|| true` — see SECURITY std| Metric | Target | Measured by | Gate |
|---|---|---|---|
make verify runs the exact CI gate set [CICD-27] |
identical command surface | CI job that only runs make verify for stages 1–5 |
AUTO-GATE |
Config consolidated in single pyproject.toml, uv sync --frozen [CICD-28] |
no ruff.toml/pytest.ini/requirements.txt strays |
CODE-QUALITY-STANDARD | AUTO-GATE |
Layout fixes this standard surfaces (resolved under CODE-QUALITY-STANDARD,
gated here via "make verify at repo root must exist and pass"):
- A service with split Python configuration consolidates it into a root
pyproject.tomland adds the type gate. - A nested project exposes a root
Makefilethat delegates to the package, or hoists its configuration and lockfile. - A not-yet-implemented privacy-sensitive tool scaffolds §1, §2, and §9 before feature code, sequenced after its consent gate per AI-EVAL/RESPONSIBLE-TECH.
- Duplicate or forked packages record one canonical repository before applying portfolio controls, so audit work is neither duplicated nor lost.
Each repo's ROADMAP.md Metrics ledger (shape defined in QUALITY-AND-METRICS-STANDARD.md) carries the CI-specific rows so enforcement is unambiguous and every optional stage is explicitly applicable or N/A-with-reason:
| Metric | Target | Measured by | Gate | Owner |
|---|---|---|---|---|
| Token-Permissions [CICD-03] | 10/10 | scorecard-action | AUTO-GATE | — |
| Pinned-Dependencies [CICD-09] | ≥ 9/10 | scorecard-action | AUTO-GATE | — |
| Branch-Protection [CICD-11] | ≥ 8/10 | scorecard-action | AUTO-GATE | — |
| Dangerous-Workflow [CICD-21] | 10/10 | scorecard-action | AUTO-GATE | — |
| Cloud auth via OIDC [CICD-05] | 100% | workflow grep + zizmor | AUTO-GATE | — |
| zizmor on workflow PRs [CICD-19] | 0 High/Crit | required check | AUTO-GATE | — |
make verify ≡ CI [CICD-27] |
identical | CI job | AUTO-GATE | — |
| Deploy reviewer gate [CICD-22] | env + ≥1 reviewer | environments-audit | REVIEW-GATE | — |
| a11y / perf / responsible stages [CICD-29] | applicable or N/A-with-reason | ledger row | per stage | — |
A stage marked N/A must carry a one-line reason (e.g. "perf: pure library, no latency contract"; "i18n: single-user English-only CLI, entry point _() documented"). A blank or missing row is a merge-blocking defect.
Minutes are both a cost and a failure mode: a private repo that exhausts its Actions quota (or hits a billing lapse) stops gating merges entirely. Spend the fewest minutes that still enforces every metric in §10.
11a. Visibility is a governance boundary, not a cost lever. Repository visibility defaults to restricted until an explicit publication review clears it. Never publish a repository merely to reduce CI cost; reduce runner usage or fund the required private checks instead.
11b. Runners. ubuntu-latest is the default. macos-* (10× minutes) and windows-* (2×) are forbidden on per-push/PR CI; if a platform genuinely needs coverage it runs on a nightly schedule matrix only, with the reason in a comment. CI does not exercise OS-specific device I/O (e.g. live audio), so OS-matrixing the unit suite is pure cost.
11c. Cancel superseded runs. Every CI/lint workflow sets, at the top level:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true(Release/publish/deploy jobs keep cancel-in-progress: false per §8b.)
11d. Gate heavy & advisory jobs. Browser E2E, fuzz, load, and other multi-minute jobs: a blocking E2E may run on PRs into main; any non-blocking/advisory variant (continue-on-error: true) runs on schedule (nightly) or behind a label, never on every push. An advisory job that cannot fail the merge has no business spending minutes on every commit.
11e. CodeQL. Run on PRs into main + a weekly schedule; drop the redundant push: main trigger when the PR run already covers it. Never on feature-branch pushes.
11f. Cache dependencies. Use the toolchain action's built-in cache (setup-python cache: pip, setup-node cache: npm, setup-uv cache) on all CI — except release/publish jobs (§8c).
11g. Trim PR matrices. Test the single canonical version on PRs; run the full version matrix on main + nightly.
11h. Skip CI on docs-only changes without breaking required checks: filter heavy jobs with paths-ignore: ['**.md', 'docs/**', 'LICENSE'], and provide a same-named always-green skip job so the required status still reports success and the PR stays mergeable. (A required check that simply never runs leaves a PR stuck "Expected — waiting".)
11i. push: scope. Expensive build, coverage, and deploy jobs may restrict
push: to main, with feature validation coming from pull_request. A
repository that can publish source or documentation runs its lightweight
publication-boundary guard on every branch push as defense in depth; the
local pre-push guard and hosting policy remain responsible for preventing an
unsafe push before server-side CI can react.
Measured by a ci-minutes review-gate row in the ledger; the merge-blocking floor is: no macos/windows on PR CI · concurrency-cancel present · advisory jobs scheduled, not per-push.
Last verified: 2026-07-16 · Recheck cadence: per GitHub Actions security-feature release, OpenSSF Scorecard minor (currently v5.5), SLSA spec revision (currently v1.2), and OWASP Top-10 CI/CD update — review at least quarterly given the active supply-chain threat environment. Confirm current action SHAs, Scorecard check weights, and GitHub ruleset schema at build time.