Skip to content
Merged
10 changes: 9 additions & 1 deletion .github/workflows/language-gates-self-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ on:
- "actions/kanren-check/**"
- "scripts/verify-suite-checkout.sh"
- "scripts/deonboard-workflow.sh"
- "tests/gate-action-owners.sh"
- ".github/workflows/rust-ci.yml"
- ".github/workflows/creusot-gate.yml"
- ".github/workflows/spark-gate.yml"
Expand All @@ -39,6 +40,7 @@ on:
- "actions/kanren-check/**"
- "scripts/verify-suite-checkout.sh"
- "scripts/deonboard-workflow.sh"
- "tests/gate-action-owners.sh"
- ".github/workflows/rust-ci.yml"
- ".github/workflows/creusot-gate.yml"
- ".github/workflows/spark-gate.yml"
Expand Down Expand Up @@ -85,7 +87,7 @@ jobs:
- name: The suite-checkout verifier refuses an empty or wrong SHA
run: |
set -euo pipefail
rc=0; EXPECTED= SUITE_DIR=. bash scripts/verify-suite-checkout.sh || rc=$?
rc=0; EXPECTED='' SUITE_DIR=. bash scripts/verify-suite-checkout.sh || rc=$?
[ "$rc" -eq 2 ] || { echo "verifier accepted an empty SHA (rc=$rc)"; exit 1; }
rc=0; EXPECTED=0000000000000000000000000000000000000000 SUITE_DIR=. bash scripts/verify-suite-checkout.sh || rc=$?
[ "$rc" -eq 2 ] || { echo "verifier accepted a wrong SHA (rc=$rc)"; exit 1; }
Expand All @@ -98,6 +100,12 @@ jobs:
run: bash actions/spark-check/test.sh
- name: miniKanren gate
run: bash actions/kanren-check/test.sh
- name: Every gate uses only action owners the callers' Actions policy admits, at full SHAs
# The policy is evaluated in the CALLER's repository, so cicd-suite's own
# CI never sees a refusal; this guard is that refusal, moved here
# (plasma-parser-writer#108: embarkstudios/cargo-deny-action was refused
# at startup while the Creusot gate from the same commit ran).
run: bash tests/gate-action-owners.sh
- name: Caller templates carry the placeholder, a full-SHA form and no secrets
run: |
set -euo pipefail
Expand Down
63 changes: 51 additions & 12 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,26 @@ on:
type: boolean
required: false
default: true
cargo_deny_version:
description: cargo-deny release installed for the deny job (prebuilt, checksummed by taiki-e/install-action)
type: string
required: false
default: "0.20.2"
deny_args:
description: Global cargo-deny arguments (placed before the command)
description: >-
cargo-deny global options, placed before `check` (e.g. `--offline`,
`--all-features`, `--locked`). Replaces the default; keep its two
flags unless you mean to drop them.
type: string
required: false
default: "--all-features --locked --warn unmaintained --warn yanked"
default: "--all-features --locked"
deny_check_args:
description: >-
`cargo deny check` options, placed after `check` (e.g. `--warn
unmaintained`, `--hide-inclusion-graph`).
type: string
required: false
default: "--warn unmaintained --warn yanked"
enable_audit:
description: >-
Accepted for compatibility with standards' rust-ci-reusable; advisories
Expand Down Expand Up @@ -514,22 +529,46 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: cargo-deny — advisories, bans, sources
uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
# cargo-deny is installed as a prebuilt, checksummed binary through
# taiki-e/install-action — the installer the coverage job already uses
# and hypatia's green workflows use — and run as plain `cargo deny`
# steps. It was EmbarkStudios/cargo-deny-action until 2026-10-19:
# that action is neither hyperpolymath-owned, GitHub-made nor
# Marketplace-verified, and the estate's Actions policy refused it at
# startup for every caller ("The action embarkstudios/cargo-deny-action
# … is not allowed", plasma-parser-writer#108 run 39251770946) while the
# Creusot gate from the same commit ran. tests/gate-action-owners.sh
# keeps every gate inside the owners the policy admits.
# The pinned cargo-deny is the version that action shipped (0.20.2).
- name: Install cargo-deny ${{ inputs.cargo_deny_version }} (prebuilt, pinned)
uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
command: check advisories bans sources
arguments: ${{ inputs.deny_args }}
manifest-path: ${{ inputs.working_directory }}/Cargo.toml
tool: cargo-deny@${{ inputs.cargo_deny_version }}
- name: cargo-deny — advisories, bans, sources
env:
WORKING_DIRECTORY: ${{ inputs.working_directory }}
DENY_ARGS: ${{ inputs.deny_args }}
DENY_CHECK_ARGS: ${{ inputs.deny_check_args }}
# Global options (deny_args) precede `check`; check options
# (deny_check_args) follow it. Both are word-split on purpose: they are
# caller-authored argument lists.
run: |
set -euo pipefail
# shellcheck disable=SC2086
cargo deny --log-level warn $DENY_ARGS --manifest-path "${WORKING_DIRECTORY}/Cargo.toml" check $DENY_CHECK_ARGS advisories bans sources
- name: cargo-deny — licences (only against an owner-authored deny.toml)
# Licence policy is manual and owner-only (doctrine #6). With no
# deny.toml there is no policy to check against, and the gate does
# not invent one.
if: ${{ needs.detect.outputs.has_deny_toml == 'true' }}
uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with:
command: check licenses
arguments: ${{ inputs.deny_args }}
manifest-path: ${{ inputs.working_directory }}/Cargo.toml
env:
WORKING_DIRECTORY: ${{ inputs.working_directory }}
DENY_ARGS: ${{ inputs.deny_args }}
DENY_CHECK_ARGS: ${{ inputs.deny_check_args }}
run: |
set -euo pipefail
# shellcheck disable=SC2086
cargo deny --log-level warn $DENY_ARGS --manifest-path "${WORKING_DIRECTORY}/Cargo.toml" check $DENY_CHECK_ARGS licenses

