Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 8 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-python@v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -35,8 +35,11 @@ jobs:
# That is the same defect class test_dev_extra_completeness.py exists to
# prevent for optional Python dependencies; the assertion that this step
# stays here lives in tests/test_ci_contract.py.
uses: dtolnay/rust-toolchain@stable
# Pinned to the `v1` tag's commit. The `@stable` form picks the toolchain
# from the ref name, so with a SHA ref the toolchain is passed explicitly.
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # v1
with:
toolchain: stable
components: clippy

- name: Install the deterministic layer
Expand All @@ -63,9 +66,9 @@ jobs:
# or data file missing from the wheel stays invisible until a user installs it.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-python@v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

Expand Down
14 changes: 8 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ on:
required: true
type: string

# Least privilege by default; the publish job opts into `id-token` alone.
# Least privilege by default; each job opts into exactly what it needs.
permissions: {}

jobs:
Expand All @@ -57,15 +57,15 @@ jobs:
outputs:
version: ${{ steps.verify.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
# Nothing here pushes, and a checkout that keeps a credential around
# is a credential available to every later step, including a build
# backend running code from the tree.
persist-credentials: false

- uses: actions/setup-python@v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

Expand Down Expand Up @@ -104,7 +104,7 @@ jobs:
- name: twine check
run: python -m twine check dist/*

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
Expand All @@ -124,12 +124,14 @@ jobs:
# and requesting a credential this job cannot use is the kind of leftover
# that reads as intentional to the next person.
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/

- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
# This step receives PYPI_API_TOKEN, so it runs a commit SHA, never a
# tag or branch its owner could move. tests/test_ci_contract.py holds that.
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
password: ${{ secrets.PYPI_API_TOKEN }}
46 changes: 46 additions & 0 deletions tests/test_ci_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,52 @@ def test_ci_runs_the_configured_type_checker():
assert "run: python -m pyright" in _ci_text()


WORKFLOWS = REPO / ".github" / "workflows"

# `uses: owner/repo[/path]@<ref>` with whatever follows on the line. Local actions
# (`./...`) and `docker://` images are not fetched from a git ref, so they are left
# to their own rules rather than matched here.
_USES = re.compile(r"^\s*(?:-\s+)?uses:\s*([^\s#]+)(.*)$", re.MULTILINE)
_SHA_PIN = re.compile(r"^[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-/]+@[0-9a-f]{40}$")
_VERSION_COMMENT = re.compile(r"^\s*#\s*v\d")


def _remote_uses() -> list[tuple[str, str, str]]:
found = []
for path in sorted(WORKFLOWS.glob("*.yml")) + sorted(WORKFLOWS.glob("*.yaml")):
for ref, rest in _USES.findall(path.read_text(encoding="utf-8")):
if ref.startswith(("./", "docker://")):
continue
found.append((path.name, ref, rest))
return found


def test_some_workflow_uses_a_remote_action():
"""Otherwise the pin check below passes over nothing."""
assert _remote_uses(), "no `uses:` found in .github/workflows; this guard is inert"


def test_every_remote_action_is_pinned_to_a_commit_sha():
"""A tag or branch is a pointer its owner can move, and the publish job hands
PYPI_API_TOKEN to whatever `pypa/gh-action-pypi-publish@release/v1` resolves to
at run time. A 40-character SHA is the only ref that names fixed content.

The trailing `# vX.Y.Z` comment is required too: it is what Dependabot reads to
propose the next pin, and what a reviewer reads to know which release a SHA is.

Red proof: against origin/master at d2317de (every action on a tag or branch
ref) the first assertion fails on `actions/checkout@v7` in ci.yml. Deleting the
`# v4.3.0` comment from download-artifact in release.yml fails the second.
"""
for workflow, ref, rest in _remote_uses():
assert _SHA_PIN.match(ref), (
f"{workflow}: `{ref}` is not pinned to a full commit SHA; a tag or "
f"branch can be moved to different code after review")
assert _VERSION_COMMENT.match(rest), (
f"{workflow}: `{ref}` has no trailing `# vX.Y.Z` comment naming the "
f"release the SHA was taken from")


@pytest.mark.parametrize("language", ["python", "rust"])
def test_the_packaging_job_checks_every_backends_catalog(language):
"""A rule file missing from the wheel is invisible to the test job, which runs
Expand Down
Loading