Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# Stated, not inherited. setup-uv does not cache by default today, so
# this changes nothing now -- but the blocking gate must not begin
# taking an unreviewed input because an upstream default moved.
# 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:
Expand Down Expand Up @@ -116,9 +118,11 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# Stated, not inherited. setup-uv does not cache by default today, so
# this changes nothing now -- but the blocking gate must not begin
# taking an unreviewed input because an upstream default moved.
# 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:
Expand Down Expand Up @@ -267,6 +271,11 @@ jobs:
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 a required check. Found by deriving the required 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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# The sweep decides what maintenance debt exists; it must not
# read that from an entry written by some other ref.
enable-cache: false

- name: Install validator dependencies
env:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/private-static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: ${{ inputs.uv_version }}
# uv provisions our pinned tooling here, not the caller's
# dependencies; a restored entry would be unreviewed input to a scan.
enable-cache: false

- name: Run install command
if: ${{ inputs.install_command != '' }}
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/qt-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ jobs:

- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
# Pinned, not inherited: setup-uv's `version` defaults to the
# version in pyproject.toml or, absent one, whatever is latest.
# There is no pyproject.toml here, so this resolved to latest --
# a SHA-pinned action installing an unpinned tool.
version: 0.11.30
# uv provisions aqtinstall here, not the caller's dependencies.
# The Qt install itself is cached separately and by key, below.
enable-cache: false

