Skip to content

build(deps-dev): update mypy requirement from <2,>=1.10 to >=1.10,<3 #88

build(deps-dev): update mypy requirement from <2,>=1.10 to >=1.10,<3

build(deps-dev): update mypy requirement from <2,>=1.10 to >=1.10,<3 #88

Workflow file for this run

name: Tests
on:
push:
branches: [master]
pull_request:
# Least-privilege GITHUB_TOKEN scope. None of these jobs need write access
# (no commit, no PR comment, no release publish) — read-only is enough.
# A compromised action in any matrix cell can't write back to the repo.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Lock file + requirements sync (closes #47) ───────────────────────────
lockfile:
name: Lock file freshness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Verify requirements.txt matches pyproject.toml
run: |
python <<'PY'
import sys
import tomllib
from pathlib import Path
with open("pyproject.toml", "rb") as f:
py_deps = tomllib.load(f)["project"]["dependencies"]
req_deps = [
line.strip()
for line in Path("requirements.txt").read_text().splitlines()
if line.strip() and not line.strip().startswith("#")
]
if sorted(py_deps) != sorted(req_deps):
print(
"requirements.txt [project.dependencies] drift from pyproject.toml",
file=sys.stderr,
)
print("pyproject.toml:", sorted(py_deps), file=sys.stderr)
print("requirements.txt:", sorted(req_deps), file=sys.stderr)
sys.exit(1)
PY
- name: Install pip-tools
# Pin matches update-lock.yml so lock verification uses the same resolver.
run: python -m pip install 'pip-tools==7.5.3'
- name: Verify requirements-lock.txt is up to date
# Same pip-compile flags as update-lock.yml, without --upgrade.
run: |
pip-compile requirements.txt \
--output-file /tmp/requirements-lock.txt \
--no-header \
--annotation-style=line \
--allow-unsafe \
--quiet
diff -u \
<(grep -E '^[A-Za-z0-9_.-]+==' requirements-lock.txt | sort) \
<(grep -E '^[A-Za-z0-9_.-]+==' /tmp/requirements-lock.txt | sort)
# ── Unit tests: matrix across OS and Python version ───────────────────────
# Closes #13. The unittest suite is the merge gate. Multi-OS catches the
# rare path / line-ending issue that a single-OS run hides; multi-Python
# catches API drift across LTS / current / latest interpreters.
unittest:
name: Unit tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
# Pinned to immutable commit SHAs (not @v4 / @v5) so a compromised tag
# cannot silently swap the underlying action code on this CI runner.
# When bumping, verify the new SHA via:
# gh api repos/actions/<name>/git/ref/tags/<vN> --jq '.object.sha'
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
- name: Install runtime + test dependencies
# Install from the pinned lock file for deterministic dependency
# resolution (closes #47). pytest is added on top — it is not in
# requirements-lock.txt because it is a dev-only dep. pywebview is
# the desktop-launcher dep and pulls GTK / Qt system libraries on
# Linux — intentionally excluded from the CI unittest matrix.
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-lock.txt
python -m pip install 'pytest>=8,<9'
- name: Run unittest suite
run: python -m unittest discover tests -v
- name: Run pytest integration suite
# Pytest fixtures (tests/conftest.py) build a temp workspaceStorage
# and exercise the Flask routes via app.test_client(). Scoped to the
# new endpoint file because `pytest tests/` would also re-collect the
# 178 unittest.TestCase subclasses already run in the step above —
# ~2× the CI minutes for zero extra signal.
run: python -m pytest tests/test_api_endpoints.py -v --tb=short
# ── Typecheck: mypy ───────────────────────────────────────────────────────
# Codebase already has type hints across most of the surface (~70+ typed
# functions). Mypy runs in lenient mode (--ignore-missing-imports for
# untyped third-party deps; no strict-optional) so the gate isn't a wall
# of false positives. The transitional `continue-on-error: true` was
# removed in #29 once mypy reached zero errors on this repo — type
# failures now block merges.
typecheck:
name: Typecheck (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install runtime deps + mypy
# Install from the pinned lock file for deterministic resolution,
# then add mypy (dev-only; not in requirements-lock.txt).
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-lock.txt
python -m pip install 'mypy>=1.10,<2'
- name: Run mypy
# No `continue-on-error` — mypy now exits zero on this repo (closes #29),
# so type errors must fail the job from here on.
run: mypy --ignore-missing-imports --no-strict-optional --pretty .
# ── Secret scan: gitleaks ─────────────────────────────────────────────────
# Catches accidentally committed credentials. Runs over full git history
# (fetch-depth: 0). No project-specific .gitleaks.toml — defaults cover
# standard credential patterns (API keys, AWS, GitHub tokens, etc.).
secret-scan:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Install gitleaks
run: |
GITLEAKS_VERSION=8.21.2
base_url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
checksums="gitleaks_${GITLEAKS_VERSION}_checksums.txt"
# Download tarball and checksums file to temp; retries prevent
# transient 5xx failures.
curl --fail --location --silent --show-error \
--retry 5 --retry-delay 2 --retry-all-errors \
-o "/tmp/${tarball}" "${base_url}/${tarball}"
curl --fail --location --silent --show-error \
--retry 5 --retry-delay 2 --retry-all-errors \
-o "/tmp/${checksums}" "${base_url}/${checksums}"
# Verify SHA-256 before extraction; fail and clean up on mismatch.
expected=$(grep " ${tarball}$" "/tmp/${checksums}" | awk '{print $1}')
if [ -z "${expected}" ]; then
echo "::error::No checksum entry found for ${tarball}" >&2
rm -f "/tmp/${tarball}" "/tmp/${checksums}"
exit 1
fi
actual=$(sha256sum "/tmp/${tarball}" | awk '{print $1}')
if [ "${expected}" != "${actual}" ]; then
echo "::error::SHA-256 mismatch for ${tarball}: expected ${expected}, got ${actual}" >&2
rm -f "/tmp/${tarball}" "/tmp/${checksums}"
exit 1
fi
tar -xz -f "/tmp/${tarball}" gitleaks
sudo mv gitleaks /usr/local/bin/gitleaks
rm -f "/tmp/${tarball}" "/tmp/${checksums}"
- name: Run gitleaks
run: |
gitleaks detect \
--source . \
--verbose \
--redact \
--exit-code 1