Skip to content

chore(main): release 0.22.3 #1813

chore(main): release 0.22.3

chore(main): release 0.22.3 #1813

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# CI only reads code — no releases or package pushes.
permissions:
contents: read
jobs:
# ── Job 1: All linters ──────────────────────────────────────────────────
lint:
runs-on: ubuntu-latest
# Extra scopes are needed by EnricoMi/publish-unit-test-result-action so
# it can post the sticky comment + checks summary on pull requests. The
# workflow-level default is contents:read; per-job permissions replace
# rather than merge, so contents:read is re-listed below.
permissions:
contents: read
checks: write
pull-requests: write
issues: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# mise provisions this job's slice of the toolchain from mise.toml —
# go, node, task, golangci-lint (built from source with this Go so it
# accepts the project's go directive — a prebuilt binary on an older Go
# would reject it), shfmt, shellcheck, ruff, jq, go-junit-report — and
# loads mise.toml [env] (SPECTRAL_VERSION, …, GOTOOLCHAIN=local) into
# $GITHUB_ENV regardless of install_args. Replaces setup-go + the
# tool-versions.env grep + every per-tool `go install`/pipx step.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: >-
go node task ruff shfmt shellcheck jq
go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint
go:github.com/jstemmer/go-junit-report/v2
# Wails v3's desktop code imports a CGo WebKit2GTK webview, so the default
# build tags need these system libs to typecheck on the Ubuntu runner.
- uses: ./.github/actions/linux-webview-deps
# Frontend — set up Node and build frontend/dist before Go linting so the
# //go:embed all:frontend/dist directive in assets.go passes type-checking,
# AND so the bundle-size budget step below can measure the real artifacts.
- name: Prepare frontend/dist (real Vite build)
uses: ./.github/actions/prepare-frontend-dist
with:
real-assets: "true"
# Bundle-size budget. Catches accidental regressions (e.g. someone
# imports a 200KB library by mistake). Two budgets:
# - Initial chunk (index-*.{js,css}): what every page-load pays.
# The four view components are lazy-loaded via
# defineAsyncComponent so each becomes a separate chunk that
# isn't counted here — only the masthead/modal/router-shell
# code lands in index-*.
# - Total assets/: catches the case where lazy-splitting masks a
# real regression by smearing it across chunks.
# Bump deliberately when a real feature needs the room.
# Bundle-size budget. Logic lives in scripts/ci/check-bundle-size.sh
# so the lefthook pre-push hook can reuse the same assertion —
# contributors fail locally before push instead of finding out
# from a red CI badge. Per-chunk + total caps live in the script
# as defaults; override via env var if a one-off needs room.
- name: Enforce bundle-size budget
run: bash scripts/ci/check-bundle-size.sh
- name: Lint Go (default build tags)
run: golangci-lint run ./...
- name: Lint Go (serveronly build tag)
run: golangci-lint run --build-tags serveronly ./...
# GOOS=windows lints the //go:build windows files (HideWindow,
# probe_windows, exec_windows) the Linux-runner GOOS skips. Pure-Go,
# so cross-GOOS analysis just swaps the stdlib view — no compilation.
- name: Lint Go (windows GOOS, default build tags)
run: GOOS=windows golangci-lint run ./...
- name: Lint Go (windows GOOS, serveronly build tag)
run: GOOS=windows golangci-lint run --build-tags serveronly ./...
# Frontend linters — deps already installed above
- name: Lint JavaScript/Vue (ESLint)
run: cd frontend && npm run lint:js
- name: Lint CSS (Stylelint)
run: cd frontend && npm run lint:css
- name: Lint HTML (HTMLHint)
run: cd frontend && npm run lint:html
# Python — ruff lint + format-check. ruff comes from mise (pinned in
# mise.toml [tools]); no pipx install needed.
- name: Lint Python (ruff)
run: task lint-py
# Shell scripts in scripts/ — shellcheck for correctness, shfmt for
# style. Both come from mise (pinned in mise.toml), so no install step.
- name: Lint shell scripts (shellcheck + shfmt)
run: task lint-shell
# Dockerfile
- name: Lint Dockerfile (Hadolint)
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: Dockerfile.build
# OpenAPI spec — Spectral with the spectral:oas ruleset + project
# overrides in .spectral.yaml. SPECTRAL_VERSION comes from mise.toml
# [env] (loaded into $GITHUB_ENV by mise-action).
- name: Lint OpenAPI (Spectral)
run: task lint-openapi
# Spell-check code, docs, and identifiers. The action ships the
# typos binary so the runner doesn't need a separate install
# step; config lives in _typos.toml at the repo root.
- name: Spell-check (typos)
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # v1.47.2
# Markdown lint — mirrors `task lint-md`. Config in
# .markdownlint-cli2.yaml at the repo root; npx pulls the
# version on demand so no global install is required.
- name: Lint Markdown (markdownlint-cli2)
run: npx --yes markdownlint-cli2
# GitHub Actions workflow lint — catches deprecated inputs,
# syntax errors, and shellcheck issues in `run:` blocks. The
# action ships the actionlint binary so no install step.
- name: Lint workflows (actionlint)
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2
# Enforce the project's SHA-pinning policy for third-party
# GitHub Actions. A tag-pinned action could be silently
# re-pointed to a malicious commit; see CONTRIBUTING.md
# → "Pinning GitHub Actions".
- name: Enforce SHA-pinned actions
run: bash scripts/ci/check-action-pins.sh
# Go unit tests (merge logic, inference helpers, screenshotType
# classification). Parser golden-file tests are skipped by -short.
# Tee through go-junit-report so the XML is always produced — the
# final pass/fail decision is delegated to the EnricoMi step below
# via `fail_on: test failures` so a fork PR without comment perms
# still surfaces failures via the workflow conclusion.
# Test-skip inventory gate. Diffs every `t.Skip*` line under
# pkg/ against `scripts/ci/test-skips-allow.txt`. A new skip
# without an allow-list entry fails CI — flake-suppression
# skips are forbidden; the allow-list is for documented
# environment gates (OS-conditional probe tests, `-short`-mode
# tesseract integration). Mirrors the lefthook pre-push
# `test-skips` hook so the gate fires both locally and in CI.
- name: Inventory test skips
run: bash scripts/ci/check-test-skips.sh
- name: Run Go unit tests
id: gotest
# continue-on-error so BOTH test steps run and the JUnit XML always
# uploads + EnricoMi always posts the "Unit test results" check. The
# real pass/fail is enforced by the "Fail the job on any unit-test
# failure" gate at the end of this job (reads `.outcome`) — so a
# failure, including a -race-only one, reds the workflow run, not just
# the check-run.
continue-on-error: true
run: |
set -o pipefail
go test -race -short -v ./... 2>&1 \
| tee go-test.log \
| go-junit-report -set-exit-code > go-junit.xml
# Frontend unit tests (Vitest) — pure-helper coverage for the
# extracted match-helpers.ts module. `--reporter=default` keeps the
# log readable in the Actions run; `--reporter=junit` adds the XML
# consumed by the EnricoMi step. The `--outputFile.junit=` form
# scopes the path to the junit reporter only, so the default
# reporter still writes to stdout.
- name: Run frontend unit tests
id: fetest
continue-on-error: true
run: |
cd frontend
npx vitest run \
--reporter=default \
--reporter=junit \
--outputFile.junit=../frontend-junit.xml
# Upload the JUnit XML so the coverage-comment job can fold its
# numbers + per-failure details into the single combined PR
# comment. `if: always()` so the artifact still uploads when a
# test step exited non-zero (which is the normal path on a
# failing test — `continue-on-error: true` makes the step succeed,
# but EnricoMi below is what flips the workflow to red).
- name: Upload unit-test JUnit XML
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: unit-test-results
path: |
go-junit.xml
frontend-junit.xml
if-no-files-found: warn
retention-days: 7
# EnricoMi posts the "Unit test results" check (red via `fail_on:
# test failures` when the JUnit XML carries a failure) — that's the
# per-PR/commit status + the detailed breakdown. It does NOT fail the
# workflow run itself, so the explicit `.outcome` gate at the end of
# this job is what reds CI. Its own commenter is OFF — the
# coverage-comment job renders one combined sticky comment.
- name: Publish unit-test results check
if: always()
uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2
with:
check_name: Unit test results
comment_mode: off
files: |
go-junit.xml
frontend-junit.xml
fail_on: test failures
# TypeScript type-check — confirms api.ts agrees with the
# OpenAPI-generated types in api.gen.d.ts.
- name: TypeScript type-check
run: cd frontend && npm run typecheck
# Verify the committed types match the current OpenAPI spec.
# Catches "you forgot to run `task gen-types` after editing
# api/openapi.yaml" before it lands on main.
- name: Verify generated types are in sync with OpenAPI spec
run: |
task gen-types
if ! git diff --quiet frontend/src/api.gen.d.ts; then
echo "::error::frontend/src/api.gen.d.ts is out of sync with api/openapi.yaml — run 'task gen-types' and commit"
git diff frontend/src/api.gen.d.ts | head -50
exit 1
fi
# Hard-fail CI on any unit-test failure. The two test steps are
# continue-on-error (so JUnit always uploads + EnricoMi always posts its
# check), which means their failure alone only reds the "Unit test
# results" check — the workflow run stays green. Gate on their real
# `.outcome` here so a failing test, including one that only fails under
# `-race`, turns this job (and the CI run) red directly. Last step in the
# job so every other check still runs first.
- name: Fail the job on any unit-test failure
if: always() && (steps.gotest.outcome == 'failure' || steps.fetest.outcome == 'failure')
env:
GO_OUTCOME: ${{ steps.gotest.outcome }}
FE_OUTCOME: ${{ steps.fetest.outcome }}
run: |
echo "::error::Unit tests failed (Go=${GO_OUTCOME}, frontend=${FE_OUTCOME}). See the 'Unit test results' check and the test-run step logs."
exit 1
# ── Job 2: Linux/amd64 Wails app ────────────────────────────────────────
build-linux:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
# Docker does the heavy build; the host only needs task + jq (the
# build-* tasks compute BUILD_VERSION via jq), so install just those.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: task jq
- name: Build Linux/amd64 Wails app
run: task build-linux
# ── Job 3: Windows/amd64 Wails app ──────────────────────────────────────
build-windows:
# Native cross-compile (no Docker) — v3's Windows WebView2 loader is pure Go,
# so this needs wails3 + node + makensis, not buildx.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Building the wails3 CLI itself (mise/go install) pulls in v3's CGo GTK4
# webview, so the Ubuntu runner needs the WebKit dev libs — even though the
# Windows target is CGO_ENABLED=0.
- uses: ./.github/actions/linux-webview-deps
- uses: ./.github/actions/wails-build-env
- name: Install makensis
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends nsis
- name: Build Windows/amd64 Wails app + NSIS installer
run: task build-windows
# ── Job 4: Server binaries + container image ─────────────────────────────
build-server:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: task jq
- name: Build all server binaries (Linux, Windows, macOS arm64+amd64)
run: task build-server-all
- name: Build Linux server container image (with Tesseract)
run: task build-server-container
# ── Job 5: macOS Wails build (requires Apple SDK / Xcode CLT) ────────────
build-mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Go + Node + Wails CLI (+ task, jq) from mise.toml's pins.
- uses: ./.github/actions/wails-build-env
- name: Build macOS universal Wails app
run: task build-mac
# ── Job 6: Vulnerability scan ────────────────────────────────────────────
# Mirrors `task trivy` but runs in CI and uploads results to the GitHub
# Security tab (Alerts → Code scanning) as well as failing the build.
trivy:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: fs
scan-ref: .
scanners: vuln
severity: HIGH,CRITICAL
exit-code: '1'
format: sarif
output: trivy.sarif
- name: Upload Trivy results to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
sarif_file: trivy.sarif
# ── Job 7: Go-specific vulnerability scan ────────────────────────────────
# govulncheck is call-graph-aware: it only flags CVEs in code paths Recall
# actually invokes. Complements Trivy (which scans all module versions
# regardless of reachability) and typically produces fewer false positives.
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# go + govulncheck from mise.toml (govulncheck built with this Go).
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: go go:golang.org/x/vuln/cmd/govulncheck
# Wails v3's desktop code imports a CGo WebKit2GTK webview — the default
# build tags need these system libs to build on the Ubuntu runner.
- uses: ./.github/actions/linux-webview-deps
# Wrapped in a retry that fires ONLY on a transient vuln.go.dev fetch
# error (the DB download occasionally times out on the runner) — a real
# CVE finding still fails immediately. See scripts/ci/govulncheck-retry.sh.
- name: Scan Go modules (default build tag)
run: bash scripts/ci/govulncheck-retry.sh ./...
- name: Scan Go modules (serveronly build tag)
run: bash scripts/ci/govulncheck-retry.sh -tags serveronly ./...
# gosec (Go SAST) is rolled into Job 1's golangci-lint (the `gosec` linter,
# run on both build tags). It no longer needs its own job / tool install.
# govulncheck stays separate: it's call-graph + CVE-database driven, which
# golangci-lint has no equivalent for.
# JS/TS SAST in CI is handled by CodeQL's javascript-typescript
# language matrix in .github/workflows/codeql.yml. Semgrep runs
# locally only — via `task lint-semgrep` and the pre-push lefthook
# hook — so contributors get fast feedback before push without
# duplicating coverage with CodeQL.
# ── Job 8: Cyclomatic complexity (REPORT ONLY) ───────────────────────────
# gocyclo (Go) + ESLint's `complexity` rule (frontend) at threshold 10.
# `continue-on-error: true` keeps the job NON-BLOCKING — a high number
# never fails the build, it just shows up in the job log. We watch the
# trend across PRs to know when to refactor; tightening into a hard
# gate is its own deliberate decision, not a side-effect of this job.
# Threshold + tooling pins both live in scripts/ci/check-complexity.sh +
# mise.toml (gocyclo).
complexity:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# go (gocyclo) + node (eslint pass) from mise.toml.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: go node go:github.com/fzipp/gocyclo/cmd/gocyclo
# gocyclo doesn't need real frontend assets — Go source only.
- name: Prepare frontend/dist (stub for //go:embed)
uses: ./.github/actions/prepare-frontend-dist
with:
real-assets: "false"
- name: Install frontend deps (for the eslint pass)
run: npm --prefix frontend ci
- name: Run complexity sweep (Go + frontend, threshold 10)
run: bash scripts/ci/check-complexity.sh
# Diff the current Go scores against the checked-in top-20
# baseline. Same threshold, same exclusions; emits a summary
# line + per-jump/newcomer record. continue-on-error keeps the
# job non-blocking; a future PR-comment step can hook into
# this output once the baseline shape stabilises.
- name: Diff against baseline (report-only)
run: bash scripts/ci/check-complexity-baseline.sh
# ── Job 9: Dead code analysis ────────────────────────────────────────────
# deadcode does whole-program call-graph analysis for Go (serveronly variant
# only — the Wails variant registers App methods via reflection so deadcode
# would report false positives; golangci-lint `unused` covers that variant).
# knip scans the frontend for unused TypeScript exports and stale deps.
dead-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# go (+ deadcode) and node (+ knip) from mise.toml.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: go node go:golang.org/x/tools/cmd/deadcode
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Build frontend (required for Go embed typecheck)
run: cd frontend && npm run build
- name: Dead Go code (serveronly build tag)
# Delegates to scripts/ci/deadcode-check.sh; the allow-list of
# intentional unreachables lives in scripts/ci/deadcode-allow.txt
# and is shared with Make + lefthook so all three call sites
# stay in sync.
run: bash scripts/ci/deadcode-check.sh
- name: Dead TypeScript code (knip)
run: cd frontend && npm run dead:ts
# ── Job 10: Frontend coverage report ─────────────────────────────────────
# Informational only — no thresholds enforced. The HTML report and lcov
# file are uploaded as a workflow artifact so they can be downloaded from
# the Actions run page for any push or PR.
coverage-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: node
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Generate coverage report
run: cd frontend && npm run test:coverage
# Surface line + branch coverage (V8 reports real branch coverage) in the
# job summary from the Cobertura rates. Informational; the gate is
# vitest.config.ts coverage.thresholds.
- name: Coverage summary (line + branch)
run: |
xml=frontend/coverage/cobertura-coverage.xml
line=$(grep -oE 'line-rate="[0-9.]+"' "$xml" | head -1 | grep -oE '[0-9.]+')
branch=$(grep -oE 'branch-rate="[0-9.]+"' "$xml" | head -1 | grep -oE '[0-9.]+')
{
echo "### Frontend coverage (unit)"
echo ""
echo "| Metric | Coverage |"
echo "|---|---:|"
awk -v l="$line" -v b="$branch" 'BEGIN { printf "| Line | %.1f%% |\n| Branch | %.1f%% |\n", l*100, b*100 }'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: frontend-coverage
path: frontend/coverage/
retention-days: 30
# ── Job 11: Go coverage report ───────────────────────────────────────────
# Informational only — no thresholds enforced. The HTML report and func
# summary are uploaded as a workflow artifact so they can be downloaded
# from the Actions run page for any push or PR.
coverage-go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# go + gocover-cobertura (Cobertura XML converter) from mise.toml.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: go task go:github.com/boumenot/gocover-cobertura
# Wails v3's desktop code imports a CGo WebKit2GTK webview — the default
# build tags need these system libs to build on the Ubuntu runner.
- uses: ./.github/actions/linux-webview-deps
# Frontend build is required because assets.go uses
# //go:embed all:frontend/dist — compiling the root `recall`
# package (via `go test ./...`) fails if dist/ is missing.
- name: Prepare frontend/dist (real Vite build)
uses: ./.github/actions/prepare-frontend-dist
with:
real-assets: "true"
- name: Generate Go coverage report
run: task cover-go
# gocover-cobertura converts Go's native coverprofile into the
# Cobertura XML format that irongut/CodeCoverageSummary parses.
# Bundled into the artifact so the coverage-comment job downloads
# both formats in a single fetch.
- name: Convert coverage profile to Cobertura XML
run: gocover-cobertura < coverage/go/coverage.out > coverage/go/cobertura.xml
# Surface line + branch coverage in the job summary. Go has no native
# branch coverage, so "branch" is basic-block coverage (covered/total
# blocks in the coverprofile) — its branch-equivalent. Mirrors e2e.yml's
# coverage summary; informational (the func total above is what gates).
- name: Coverage summary (line + branch)
run: |
line=$(awk '/^total:/ { gsub(/%/,"",$NF); print $NF }' coverage/go/coverage.txt)
block=$(awk 'NR>1 { t++; if ($NF+0>0) c++ } END { if (t) printf "%.1f", 100*c/t }' coverage/go/coverage.out)
{
echo "### Go coverage (unit)"
echo ""
echo "| Metric | Coverage |"
echo "|---|---:|"
echo "| Line (statement) | ${line}% |"
echo "| Branch (basic-block) | ${block}% |"
echo ""
echo "_Go has no native branch coverage; basic-block coverage is its branch-equivalent._"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: go-coverage
path: coverage/go/
retention-days: 30
# ── Job 12: OpenAPI ↔ implementation drift check ─────────────────────────
# Spectral checks the spec is well-formed; schemathesis checks the spec
# matches the running server. Fuzzes spec-derived requests against a
# freshly-built serveronly binary and verifies every response conforms to
# the declared schema. Catches the "I added a route but forgot to update
# api/openapi.yaml" class of bug that Spectral can't see.
schemathesis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# go (build serveronly) + schemathesis (pipx) from mise.toml; also loads
# TESSERACT_VERSION + SCHEMATHESIS_VERSION from [env] into $GITHUB_ENV.
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
with:
install_args: go task pipx:schemathesis
- name: Prepare frontend/dist (real Vite build)
uses: ./.github/actions/prepare-frontend-dist
with:
real-assets: "true"
- name: Install Tesseract (no-op for API surface but Startup checks for it)
# Assert installed major.minor matches TESSERACT_VERSION from mise.toml
# [env] (loaded into $GITHUB_ENV by mise-action) so OCR drift can't
# silently land between what dev baselined the goldens against and what
# CI runs. Loud-fail when Ubuntu bumps the package version — that's the
# signal to re-baseline goldens + bump the pin.
run: |
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends tesseract-ocr
installed=$(tesseract --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
expected_mm=$(printf '%s' "$TESSERACT_VERSION" | cut -d. -f1-2)
installed_mm=$(printf '%s' "$installed" | cut -d. -f1-2)
if [ "$installed_mm" != "$expected_mm" ]; then
echo "::error::Tesseract major.minor mismatch: installed=$installed_mm expected=$expected_mm (TESSERACT_VERSION=$TESSERACT_VERSION in mise.toml). Bump the pin + re-baseline testdata/*.golden.json."
exit 1
fi
echo "Tesseract $installed matches pinned major.minor $expected_mm"
- name: Build + boot + fuzz + teardown via shared script
# scripts/ci/check-api-drift.sh handles the build-serveronly →
# boot-on-isolated-tempdir → schemathesis-run → teardown
# sequence. Same script lefthook's pre-push schemathesis hook
# invokes so the local + CI signals stay 1:1. The script
# short-circuits with an actionable error if schemathesis or
# frontend/dist are missing.
run: SKIP_FRONTEND_BUILD=1 bash scripts/ci/check-api-drift.sh