Skip to content

chore(deps): bump actions-rust-lang/setup-rust-toolchain from 1.17.0 to 2.0.0 in the github-actions group across 1 directory #254

chore(deps): bump actions-rust-lang/setup-rust-toolchain from 1.17.0 to 2.0.0 in the github-actions group across 1 directory

chore(deps): bump actions-rust-lang/setup-rust-toolchain from 1.17.0 to 2.0.0 in the github-actions group across 1 directory #254

Workflow file for this run

name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
# ci-gate is truthful advisory evidence here, not a required merge context.
# A status check is never reported to a merge queue unless its workflow
# subscribes to merge_group, so this stays for consumers who require
# ci-gate and for a later queue enablement.
merge_group:
types: [checks_requested]
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.run_id }}
# Nothing here is ever cancelled, and that is deliberate. `github.run_id` puts
# every run in a group of its own, so no run can be superseded by another, and
# `check_workflow_contracts.py` enforces exactly this shape: GitHub retains
# only one *pending* run in a shared group, so ref grouping can silently erase
# a queued iteration that was never observed. In a repository whose product is
# evidence, losing a run is worse than paying for a superseded one.
#
# This comment used to open "Cancel superseded PR iterations", which the
# configuration has never been able to do and the contract forbids. The
# trade-off it names is real either way: cancelling a merge-group candidate or
# a main push makes branch protection observe a partial-cancel instead of a
# decisive result.
cancel-in-progress: false
jobs:
# Blocking tier. Every check here is a property of the tree in hand, so it can
# only fail because of the change. Calendar-driven external facts are NOT here
# — see maintenance.yml. One required job used to mix the two, which made a
# third party's pricing page going stale enough to block an unrelated bugfix.
validate:
name: static validators
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: block
allowed-endpoints: >
agent.api.stepsecurity.io:443
github.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
raw.githubusercontent.com:443
astral.sh:443
pypi.org:443
files.pythonhosted.org:443
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: 0.11.30
# Stated, not inherited -- and it does change something. The pinned
# action declares `enable-cache: auto`, and `auto` resolves to true on
# a GitHub-hosted runner. This comment used to claim setup-uv did not
# cache by default; `catalog/cache-contract.yml` now records the real
# default and the sweep resolves it from the pinned action.yml.
enable-cache: false
- name: Install validator dependencies
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
run: |
"$PYTHON_PATH" -I -B -m venv --copies .venv
uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt
- name: Run static validators
run: |
.venv/bin/python -I -B scripts/check_python_syntax.py
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch validate_all.py -- --tier core
# Blocking, but scoped: a product fact is checked for expiry only when the
# changed capability declares it, and a runtime-coverage waiver only when its
# workflow was touched. Needs full history to resolve a merge base.
validate-touched:
name: changed-path validators
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: block
allowed-endpoints: >
agent.api.stepsecurity.io:443
github.com:443
api.github.com:443
objects.githubusercontent.com:443
release-assets.githubusercontent.com:443
raw.githubusercontent.com:443
astral.sh:443
pypi.org:443
files.pythonhosted.org:443
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: 0.11.30
# Stated, not inherited -- and it does change something. The pinned
# action declares `enable-cache: auto`, and `auto` resolves to true on
# a GitHub-hosted runner. This comment used to claim setup-uv did not
# cache by default; `catalog/cache-contract.yml` now records the real
# default and the sweep resolves it from the pinned action.yml.
enable-cache: false
- name: Install validator dependencies
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
run: |
"$PYTHON_PATH" -I -B -m venv --copies .venv
uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt
# Resolving the base is the whole job of this step, and giving up on it
# used to divert the run into `--tier scheduled`. That tier is advisory
# calendar and network work: it does not contain these blocking
# changed-path checks at all, and it does contain checks that need a token
# and two external hosts this job deliberately does not grant. So the
# fallback both skipped what it was required to prove and made a required
# gate fail on an expired third-party fact. `--tier touched --all-paths`
# would not fix it either: `facts_reached_by` returns "all facts" as soon
# as the ledger itself is in scope, so a whole-tree touched run *is* the
# calendar sweep -- the exact thing the tier split moved off the PR path.
#
# No event base is not "nothing to scope". Everything this ref adds on top
# of the default branch is the change, and that is resolvable.
- name: Resolve the change base
id: base
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
PUSH_BEFORE: ${{ github.event.before }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
python3 -I <<'PY'
import os
import subprocess
import sys
ZERO_OID = "0" * 40
event = os.environ.get("EVENT_NAME", "")
default_branch = os.environ.get("DEFAULT_BRANCH", "")
base = {
"pull_request": os.environ.get("PR_BASE_SHA", ""),
"merge_group": os.environ.get("MERGE_GROUP_BASE_SHA", ""),
"push": os.environ.get("PUSH_BEFORE", ""),
}.get(event, "")
# An all-zero oid means "no previous tip": branch creation, or a
# force-push beyond reachable history.
if base == ZERO_OID:
base = ""
def git(*args):
return subprocess.run(["git", *args], capture_output=True, text=True, check=False)
if not base:
if not default_branch:
sys.exit("no event base, and no default branch to resolve one against")
remote = f"refs/remotes/origin/{default_branch}"
# `fetch-depth: 0` fetches every branch, so this ref is normally
# already here. Fetch it explicitly when it is not, rather than
# let a missing ref become an empty scope that reports success.
if git("rev-parse", "--verify", "--quiet", remote).returncode != 0:
fetched = git(
"fetch", "--no-tags", "origin",
f"+refs/heads/{default_branch}:{remote}")
if fetched.returncode != 0:
sys.exit(f"cannot fetch {remote}: {fetched.stderr.strip()}")
resolved = git("merge-base", "HEAD", remote)
if resolved.returncode != 0 or not resolved.stdout.strip():
sys.exit(
f"cannot resolve a merge base between HEAD and {remote}: "
f"{resolved.stderr.strip()}")
base = resolved.stdout.strip()
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle:
handle.write(f"base={base}\n")
PY
# One command, no branch: the tier is not a function of how the run was
# triggered. A base that resolves to HEAD itself -- dispatching on the
# default branch -- scopes to nothing and says so, which is honest; the
# structural half of these ledgers already blocks in `core`.
- name: Run changed-path validators
env:
BASE_REF: ${{ steps.base.outputs.base }}
run: |
set -euo pipefail
.venv/bin/python -I -B scripts/check_python_syntax.py
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch validate_all.py -- --tier touched --changed-from "$BASE_REF"
actionlint:
permissions:
contents: read
uses: ./.github/workflows/actionlint.yml
with:
runner: ubuntu-latest
zizmor:
permissions:
actions: read # upload-sarif reads this workflow run
contents: read # check out the tree being analysed
security-events: write # publish zizmor SARIF to code scanning
uses: ./.github/workflows/zizmor-sarif.yml
with:
runner: ubuntu-latest
# pedantic adds undocumented-permissions on top of the regular persona:
# every granted scope must say why it is granted. That is enforceable
# here precisely because this repository grants few of them, and a
# reviewer reading a permissions block should never have to guess.
persona: pedantic
# --- Negative gates -------------------------------------------------------
# Proves the gates still REFUSE bad input, not merely that they pass on good
# input. A gate that never fails is not a gate, and nothing else here would
# notice one silently becoming a no-op.
#
# These live in ci.yml rather than in a workflow of their own because a
# aggregate must be caller-native: ci-gate's `needs` is this run's real
# dependency graph, and nothing may cross a workflow boundary. They cannot be
# a called reusable either — a self workflow must not be `on: workflow_call`,
# since everything that is becomes part of the product consumers pin.
#
# Each probe lifts a gate's real step out of its workflow and runs it here,
# where an exit code is just data. `continue-on-error` is rejected on a job
# that calls a reusable with `uses:`, so calling the reusables and letting
# them fail would turn every run red; scripts/negative_gate_probe.py exists to
# avoid exactly that. See its docstring for the properties it enforces on
# itself.
shell-gates:
name: gates refuse bad input
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.24'
# setup-go caches the module and build cache with no input at all.
# This job is in `ci-gate.needs`, so that entry would be an unreviewed
# input to the self-CI aggregate. Found by deriving that surface
# from the gate's own graph rather than from a hand-written list.
cache: false
- name: Set up Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_wrapper: false
- name: Set up Python
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: 0.11.30
enable-cache: false
- name: Install the probe's dependency and both Python fixtures
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
run: |
set -euo pipefail
"$PYTHON_PATH" -I -B -m venv --copies .venv
# The probe reads workflow YAML through the repository's strict
# loader, so it needs PyYAML — from the hash-locked file CI already
# uses, not an ad-hoc install.
uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt
# Both fixture packages must be importable in one environment: the
# probe runs the same step twice and only the directory changes.
uv pip install --python .venv/bin/python \
-e tests/fixtures/python -e tests/fixtures/negative/python pytest
- name: terraform-ci must reject unformatted HCL
run: |
.venv/bin/python -I -B scripts/check_python_syntax.py
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/terraform-ci.yml --job terraform \
--step 'Format check' \
--bad tests/fixtures/negative/terraform --good tests/fixtures/terraform
- name: go-ci must report a failing test
run: |
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/go-ci.yml --job go \
--step 'Run tests' \
--bad tests/fixtures/negative/go --good tests/fixtures/go
- name: sql-ci must reject SQL the dialect refuses
run: |
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/sql-ci.yml --job sql \
--step 'Lint' \
--bad tests/fixtures/negative/sql --good tests/fixtures/sql
- name: python-ci must report a failing test
run: |
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/python-ci.yml --job python \
--step 'Run tests' \
--bad tests/fixtures/negative/python --good tests/fixtures/python \
--python-input test_command='-m pytest -q'
- name: docs-ci must reject a broken relative link
run: |
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/docs-ci.yml --job docs \
--step 'Check relative Markdown links' \
--bad . --good . \
--bad-input docs_glob='tests/fixtures/negative/docs-links/**/*.md' \
--good-input docs_glob='tests/fixtures/docs/**/*.md'
# The token goes through env, never the command line: putting a
# credential in an argument is the interpolation pattern zizmor itself
# exists to catch, and it would show up in the process list.
- name: zizmor must reject a template injection
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/zizmor-no-sarif.yml --job zizmor \
--step 'Run zizmor (plain, enforced)' \
--bad . --good . \
--context-env github.token=GITHUB_TOKEN \
--bad-input target=tests/fixtures/negative/zizmor/injection.yml \
--good-input target=tests/fixtures/zizmor-clean/.github/workflows/clean.yml
dockerfile-gate:
name: hadolint refuses an unpinned image
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: 0.11.30
enable-cache: false
- name: Install the probe's dependency
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
run: |
set -euo pipefail
"$PYTHON_PATH" -I -B -m venv --copies .venv
uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txt
# hadolint varies by input rather than by directory, and its binary is
# installed by an earlier step of the workflow itself, so --before runs
# that download instead of assuming a hadolint is already present.
- name: hadolint-ci must reject an unpinned base and package
run: |
.venv/bin/python -I -B scripts/check_python_syntax.py
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch negative_gate_probe.py -- \
--workflow .github/workflows/hadolint-ci.yml --job hadolint \
--before 'Download hadolint (pinned + checksum-verified)' \
--step 'Run hadolint' \
--bad . --good . \
--bad-input dockerfile=tests/fixtures/negative/docker/Dockerfile \
--good-input dockerfile=tests/fixtures/docker/Dockerfile
ci-gate:
name: ci-gate
needs: [validate, validate-touched, actionlint, zizmor, shell-gates, dockerfile-gate]
runs-on: ubuntu-latest
if: always()
timeout-minutes: 5
permissions:
contents: read
steps:
# Caller-native: `needs` is this run's real dependency graph, so nothing
# crosses a workflow boundary and nothing can be substituted. Consumers
# who require `ci-gate` need this shape — see docs/08 and gate.yml.
- name: Aggregate required results
env:
RESULTS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
python3 -I <<'PY'
import json
import os
import sys
results = json.loads(os.environ["RESULTS"])
if not results:
print("ci-gate: needs context is empty", file=sys.stderr)
raise SystemExit(1)
failures = [
f" {job}: {(entry or {}).get('result', 'unknown')}"
for job, entry in sorted(results.items())
if (entry or {}).get("result") != "success"
]
if failures:
print("ci-gate FAILED:", file=sys.stderr)
for line in failures:
print(line, file=sys.stderr)
raise SystemExit(1)
print(f"ci-gate passed ({len(results)} jobs)")
PY