diff --git a/.github/workflows/main-estate-audit.yml b/.github/workflows/main-estate-audit.yml index 348cb5d..98bb68c 100755 --- a/.github/workflows/main-estate-audit.yml +++ b/.github/workflows/main-estate-audit.yml @@ -1,7 +1,48 @@ # This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 name: Central Estate CI/CD Audit +# The estate audit, as FIVE component checks instead of one job of 26 steps. +# +# Why components. One job stopped at its first failing gate, so a single red +# gate hid the other 25 and every failure read "estate-audit: failing after +# 8s" with no gate named. Now each component is its own check run, and inside +# a component every gate runs even after an earlier one fails (the +# `!cancelled()` guard), so one push reports every failing gate at once. +# +# governance required-files, spdx-license, affirmation, referencing +# hygiene code-hygiene, formatting, linguist, secrets, vaulted-tokens +# manifests manifest, recipes-set +# languages idris2-abi, zig-unified-api-adapter, proof-runner, boj-cartridge +# advisory nine gates that cannot fail BY DESIGN (README: "What actually +# enforces"); a green tick here is information, not assurance. +# +# Removed: prat and semantic-audit. They were stubs (README, Known defect 5): +# semantic-audit printed "Semantic Audit Passed." unconditionally and prat +# passed any repo with a tests/ directory, so a green tick credited assurance +# that nothing checked. Their directories stay under actions/ only so that +# not-yet-migrated copies of the old 26-step workflow still start; delete them +# once every consumer calls this reusable workflow. +# +# Consumers call this workflow; they do not copy it (see README, "Consuming +# it"). The caller pins this file at a full SHA and is de-onboarded from its +# own actions.lock, the form proven for callers with sha_pinning_required. +# +# Refs (empirical cross-repo matrix, all witnessed 2026-09-17, runs +# 35277504987 / 35277227942 / 35278067796 / 35279718644 / 35280642055 / +# 35280317253): +# $/actions/* -> resolves at the CALLER's commit cross-repo: 0-job. +# ./actions/* + lock -> "local path actions not supported": 0-job. +# ./actions/* de-onboard -> ./ resolves against the CALLER: no action.yml. +# remote @main/tag + lock (onboarded) -> 0-job startup_failure cross-repo. +# remote @SHA + de-onboarded -> works (35279718644), deed/k9@SHA fetched. +# So every gate is a remote ref at a FULL SHA and this workflow is +# de-onboarded from actions.lock. The bare-by-SHA checkout fetches the CALLING +# repo, so the gates audit the caller's tree, which is the design intent. +# Gate IMPLEMENTATIONS are pinned to the SHA below: repin when actions/** +# changes (tests/lock-transitive-closure.sh fails until you do). + on: push: branches: [ "main" ] @@ -17,88 +58,119 @@ concurrency: cancel-in-progress: true jobs: - estate-audit: + governance: + name: governance runs-on: ubuntu-24.04 steps: - # Remote-form refs to THIS repo's actions at a FULL COMMIT SHA, with - # this workflow DE-ONBOARDED from actions.lock. The empirical cross-repo - # matrix (all witnessed 2026-09-17, runs 35277504987 / 35277227942 / - # 35278067796 / 35279718644 / 35280642055 / 35280317253): - # $/actions/* -> resolves at the RUNNING commit = the - # CALLER's commit cross-repo: 0-job death. - # ./actions/* + lock -> "local path actions not supported": 0-job. - # ./actions/* de-onboard -> jobs spawn, but ./ resolves against the - # CALLER's repo: "Can't find action.yml". - # remote @main/tag + lock (onboarded) -> 0-job startup_failure - # cross-repo (creation-time enforcement under - # the CALLER's jurisdiction; silent). - # remote @SHA + de-onboarded -> BOTH components proven: creation - # passes de-onboarded (35279718644 spawned), - # prepare downloads remote SHA composites - # under the caller's sha_pinning_required - # (same run: deed/k9@SHA fetched fine). - # The bare-by-SHA checkout fetches the CALLING repo cross-repo, so the - # 26 gates audit the caller's estate — the design intent. Trade-offs, - # disclosed: gate IMPLEMENTATIONS are pinned to the SHA above (repin - # when actions/** change; the onboarded code-hygiene-self-test.yml — - # $/ form, full lockfile enforcement — covers composite changes), and - # this workflow sits in the gh-actions-lock local-path/remote-self - # coverage gap ("1 workflow skipped", tolerated not enforced). Revisit - # when the Technical Preview supports $/ across workflow_call. Revisit when - # gh-actions-lock supports local-path actions or $/ across calls. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Required Files Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/required-files-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: SPDX License Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/spdx-license-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Affirmation Document Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/affirmation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Academic Referencing Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/referencing-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + + hygiene: + name: hygiene + runs-on: ubuntu-24.04 + steps: + - id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Code Hygiene Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/code-hygiene-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Formatting Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/formatting-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Linguist & Banned Languages Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/linguist-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Are We UnAPI Gate (Secret Scanning) + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/secrets-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Reasonably Good Token Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/vaulted-tokens-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + + manifests: + name: manifests + runs-on: ubuntu-24.04 + steps: + - id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Manifest Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/manifest-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: Recipes Set Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/recipes-set-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + + languages: + name: languages + runs-on: ubuntu-24.04 + steps: + - id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Idris2 ABI Purity Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/idris2-abi-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Zig UnifiedApiAdapter API Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/zig-unified-api-adapter-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Contractile Validation Gate - uses: hyperpolymath/cicd-suite/actions/contractile-validation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Recipes Set Validation Gate - uses: hyperpolymath/cicd-suite/actions/recipes-set-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Affirmation Document Gate - uses: hyperpolymath/cicd-suite/actions/affirmation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Academic Referencing Gate - uses: hyperpolymath/cicd-suite/actions/referencing-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Semantic Audit Gate - uses: hyperpolymath/cicd-suite/actions/semantic-audit-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: SPDX License Gate - uses: hyperpolymath/cicd-suite/actions/spdx-license-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Proof Runner Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/proof-runner-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: PRAT Testing Gate - uses: hyperpolymath/cicd-suite/actions/prat-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + - name: BoJ Cartridge Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/boj-cartridge-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + + advisory: + name: advisory + runs-on: ubuntu-24.04 + steps: + - id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Contractile Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: hyperpolymath/cicd-suite/actions/contractile-validation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Panic Attack & Pons Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/custom-tools-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: WWW & Well-Known Compliance Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/www-compliance-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: BoJ Cartridge Validation Gate - uses: hyperpolymath/cicd-suite/actions/boj-cartridge-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Formatting Validation Gate - uses: hyperpolymath/cicd-suite/actions/formatting-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Accreditations & Badges Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/badges-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Metrics Extraction Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/metrics-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Linguist & Banned Languages Gate - uses: hyperpolymath/cicd-suite/actions/linguist-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Test & Benchmarks Dashboard Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/tests-benches-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Hosting & Site Status Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/hosting-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Git-Sea Analytics Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/gitsea-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - name: Trust & Humans Validation Gate + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} uses: hyperpolymath/cicd-suite/actions/trust-humans-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Are We UnAPI Gate (Secret Scanning) - uses: hyperpolymath/cicd-suite/actions/secrets-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) - - name: Reasonably Good Token Validation Gate - uses: hyperpolymath/cicd-suite/actions/vaulted-tokens-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) \ No newline at end of file diff --git a/ARCHITECTURE.adoc b/ARCHITECTURE.adoc index ec61161..6305a32 100644 --- a/ARCHITECTURE.adoc +++ b/ARCHITECTURE.adoc @@ -8,14 +8,17 @@ shared library: each gate is a self-contained GitHub composite action whose entire implementation is the bash in its `action.yml`. .... -actions/-check/action.yml one composite action per gate (26) -.github/workflows/ the audit workflow consumers copy -rollout_estate.sh copies that workflow into every estate repo +actions/-check/action.yml one composite action per gate (26; 24 run) +.github/workflows/main-estate-audit.yml the reusable audit: five component jobs +templates/estate-audit-caller.yml the ten-line caller each repository holds +templates/track-estate-audit.yml weekly bot: moves the caller's pin to HEAD +scripts/deonboard-estate-audit.sh removes the caller from a repo's actions.lock +rollout_estate.sh installs the three above in every estate clone .... -Consumers do not depend on this repository as a whole. They reference -individual actions by path, so a broken gate affects only the repositories that -name it. +Consumers call the reusable workflow at a full SHA. They do not copy its steps. +A copy froze at whatever commit its lock held, so fixes made here never +arrived. The caller is one line to move, and the track bot moves it. == Why composite, and why bash @@ -40,7 +43,8 @@ the README carries the classification and not just prose. == Blast radius -`rollout_estate.sh` copies the audit workflow into every repository in the -estate. A change to an action here therefore changes CI behaviour across all of -them at once, which is why consumers should pin by commit SHA rather than -`@main`, and why `CODEOWNERS` requires review on everything under `actions/`. +Every estate repository calls the audit workflow, so a change here reaches all +of them. It does not reach them at once: each caller pins a full SHA, and the +weekly track bot moves that pin by PR, whose own audit run is the check. That +PR is where a regression shows up, one repository at a time. `CODEOWNERS` +requires review on everything under `actions/`. diff --git a/README.adoc b/README.adoc index 7269c38..a132235 100644 --- a/README.adoc +++ b/README.adoc @@ -1,17 +1,35 @@ == cicd-suite -Composite GitHub Actions for estate-wide CI/CD auditing. Consumed by -`+.github/workflows/main-estate-audit.yml+`, which `+rollout_estate.sh+` -copies into each repository. +Composite GitHub Actions for estate-wide CI/CD auditing. They are run by +the reusable workflow `+.github/workflows/main-estate-audit.yml+`. Each +repository *calls* that workflow from a ten-line caller; it does not copy +it (see _Consuming it_). + +The audit reports five component checks, one check run each: + +[cols="1,3",options="header"] +|=== +|Check |Gates +|`governance` |required-files · spdx-license · affirmation · referencing +|`hygiene` |code-hygiene · formatting · linguist · secrets · vaulted-tokens +|`manifests` |manifest · recipes-set +|`languages` |idris2-abi · zig-unified-api-adapter · proof-runner · boj-cartridge +|`advisory` |the nine advisory gates below; this check cannot fail, by design +|=== + +Within a component every gate runs even after an earlier gate fails, so one +push reports every failing gate. Before this change one job ran all 26 in +sequence, and the first failure hid the rest behind "estate-audit: failing +after 8s". === Status -Published 2026-08-07. Three of the four defects listed below are *fixed -and verified against two live estate repositories*; the -advisory/enforcing split (13 of 26 cannot fail) is still open. +Published 2026-08-07. Defects (1)–(3) are *fixed and verified against two +live estate repositories*; (4) is partly fixed; (5) is closed by removing +the stubs from the workflow; (6) is open. -Before wiring this into a repository, read _Known defects_ — one item -remains. +Before wiring this into a repository, read _Known defects_ and _Consuming +it_. === What actually enforces @@ -31,7 +49,8 @@ vaulted-tokens · zig-unified-api-adapter |Can fail. A green tick means somethin real work, warn on genuinely soft findings. Failing the estate over a missing Keybase link would be worse than not checking. -|*Unimplemented* (2) |prat · semantic-audit |Stubs. See below. +|*Unimplemented* (2) |prat · semantic-audit |Stubs. Not referenced by the +audit workflow. See (5). |=== `+formatting+` and `+recipes-set+` moved out of the toothless column; @@ -90,7 +109,8 @@ not parse. `+formatting+` gained teeth in (3). The nine remaining advisories are advisory by design and defensible; the honest fix for them is a rename to `+*-advisory+`, not teeth. -*5. OPEN — two actions are unimplemented stubs.* +*5. CLOSED — two actions are unimplemented stubs, and the workflow no +longer runs them.* * `+semantic-audit+` is pure `+echo+` with "`Placeholder for actual invocation`"; its real body is `+cicd-squabbler audit --semantic+`, and @@ -103,8 +123,13 @@ any repository that has a `+tests/+` directory _or_ contains the letters "`prat`" anywhere. It cannot be honestly implemented until somebody says what PRAT is. -Both should move to a `+draft/+` directory so the workflow cannot -reference them. A stub you cannot call cannot mislead. +A stub that prints success cannot be credited as assurance, so neither +step is in `+main-estate-audit.yml+` any more. The two directories stay +under `+actions/+` for now, for two reasons. Repositories still running a +pre-caller copy of the workflow reference them by path. And +`+tests/lock-transitive-closure.sh+` requires that the SHA the workflow +pins serves the whole `+actions/+` tree at HEAD. Move them to `+draft/+` +once the estate runs the caller (see _Consuming it_). *6. OPEN — six actions use bare `+git grep+`* with no path restriction (`+idris2-abi+`, `+metrics+`, `+secrets+`, `+spdx-license+`, @@ -113,26 +138,49 @@ defect (1). === Blast radius -`+rollout_estate.sh+` has already copied `+main-estate-audit.yml+` into -*199 repositories*; in *198* of them the file is untracked and has -therefore never run. Committing it in those repos is what arms these -gates — so it should follow closing (4), the last open defect, not -precede it. +An earlier `+rollout_estate.sh+` copied the whole workflow into *199 +repositories*. Each copy froze at whatever cicd-suite commit its +`+actions.lock+` held. That is why a gate fixed here kept failing +downstream. The composite-shell fix to required-files (#32, 2026-09-22) +had still not reached hyperpolymath/marches on 2026-10-14, because its +lock held `+5a10b72+`, the commit before it. The caller replaces those +copies. -The workflow also has no `+permissions:+` block and pins -`+actions/checkout+` by tag rather than SHA. +The workflow now has `+permissions: contents: read+` and pins +`+actions/checkout+` and every gate by full SHA. === Consuming it -Once (4) is resolved, pin by commit SHA rather than `+@main+`: +A repository calls the audit; it does not copy it. Three pieces: +. `+templates/estate-audit-caller.yml+` becomes +`+.github/workflows/main-estate-audit.yml+`, with `+__CICD_SUITE_SHA__+` +replaced by a full cicd-suite commit SHA: ++ [source,yaml] ---- -- uses: hyperpolymath/cicd-suite/actions/required-files-check@ # vX.Y.Z +jobs: + estate: + uses: hyperpolymath/cicd-suite/.github/workflows/main-estate-audit.yml@<40-hex sha> # cicd-suite main ---- - -`+@main+` is a moving target: a change here silently changes the gate in ++ +Pin a full SHA, never `+@main+` or a tag: the account enforces +`+sha_pinning_required+`, and `+@main+` would silently change the gate in every consuming repository at once. +. The caller must be *de-onboarded* from the repository's +`+actions.lock+`. Onboarded, a cross-repository reusable-workflow call +startup-fails with zero jobs. `+scripts/deonboard-estate-audit.sh +` +removes the entry and prunes the dependencies only it reached. It needs +mikefarah `+yq+` v4, and it is idempotent. +. `+templates/track-estate-audit.yml+` becomes +`+.github/workflows/track-estate-audit.yml+`. Weekly, it opens a PR that +moves the pin to cicd-suite HEAD; that PR's own audit run is the check. +It needs an `+AUTOMATION_TOKEN+` secret (a fine-grained token on that +repository with Contents, Pull requests and Workflows set to read/write), +because `+GITHUB_TOKEN+` may not write under `+.github/workflows/+`. + +`+rollout_estate.sh+` does all three for every local estate clone. It +writes files and does not commit them. === Licence diff --git a/rollout_estate.sh b/rollout_estate.sh index 3cdbb58..403028d 100755 --- a/rollout_estate.sh +++ b/rollout_estate.sh @@ -1,14 +1,44 @@ #!/bin/bash -echo "Rolling out Centralized CI/CD Suite to all repositories in the estate..." +# SPDX-License-Identifier: MPL-2.0 +# +# Install the estate audit into every local estate clone as a THIN CALLER of +# this repository's reusable workflow, plus the weekly bot that keeps its pin +# current. This script used to copy the whole 26-step workflow into each repo. +# Every copy then froze at whatever cicd-suite commit its actions.lock held, +# so fixes made here never reached a consumer. +# +# Per repository, the script: +# 1. writes .github/workflows/main-estate-audit.yml from +# templates/estate-audit-caller.yml, pinned at this clone's origin/main; +# 2. writes .github/workflows/track-estate-audit.yml (weekly pin bump); +# 3. de-onboards the caller from actions.lock +# (scripts/deonboard-estate-audit.sh). +# Files are written, not committed: review and commit them per repository. +set -euo pipefail -central_workflow="/home/hyperpolymath/developer/hyper-repos/cicd-suite/.github/workflows/main-estate-audit.yml" +suite="${CICD_SUITE:-/home/hyperpolymath/developer/hyper-repos/cicd-suite}" +repos="${HYPER_REPOS:-/home/hyperpolymath/developer/hyper-repos}" -for repo in /home/hyperpolymath/developer/hyper-repos/*; do - if [ -d "$repo/.git" ] && [ "$(basename "$repo")" != "cicd-suite" ]; then - echo "Migrating $repo..." - mkdir -p "$repo/.github/workflows" - cp "$central_workflow" "$repo/.github/workflows/main-estate-audit.yml" - fi +git -C "$suite" fetch --quiet origin main +sha=$(git -C "$suite" rev-parse origin/main) +# A caller pinned at a commit whose audit is not a reusable workflow cannot +# start, so refuse before writing anything. +if ! git -C "$suite" show "$sha:.github/workflows/main-estate-audit.yml" 2>/dev/null \ + | grep -qE '^[[:space:]]+workflow_call:'; then + echo "cicd-suite ${sha:0:7}: main-estate-audit.yml has no workflow_call trigger; refusing to roll out." >&2 + exit 1 +fi +echo "Installing the estate-audit caller (cicd-suite ${sha:0:7}) into $repos/* ..." + +for repo in "$repos"/*; do + [ -d "$repo/.git" ] || continue + [ "$(basename "$repo")" != "cicd-suite" ] || continue + echo "== $repo" + mkdir -p "$repo/.github/workflows" + sed "s/__CICD_SUITE_SHA__/$sha/" "$suite/templates/estate-audit-caller.yml" \ + > "$repo/.github/workflows/main-estate-audit.yml" + cp "$suite/templates/track-estate-audit.yml" "$repo/.github/workflows/track-estate-audit.yml" + "$suite/scripts/deonboard-estate-audit.sh" "$repo" done -echo "Rollout complete!" +echo "Rollout complete. Commit per repository after review." diff --git a/scripts/deonboard-estate-audit.sh b/scripts/deonboard-estate-audit.sh new file mode 100755 index 0000000..a990903 --- /dev/null +++ b/scripts/deonboard-estate-audit.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# deonboard-estate-audit.sh +# +# De-onboard .github/workflows/main-estate-audit.yml from 's +# actions.lock. The script deletes that workflow's `workflows:` entry, then +# prunes every `dependencies:` entry that no remaining workflow, and no +# remaining dependency, still reaches. The result is pure removals: +# gh-actions-lock's 4-space layout is kept, so the diff is reviewable line by +# line. +# +# Why: the estate audit is now a thin caller of cicd-suite's reusable +# workflow, pinned at a full SHA. Onboarded, such a caller startup-fails with +# zero jobs under sha_pinning_required. The header of +# .github/workflows/main-estate-audit.yml records the evidence. +# +# Idempotent: a lock that does not list the workflow is left untouched. +set -euo pipefail + +repo="${1:?usage: deonboard-estate-audit.sh }" +lock="$repo/.github/workflows/actions.lock" +wf=".github/workflows/main-estate-audit.yml" + +if ! yq --version 2>/dev/null | grep -q mikefarah; then + echo "deonboard-estate-audit.sh needs mikefarah yq v4 (https://github.com/mikefarah/yq)" >&2 + exit 2 +fi +if [ ! -f "$lock" ]; then + echo "$repo: no actions.lock; nothing to de-onboard" + exit 0 +fi +if ! w="$wf" yq -e '.workflows | has(strenv(w))' "$lock" >/dev/null 2>&1; then + echo "$repo: $wf is not onboarded; nothing to do" + exit 0 +fi + +w="$wf" yq -i -I4 'del(.workflows[strenv(w)])' "$lock" + +pruned=() +while :; do + used=$( { yq '.workflows[][]' "$lock" + yq '.dependencies[] | select(has("uses")) | .uses[]' "$lock" + } | sort -u ) + unused=$(yq '.dependencies | keys | .[]' "$lock" | grep -vxF -f <(printf '%s\n' "$used") || true) + [ -n "$unused" ] || break + while IFS= read -r k; do + k="$k" yq -i -I4 'del(.dependencies[strenv(k)])' "$lock" + pruned+=("$k") + done <<< "$unused" +done + +echo "$repo: de-onboarded $wf; pruned ${#pruned[@]} orphaned dependenc(ies):" +if [ "${#pruned[@]}" -gt 0 ]; then printf ' %s\n' "${pruned[@]}"; fi diff --git a/templates/estate-audit-caller.yml b/templates/estate-audit-caller.yml new file mode 100644 index 0000000..b35b904 --- /dev/null +++ b/templates/estate-audit-caller.yml @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Central Estate CI/CD Audit + +# Thin caller of the estate audit, which lives in hyperpolymath/cicd-suite as +# a reusable workflow and reports five component checks: governance, +# hygiene, manifests, languages, advisory. Do not copy the audit's steps in +# here. A copy freezes this repository at whatever cicd-suite commit its +# actions.lock happened to hold, and fixes made upstream never arrive. +# +# The pin is a full SHA, advanced weekly by track-estate-audit.yml (a PR +# whose own run of this audit is the check). +# +# ⚠ DE-ONBOARDED from actions.lock, deliberately, like the secret-scanner +# caller. A reusable workflow onboarded cross-repo startup-fails with zero +# jobs under sha_pinning_required (see the header of cicd-suite's +# main-estate-audit.yml). Do not run a full-directory `gh actions-lock` that +# re-onboards this file. If one does, cicd-suite/scripts/deonboard-estate-audit.sh +# reverses it. + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + estate: + uses: hyperpolymath/cicd-suite/.github/workflows/main-estate-audit.yml@__CICD_SUITE_SHA__ # cicd-suite main diff --git a/templates/track-estate-audit.yml b/templates/track-estate-audit.yml new file mode 100644 index 0000000..b52c28a --- /dev/null +++ b/templates/track-estate-audit.yml @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Track latest estate audit + +# Weekly: is the cicd-suite estate audit this repository calls behind +# cicd-suite's main? If so, open a PR that moves the pin in +# main-estate-audit.yml to cicd-suite HEAD. The PR's own estate-audit run is +# the check: merge it when the five components are green. Nothing reaches +# main unreviewed. +# +# ⚠ NO `uses:` ANYWHERE, DELIBERATELY: actions.lock is keyed by workflow path, +# and this must run even when the lock is stale. +# +# Token: AUTOMATION_TOKEN, a fine-grained token on this repository with +# Contents, Pull requests and Workflows set to read/write. In practice it is +# required: the bump edits a file under .github/workflows/, which +# GITHUB_TOKEN may not write, and a PR opened by GITHUB_TOKEN does not trigger +# the estate-audit run that validates it. + +on: + schedule: + - cron: "23 7 * * 1" # weekly, Monday + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: track-estate-audit + cancel-in-progress: false + +jobs: + bump: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN || secrets.GITHUB_TOKEN }} + BRANCH: ci/track-estate-audit + CALLER: .github/workflows/main-estate-audit.yml + BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com + steps: + - name: Move the cicd-suite pin to HEAD and open a PR + run: | + set -euo pipefail + gh auth setup-git + base=$(gh repo view "$GITHUB_REPOSITORY" --json defaultBranchRef --jq .defaultBranchRef.name) + gh repo clone "$GITHUB_REPOSITORY" repo -- --quiet --depth 1 --branch "$base" + cd repo + + pinned=$(grep -oE 'cicd-suite/\.github/workflows/main-estate-audit\.yml@[0-9a-f]{40}' "$CALLER" \ + | sed 's/.*@//' | head -n 1 || true) + if [ -z "$pinned" ]; then + echo "::error::$CALLER does not call cicd-suite's reusable estate audit at a full SHA." + exit 1 + fi + head=$(git ls-remote https://github.com/hyperpolymath/cicd-suite.git HEAD | cut -f1) + echo "pinned ${pinned:0:7}, cicd-suite HEAD ${head:0:7}" + if [ "$pinned" = "$head" ]; then + echo "Already at cicd-suite HEAD." + exit 0 + fi + + # The bot branch is rebuilt from the default branch each run, which + # would discard anything else on it. If someone has pushed to it + # (say, a fix the newer audit needed), leave it for review. + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then + foreign=$(gh api "repos/$GITHUB_REPOSITORY/compare/$base...$BRANCH" \ + --jq "[.commits[] | select(.commit.author.email != \"$BOT_EMAIL\")] | length") + if [ "$foreign" != 0 ]; then + echo "::warning::$BRANCH carries $foreign commit(s) not made by this workflow; leaving it alone. Merge its PR or delete the branch, then re-run." + exit 0 + fi + fi + + sed -i "s/$pinned/$head/g" "$CALLER" + git switch --quiet -c "$BRANCH" + git -c user.name='github-actions[bot]' -c user.email="$BOT_EMAIL" \ + commit --quiet -am "ci(estate-audit): track cicd-suite ${head:0:7}" + if ! git push --quiet --force origin "$BRANCH"; then + echo "::error::Push refused: writing under .github/workflows/ needs the AUTOMATION_TOKEN secret described above." + exit 1 + fi + body="Moves the estate audit to cicd-suite \`${head:0:7}\` (was \`${pinned:0:7}\`): https://github.com/hyperpolymath/cicd-suite/compare/$pinned...$head. This PR's estate-audit run is the check. Opened by \`track-estate-audit.yml\`." + if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then + gh pr edit "$BRANCH" --title "ci(estate-audit): track cicd-suite ${head:0:7}" --body "$body" + else + gh pr create --base "$base" --head "$BRANCH" \ + --title "ci(estate-audit): track cicd-suite ${head:0:7}" --body "$body" + fi