coverage:
name: llvm-cov line coverage
Expand Down
51 changes: 46 additions & 5 deletions .github/workflows/spark-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ on:
workflow_call:
inputs:
runs-on:
description: Linux x86_64 runner label (the pinned Alire archive supports Linux x86_64 only)
type: string
required: false
default: ubuntu-24.04
Expand Down Expand Up @@ -71,6 +72,16 @@ on:
type: string
required: false
default: "16.1.0"
alr_version:
description: Alire release installed from alire-project/alire's GitHub release (x86_64 Linux zip), the version setup-alire v6.0.0 defaults to
type: string
required: false
default: "2.1.1"
alr_sha256:
description: sha256 of that release zip (alr-<version>-bin-x86_64-linux.zip); the install refuses a mismatch. Recorded 2026-10-19 from the downloaded asset.
type: string
required: false
default: "09c66bcd8c35dd4b97b72c3d9b76e44caa6964a2db35aba069f396f00f1f64c7"
gnatprove_version:
description: Alire gnatprove crate version
type: string
Expand All @@ -91,7 +102,7 @@ defaults:
jobs:
theatre:
name: SPARK theatre gate (standards)
uses: hyperpolymath/standards/.github/workflows/spark-theatre-gate.yml@95de3dfc3fcf09a1cf3325ab42723e3d81682b45 # standards main 2026-10-14
uses: hyperpolymath/standards/.github/workflows/spark-theatre-gate.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 # standards main 2026-10-19 — the first commit whose own actions.lock GitHub accepts at job setup (hypatia PIN-003)
permissions:
actions: read
contents: read
Expand Down Expand Up @@ -206,10 +217,40 @@ jobs:
env:
EXPECTED: ${{ steps.suite.outputs.sha }}
run: bash .cicd-suite/scripts/verify-suite-checkout.sh
- name: Set up Alire with GNAT ${{ inputs.gnat_version }} and gprbuild
uses: alire-project/setup-alire@fc4c8ce471aa30b3af0d39ce15add1ee9eaf28f2 # v6.0.0
with:
toolchain: gnat_native=${{ inputs.gnat_version }} gprbuild
# Alire is a pinned release binary, sha256-verified, not the
# alire-project/setup-alire action: that action is neither
# hyperpolymath-owned, GitHub-made nor Marketplace-verified, so the
# estate's Actions policy would refuse it at startup for every caller
# (as it refused EmbarkStudios/cargo-deny-action, plasma-parser-writer#108),
# and it nests `actions/cache/restore@v4` and `actions/setup-node@v4`,
# floating tags no caller under sha_pinning_required may carry. The
# steps below are what that action does on Linux, pinned by hash.
- name: Install Alire ${{ inputs.alr_version }} (pinned release, sha256-verified)
env:
ALR_VERSION: ${{ inputs.alr_version }}
ALR_SHA256: ${{ inputs.alr_sha256 }}
run: |
set -euo pipefail
case "$(uname -s)/$(uname -m)" in
Linux/x86_64) arch=x86_64 ;;
*) echo "::error::Alire release binary is pinned for Linux x86_64 only; runner is $(uname -s)/$(uname -m). NO CHECK WAS PERFORMED."; exit 2 ;;
esac
zip="alr-${ALR_VERSION}-bin-${arch}-linux.zip"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
curl -fsSL --proto '=https' --proto-redir '=https' --retry 3 -o "$zip" "https://github.com/alire-project/alire/releases/download/v${ALR_VERSION}/${zip}"
echo "${ALR_SHA256} ${zip}" | sha256sum -c -
mkdir -p "$HOME/.local/alr"
unzip -o -q "$zip" 'bin/alr*' -d "$HOME/.local/alr"
rm -f "$zip"
echo "$HOME/.local/alr/bin" >> "$GITHUB_PATH"
"$HOME/.local/alr/bin/alr" --version
- name: Select GNAT ${{ inputs.gnat_version }} and gprbuild through Alire
env:
GNAT_VERSION: ${{ inputs.gnat_version }}
run: |
set -euo pipefail
alr -n settings --global --set msys2.do_not_install true >/dev/null 2>&1 || true
alr -n toolchain --select "gnat_native=${GNAT_VERSION}" gprbuild
alr -n toolchain
- name: Cache the GNATprove ${{ inputs.gnatprove_version }} install
id: cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down
45 changes: 44 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ one fixture per supported schema (v0.0.2, v0.0.3).
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/+`.
because `+GITHUB_TOKEN+` may not write under `+.github/workflows/+`. Its one
`+uses:+` is `+step-security/harden-runner+` at a full SHA (egress to GitHub
only); a repository that carries `+actions.lock+` must add that record
(`+gh actions-lock+`) or the first run dies at Set up job.

`+rollout_estate.sh+` does all three for every local estate clone. It
writes files and does not commit them.
Expand Down Expand Up @@ -287,6 +290,46 @@ first live subject for SPARK is cicd-squabbler's
`+spark/squabble_gate.gpr+`; no repository in the first rollout carries
Creusot obligations yet.

==== What a gate may use, and what a consumer's lock must hold

Two constraints are evaluated in the *caller's* repository, so nothing in
this repository's own CI can witness them; both are now guarded here.

* *Actions policy.* The estate's GitHub Actions policy admits actions that
are hyperpolymath-owned, GitHub-made, Marketplace-verified, or on a short
pattern list. A reusable that uses anything else startup-fails for every
caller with "The action … is not allowed" — which is how
`EmbarkStudios/cargo-deny-action` killed the first live Rust gate caller
(plasma-parser-writer#108, 2026-10-19) while the Creusot gate from the same
commit ran. The gates now use only `actions/*`, `github/*`,
`hyperpolymath/*`, `dtolnay`, `swatinem`, `goto-bus-stop` and `taiki-e`,
every one at a full SHA, and `tests/gate-action-owners.sh` (run by the
self-test workflow) refuses any other owner or a tag pin. cargo-deny is
installed through `taiki-e/install-action`; Alire is a sha256-pinned
release binary rather than `alire-project/setup-alire`, which also nested
floating `@v4` tags.

* *Callee lock.* Since about 2026-10-15 GitHub validates a called reusable
against the *callee* repository's `actions.lock` at the called SHA and
rejects a dependency record whose `ref:` is its own commit SHA
("invalid lockfile pin"). The gate workflows are deliberately not keyed in
this repository's lock, and that resolves (witnessed: the Creusot gate on
plasma-parser-writer#108). The SPARK gate's nested standards theatre gate
is pinned at standards `75bd599`, the first standards commit whose lock
passes that validation; hypatia's PIN-003 lists the 545 that do not.

* *Consumers with a lock-sync gate.* A repository whose CI requires every
job-level reusable ref to be keyed in its own `actions.lock` with a
transitively closed `dependencies:` section (cicd-squabbler's
`scripts/check-lock-sync.sh`) cannot de-onboard the caller as the
templates assume. Such a repository onboards instead: one
`hyperpolymath/cicd-suite@<sha>` record whose `uses:` is the union of
what the called gates use at that SHA, plus a record for each of those
copied from a lock GitHub accepts, with the tag — never the SHA — as
`ref:`. cicd-squabbler#135 is the worked example (for its standards
callers); gitbot-fleet's `scripts/fix-lock-self-sha-refs.sh` and
`scripts/lib/action-ref-tags.tsv` carry the tag map.

=== Licence

Code MPL-2.0, prose CC-BY-SA-4.0. See link:LICENSE[`+LICENSE+`].
28 changes: 26 additions & 2 deletions templates/track-estate-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ name: Track latest cicd-suite gates
# The file keeps its original name (track-estate-audit.yml) so repositories
# that already carry it receive this version in place.
#
# ⚠ NO `uses:` ANYWHERE, DELIBERATELY: actions.lock is keyed by workflow path,
# and this must run even when the lock is stale.
# The only `uses:` here is step-security/harden-runner (hypatia RE001: a job
# that holds a token monitors its egress), pinned at a full SHA. Everything
# else is plain `run:`, so a stale cicd-suite pin can never stop the workflow
# that moves cicd-suite pins. A repository that carries
# .github/workflows/actions.lock must hold a record for this workflow
# (`gh actions-lock`), or its first run dies at Set up job with "invalid
# lockfile pin".
#
# Token: AUTOMATION_TOKEN, a fine-grained token on this repository with
# Contents, Pull requests and Workflows set to read/write. In practice it is
Expand All @@ -34,6 +39,9 @@ concurrency:
jobs:
bump:
runs-on: ubuntu-latest
# A hung `gh` call must not hold a runner for the default six hours
# (hypatia WF013 / WH006). The whole job is a clone, a sed and a PR.
timeout-minutes: 20
permissions:
contents: write
pull-requests: write
Expand All @@ -42,6 +50,15 @@ jobs:
BRANCH: ci/track-estate-audit
BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com
steps:
- name: Harden the runner
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
# The job clones, reads cicd-suite HEAD and pushes one branch: it
# talks to GitHub only, so block (RE002) rather than audit.
egress-policy: block
allowed-endpoints: >
api.github.com:443
github.com:443
- name: Move every cicd-suite pin to HEAD and open a PR
run: |
set -euo pipefail
Expand All @@ -57,6 +74,13 @@ jobs:
exit 1
fi
head=$(git ls-remote https://github.com/hyperpolymath/cicd-suite.git HEAD | cut -f1)
# A lookup that succeeds but returns no HEAD leaves $head empty, and
# the sed below would then strip every pin. Refuse anything that is
# not a full SHA.
if ! [[ "$head" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::could not resolve cicd-suite HEAD (got '${head}'); no pin was touched."
exit 1
fi
changed=0
for caller in "${callers[@]}"; do
while IFS= read -r pinned; do
Expand Down
Loading
Loading