Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.

CI

CI #232

Workflow file for this run

name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
# Least privilege for GITHUB_TOKEN (CodeQL actions/missing-workflow-permissions).
# Every job below only READS the repository — checkout, install, run gates.
# Nothing here publishes, comments, or writes back, so read access to contents
# is the entire requirement. Declared at the workflow level so a job added later
# inherits the restriction instead of silently getting the default write set;
# a job that genuinely needs more should widen it for ITSELF, not here.
permissions:
contents: read
jobs:
lint-and-build:
name: Lint & Build
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
- name: Install Node dependencies
run: npm ci
- name: Lint (ESLint — errors gate; advisory warnings are not CI noise)
run: npm run lint -- --quiet
# CI ran lint, build, cargo check and pytest -- but never the RENDERER
# unit tests, so nothing here exercised application logic. That gap let a
# pdfjs-dist 6.0->6.1 bump through on 2026-07-25 that returned an empty
# attachment set and silently broke `.pdfx` manifest reading (the whole
# multi-document format); only a local vitest run caught it. A green
# build proves it compiles, not that it works.
- name: Unit tests (vitest)
run: npm test
- name: Build renderer (Vite)
run: npm run build:renderer
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Create resource stubs for Tauri build script
run: |
mkdir -p resources/python
mkdir -p resources/ghostscript
mkdir -p resources/fonts
mkdir -p resources/libreoffice
mkdir -p resources/tesseract
shell: bash
- name: Check Rust compilation
run: cd src-tauri && cargo check
test-engine:
name: Python Engine Tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install Python dependencies
# Versions pinned to the shipped engine runtime (scripts/python-requirements.in)
# so tests exercise what actually ships. pyHanko is required — the engine's
# signatures handler imports it at module load, so test_engine.py fails to
# collect without it (pyHanko pulls cryptography/certvalidator transitively).
run: pip install pikepdf==10.10.0 pdfminer.six==20260107 pyHanko==0.36.1 pytest
- name: Run engine + validation tests
run: python -m pytest tests/test_engine.py tests/test_validation.py -v
audit:
name: Dependency Audit
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: npm audit (production)
run: npm audit --production --audit-level=high
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: cargo audit
run: |
cargo install cargo-audit
cd src-tauri && cargo audit
# The third dependency graph. npm audit and cargo audit between them
# cover the renderer and the Rust backend, but the PYTHON engine ships
# inside the installer and is the code that parses untrusted PDFs
# (pikepdf/qpdf, pdfminer.six, Pillow, lxml, cryptography) -- until now
# nothing checked it. Audited against the hash-pinned lockfile, which is
# exactly what setup-python-embed.ps1 installs into the shipped runtime.
- name: Setup Python (for pip-audit)
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: pip audit (bundled engine runtime)
run: |
pip install pip-audit
pip-audit -r scripts/python-requirements.txt --no-deps
e2e:
name: E2E (WebdriverIO + tauri-driver)
# DISABLED (owner call, 2026-07-16) until the WebView2 v150 elevated-
# DevTools regression (MicrosoftEdge/WebView2Feedback#5640) is fixed and
# reaches the runner image. Hosted runners execute jobs elevated and the
# image runs UAC-disabled — a -RunLevel Limited scheduled task AND a
# SAFER restricted token BOTH still ran at High IL (proven by in-child
# TOKEN-IL banners), so no de-elevation primitive exists there and every
# WebDriver session fails at creation. Do NOT pin a Fixed Version 149
# runtime instead: users are evergreen on 150 (where the app works —
# the regression only gates elevated debugger attach), so a pinned CI
# runtime tests what nobody runs and rots (see CLAUDE.md's never-pin
# rule, written after the last stale-pin e2e breakage).
# The full 39-spec suite remains the release gate, run locally on the
# rebuilt binary; its green is recorded per milestone in the punchlist.
#
# EXPERIMENT RUN AND FAILED, 2026-07-30. Correcting the note above first:
# #5640 is CLOSED AS BY DESIGN, so there is NO upstream fix to wait for and
# the old "re-enable once the fix ships" plan was waiting on nothing. v150
# deliberately drops the user-writable config channels (WEBVIEW2_* env vars
# and HKCU policy) for an elevated host, and Microsoft named HKLM policy as
# a surviving channel.
#
# We tried it. The policy applied correctly (the step below echoed
# AdditionalBrowserArguments = --remote-debugging-port=9222 back), and the
# session STILL failed `DevToolsActivePort file doesn't exist`. So HKLM
# alone is not sufficient for THIS chain.
#
# Likely why, and why the remaining channel probably cannot close it either
# (reasoning, NOT verified): msedgedriver also passes the profile location
# via WEBVIEW2_USER_DATA_FOLDER — the same channel class that gets dropped —
# so even with debugging enabled by policy, the webview writes
# DevToolsActivePort into its default profile while the driver watches the
# folder it intended. Setting args from app code would not fix that half.
#
# Realistic options if this is ever revisited: a SELF-HOSTED runner running
# as a normal user (our local suite proves the stack is fine non-elevated),
# or leave the local 79-spec suite as the release gate, which is the
# status quo. Do NOT pin a Fixed Version 149 runtime (never-pin rule).
if: false
runs-on: windows-latest
# The job cap is a backstop, not the failure detector — the pre-flight
# step below is what catches "no session can be created at all" (runner
# image drift) in minutes instead of grinding 39 specs' worth of session
# retries into this cap and reporting a useless grey "cancelled".
timeout-minutes: 50
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 22
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('src-tauri/Cargo.lock') }}
- name: Install Node dependencies (root)
run: npm ci
- name: Install Node dependencies (e2e)
run: npm ci
working-directory: e2e-tests
- name: Provision embedded Python for engine sidecar
run: powershell -ExecutionPolicy Bypass -File scripts/setup-python-embed.ps1
shell: pwsh
- name: Create resource stubs for the Tauri build script
# `--no-bundle` does NOT skip resource validation — the punchlist said it
# did, and that claim was wrong (proven 2026-07-29: this job failed with
# did, and that claim was wrong (proven 2026-07-29: this job failed with a missing-resource error for resources/libreoffice). Every path in
# tauri.conf.json must EXIST at build time; an empty dir satisfies it.
# fonts and libreoffice are stubbed rather than vendored because this job
# is currently a WebView2 experiment — the specs that genuinely need them
# (font fallback, 56-office-export) will fail until they are vendored for
# real, and that is a separate decision about CI minutes.
run: |
mkdir -p resources/fonts
mkdir -p resources/libreoffice
shell: bash
- name: Vendor Ghostscript (the visual-compare e2e drives the real GS raster)
# 11-compare.spec.ts (visual mode) exercises engine compare_visual, which
# shells out to bundled Ghostscript — so the suite DOES need real GS, not
# a stub. bundle-ghostscript.ps1 downloads + checksum-verifies + extracts
# with 7-Zip (preinstalled on windows runners).
run: powershell -ExecutionPolicy Bypass -File scripts/bundle-ghostscript.ps1
- name: Vendor native Tesseract + stage OCR models
# Phase 12 made recognition native and engine-side, so 14-ocr-find and
# 40-batch-ocr now need the real tesseract.exe and real traineddata —
# a stub would fail them for a reason that has nothing to do with the
# WebView2 experiment this job is testing. Staging MUST follow the
# vendoring: the script supplies tessdata/configs (TSV word-box output)
# and osd, which sync-ocr-assets.mjs then adds the 47 models beside.
run: |
powershell -ExecutionPolicy Bypass -File scripts/bundle-tesseract.ps1
node scripts/sync-ocr-assets.mjs
shell: pwsh
- name: Build Tauri binary (debug, frontend embedded, no installer)
run: npx tauri build --debug --no-bundle
env:
VITE_E2E: "1"
- name: Install tauri-driver
run: cargo install tauri-driver --locked
- name: Install msedgedriver matching runner WebView2 Runtime
# msedgedriver-tool resolves against the installed WebView2 RUNTIME
# (registry pv of the EdgeUpdate WebView2 client), not Edge-the-browser
# — the step's old name said "Edge" and misled a whole investigation.
# wdio.conf.ts onPrepare re-runs the tool anyway (self-healing rule);
# this step exists to have the tool compiled + on PATH.
run: |
cargo install --git https://github.com/chippers/msedgedriver-tool
$cargoBin = "$env:USERPROFILE\.cargo\bin"
Push-Location $cargoBin
msedgedriver-tool
Pop-Location
echo "$cargoBin" >> $env:GITHUB_PATH
shell: pwsh
# WebView2 Runtime 150 deliberately IGNORES the user-writable config
# channels when the host process is elevated (a local privilege-escalation
# hardening): WEBVIEW2_* environment variables and HKCU policy are both
# dropped. That is our failure exactly — msedgedriver passes
# --remote-debugging-port through WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS,
# which works in a normal shell and is silently discarded on an elevated
# runner, so no DevToolsActivePort file is ever written and every session
# fails.
#
# Upstream MicrosoftEdge/WebView2Feedback#5640 is CLOSED AS BY DESIGN —
# there is no fix to wait for. Microsoft's own guidance names the two
# channels that survive elevation, and HKLM policy is the one available to
# a CI runner (runners are elevated, so we can write it).
#
# De-elevation, the previous approach, is DEAD and proven so: both a
# -RunLevel Limited scheduled task and `runas /trustlevel` executed at
# High integrity on this image (in-child TOKEN-IL banners), because the
# image runs UAC-disabled and grants no path down.
#
# This is an EXPERIMENT. The unknown is the port handshake: msedgedriver
# chooses a port itself and then reads the actual one back out of the
# DevToolsActivePort file, so a fixed policy port may or may not satisfy
# it. The pre-flight below is deliberately one spec, so a failure costs
# minutes and names itself instead of grinding the whole suite.
- name: Enable DevTools via HKLM policy (survives elevation)
shell: pwsh
run: |
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge\WebView2'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'AdditionalBrowserArguments' `
-Value '--remote-debugging-port=9222' -Type String
Get-ItemProperty -Path $key | Select-Object AdditionalBrowserArguments
- name: Session pre-flight (fail fast on runner-image drift)
timeout-minutes: 12
shell: pwsh
working-directory: e2e-tests
run: npx wdio run wdio.conf.ts --spec specs/01-boot.spec.ts
# DISABLED FOR THE EXPERIMENT. The question being answered is narrow — does
# the HKLM policy channel restore the DevTools endpoint for an elevated
# host? The pre-flight above answers it in minutes. Running the full suite
# now would also fail on the stubbed fonts/libreoffice above and muddy the
# result. Re-enable once the pre-flight is green AND those are vendored.
- name: Run WebdriverIO suite
if: false
timeout-minutes: 40
shell: pwsh
working-directory: e2e-tests
run: npm test
- name: Dump WebView2 crash diagnostics
if: failure()
shell: pwsh
run: |
try {
Get-WinEvent -FilterHashtable @{LogName='Application'; Id=1000,1001} -MaxEvents 40 -ErrorAction Stop |
Where-Object { $_.Message -match 'msedgewebview2|openpdfstudio|msedgedriver' } |
Format-List TimeCreated, Id, Message
} catch { Write-Host "no matching crash events" }
try {
Get-ChildItem "$env:TEMP" -Filter "*EBWebView*" -Recurse -Depth 2 -ErrorAction Stop |
Select-Object -First 20 -ExpandProperty FullName
} catch { Write-Host "no EBWebView temp dirs" }