- name: Resolve the Qt release
id: release
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/runtime-fixtures-languages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,15 @@ jobs:
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
# Pinned, not inherited: setup-uv's `version` defaults to the
# version in pyproject.toml or, absent one, whatever is latest.
# There is no pyproject.toml here, so this resolved to latest --
# a SHA-pinned action installing an unpinned tool.
version: 0.11.30
# Evidence must come from a cold, reviewable run: a restored entry
# would make the receipt describe something other than this run.
enable-cache: false
- name: Provision validator
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
Expand Down Expand Up @@ -491,6 +500,15 @@ jobs:
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
# Pinned, not inherited: setup-uv's `version` defaults to the
# version in pyproject.toml or, absent one, whatever is latest.
# There is no pyproject.toml here, so this resolved to latest --
# a SHA-pinned action installing an unpinned tool.
version: 0.11.30
# Evidence must come from a cold, reviewable run: a restored entry
# would make the receipt describe something other than this run.
enable-cache: false
- name: Provision validator
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
Expand Down Expand Up @@ -539,6 +557,15 @@ jobs:
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
# Pinned, not inherited: setup-uv's `version` defaults to the
# version in pyproject.toml or, absent one, whatever is latest.
# There is no pyproject.toml here, so this resolved to latest --
# a SHA-pinned action installing an unpinned tool.
version: 0.11.30
# Evidence must come from a cold, reviewable run: a restored entry
# would make the receipt describe something other than this run.
enable-cache: false
- name: Provision validator
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
Expand Down Expand Up @@ -589,6 +616,15 @@ jobs:
update-environment: false
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
# Pinned, not inherited: setup-uv's `version` defaults to the
# version in pyproject.toml or, absent one, whatever is latest.
# There is no pyproject.toml here, so this resolved to latest --
# a SHA-pinned action installing an unpinned tool.
version: 0.11.30
# Evidence must come from a cold, reviewable run: a restored entry
# would make the receipt describe something other than this run.
enable-cache: false
- name: Provision validator
env:
PYTHON_PATH: ${{ steps.python.outputs.python-path }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/semgrep-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# uv provisions our pinned analyser here, not the caller's
# dependencies; a restored entry would be unreviewed input to a scan.
enable-cache: false

- name: Semgrep scan
env:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/sql-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# uv provisions our pinned tooling here, not the caller's
# dependencies; a restored entry would be unreviewed input to a scan.
enable-cache: false

- name: Lint
if: ${{ inputs.lint }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/zizmor-no-sarif.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# Consumer-facing gate. uv provisions our pinned analyser here,
# not the caller's dependencies, so there is nothing to choose.
enable-cache: false

- name: Run zizmor (plain, enforced)
env:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/zizmor-sarif.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: 0.11.30
# This workflow backs the `zizmor` job, which is in `ci-gate.needs`.
# A required gate must not take an unreviewed input.
enable-cache: false

- name: Run zizmor (SARIF)
id: scan
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,43 @@

## [Unreleased]

- Make the cache contract true, and derive the surface it has to cover. The
catalog recorded `astral-sh/setup-uv` as not caching by default. The pinned
action declares `enable-cache: auto`, and its `getEnableCache()` returns true
when `RUNNER_ENVIRONMENT` is `github-hosted` — so the entry asserted the
opposite of the behaviour, and nothing executed the assertion. Two gate jobs
carried a comment saying their explicit `false` "changes nothing today"; it
changed something.

`upstream_default` now records the literal default the pinned `action.yml`
declares, and `check_cache_upstream_defaults.py` resolves it from that exact
commit each sweep. `default_caches` stops being prose about somebody else's
code.

The refusal list was hand-written, and the contract's own closing paragraph
claimed the undeclared remainder could not reach a release or a required check.
It could. `check_cache_contract.py` now derives the required surface from
`ci-gate`'s own `needs` graph and follows a `uses:` job into the workflow it
calls, demanding a declared refusal for every step there that caches with no
input. That immediately found **two** leaks, not the one that was known:
`zizmor-sarif.yml`, which backs the required `zizmor` job, and `actions/setup-go`
in `shell-gates`, which caches its module and build cache by default and had
never been named.

Twelve call sites now state their refusal rather than inheriting a default, each
with the reason it applies there. `python-ci.yml` keeps `enable-cache: true`,
which is deliberate: it caches the consumer's own dependencies.

Two of those call sites — `qt-ci.yml` and `runtime-fixtures-languages.yml` —
had no `with:` block at all, so they were also not pinning **uv itself**.
setup-uv's `version` defaults to the version in `pyproject.toml` or, absent one,
latest; there is no `pyproject.toml` here. A SHA-pinned action was installing an
unpinned tool, in a product reusable consumers call. Both now pin `0.11.30` like
everywhere else.

All six affected reusables were re-proven by real fixture runs before their
`proven_digest` was updated, not after.

- Read action definitions from the raw host, so the check can finish in the job
that owns it. With the graph walk corrected, the first real sweep still failed
on two of forty-three pins — `aquasecurity/trivy-action` and
Expand Down
74 changes: 65 additions & 9 deletions catalog/cache-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,66 @@
# classified rather than inherited.
schema_version: 1

# Every action in the tree that can write to a cache, and the input that decides
# whether it does. `default_caches` is the behaviour with no input at all -- the
# three `true` entries are the ones that cache without being asked.
# Every action in the tree that can write to a cache, the input that decides
# whether it does, and what happens with no input at all.
#
# `upstream_default` is the literal default declared by the pinned action's own
# `action.yml`, and `check_cache_upstream_defaults.py` resolves it from that file
# every sweep. It used to be prose: `astral-sh/setup-uv` was recorded as not
# caching by default when the pinned action declares `auto`, and `auto` resolves
# to true on a GitHub-hosted runner. So the contract asserted the opposite of the
# behaviour, and nothing executed the assertion.
#
# `default_caches` is the effective behaviour on the runners this library targets.
# Five of the eight cache with no input: two because a cache is what they are
# (`actions/cache`, `hendrikmuhs/ccache-action`, added deliberately by a caller),
# and three because their own default says so.
producers:
- action: actions/cache
control: null
upstream_default: null
default_caches: true
note: Explicit cache; the key is written by the calling workflow.
- action: astral-sh/setup-uv
control: enable-cache
default_caches: false
upstream_default: auto
default_caches: true
note: >-
`auto` in the pinned action.yml, and its `getEnableCache()` returns true
when RUNNER_ENVIRONMENT is `github-hosted`. Recorded as caching by default
because that is what it does on the runners this library targets. This was
recorded as `false` and believed for long enough that two gate jobs carried
a comment saying the input changed nothing today.
- action: actions/setup-go
control: cache
upstream_default: true
default_caches: true
note: Caches the module and build cache unless asked not to.
- action: actions/setup-java
control: cache
upstream_default: null
default_caches: false
note: No default declared, so an absent input caches nothing.
- action: actions/setup-node
control: cache
upstream_default: null
default_caches: false
note: No default declared, so an absent input caches nothing.
- action: actions/setup-python
control: cache
upstream_default: null
default_caches: false
note: No default declared, so an absent input caches nothing.
- action: gradle/actions/setup-gradle
control: cache-provider
upstream_default: enhanced
default_caches: true
note: >-
The input selects a provider rather than switching caching off; a provider
is always chosen, so caching happens unless disabled by other means.
- action: hendrikmuhs/ccache-action
control: null
upstream_default: null
default_caches: true
note: A cache action by construction; callers opt in by adding the step.

Expand Down Expand Up @@ -91,9 +122,34 @@ refusals:
input: enable-cache
value: false
reason: The blocking gate must not depend on an unreviewed input.
- workflow: .github/workflows/ci.yml
job: shell-gates
action: actions/setup-go
input: cache
value: false
reason: >-
setup-go caches the module and build cache with no input at all, and this
job is in `ci-gate.needs`. Found by deriving the required surface from the
gate's own graph; the hand-written list had not named it.
- workflow: .github/workflows/zizmor-sarif.yml
job: zizmor
action: astral-sh/setup-uv
input: enable-cache
value: false
reason: >-
This workflow backs the `zizmor` job, which is in `ci-gate.needs`. The
contract's own closing paragraph used to assert that the undeclared
remainder could not reach a required check; this is the counter-example.

# Everything else is left to the producer's default on purpose. Those workflows
# are reusable surfaces whose caching a consumer chooses, or advisory lanes where
# a poisoned entry cannot reach a release or a required check. Narrowing them
# would change behaviour consumers already depend on without buying a trust
# boundary that matters.
# Everything else is left to the producer's default on purpose, and there is now
# exactly one such place: `python-ci.yml`, which sets `enable-cache: true`
# deliberately -- it caches the consumer's own dependencies, which is the point of
# the workflow.
#
# The previous version of this paragraph said the remainder were "advisory lanes
# where a poisoned entry cannot reach a release or a required check". That was
# false: `zizmor-sarif.yml` is called by the `zizmor` job, which is in
# `ci-gate.needs`, and it took setup-uv's default. The refusals below are no
# longer a hand-written list -- `check_cache_contract.py` derives the required
# jobs from `ci-gate`'s own `needs` graph, follows local `uses:` into the called
# workflow, and demands a refusal for every cache-capable step it finds there.
5 changes: 4 additions & 1 deletion catalog/python-execution.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schema_version": 1,
"python": {
"major_minor": "3.13",
"subject_count": 59,
"subject_count": 60,
"launcher": "scripts/check_python_execution_contract.py",
"launcher_prefix": [".venv/bin/python", "-I", "-B", "scripts/check_python_execution_contract.py", "--launch"],
"syntax_gate_prefix": [".venv/bin/python", "-I", "-B", "scripts/check_python_syntax.py"],
Expand Down Expand Up @@ -120,6 +120,7 @@
"_strict_yaml.py", "check_actionlint_contract.py",
"check_actionlint_config.py", "check_anchor_contexts.py",
"check_benchmark_contract.py", "check_cache_contract.py",
"check_cache_upstream_defaults.py",
"check_ci_tier_selection.py",
"check_maintenance_report_contract.py",
"check_validation_tier_contract.py",
Expand Down Expand Up @@ -161,6 +162,7 @@
"check_actionlint_contract.py": ["_workflow_yaml", "check_python_execution_contract"],
"check_benchmark_contract.py": ["_workflow_yaml"],
"check_cache_contract.py": ["_strict_yaml", "_workflow_yaml"],
"check_cache_upstream_defaults.py": ["_strict_yaml", "_workflow_yaml", "check_transitive_action_pins"],
"check_ci_tier_selection.py": ["_workflow_yaml", "check_python_execution_contract"],
"check_maintenance_report_contract.py": ["_workflow_yaml", "check_python_execution_contract"],
"check_validation_tier_contract.py": ["_strict_yaml", "_workflow_yaml", "validate_all"],
Expand Down Expand Up @@ -202,6 +204,7 @@
"_strict_yaml", "check_actionlint_config", "check_actionlint_contract",
"check_anchor_contexts",
"check_benchmark_contract", "check_cache_contract",
"check_cache_upstream_defaults",
"check_ci_tier_selection",
"check_maintenance_report_contract",
"check_validation_tier_contract",
Expand Down
Loading
Loading