Skip to content

Latest commit

 

History

History
136 lines (112 loc) · 8.44 KB

File metadata and controls

136 lines (112 loc) · 8.44 KB

ci-workflows — agent instructions

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.

What is where

.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/00docs/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.

Commands

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-security

Use uv, never pip/pipx/npm, and never a mutable version (@latest). Run zizmor at the version zizmor-sarif.yml pins, not whatever is on PATHand 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.

Change impact map

Touch this → also do this:

  • a workflow → catalog entry, tools.yml used_by, an examples/ caller, regenerate docs, CHANGELOG.md under [Unreleased].
  • a proven workflow (runtime-coverage.yml says runtime-proven) → the proven_digest no longer matches and the gate says so. Push a fixtures/** branch: runtime-fixtures.yml calls 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 to static-only. Never leave a stale run masquerading as proof.
  • a catalog filepython3 scripts/generate_docs.py.
  • a skillpython3 scripts/sync_skills.py.
  • a product fact → re-read its source_urls and 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.

Contracts that are enforced by code

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.* }} inside run: — pass through env:. Embedded Python runs as python3 -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.

Tier truth

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.

Git

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.

Definition of done

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.