55 branches : [master]
66 pull_request :
77
8+ concurrency :
9+ group : ${{ github.workflow }}-${{ github.ref }}
10+ cancel-in-progress : true
11+
812jobs :
13+ # ── Unit tests: matrix across OS and Python version ───────────────────────
14+ # Closes #13. The unittest suite is the merge gate. Multi-OS catches the
15+ # rare path / line-ending issue that a single-OS run hides; multi-Python
16+ # catches API drift across LTS / current / latest interpreters.
917 unittest :
10- name : Unit tests
11- runs-on : ubuntu-latest
12-
18+ name : Unit tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
19+ runs-on : ${{ matrix.os }}
20+ strategy :
21+ fail-fast : false
22+ matrix :
23+ os : [ubuntu-latest, macos-latest, windows-latest]
24+ python-version : ["3.11", "3.12", "3.13"]
1325 steps :
1426 # Pinned to immutable commit SHAs (not @v4 / @v5) so a compromised tag
1527 # cannot silently swap the underlying action code on this CI runner.
@@ -20,15 +32,96 @@ jobs:
2032 - name : Set up Python
2133 uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
2234 with :
23- python-version : ' 3.12 '
35+ python-version : ${{ matrix.python-version }}
2436
2537 - name : Install runtime + test dependencies
26- # Only what the tests actually exercise. `pywebview` from requirements.txt
27- # is the desktop-launcher dep and pulls GTK / Qt system packages on Linux
28- # — out of scope for the unittest suite, so it's deliberately omitted here .
38+ # Only what the tests actually exercise. `pywebview` from
39+ # requirements.txt is the desktop-launcher dep and pulls GTK / Qt
40+ # system packages on Linux — out of scope for the unittest suite.
2941 run : |
3042 python -m pip install --upgrade pip
3143 python -m pip install 'flask>=3.0' 'fpdf2>=2.7'
3244
3345 - name : Run unittest suite
3446 run : python -m unittest discover tests -v
47+
48+ # ── Typecheck: mypy ───────────────────────────────────────────────────────
49+ # Codebase already has type hints across most of the surface (~70+ typed
50+ # functions). Mypy runs in lenient mode (--ignore-missing-imports for
51+ # untyped third-party deps; no strict-optional) so the gate isn't a wall
52+ # of false positives on first run. continue-on-error keeps findings as
53+ # warnings during the surface-cleanup phase; flip to required by removing
54+ # continue-on-error once the surface is clean.
55+ typecheck :
56+ name : Typecheck (mypy)
57+ runs-on : ubuntu-latest
58+ steps :
59+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
60+
61+ - name : Set up Python
62+ uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
63+ with :
64+ python-version : " 3.12"
65+
66+ - name : Install runtime deps + mypy
67+ run : |
68+ python -m pip install --upgrade pip
69+ python -m pip install 'flask>=3.0' 'fpdf2>=2.7' 'mypy>=1.10'
70+
71+ - name : Run mypy
72+ continue-on-error : true
73+ run : mypy --ignore-missing-imports --no-strict-optional --pretty .
74+
75+ # ── Secret scan: gitleaks ─────────────────────────────────────────────────
76+ # Catches accidentally committed credentials. Runs over full git history
77+ # (fetch-depth: 0). No project-specific .gitleaks.toml — defaults cover
78+ # standard credential patterns (API keys, AWS, GitHub tokens, etc.).
79+ secret-scan :
80+ name : Secret scan (gitleaks)
81+ runs-on : ubuntu-latest
82+ steps :
83+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
84+ with :
85+ fetch-depth : 0
86+
87+ - name : Install gitleaks
88+ run : |
89+ GITLEAKS_VERSION=8.21.2
90+ base_url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
91+ tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
92+ checksums="gitleaks_${GITLEAKS_VERSION}_checksums.txt"
93+
94+ # Download tarball and checksums file to temp; retries prevent
95+ # transient 5xx failures.
96+ curl --fail --location --silent --show-error \
97+ --retry 5 --retry-delay 2 --retry-all-errors \
98+ -o "/tmp/${tarball}" "${base_url}/${tarball}"
99+ curl --fail --location --silent --show-error \
100+ --retry 5 --retry-delay 2 --retry-all-errors \
101+ -o "/tmp/${checksums}" "${base_url}/${checksums}"
102+
103+ # Verify SHA-256 before extraction; fail and clean up on mismatch.
104+ expected=$(grep " ${tarball}$" "/tmp/${checksums}" | awk '{print $1}')
105+ if [ -z "${expected}" ]; then
106+ echo "::error::No checksum entry found for ${tarball}" >&2
107+ rm -f "/tmp/${tarball}" "/tmp/${checksums}"
108+ exit 1
109+ fi
110+ actual=$(sha256sum "/tmp/${tarball}" | awk '{print $1}')
111+ if [ "${expected}" != "${actual}" ]; then
112+ echo "::error::SHA-256 mismatch for ${tarball}: expected ${expected}, got ${actual}" >&2
113+ rm -f "/tmp/${tarball}" "/tmp/${checksums}"
114+ exit 1
115+ fi
116+
117+ tar -xz -f "/tmp/${tarball}" gitleaks
118+ sudo mv gitleaks /usr/local/bin/gitleaks
119+ rm -f "/tmp/${tarball}" "/tmp/${checksums}"
120+
121+ - name : Run gitleaks
122+ run : |
123+ gitleaks detect \
124+ --source . \
125+ --verbose \
126+ --redact \
127+ --exit-code 1
0 commit comments