-
Notifications
You must be signed in to change notification settings - Fork 1
219 lines (192 loc) · 9.36 KB
/
tests.yml
File metadata and controls
219 lines (192 loc) · 9.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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 and #44. Multi-OS catches path-normalisation drift and
# line-ending issues that a single-OS run hides; multi-Python catches API
# drift across LTS / current / latest interpreters. Matrix parallelism
# keeps wall-clock under ~15 min (slowest runner wins, not the sum).
unittest:
name: Unit tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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
# ── PyInstaller desktop build (Windows only, once per workflow) ────────
# Closes #44. Builds the onedir bundle and smoke-tests --help so the
# desktop entry point is verified without launching the GUI window.
- name: Install PyInstaller
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: python -m pip install 'pyinstaller>=6,<7'
- name: Build PyInstaller bundle
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: pyinstaller cursor-browser.spec --noconfirm
- name: Smoke-test PyInstaller exe (--help)
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
run: dist\CursorChatBrowser\CursorChatBrowser.exe --help
# ── 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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