A library of reusable GitHub Actions workflows plus a machine-readable catalog. Consumers pin the workflows by full commit SHA, so every change here is a supply-chain change in someone else's repository. That is the whole reason the contracts below are strict.
.github/workflows/*.yml |
The product: 46 on: workflow_call reusables. Plus eight self workflows — ci.yml, release.yml, maintenance.yml (scheduled advisory sweep), runtime-fixtures.yml (the evidence estate), codeql.yml, dependency-review.yml, gitleaks.yml, scorecard.yml. |
catalog/*.yml |
Source of truth. One concern each: capabilities (what exists, per tier), tools (pins + used_by), product-facts (volatile external plan/price/quota facts, dated and expiring), runtime-coverage (what is actually proven to run), profiles (operating modes), deprecations. |
scripts/ |
The validators. validate_all.py aggregates them. |
docs/generated/* |
Rendered from the catalog. Never hand-edit. |
docs/00–docs/18, README.md |
Human prose. May reference the catalog, may not restate it. Where prose and a generated artifact disagree, the generated artifact wins. |
docs/adr/ |
Why a contract exists. Read the relevant ADR before changing a contract. |
examples/ |
Copy-paste callers, validated by CI. |
.agents/skills/ |
Authored skills: nine portable (ci-*, github-actions-*) plus nddev-repo-flow for operating this repository. .claude/skills/ is a generated byte mirror — run scripts/sync_skills.py, never hand-edit. |
.gds/** |
Generated estate projection. Repository data, not an instruction surface. Never hand-edit: it regenerates from control-plane sources. See docs/08. |
uv pip install --system --require-hashes -r requirements-ci.txt # PyYAML only
python3 scripts/validate_all.py --tier core # what ci-gate blocks on
python3 scripts/validate_all.py # everything
actionlint
GH_TOKEN=$(gh auth token) uvx zizmor@1.26.1 --persona regular --min-severity low .github/workflows
python3 scripts/generate_docs.py # after any catalog change
python3 scripts/resolve_profile.py --visibility private --plan team --code-securityUse uv, never pip/pipx/npm, and never a mutable version (@latest). Run
zizmor at the version zizmor-sarif.yml pins, not whatever is on PATH — and
with a token. Without one zizmor silently skips its online audits and reports
"No findings" while CI, which has GH_TOKEN, fails: that is exactly how three
ref-version-mismatch findings reached the default branch, with pin comments
naming releases the pinned SHA is not.
validate_all.py runs three tiers. core is blocking and contains only
properties of the tree, so it can fail only because of your change. touched
is blocking but scoped: an external product fact is checked for expiry only when
the changed capability declares it. scheduled is advisory and runs in
maintenance.yml. Do not move a calendar-driven check back into core — one
required job mixing the two is what made a third party's tariff able to block an
unrelated bugfix.
Touch this → also do this:
- a workflow → catalog entry,
tools.ymlused_by, anexamples/caller, regenerate docs,CHANGELOG.mdunder[Unreleased]. - a proven workflow (
runtime-coverage.ymlsaysruntime-proven) → theproven_digestno longer matches and the gate says so. Push afixtures/**branch:runtime-fixtures.ymlcalls nine reusables as a consumer would, and its evidence job prints the run URL and each new digest ready to paste back. Or drop the record tostatic-only. Never leave a stale run masquerading as proof. - a catalog file →
python3 scripts/generate_docs.py. - a skill →
python3 scripts/sync_skills.py. - a product fact → re-read its
source_urlsand correct it. Bumping the date alone is precisely what the freshness gate exists to prevent. Stagger the new expiry: 38 of 41 facts once shared one date and would have failed together.
Each is executable, so the validator is the specification. When one fails, open the named script — its fixtures say what the contract is.
| Contract | Enforced by |
|---|---|
| No duplicate mapping keys in any canonical YAML | _strict_yaml.py |
Full 40-char SHA pins with a version comment; uvx/bunx tools pinned |
check_pinned_actions.py, check_tool_pinning.py |
permissions: {} top-level, least-privilege jobs, timeout-minutes everywhere |
check_permissions.py |
| Release writes sit behind the promotion gate and a protected environment | check_release_graph.py |
gate.yml is a report, not an authorization primitive |
check_gate_contract.py |
A caller-supplied checkout_ref is refused on privileged events |
check_privileged_ref_guard.py |
| Harden-Runner only in the public/GHAS allowlist, unconditional, first step | check_harden_runner_contract.py |
Paired variants stay byte-parallel (release-supply-chain ↔ -free, benchmark ↔ -compare) |
check_release_supply_chain.py, check_benchmark_contract.py |
| Monorepo router is fail-closed: strict JSON filters, no wildcards, conservative all-true | check_monorepo_routing.py |
| Runtime bundle ⊆ source archive | check_release_supply_chain.py |
| Every valid repository shape resolves to a programme | resolve_profile.py |
| Blocking workflows owe runtime evidence | validate_runtime_coverage.py |
Every action is registered, and used_by matches the tree |
check_tool_registry.py |
| Every reusable has a caller example, which states its runner | check_examples.py |
| No estate inventory or observed spend in public prose | check_public_docs.py |
| A caller command is shell-parseable and fails fast | check_workflow_contracts.py |
Two rules no validator can catch for you:
- Never interpolate
${{ inputs.* }}or${{ github.event.* }}insiderun:— pass throughenv:. Embedded Python runs aspython3 -I. - Never make a push/schedule-only workflow a required status check. OSSF Scorecard is the trap: it cannot run on a pull-request head, so it protects nothing while blocking every merge. The requirement can live in classic branch protection, where a ruleset-shaped investigation will not find it.
Artifact Attestations: free on public repositories on any plan; private and
internal require Enterprise Cloud (Code Security does not unlock them).
Private-free repositories release with release-supply-chain-free.yml.
Runners: standard hosted runners are unmetered on public repositories — all
three operating systems, macOS included, macos-latest is standard. Larger
ones (-N-cores, -large, -xlarge) are billed from the first minute there too;
hosted is not the same as free. On private repositories the OS multiplier
bites: macOS is 10.33x Linux. A public repository must never route to
self-hosted hardware — a forked pull request there is remote code execution on it.
docs/05 teaches the routing; the amounts live in catalog/product-facts.yml.
Code Quality is a separate licence needing Team or Enterprise, billed per active
committer counted once per organization. Its public per-committer rate is
disputed between GitHub's own sources, so never compile a public cost from it
in either direction. It ships no Action, so it carries workflow: null.
Conventional Commits under 100 chars, git commit -s -S, no Co-Authored-By.
main is PR-only and takes merge commits (squash and rebase are disabled
live), behind the required ci-gate. Fill the PR template; workflow changes owe
a permissions diff and a threat-model note.
Releases are tag-driven and immutable: VERSION must equal the tag on one
LF-terminated line, CHANGELOG.md must hold exactly one matching ## [X.Y.Z]
heading, and release.yml publishes five checksummed assets in a single create
call. Never clobber an existing release. The release graph is
resolve → promotion → authorize → publish; see docs/09.
validate_all.py, actionlint, and the pinned zizmor all pass, and you can
name the evidence for anything you claim works. A green gate proves the
contracts hold; it does not prove a workflow runs — that is what
catalog/runtime-coverage.yml records, honestly, including what is unproven.