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
70 changes: 70 additions & 0 deletions .github/actions/release-archives/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: The archives of a published release, each checked before it is opened
description: >-
Downloads the archives a published release attaches, holds each to the
release's checksums and to the attestation attest-release.yml made of the
files people download, and extracts it. npm.yml and pypi.yml both make their
packages of what this leaves, so the check lives in one place.

# Every archive is held to the release's checksums and to the attestation
# attest-release.yml made of the files people download. That is the only one
# that fits all of them: the Windows archives are signed and the macOS one is
# notarised after release.yml built them, so their bytes are not the ones it
# attested. The checksum says the download is the file the release lists. The
# attestation says attest-release.yml made a statement about these very bytes,
# and --signer-workflow holds it to that workflow.
#
# It leaves each archive extracted in binaries/<dir>, and binaries.txt with a
# line for each: the directory, the binary inside it, and the binary's sha256.

inputs:
tag:
description: The tag of a published release, such as v0.6.0.
required: true
archives:
description: >-
A file with a line for each archive: the directory to extract it into,
the name of the archive, and where the binary is inside it.
required: true
token:
description: A token that can read the release, github.token.
required: true

runs:
using: composite
steps:
- name: download, check and extract every archive
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
TAG: ${{ inputs.tag }}
ARCHIVES: ${{ inputs.archives }}
run: |
set -euo pipefail
mkdir -p download binaries
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir download --pattern verify-SHA256SUMS.txt
# Read into a list first, because gh reads standard input and a
# download inside the read loop eats the rest of the list.
mapfile -t lines < "$ARCHIVES"
if [ "${#lines[@]}" -eq 0 ]; then
echo "::error::$ARCHIVES names no archive, so there is nothing to check. The step before this one lists them."
exit 1
fi
for line in "${lines[@]}"; do
read -r dir archive binary <<< "$line"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir download --pattern "$archive"
awk -v name="$archive" '$2 == name' download/verify-SHA256SUMS.txt > "download/$archive.sum"
if [ "$(wc -l < "download/$archive.sum")" != "1" ]; then
echo "::error::verify-SHA256SUMS.txt of $TAG does not name $archive exactly once, so the archive cannot be checked. Compare the release's assets with what release.yml attaches before running this again."
exit 1
fi
(cd download && sha256sum -c "$archive.sum")
gh attestation verify "download/$archive" --repo "$GITHUB_REPOSITORY" \
--predicate-type https://spdx.dev/Document/v2.3 \
--signer-workflow "$GITHUB_REPOSITORY/.github/workflows/attest-release.yml"
mkdir -p "binaries/$dir"
case "$archive" in
*.zip) unzip -q "download/$archive" -d "binaries/$dir" ;;
*) tar -xzf "download/$archive" -C "binaries/$dir" ;;
esac
echo "$dir $binary $(sha256sum "binaries/$dir/$binary" | cut -d' ' -f1)" >> binaries.txt
done
30 changes: 30 additions & 0 deletions .github/requirements-pypi.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# What the PyPI packages need in CI beyond Python itself, pinned, in a file a
# bot can edit.
#
# python/testing-files-generator is tested with pytest in ci.yml, and
# .github/workflows/pypi.yml installs the wheels of a release with uv as well
# as pip, because uv keeps a binary's execute bit only when the wheel says it
# was made on Unix and pip keeps it either way (docs/PYTEST-2026-11-09.md).
# tomli reads pyproject.toml for release/assemble.py on Python 3.10, which has
# no tomllib - it is the library tomllib was taken from. None of them is
# linked into either binary or shipped in a wheel, and go.mod never sees them.
#
# The metadata of the wheels is checked by pypa/gh-action-pypi-publish, which
# runs twine check before it uploads anything. twine is not here because its
# own tree did not install on Python 3.10 and brought a licence the dependency
# gate cannot read (docs/REVIEW-226-2026-11-09.md).
#
# Licences read from the pinned versions rather than recalled, on 2026-11-09,
# from the metadata on PyPI: pytest MIT, uv MIT OR Apache-2.0, tomli MIT (and
# the LICENSE file of its tag 2.5.0).
#
# requirements-pypi.txt next to this file is compiled from it by pip-compile,
# the same way requirements-semgrep.txt is, so Dependabot proposes a set pip
# can install. It is compiled on Python 3.10, the oldest the package declares,
# because a set resolved on a newer Python can pin a version 3.10 cannot
# install. To compile it by hand, from the root of the repository:
#
# docker run --rm -v "$PWD/.github:/w" -w /w python:3.10-slim sh -c "pip install -q pip==26.1.1 pip-tools==7.5.3 && pip-compile --strip-extras --no-emit-index-url --output-file=requirements-pypi.txt requirements-pypi.in"
pytest==9.2.0
tomli==2.5.0 ; python_version < "3.11"
uv==0.13.7
26 changes: 26 additions & 0 deletions .github/requirements-pypi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --no-emit-index-url --no-index --output-file=requirements-pypi.txt --strip-extras requirements-pypi.in
#
exceptiongroup==1.3.1
# via pytest
iniconfig==2.3.1
# via pytest
packaging==26.3
# via pytest
pluggy==1.7.0
# via pytest
pygments==2.21.0
# via pytest
pytest==9.2.0
# via -r requirements-pypi.in
tomli==2.5.0 ; python_version < "3.11"
# via
# -r requirements-pypi.in
# pytest
typing-extensions==4.16.0
# via exceptiongroup
uv==0.13.7
# via -r requirements-pypi.in
8 changes: 7 additions & 1 deletion .github/scripts/dependency_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@
# names in front of this gate - four of them with a licence GitHub leaves empty
# or describes with an identifier of its own. Vulnerabilities in this file are
# still checked, by the dependency review action and by OSV-Scanner.
SKIPPED_MANIFESTS = frozenset({".github/requirements-semgrep.txt"})
#
# requirements-pypi.txt joined it on 2026-11-09 (#226): pytest, uv and what they
# bring, which ci.yml and pypi.yml install on a runner to test the Python
# package and try its wheels. On Python 3.10 pytest brings typing-extensions,
# whose PSF-2.0 is not in ALLOWED. What the Python packages themselves depend
# on is in python/*/pyproject.toml, and those are still asked.
SKIPPED_MANIFESTS = frozenset({".github/requirements-semgrep.txt", ".github/requirements-pypi.txt"})


