Skip to content

ci(vault): lint scripts/ too, and refuse to plant a tree over a real … #135

ci(vault): lint scripts/ too, and refuse to plant a tree over a real …

ci(vault): lint scripts/ too, and refuse to plant a tree over a real … #135

Workflow file for this run

# Vault CI.
#
# THE TOOLCHAIN IS PINNED ON PURPOSE, and the reason is written here rather than remembered.
# This job used to run `pip install ruff` with no pin. That means a ruff release can turn `main`
# red with nobody having changed a line of code, and because `publish` needs `lint`, a red lint
# silently keeps the LAST GOOD ARTIFACT as the public one. That is not theoretical: v1.6.0 and
# v1.7.0 were both tagged and neither reached PyPI, so `pip install qp-vault` served 1.5.2 from
# 2026-04-09 for five months, including its unauthenticated FastAPI router. Upgrading the pin is
# a deliberate line in a diff. Drifting into a new one is not.
name: Python CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
# A silent red becomes a dated red. Both tag failures went unnoticed for months because nothing
# ran again after the push that broke them.
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
permissions:
contents: read
env:
RUFF_VERSION: "0.15.5"
MYPY_VERSION: "1.20.*"
# The LINT interpreter, which is the syntax level ruff and mypy judge against. It tracks
# `requires-python`, not the newest runtime: this is a library, and its floor is the oldest
# Python a consumer runs.
LINT_PYTHON: "3.12"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: ${{ env.LINT_PYTHON }}
- run: pip install "ruff==${{ env.RUFF_VERSION }}"
# `scripts/` is in scope, and it was not. The release guard that gates publishing lives
# there, so leaving it out meant the one script CI executes to protect a release was itself
# linted by nothing. Found by running ruff over the paths the workflow does NOT name.
- run: ruff check src/ tests/ scripts/
# Formatting is checked, not merely available. 95 files were unformatted when this line was
# added, which is what happens when a formatter is installed and never asserted on.
- run: ruff format --check src/ tests/ scripts/
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: ${{ env.LINT_PYTHON }}
- run: pip install -e ".[sqlite,fastapi,integrity,encryption,cli,dev]"
- run: pip install "mypy==${{ env.MYPY_VERSION }}"
- run: mypy src/qp_vault/
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- run: pip install -e ".[sqlite,fastapi,integrity,encryption,cli,dev]"
- run: pytest tests/ -v --tb=short --cov=qp_vault --cov-report=term-missing
web-explorer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: ${{ env.LINT_PYTHON }}
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: manifest contract test (python)
run: python examples/web-explorer/tests/test_generate_manifest.py
- name: sha3 + verification test (node)
run: node --test examples/web-explorer/tests/test_sha3.mjs
publish:
# `typecheck` is in this list now. It was not, and that is how 1.5.2 reached PyPI from a run
# whose overall conclusion reads `failure`: only typecheck was red, publish did not care, and
# a reader checking the RUN instead of the JOB concluded 1.5.2 had never shipped.
needs: [lint, typecheck, test, web-explorer]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: ${{ env.LINT_PYTHON }}
# The tag and the package must agree, or the index carries bytes under a name that does not
# describe them. `pyproject` said 1.7.0 while `main` sat fourteen commits past the v1.7.0
# tag, and nothing in the build would have objected.
# The guard proves it can still go red BEFORE its pass is trusted. A release gate that
# silently stopped checking would otherwise wave every tag through, and nothing about the
# green would look different.
- name: release guard self-test
run: python3 scripts/check_release_consistency.py --self-test
- name: tag matches the declared version
run: python3 scripts/check_release_consistency.py --tag "${GITHUB_REF_NAME}"
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1