This repository was archived by the owner on Jul 31, 2026. It is now read-only.
CI #232
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" } |