Skip to content

Publish releases to PyPI through trusted publishing, when enabled #9

Publish releases to PyPI through trusted publishing, when enabled

Publish releases to PyPI through trusted publishing, when enabled #9

Workflow file for this run

name: Lint and Test
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# The release workflow calls this one, so the gate a release passes is this exact job
# set rather than a copy of it that can drift.
workflow_call:
permissions:
contents: read
concurrency:
# github.workflow is the calling workflow's name under workflow_call, so a release and a
# push to main land in their own groups and do not cancel each other.
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GITLEAKS_VERSION: 8.30.1
# From the release's checksums file. Dependabot cannot update a pinned download, so bump
# both together when moving to a new gitleaks release.
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
PIP_AUDIT_VERSION: 2.10.1
jobs:
secrets:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout (full history)
uses: actions/checkout@v7
with:
# Every commit, so a secret added and later removed is still found.
fetch-depth: 0
- name: Install gitleaks
run: |
archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -sSfL -o "$RUNNER_TEMP/$archive" \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/$archive"
echo "${GITLEAKS_SHA256} $RUNNER_TEMP/$archive" | sha256sum -c -
tar -xzf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP" gitleaks
- name: Scan every commit
run: '"$RUNNER_TEMP/gitleaks" git --config .gitleaks.toml --redact --verbose .'
lint:
name: Lint and format check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
# --locked fails the job when uv.lock is out of step with pyproject.toml.
- name: Install
run: uv sync --locked
- name: Lint
run: uv run ruff check --output-format=github src tests scripts
# Check only: formatting is fixed locally with 'just fmt', never in CI.
- name: Format check
run: uv run ruff format --check src tests scripts
audit:
name: Dependency audit (pip-audit)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
# The locked, hashed dependency tree, runtime and dev, exactly as CI installs it.
- name: Audit the locked dependencies for known vulnerabilities
run: |
uv export --frozen --all-extras --format requirements-txt --no-emit-project \
--output-file "$RUNNER_TEMP/requirements.txt"
uvx "pip-audit==${PIP_AUDIT_VERSION}" --strict --disable-pip --require-hashes \
--requirement "$RUNNER_TEMP/requirements.txt"
test:
name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
# The config path and file permission handling differ on Windows and macOS,
# so run the suite there too, on one Python version.
include:
- os: windows-latest
python-version: "3.13"
- os: macos-latest
python-version: "3.13"
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install
run: uv sync --locked
# Includes the rebrand test, which renames a copy of the repository and runs its
# whole suite, so 'just rebrand' is proven on every platform.
- name: Test
run: uv run pytest
# Line and branch coverage on one platform, so the number is the same on every run.
# The floor (fail_under) lives in pyproject.toml, beside the rest of the coverage config.
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install
run: uv sync --locked
- name: Test with coverage
run: uv run pytest --cov --cov-report=term-missing --cov-report=xml
- name: Summarise
if: always()
shell: bash
run: |
if [ -f .coverage ]; then
{
echo "## Coverage"
echo
uv run coverage report --format=markdown --fail-under=0
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Keep the report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage.xml
if-no-files-found: ignore
retention-days: 14
# The total as a shields.io endpoint; badges.yml publishes it for the README badge
# after a push to main.
- name: Write the coverage badge
shell: bash
run: |
total="$(uv run coverage report --format=total --precision=1 --fail-under=0)"
python3 - "$total" > coverage-badge.json <<'PY'
import json
import sys
total = float(sys.argv[1])
colour = next(c for floor, c in ((90, "brightgreen"), (80, "green"), (70, "yellow"), (0, "red")) if total >= floor)
print(json.dumps({"schemaVersion": 1, "label": "coverage", "message": f"{total:.1f}%", "color": colour}))
PY
cat coverage-badge.json
- name: Keep the badge
uses: actions/upload-artifact@v7
with:
name: coverage-badge
path: coverage-badge.json
if-no-files-found: error
retention-days: 14
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [secrets, lint, audit, test, coverage]
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
- name: Build the sdist and wheel
run: uv build
# Proves the wheel installs cleanly and the entry point resolves outside the source tree.
- name: Smoke test the wheel
shell: bash
run: |
uv venv "$RUNNER_TEMP/smoke"
uv pip install --python "$RUNNER_TEMP/smoke" dist/*.whl
"$RUNNER_TEMP/smoke/bin/ldo" --version
"$RUNNER_TEMP/smoke/bin/ldo" --help > /dev/null
# Built once here; a release publishes these exact files rather than rebuilding.
- name: Keep the build
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
if-no-files-found: error
retention-days: 14