Skip to content

πŸ‘· ci: adopt shared check, CodeQL and release workflows #2901

πŸ‘· ci: adopt shared check, CodeQL and release workflows

πŸ‘· ci: adopt shared check, CodeQL and release workflows #2901

Workflow file for this run

name: "πŸ§ͺ check"
on:
workflow_dispatch:
push:
branches: ["main"]
tags-ignore: ["**"]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: "0 8 * * *"
- cron: "0 6 * * 1"
concurrency:
group: check-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
name: "πŸ§ͺ Test ${{ matrix.py }} - ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
py:
- "3.15t"
- "3.15"
- "3.14t"
- "3.14"
- "3.13"
- "3.12"
- "3.11"
- "pypy3.11"
os:
- ubuntu-24.04
- windows-2025
- macos-15
exclude:
- {os: windows-2025, py: pypy3.11}
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ“¦ Install tox"
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
- name: "🐍 Install Python ${{ matrix.py }}"
run: uv python install --python-preference only-managed ${{ matrix.py }}
- name: "πŸ“¦ Set up test suite"
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py }}
env:
UV_PYTHON_PREFERENCE: only-managed
# PyPy runs unmeasured: coverage has no JIT path there, so the suite takes 12m rather than 2m and roughly 60 of
# its lock, heartbeat and poll deadlines lapse under the added latency, though they pass measured in isolation.
# PyPy is not a coverage source either: the 100% gate stays CPython's.
- name: "πŸ§ͺ Run test suite"
run: |
if [[ "${{ matrix.py }}" == pypy* ]]; then
tox run --skip-pkg-install -e ${{ matrix.py }} --
else
tox run --skip-pkg-install -e ${{ matrix.py }}
fi
shell: bash
env:
PYTEST_ADDOPTS: "-vv --durations=20"
UV_PYTHON_PREFERENCE: only-managed
DIFF_AGAINST: HEAD
- name: "πŸ“Š Rename coverage report file"
if: ${{ !startsWith(matrix.py, 'pypy') }}
run: |
import os; import sys
os.rename(f".tox/.coverage.${{ matrix.py }}", f".tox/.coverage.${{ matrix.py }}-{sys.platform}")
shell: python
- name: "πŸ“€ Upload coverage data"
if: ${{ !startsWith(matrix.py, 'pypy') }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
include-hidden-files: true
name: .coverage.${{ matrix.os }}.${{ matrix.py }}
path: ".tox/.coverage.*"
retention-days: 3
graalpy:
name: "πŸ§ͺ Test graalpy3.13 - ubuntu-24.04 (weekly)"
# GraalPy 25.4.4 can freeze every thread at once under the threaded lock tests, so it stays off pull requests and
# pushes. A weekly run keeps the capability probes the suite gates on honest on a second runtime. It runs on one
# Linux job because those probes are a per-OS answer, and unmeasured, so the 100% gate stays CPython's.
if: github.event_name == 'workflow_dispatch' || github.event.schedule == '0 6 * * 1'
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ“¦ Install tox"
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
- name: "🐍 Install Python"
run: uv python install --python-preference only-managed graalpy3.13
- name: "πŸ“¦ Set up test suite"
run: tox run -vv --notest --skip-missing-interpreters false -e graalpy3.13
env:
UV_PYTHON_PREFERENCE: only-managed
- name: "πŸ§ͺ Run test suite"
run: tox run --skip-pkg-install -e graalpy3.13 --
env:
PYTEST_ADDOPTS: "-vv --durations=20"
UV_PYTHON_PREFERENCE: only-managed
coverage:
name: "πŸ“Š Combine coverage"
runs-on: ubuntu-24.04
needs: test
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "πŸ“© Download coverage data"
id: coverage-data
background: true
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: .tox
pattern: .coverage.*
merge-multiple: true
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ“¦ Install tox"
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
- name: "πŸ—οΈ Build package to generate version"
run: uv build --python 3.15 --python-preference only-managed --wheel . --out-dir dist
- name: "πŸ“¦ Set up coverage tool"
run: tox -e coverage --notest
env:
UV_PYTHON_PREFERENCE: only-managed
- name: "⏳ Wait for coverage data"
wait: coverage-data
- name: "πŸ“Š Combine and report coverage"
run: tox -e coverage --skip-pkg-install
env:
UV_PYTHON_PREFERENCE: only-managed
- name: "πŸ“€ Upload HTML report"
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: html-report
path: .tox/htmlcov
check:
name: "πŸ” ${{ matrix.tox_env }} - ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- windows-2025
tox_env:
- dev
- type
- docs
- pkg_meta
exclude:
- {os: windows-2025, tox_env: pkg_meta}
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ“¦ Install tox"
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
- name: "πŸ“¦ Set up check suite"
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.tox_env }}
env:
UV_PYTHON_PREFERENCE: only-managed
- name: "πŸ§ͺ Run check for ${{ matrix.tox_env }}"
run: tox run --skip-pkg-install -e ${{ matrix.tox_env }}
env:
UV_PYTHON_PREFERENCE: only-managed
nfs:
name: "πŸ§ͺ NFS mutual exclusion"
runs-on: ubuntu-24.04
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "βš™οΈ Export and mount loopback NFS shares"
run: |
sudo apt-get update
sudo apt-get install -y nfs-kernel-server nfs-common
sudo mkdir -p /export/filelock
sudo chmod 1777 /export/filelock
echo "/export/filelock 127.0.0.1(rw,sync,no_subtree_check,no_root_squash,insecure)" | sudo tee /etc/exports
sudo exportfs -ra
sudo systemctl restart nfs-server
# NFSv3 sends flock to the server through the NLM lock manager. rpcbind must be enabled; rpc-statd is a
# static unit that enable cannot touch, so start it explicitly or a v3 flock blocks in the kernel forever.
sudo systemctl enable --now rpcbind
sudo systemctl start rpc-statd
# A client can otherwise reopen a removed claim from its cache and block until the acquire timeout.
# Disable attribute and lookup caching, and use nosharecache to test independent client caches.
for m in nfs4a nfs4b nfs3a nfs3b; do sudo mkdir -p "/mnt/$m"; done
sudo mount -t nfs -o vers=4.2,noac,lookupcache=none 127.0.0.1:/export/filelock /mnt/nfs4a
sudo mount -t nfs -o vers=4.2,nosharecache,noac,lookupcache=none 127.0.0.1:/export/filelock /mnt/nfs4b
sudo mount -t nfs -o vers=3,noac,lookupcache=none 127.0.0.1:/export/filelock /mnt/nfs3a
sudo mount -t nfs -o vers=3,nosharecache,noac,lookupcache=none 127.0.0.1:/export/filelock /mnt/nfs3b
for m in nfs4a nfs4b nfs3a nfs3b; do sudo chmod 1777 "/mnt/$m"; done
mount | grep /mnt/nfs
- name: "πŸ§ͺ Verify NFSv4 mutual exclusion across two client caches"
env:
# StrictSoftFileLock is the lock the gate enforces on NFS: its claim queue hands off fairly, so it finishes
# every hold with no overlap on both versions (the strict claim retries the transient ESTALE that NFSv3's
# churn raises). The other two print their behavior ungated and neither is safe to gate on here: native flock
# does not exclude across NFS client caches (overlaps appear), and the poll-based soft lock starves out under
# NFS latency before finishing its holds. Prefer StrictSoftFileLock on NFS.
FILELOCK_VERIFY_LOCKS: StrictSoftFileLock
run: |
uv venv
uv pip install .
if ! timeout 420 .venv/bin/python tasks/verify_filesystem.py /mnt/nfs4a /mnt/nfs4b; then
for root in /export/filelock /mnt/nfs4a /mnt/nfs4b; do
echo "$root claims:"
find "$root/StrictSoftFileLock.lock.filelock/claims" -maxdepth 1 -type f -printf '%f\n'
done
exit 1
fi
- name: "πŸ§ͺ Verify NFSv3 mutual exclusion across two client caches"
env:
FILELOCK_VERIFY_LOCKS: StrictSoftFileLock
run: timeout 420 .venv/bin/python tasks/verify_filesystem.py /mnt/nfs3a /mnt/nfs3b
smb:
name: "πŸ§ͺ SMB mutual exclusion"
runs-on: ubuntu-24.04
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "βš™οΈ Export and mount a loopback SMB share"
run: |
sudo apt-get update
sudo apt-get install -y samba cifs-utils
sudo mkdir -p /srv/filelock
sudo chmod 1777 /srv/filelock
sudo tee -a /etc/samba/smb.conf >/dev/null <<'CONF'
[filelock]
path = /srv/filelock
writable = yes
guest ok = yes
create mask = 0666
directory mask = 0777
CONF
sudo sed -i '/\[global\]/a \ map to guest = Bad User\n server min protocol = SMB2' /etc/samba/smb.conf
sudo systemctl restart smbd
# Two mounts are two independent CIFS client caches over one server, for a multi-client check.
sudo mkdir -p /mnt/smba /mnt/smbb
for m in smba smbb; do
sudo mount -t cifs //127.0.0.1/filelock "/mnt/$m" \
-o "guest,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777,vers=3.0"
done
mount | grep /mnt/smb
- name: "πŸ§ͺ Verify SMB mutual exclusion across two client caches"
# CIFS byte-range locks are mandatory and server-enforced, so FileLock excludes and finishes every hold: it is
# the lock the gate enforces here. SoftFileLock prints ungated (it excludes but its poll-acquire can starve
# under contention), and the strict claim needs an atomic no-replace hard link SMB does not reliably provide.
env:
FILELOCK_VERIFY_LOCKS: FileLock
run: |
uv venv
uv pip install .
timeout 240 .venv/bin/python tasks/verify_filesystem.py /mnt/smba /mnt/smbb
capability:
name: "πŸ§ͺ without ${{ matrix.block }}"
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
block:
- fcntl
- sqlite3
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ§ͺ Verify the fallback without ${{ matrix.block }}"
env:
FILELOCK_BLOCK_MODULE: ${{ matrix.block }}
run: |
uv venv
uv pip install .
export PYTHONPATH="$PWD/tasks/capability"
.venv/bin/python tasks/capability/assert_fallback.py
.venv/bin/python tasks/verify_filesystem.py
symlink:
name: "πŸ§ͺ Windows symlinks ${{ matrix.developer_mode == '1' && 'granted' || 'denied' }}"
runs-on: windows-2025
strategy:
fail-fast: false
matrix:
developer_mode: ["0", "1"]
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "🐍 Install uv"
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
version: latest-known
python-version: "3.15"
enable-cache: false
- name: "πŸ“¦ Install tox"
run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv
# Windows grants symlink creation two ways, so both have to be closed to deny it: Developer Mode lets an
# unprivileged process create them, and SeCreateSymbolicLinkPrivilege lets any holder create them regardless.
# Clearing only the registry value left the runner's privilege intact and both variants probed identical.
- name: "βš™οΈ Set Developer Mode to ${{ matrix.developer_mode }}"
run: >
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v AllowDevelopmentWithoutDevLicense /d ${{ matrix.developer_mode }}
- name: "πŸ“¦ Set up test suite"
run: tox run -vv --notest --skip-missing-interpreters false -e 3.15
- name: "πŸ“Š Report the probed capability"
if: matrix.developer_mode == '1'
run: |
$env:PYTHONPATH = "$PWD\tasks"
uv run --no-project --python 3.15 --with coverage python -c "import coverage_pragmas; print(coverage_pragmas.CAPABILITIES)"
- name: "πŸ§ͺ Run test suite with symlinks granted"
if: matrix.developer_mode == '1'
run: tox run --skip-pkg-install -e 3.15 --
# deny_symlink reports the token privilege, the Developer Mode value, and whether a symlink still succeeds in
# this process and in a child, so the log says which mechanism actually governs rather than assuming either.
- name: "πŸ“Š Report the probed capability"
if: matrix.developer_mode == '0'
run: |
$env:PYTHONPATH = "$PWD\tasks"
uv run --no-project --python 3.15 python tasks\deny_symlink.py uv run --no-project --python 3.15 --with coverage python -c "import coverage_pragmas; print(coverage_pragmas.CAPABILITIES)"
- name: "πŸ§ͺ Run test suite with symlinks denied"
if: matrix.developer_mode == '0'
run: uv run --no-project --python 3.15 python tasks\deny_symlink.py tox run --skip-pkg-install -e 3.15 --
termux:
name: "πŸ§ͺ Termux (Android)"
runs-on: ubuntu-24.04-arm
steps:
- name: "πŸ“₯ Check out code"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "πŸ§ͺ Run the suite in a real Termux userland via tox"
run: docker compose run --rm termux
gate:
name: "🚦 ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Non-PR checks pass' }}"
if: ${{ !cancelled() }} # codespell:ignore cancelled
needs: [test, graalpy, coverage, check, nfs, smb, capability, symlink, termux]
runs-on: ubuntu-24.04
steps:
- name: "🚦 Check all jobs passed"
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
jq -e '
all(
to_entries[];
.value.result == "success" or
(.key == "graalpy" and .value.result == "skipped")
)
' <<<"$NEEDS_JSON"