-
Notifications
You must be signed in to change notification settings - Fork 4
353 lines (313 loc) · 17.2 KB
/
Copy pathci.yml
File metadata and controls
353 lines (313 loc) · 17.2 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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
name: CI
# Minimal CI: re-verifies on every PR exactly what has been run manually
# before every merge - pytest, npm run check. python-tests' PRIMARY cell
# (windows-latest, Python 3.12) exists because backend/tests/
# test_backend_secrets_at_rest.py exercises real Windows DPAPI
# (CryptProtectData/CryptUnprotectData) - the secrets-at-rest encryption
# this app actually ships - which only exists on Windows; that's still the
# one cell every other job effectively depends on for real DPAPI coverage.
# This used to also be pinned for PySide6/QtWebEngine's offscreen mode, but
# Qt was fully removed at the R7.6b cutover.
#
# ADR-015 stage 15.6's OS/Python MATRIX IS NOT DONE, and this job is
# deliberately still a single windows-latest/3.12 cell. It was attempted
# (4 cells: windows 3.10/3.12 + ubuntu 3.11/3.12) and reverted after CI
# proved two hard blockers, both of which are real work rather than a
# config tweak:
#
# 1. requirements.txt cannot install on 3.10/3.11 at all. It is a
# hash-locked pip-compile output resolved against 3.12 - numpy==2.5.1
# requires >=3.12, contourpy==1.3.3 requires >=3.11 - so those cells
# die at `pip install -r requirements.txt` before running a thing.
# (This file's build-check note below already said the lock is
# 3.12-specific; that warning was correct.) Fixing it means either
# per-version lockfiles or an unpinned install path for non-primary
# cells - a real dependency-management decision.
# 2. ~18 tests genuinely assume Windows and have no platform guard: the
# DPAPI secrets-at-rest suite (which asserts encryption actually
# happened, only true on Windows), subprocess.CREATE_NO_WINDOW in
# test_code_sandbox_domain.py, and os.startfile in
# test_diagnostic_bundle.py. They fail on ubuntu even at 3.12 where
# the lock installs fine. Guarding them touches a security-boundary
# test file and deserves its own reviewed change.
#
# The one skipif this stage DID land (the unguarded real-DPAPI probe in
# test_backend_secrets_at_rest.py) is kept - it is correct on its own
# merits regardless of when the matrix returns.
#
# The pytest/compileall steps run from the repo root, never a subdirectory:
# pinning working-directory scopes collection to that directory's tests and
# silently skips the rest. Collection today (2026-08-25) is backend/tests/,
# contracts/tests/, tests/ (the permanent Qt-removal gate), and mutation_tests/
# (property-based tests added after this comment was first written) - the
# exact set `python -m pytest -q` from the repo root runs locally, so a green
# local run and a green CI run mean the same thing. Verify with that command,
# not a narrowed path - and re-check this directory list occasionally, since
# it has drifted before.
#
# R7.6b: graphlink_app/tests/ is gone from this list because graphlink_app/
# itself was deleted at the Qt-removal cutover.
#
# ADR-015 stage 15.1: build-check is a third, independent job. It exists
# because `pip install -r requirements.txt` (the python-tests job, above)
# never actually builds this project's own wheel - so a broken py-modules
# list (a real incident: graphlink_note_agent and graphlink_process_env were
# both missing, the latter silently disabling the subprocess secret-scrubbing
# allowlist) shipped invisibly. This job builds the wheel, twine-checks its
# metadata, and imports backend.agents from a clean venv with the repo
# checkout NOT on sys.path (Push-Location $env:TEMP before the import test) -
# the same failure mode a `pip install .` from the loose repo directory would
# hide. `build`/`twine` are installed directly in this job rather than added
# to requirements.txt: they are CI-only tooling, not an app runtime
# dependency, and requirements.txt's hash-lock is compiled against the
# project's pinned Python 3.12 - regenerating it from a different interpreter
# risks a lock that resolves differently than what 3.12 actually needs.
# ADR-015 stage 15.6: e2e-playwright is a fourth, independent job. Every
# job above exercises the backend or the frontend in isolation (unit/
# integration tests, a mock DOM for React, a wheel import) - none of them
# ever boots the real backend/app.py factory under uvicorn AND drives the
# real BUILT SPA against it with a real browser, the one gap those layers
# structurally cannot close (a wiring break between the two - a renamed
# WS message shape, a selector that silently stopped matching real DOM -
# can pass every unit suite and still leave the shipped app broken).
# tests_e2e/run_backend.py's own module docstring has the full "why
# Chromium is a faithful proxy for pywebview's WebView2, why no auth
# token, why a fresh temp dir every run" reasoning; web_ui/playwright.
# config.ts's own comment has the "why Chromium-only, why single-worker"
# half. ubuntu-latest, not windows: this suite never touches DPAPI/Job
# Objects/ACLs (python-tests' own windows-latest cells already cover
# those), so there is no reason to pay the 2x Windows-runner bill here -
# same economy note build-check/frontend-checks already follow.
# Actions-minutes economy (2026-08-07, ~90% of the month's quota burned by
# 189 runs in one week): the push->main trigger is REMOVED - every merge is a
# squash whose tree is byte-identical to the PR head that CI just validated,
# so the post-merge run was a pure duplicate. The PR run is the gate.
# Re-evaluate next cycle if non-squash merges ever appear.
on:
pull_request:
branches: [main]
# ADR-015 stage 15.2: least-privilege token (this workflow only ever reads
# the checkout - no job pushes, comments, or creates releases), and a
# concurrency group so a rapid string of pushes to the same PR/branch cancels
# the now-stale in-flight run instead of queueing redundant Windows minutes
# behind it.
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
python-tests:
# ADR-015 stage 15.3: was "Python tests (offscreen)" - a Qt-removal-era
# name for a job that no longer runs anything Qt-related (the offscreen
# QT_QPA_PLATFORM setting it referred to is gone, see pyproject.toml's
# own [tool.pytest.ini_options]). Windows-pinned for DPAPI, not Qt - see
# this file's own module comment above.
name: Python checks (Windows, DPAPI)
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
# ADR-015 stage 15.7: pytest/pytest-cov/ruff/mypy are dev-only
# tooling, deliberately NOT in requirements.txt (the prod lockfile a
# real install pulls in) - see pyproject.toml's
# [project.optional-dependencies] dev extra, which lists the
# identical set for a contributor's own `pip install -e .[dev]`.
# pip-tools is deliberately NOT installed here: the lock-currency
# check (tests/test_dependency_lock_currency.py) compares the two
# requirements files as text rather than shelling out to
# pip-compile, precisely so CI never depends on pip-tools-vs-pip
# version skew - see that test's own module docstring for the CI
# failure that proved the shell-out approach unusable.
- name: Install dev tooling (pytest, ruff, mypy - CI-only, not in the prod lockfile)
run: pip install --quiet pytest==9.1.1 pytest-cov==7.1.0 pytest-xdist==3.8.0 ruff==0.16.4 mypy==2.3.1 hypothesis==6.165.10
- name: Compile check
run: python -m compileall -q .
- name: ruff (scoped rule set - see pyproject.toml's own [tool.ruff])
run: python -m ruff check .
- name: mypy (scoped rule set - see pyproject.toml's own [tool.mypy])
run: python -m mypy
- name: Run pytest (with coverage floor - see pyproject.toml's own [tool.coverage])
run: python -m pytest -q --cov --cov-report=term-missing --ignore=backend/tests/perf
# ADR-015 stage 15.4 review: coverage.py's line tracer adds real
# per-line overhead - enough to trip test_loop_watchdog.py's own
# 100ms stall ceiling under instrumentation (confirmed: it failed
# under --cov, passed clean without it). Running the perf suite in
# its own uninstrumented invocation is what test_loop_watchdog.py's
# own module docstring already asks for (stable timing, no
# environmental noise) - coverage instrumentation is exactly the
# kind of noise that docstring means.
- name: Run pytest (perf suite, uninstrumented AND serial - coverage tracing and parallel sibling workers would both corrupt its timing assertions)
run: python -m pytest -q -n0 backend/tests/perf
- name: pip-audit (known-vulnerability scan of the locked dependency set)
run: pip install --quiet pip-audit==2.10.1 && python -m pip_audit -r requirements.txt
build-check:
name: Build check (wheel builds, installs, and imports cleanly)
# ubuntu, not windows (2026-08-07 minutes economy): Windows runners bill
# at 2x. Nothing here is Windows-bound - the wheel is pure-Python and
# graphlink_execution_guard's WinDLL load is behind `sys.platform ==
# "win32"`, so `import backend.agents` is clean on Linux. Only
# python-tests genuinely needs Windows (DPAPI, Job Objects, ACLs).
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install build tooling
run: pip install build==1.5.0 twine==7.0.0
- name: Build the wheel
run: python -m build --wheel
- name: Twine check (validates package metadata, incl. the version string)
# python -m twine, not a bare `twine` invocation: pip's script-install
# directory isn't reliably on PATH across runner/pip configurations
# (confirmed locally - `twine` alone was not found even right after
# `pip install twine` succeeded), and the module form has no such
# dependency.
run: python -m twine check dist/*.whl
- name: Install wheel + real runtime deps into a clean venv, import backend.agents
shell: bash
run: |
python -m venv wheel_test_venv
wheel_test_venv/bin/pip install --quiet -r requirements.txt
wheel=$(ls dist/*.whl | head -1)
wheel_test_venv/bin/pip install --quiet --no-deps "$wheel"
cd /tmp
"$GITHUB_WORKSPACE/wheel_test_venv/bin/python" -c "import backend.agents, graphlink_note_agent, graphlink_process_env; print('OK: backend.agents and all py-modules import cleanly from the installed wheel')"
frontend-checks:
name: Frontend checks (npm run check)
# ubuntu, not windows - same 2x-billing economy note as build-check.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# check:schema shells out to the Python codegen script (stdlib-only,
# no pip install needed) to prove the generated TS/JSON Schema
# artifacts aren't stale relative to the Python dataclasses. No
# cache: pip here (unlike the other two jobs' setup-python) - this
# step never runs a pip install, so the cache dir never gets
# populated and setup-python's own post-job cache-save step fails
# ("Cache folder path is retrieved for pip but doesn't exist on
# disk") - confirmed live on PR #212's first CI run.
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: web_ui/.nvmrc
cache: npm
cache-dependency-path: web_ui/package-lock.json
- name: Install npm dependencies
working-directory: web_ui
run: npm ci
# Wrapped rather than a bare `npm audit --audit-level=high`: that
# conflates "this dependency set has a high-severity advisory" with
# "registry.npmjs.org did not answer" into the same exit code, and the
# second failed two consecutive Python-only pull requests in one
# afternoon. See web_ui/scripts/audit-with-retry.mjs's own header for
# the full reasoning and the trade it makes. A real advisory still
# fails the build on the first attempt.
- name: npm audit (known-vulnerability scan, high+critical only)
working-directory: web_ui
run: node scripts/audit-with-retry.mjs
- name: Run checks (schema drift, typecheck, lint, vitest, build)
working-directory: web_ui
run: npm run check
e2e-playwright:
name: E2E (Playwright boot-smoke)
# ubuntu, not windows - see this file's own module comment above for
# why this suite specifically doesn't need a Windows runner.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: web_ui/.nvmrc
cache: npm
cache-dependency-path: web_ui/package-lock.json
# The prod lockfile, not the dev extras python-tests installs - this
# job never runs pytest/ruff/mypy, it only needs a real Python
# environment for tests_e2e/run_backend.py's own `from backend.app
# import create_app`, exactly what a real `pip install -r
# requirements.txt` gives a contributor.
- name: Install Python dependencies
run: pip install -r requirements.txt
- name: Install npm dependencies
working-directory: web_ui
run: npm ci
# Playwright's own webServer (playwright.config.ts) serves the SPA
# from web_ui/dist/app - the real production build, the same
# directory graphlink_desktop.py's own real launch serves from - not
# a vite dev server, so this build has to exist before the suite runs
# at all.
- name: Build the SPA
working-directory: web_ui
run: npm run build
# Chromium only (see playwright.config.ts's own comment for why) -
# `--with-deps` also apt-installs the handful of shared libraries a
# headless Chromium needs on a bare ubuntu-latest runner, which this
# job otherwise doesn't have.
- name: Install Playwright's Chromium browser
working-directory: web_ui
run: npx playwright install --with-deps chromium
- name: Run Playwright E2E suite
working-directory: web_ui
run: npm run test:e2e
# A fifth job, and the smallest. python-tests is the only job that runs
# pytest and it is pinned to windows-latest for DPAPI, so the 9 tests that
# assert POSIX file mode bits - on chats.db, the knowledge store, DB
# backups, and session.dat, the secrets-at-rest file - skipped in CI and
# ran nowhere else. Nine assertions about the permissions of a file holding
# API keys, executing on no machine anyone checked.
#
# Scoped to the four files that contain them, NOT all of backend/tests:
# pytest imports every module it collects, and this file's own comment
# above records that ~18 tests elsewhere genuinely assume Windows.
# Narrowing collection is what keeps this job about permissions rather
# than about porting the suite to Linux. tests/test_posix_permission_
# coverage.py ties the file list below to the marked tests, so a tenth
# marked test landing in a fifth file fails the build instead of silently
# not running here.
#
# ubuntu-latest, and deliberately cheap: no coverage, no xdist workers
# (-n0 for 9 tests), no dev tooling beyond pytest itself.
posix-permissions:
name: POSIX file permissions (ubuntu)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
# pytest-xdist is needed even at -n0: [tool.pytest.ini_options].addopts
# names -n, so the plugin has to be importable for the run to start.
# hypothesis for the same class of reason - backend/tests/conftest.py
# imports it at module scope, so pytest cannot load the conftest (and
# therefore collects nothing at all) without it. Versions match the
# python-tests job's own pins.
- name: Install pytest
run: pip install --quiet pytest==9.1.1 pytest-xdist==3.8.0 hypothesis==6.165.10
- name: Run the POSIX permission assertions
run: >
python -m pytest -q -n0 -m posix_permissions
backend/tests/test_chat_library.py
backend/tests/test_db_backup.py
backend/tests/test_knowledge_store.py
backend/tests/test_backend_secrets_at_rest.py