def added(review):
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,59 @@ jobs:
node check.mjs
shell: bash

python:
name: Python package on ${{ matrix.os }} with Python ${{ matrix.python }}
# The Python package (python/testing-files-generator) runs tfg and reads
# what it writes, so nothing in the Go suite runs it. Its own tests run
# here against the binary this commit builds, on the three systems a
# release has binaries for - Windows above all, because the rename that
# settles a race between test workers is refused there in ways it is not
# elsewhere. Python 3.10 is the oldest the package declares, and the
# newest is tried beside it on Linux. The tests of the release tools are
# among them: the wheels are laid out from archives the tests make.
# docs/PYTEST-2026-11-09.md.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: "3.10"
- os: ubuntu-latest
python: "3.14"
- os: windows-latest
python: "3.10"
- os: macos-latest
python: "3.10"
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
check-latest: false
cache: true

- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ matrix.python }}

- name: build tfg where the package's tests look for it
run: go build -tags "$(cat .github/build-tags)" -o "tfg$(go env GOEXE)" ./cmd/tfg
shell: bash

- name: the package's own tests
working-directory: python/testing-files-generator
run: |
python -m pip install --disable-pip-version-check -r ../../.github/requirements-pypi.txt
python -m pytest -p no:cacheprovider
shell: bash

fuzz:
name: fuzzing with a time budget
# Weekly and by hand, never on a push. Fuzzing searches without end, so a
Expand Down
42 changes: 12 additions & 30 deletions .github/workflows/npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ on:
paths:
- "npm/**"
- ".github/workflows/npm.yml"
- ".github/actions/release-archives/**"

permissions:
contents: read
Expand Down Expand Up @@ -110,44 +111,25 @@ jobs:
with:
node-version: "22"

- name: the published archives, each checked before it is opened
env:
GH_TOKEN: ${{ github.token }}
# The checksum says the download is the file the release lists. The
# attestation says attest-release.yml made a statement about these very
# bytes, and --signer-workflow holds it to that workflow.
- name: which archives the packages are made of
run: |
set -euo pipefail
assemble=npm/testing-files-generator/release/assemble.js
if [ ! -f "$assemble" ]; then
echo "::error::the tree of $TAG has no $assemble, so it has no npm packages to make. npm gets the releases tagged after the packages came into the repository, so run this workflow for one of those."
exit 1
fi
mkdir -p download binaries
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir download --pattern verify-SHA256SUMS.txt
node "$assemble" archives "$VERSION" > archives.txt
# Read into a list first, because gh reads standard input and a
# download inside the read loop eats the rest of the list.
mapfile -t lines < archives.txt
for line in "${lines[@]}"; do
read -r dir archive binary <<< "$line"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir download --pattern "$archive"
awk -v name="$archive" '$2 == name' download/verify-SHA256SUMS.txt > "download/$archive.sum"
if [ "$(wc -l < "download/$archive.sum")" != "1" ]; then
echo "::error::verify-SHA256SUMS.txt of $TAG does not name $archive exactly once, so the archive cannot be checked. Compare the release's assets with what release.yml attaches before running this again."
exit 1
fi
(cd download && sha256sum -c "$archive.sum")
gh attestation verify "download/$archive" --repo "$GITHUB_REPOSITORY" \
--predicate-type https://spdx.dev/Document/v2.3 \
--signer-workflow "$GITHUB_REPOSITORY/.github/workflows/attest-release.yml"
mkdir -p "binaries/$dir"
case "$archive" in
*.zip) unzip -q "download/$archive" -d "binaries/$dir" ;;
*) tar -xzf "download/$archive" -C "binaries/$dir" ;;
esac
echo "$dir $binary $(sha256sum "binaries/$dir/$binary" | cut -d' ' -f1)" >> binaries.txt
done

# The same check pypi.yml makes, in one place: the checksum says the
# download is the file the release lists, and the attestation that
# attest-release.yml made a statement about these very bytes.
- name: the published archives, each checked before it is opened
uses: ./.github/actions/release-archives
with:
tag: ${{ needs.which.outputs.tag }}
archives: archives.txt
token: ${{ github.token }}

- name: lay out and pack them
# Packed here, on Linux, because npm packs a file without its
Expand Down
Loading
Loading