mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
a1716d8170
* ci(web): decide the npm audit gate from the report, not the exit code, and run it last (BUG-2881) `npm audit` exits non-zero identically for "a HIGH/CRITICAL advisory exists" and "the advisory service was unreachable". The Web job ran it before Build / Type check / vitest under `bash -e`, so a registry timeout (main, 03:50Z) and a 503 (#1246, 04:33Z) on 2026-09-04 each produced a red row with every frontend verification step SKIPPED — a lane that read like a failure and had asked nothing. scripts/ci-audit.mjs runs the audit in --json mode and decides from the report: metadata.vulnerabilities present → fail iff high+critical > 0, naming the advisories; an error envelope or unparseable output → a GitHub warning annotation saying the gate did not run, exit 0. The step moves to the end of the job so the frontend's own verdict always exists whatever the audit does. Verified locally against five report shapes (transport timeout envelope, E503 envelope, one high advisory, clean, garbage) and two live runs (the real registry: clean; a dead registry: warning, exit 0). `--input <file>` is the seam those checks use. Fixes BUG-2881 * ci(web): the audit gate fails closed — retry an unreachable advisory service, then fail under its own title Codex round 1 on #1247: the first draft warned and exited 0 when the advisory service could not be asked, which made the only supply-chain gate pass exactly when it had not run. A gate that passes when it cannot run is not a gate. Now: up to three attempts with backoff (registry blips are usually seconds long), then `::error title=npm audit did not run` and exit 1. The title is distinct from `::error title=npm audit` (a real advisory) so the checks tab tells the two apart without opening the log; re-running is the remedy for the first and never for the second. Because the step runs last, Build / Type check / vitest have already produced their result either way — the original blindness is gone regardless of which way this step fails. Verified against the same five saved shapes (transport and E503 envelopes and garbage now exit 1 under the did-not-run title; a high advisory exits 1 under the advisory title; clean exits 0) and two live runs (real registry: clean; dead registry: three attempts logged, exit 1). Refs BUG-2881 * ci(web): the audit gate refuses counts it cannot read, and refuses bad tuning without crashing Codex round 2 on #1247. (1) metadata.vulnerabilities was checked for presence, not for shape: Number("x") + Number(null) > 0 is false, so a malformed count read as a clean audit — a second fail-open, one layer deeper than round 1's. high/critical must now be non-negative integers or the report is unreadable, which is the fail-closed path. (2) The two env knobs are operator-set, but CI_AUDIT_ATTEMPTS=NaN left the retry loop unexecuted and threw a TypeError, and CI_AUDIT_BACKOFF_MS=Infinity parked Atomics.wait forever; both now fall back to the default with a line saying so. Refs BUG-2881 * build: the local preflight runs the same audit gate CI does, and runs it last Codex round 3 on #1247 (blast radius): `make web-check` still chained bare `npm audit && npm run check`, so a registry blip stopped svelte-check locally exactly as it had in CI, and CONTRIBUTING documented the bare command as the way to reproduce the gate. New `web-audit` target runs `npm run audit:ci`; `check` runs it after web-check and web-test, mirroring the Web job's order. CONTRIBUTING and docs/architecture.md say so. Refs BUG-2881 * build: web-audit stands alone — no `web` prerequisite, so `check` runs npm ci once and no new target reaches it Codex round 4 on #1247: `web-audit: web` made `check` run `npm ci` twice (`web` is .PHONY) and added a target CLAUDE.md's worktree rule did not list as reaching `npm ci`. `npm audit` reads the lockfile and needs neither node_modules nor a build — verified by running it with node_modules removed — so the prerequisite goes; CLAUDE.md's safe list gains `web-audit`. Refs BUG-2881
487 lines
22 KiB
YAML
487 lines
22 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# All third-party Actions are pinned to a 40-char commit SHA with a trailing
|
|
# '# vX.Y.Z' comment so a compromised maintainer or moved tag cannot silently
|
|
# execute attacker code in CI. Bump the SHA + comment together when updating.
|
|
|
|
jobs:
|
|
go:
|
|
# Backstop only. A hung TEST is caught by -timeout=45m on the steps
|
|
# below, which fails fast AND prints the goroutine dump that tells
|
|
# you which test hung; this cap exists because a job with no
|
|
# timeout-minutes inherits GitHub's 6-HOUR default, so a runaway
|
|
# that isn't a single test (a wedged service container, a stuck
|
|
# download) would otherwise sit there. Set well above the two 45m test
|
|
# steps so that in practice the per-binary timeout is what fires — not a
|
|
# guarantee, since this cap covers setup and every step, not just those
|
|
# two (TASK-2545).
|
|
timeout-minutes: 100
|
|
name: Go
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version: "1.26"
|
|
|
|
- name: Allow Go to fetch the toolchain go.mod pins
|
|
# actions/setup-go unconditionally exports GOTOOLCHAIN=local, which
|
|
# forbids Go from fetching the toolchain go.mod requires. go.mod pins
|
|
# `go 1.26.5`, but setup-go's "1.26" resolves to the newest patch in
|
|
# its manifest (1.26.4 here), so every `go` command fails with
|
|
# "go.mod requires go >= 1.26.5 (running go 1.26.4)". Written AFTER
|
|
# setup-go so it wins the $GITHUB_ENV last-write; `auto` lets Go pull
|
|
# what go.mod asks for on demand. Added after #896 raised the go.mod
|
|
# floor.
|
|
#
|
|
# Load-bearing for the govulncheck step below as of BUG-2565: go.mod
|
|
# also carries `toolchain go1.26.6`, which is what actually gets
|
|
# stamped into the scanned binary and clears 8 reachable stdlib
|
|
# advisories. `auto` is what honours that line — under `local` the
|
|
# build silently falls back to whatever patch setup-go installed and
|
|
# the gate goes red again.
|
|
run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV"
|
|
|
|
- name: Create web build placeholder for embed
|
|
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
|
|
|
|
- name: Run go vet
|
|
run: go vet ./...
|
|
|
|
- name: Run golangci-lint
|
|
# only-new-issues: false means CI fails on ANY linter finding,
|
|
# not just findings on PR-changed lines. The IDEA-732 cleanup
|
|
# (PRs #247/#249/#251/#252) cleared the existing findings under
|
|
# the configured linter set in .golangci.yml — staticcheck SA*,
|
|
# govet, ineffassign, gofmt, and the standalone `unused` linter
|
|
# (which reports U1000). Flipping the gate now prevents
|
|
# regression drift going forward.
|
|
# v2 of golangci-lint is required because v1 is capped at older
|
|
# Go releases that we no longer support.
|
|
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
|
with:
|
|
version: v2.11.4
|
|
args: --timeout=5m
|
|
only-new-issues: false
|
|
# Skip the pre-lint `golangci-lint config verify`, which fetches its
|
|
# JSON schema over the network from golangci-lint.run and fails the
|
|
# required Go gate when that host is slow/unreachable — no lint
|
|
# finding, no code involvement (BUG-2568). A malformed .golangci.yml
|
|
# is still caught by the lint run itself, just with a worse message.
|
|
verify: false
|
|
# Cap the blast radius of a stale-cache event to ~24h. The action's
|
|
# cache stores the resolved issue list from a prior pass; if any
|
|
# one pass writes degenerate results (analyzer upgrade, plugin
|
|
# reset, sub-package version drift), every downstream restore
|
|
# replays that list verbatim until the cache key rotates. PR #635
|
|
# hit exactly this — 30+ SA5011/SA4023 false positives against
|
|
# unchanged code while local (cold-cache) runs reported 0 issues.
|
|
# Default is 7 days; 1 day still keeps most runs cache-hot while
|
|
# guaranteeing daily refresh. See BUG-1624.
|
|
cache-invalidation-interval: "1"
|
|
|
|
- name: Run govulncheck
|
|
# Fails the build on any known vulnerability in a package we
|
|
# actually reach via the call graph. Net-positive: catches CVEs
|
|
# in indirect deps early, without the noise of hitting every
|
|
# stale entry in our dependency tree.
|
|
#
|
|
# Runs in BINARY mode against a freshly-built pad binary rather than
|
|
# source mode (`govulncheck ./...`). Source mode builds an SSA call-
|
|
# graph over the whole dependency tree and can balloon to multiple GB
|
|
# of RAM (BUG-2084); binary mode reads the binary's symbol table — a
|
|
# fraction of the memory, still call-graph-precise, and detects stdlib
|
|
# vulns from the Go version stamped in the binary. The "Create web
|
|
# build placeholder for embed" step above satisfies the //go:embed so
|
|
# the scan build succeeds. Mirrors the Makefile `vuln` target — keep
|
|
# the two in sync.
|
|
#
|
|
# Pinned to a specific govulncheck release. Track upstream in
|
|
# Pad's workspace; bump intentionally so an upstream behavior
|
|
# change can't break unrelated PRs. Update via:
|
|
# go install golang.org/x/vuln/cmd/govulncheck@<new-tag>
|
|
run: |
|
|
go install golang.org/x/vuln/cmd/govulncheck@v1.2.0
|
|
go build -o pad-vulnscan ./cmd/pad
|
|
"$(go env GOPATH)/bin/govulncheck" -mode binary pad-vulnscan
|
|
|
|
- name: Run tests
|
|
# -timeout is EXPLICIT here on purpose. `go test` without it uses a
|
|
# 10m per-test-binary default that nobody chose. The two race steps
|
|
# in this file were raised to 45m when the suite outgrew 30m
|
|
# (BUG-1913) while their non-race siblings silently kept the
|
|
# default — which is how the PostgreSQL leg below panicked at 10m on
|
|
# the v0.13.0 release commit (TASK-2545, which also found the ~40
|
|
# minutes it cost the cut and the goroutine dump showing nothing
|
|
# actually hung). Matching the race
|
|
# legs keeps ONE number in this file. It is a hang-catcher, not a
|
|
# performance budget: the signal for "the suite got slow" is job
|
|
# wall-clock, not this value.
|
|
run: go test -timeout=45m ./...
|
|
|
|
- name: Run tests with race detector
|
|
# Runs on both push-to-main AND pull_request. Previously gated to
|
|
# main only because GitHub Actions minutes were billed on private
|
|
# repos; the repo is public now, so PR minutes are free and we'd
|
|
# rather catch race regressions on the contributing branch than
|
|
# after merge. See BUG-1371 (also dropped test-only bcrypt cost via
|
|
# TestMain, which brought this step back under the then-30m budget —
|
|
# it did NOT raise the budget; BUG-1913 later did, 30m→45m, when the
|
|
# suite outgrew it again).
|
|
#
|
|
# Default 10m is tight: the full server-package suite under -race
|
|
# measures ~13m locally on a developer laptop after BUG-851 (the
|
|
# ipRateLimiter goroutine drain). The PLAN-866 attachment work
|
|
# (image decode/encode/resize across thumbnail + transform tests)
|
|
# pushes total race-step runtime past 20m on the GitHub-hosted
|
|
# runner. BUG-1913: the suite organically grew past the old 30m
|
|
# budget (734 server-package tests, ~30.3m under -race even on a
|
|
# fast local machine; no single test exceeds 14s — aggregate
|
|
# weight, not a hang), turning most main runs red. 45m restores
|
|
# headroom; genuine deadlocks still hit this and produce the
|
|
# goroutine-dump panic, just up to 15m later. The real fix
|
|
# (cheaper suite: shared fixtures / sharding) is tracked on
|
|
# BUG-1913.
|
|
run: go test -race -timeout=45m ./...
|
|
|
|
- name: Build binary
|
|
run: go build -o pad ./cmd/pad
|
|
|
|
- name: Verify binary runs
|
|
run: ./pad --help
|
|
|
|
native-smoke:
|
|
name: Smoke (${{ matrix.os }})
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 10
|
|
defaults:
|
|
run:
|
|
shell: pwsh
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: macos-latest
|
|
binary: pad
|
|
path: ./pad
|
|
- os: windows-latest
|
|
binary: pad.exe
|
|
path: .\pad.exe
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
# No GOTOOLCHAIN=auto step here, unlike every other Go job in this
|
|
# file, and the reason is worth stating because the asymmetry looks
|
|
# like an oversight: `go-version-file` makes setup-go PARSE go.mod
|
|
# itself, and v7's parser prefers the `toolchain` directive over the
|
|
# `go` directive, so it installs go1.26.6 (BUG-2565) directly. The
|
|
# GOTOOLCHAIN=local it then exports is satisfied by that install —
|
|
# nothing left to fetch. The other jobs pass `go-version: "1.26"`,
|
|
# which never reads go.mod at all, so they genuinely need `auto` to
|
|
# pick the toolchain line up at build time.
|
|
#
|
|
# The one way this silently regresses: setup-go's parser falls back
|
|
# to the `go` directive when GOTOOLCHAIN is ALREADY `local` in the
|
|
# environment at parse time (installer.ts parseGoVersionFile). So if
|
|
# a job- or workflow-level `env: GOTOOLCHAIN: local` is ever added
|
|
# above this step, these smoke binaries drop to the 1.26.5 floor
|
|
# while everything else ships 1.26.6 — no red, just coverage that
|
|
# quietly stops matching the artifact.
|
|
with:
|
|
go-version-file: go.mod
|
|
|
|
- name: Create web build placeholder
|
|
run: |
|
|
New-Item -ItemType Directory -Force web/build | Out-Null
|
|
Set-Content web/build/index.html '<!doctype html>'
|
|
|
|
- name: Build binary
|
|
run: go build -o "${{ matrix.binary }}" ./cmd/pad
|
|
|
|
- name: Verify help output
|
|
run: '& "${{ matrix.path }}" --help'
|
|
|
|
- name: Run CLI smoke test
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
|
$binary = '${{ matrix.path }}'
|
|
$env:PAD_DATA_DIR = Join-Path $env:RUNNER_TEMP 'pad-smoke'
|
|
$env:PAD_BYPASS_SETUP_TOKEN = 'true'
|
|
$stdout = Join-Path $env:RUNNER_TEMP 'pad-server.out.log'
|
|
$stderr = Join-Path $env:RUNNER_TEMP 'pad-server.err.log'
|
|
$server = Start-Process -FilePath $binary -ArgumentList 'server','start','--port','17777' -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
|
|
try {
|
|
$ready = $false
|
|
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
|
try {
|
|
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:17777/api/v1/health | Out-Null
|
|
$ready = $true
|
|
break
|
|
} catch {
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
}
|
|
if (-not $ready) {
|
|
Get-Content $stdout, $stderr -ErrorAction SilentlyContinue
|
|
throw 'Pad server did not become ready'
|
|
}
|
|
|
|
& $binary auth configure --mode local --port 17777
|
|
& $binary auth setup --email smoke@example.com --name Smoke --password 'SmokePass123!'
|
|
& $binary workspace create Smoke --slug smoke --template startup
|
|
|
|
$item = (& $binary --workspace smoke --format json item create task 'Native smoke item') | ConvertFrom-Json
|
|
$shown = (& $binary --workspace smoke --format json item show $item.ref) | ConvertFrom-Json
|
|
if ($shown.title -ne 'Native smoke item') {
|
|
throw "Unexpected item title: $($shown.title)"
|
|
}
|
|
} finally {
|
|
Stop-Process -Id $server.Id -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
go-postgres:
|
|
# Backstop only. A hung TEST is caught by -timeout=45m on the steps
|
|
# below, which fails fast AND prints the goroutine dump that tells
|
|
# you which test hung; this cap exists because a job with no
|
|
# timeout-minutes inherits GitHub's 6-HOUR default, so a runaway
|
|
# that isn't a single test (a wedged service container, a stuck
|
|
# download) would otherwise sit there. Set well above the two 45m test
|
|
# steps so that in practice the per-binary timeout is what fires — not a
|
|
# guarantee, since this cap covers setup and every step, not just those
|
|
# two (TASK-2545).
|
|
timeout-minutes: 100
|
|
name: Go (PostgreSQL)
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
env:
|
|
POSTGRES_USER: pad
|
|
POSTGRES_PASSWORD: pad
|
|
POSTGRES_DB: pad
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd "pg_isready -U pad"
|
|
--health-interval 5s
|
|
--health-timeout 3s
|
|
--health-retries 10
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version: "1.26"
|
|
|
|
- name: Allow Go to fetch the toolchain go.mod pins
|
|
# See the identical step in the `go` job — setup-go pins
|
|
# GOTOOLCHAIN=local, blocking the toolchain fetch go.mod requires
|
|
# (`go 1.26.5` floor, `toolchain go1.26.6` per BUG-2565).
|
|
run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV"
|
|
|
|
- name: Create web build placeholder for embed
|
|
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
|
|
|
|
- name: Run tests against PostgreSQL
|
|
env:
|
|
PAD_TEST_POSTGRES_URL: "postgres://pad:pad@localhost:5432/pad?sslmode=disable"
|
|
# Explicit -timeout: see the SQLite "Run tests" step. This is the
|
|
# binary that actually blew the 10m default — internal/store creates a
|
|
# fresh database AND replays the full migration chain per test
|
|
# (~0.43s each locally), so the package's runtime grows linearly with
|
|
# the test count. Measured on a dev box at 212d59e7: internal/store
|
|
# 280s, whole PG suite 4m43s wall. CI's own runtime is UNMEASURED
|
|
# here; all that follows from the panic is that its store binary
|
|
# exceeded 10m, i.e. >2.14x this box on that package. (An earlier
|
|
# version of this comment said "~2x slower, which put store over
|
|
# 10m" — 280s x 2 is 9m20s, so that explained nothing.)
|
|
run: go test -timeout=45m ./... -count=1
|
|
|
|
- name: Run tests with race detector against PostgreSQL
|
|
env:
|
|
PAD_TEST_POSTGRES_URL: "postgres://pad:pad@localhost:5432/pad?sslmode=disable"
|
|
# Runs on both push-to-main AND pull_request — see SQLite race-step
|
|
# comment for the public-repo / BUG-1371 reasoning.
|
|
#
|
|
# Headroom over the default 10m. PostgreSQL adds latency on
|
|
# every CREATE/DROP, and the PLAN-866 attachment work pushed the
|
|
# cumulative wall over 20m. The bootstrap-user bcrypt cost that
|
|
# blew past 30m on main (BUG-1371) is now handled by TestMain
|
|
# dropping the cost to bcrypt.MinCost for test binaries.
|
|
# BUG-1913: raised 30m → 45m alongside the SQLite step — the
|
|
# suite's aggregate runtime crossed the old budget (this job
|
|
# variant failed main at f235a04 with the same timeout panic).
|
|
run: go test -race -timeout=45m ./... -count=1
|
|
|
|
web:
|
|
name: Web
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: web
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "24"
|
|
cache: "npm"
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Check coordinated Tiptap pins
|
|
# The Y.Doc/ProseMirror schema is shared across @tiptap/core,
|
|
# @tiptap/extension-collaboration, @tiptap/y-tiptap and @tiptap/pm.
|
|
# A stray `npm update` that slides one of them can silently change
|
|
# the persisted Y.Doc shape, producing divergent collab ops the
|
|
# relay can't reconcile (see CLAUDE.md "Tiptap multi-package
|
|
# coordinated bumps" and BUG-2009). This guard fails if any of them
|
|
# loses its exact pin in package.json or resolves to more than one
|
|
# version in the lockfile.
|
|
run: npm run check:tiptap-pins
|
|
|
|
- name: Build
|
|
run: npm run build
|
|
|
|
- name: Type check (svelte-check)
|
|
run: npm run check
|
|
|
|
- name: Run web unit tests (vitest)
|
|
run: npm run test
|
|
|
|
- name: Audit npm dependencies (production, high+)
|
|
# Fail the build on any HIGH or CRITICAL advisory in production deps.
|
|
# Dev-only advisories are treated as informational — they don't ship
|
|
# and fixing them can require waiting on upstream maintainers.
|
|
#
|
|
# LAST, and through scripts/ci-audit.mjs, not bare `npm audit`
|
|
# (BUG-2881). Bare `npm audit` exits non-zero the same way for "an
|
|
# advisory exists" and "the advisory service was unreachable", and this
|
|
# job's shell is `bash -e` — so a registry timeout here used to SKIP
|
|
# Build, Type check and vitest and leave a red row that read like the
|
|
# web tests caught something, when nothing had run at all (2026-09-04,
|
|
# main and #1246, two transports, one bad window). The script decides
|
|
# from the JSON report: real advisories fail the step and are named; an
|
|
# unreachable service retries, then fails under a DISTINCT title saying
|
|
# the gate did not run — closed, not open, because a gate that passes
|
|
# when it cannot run is not a gate. Running last means the frontend's
|
|
# own verdict always exists, whichever way the audit goes.
|
|
run: npm run audit:ci
|
|
|
|
e2e:
|
|
name: E2E (Playwright)
|
|
runs-on: ubuntu-latest
|
|
# Build the binary + UI once and reuse across Playwright projects.
|
|
# The cap is a runaway sanity check, not a perf budget. It covers
|
|
# build-web + build-binary + install-playwright + ~190 tests: a clean
|
|
# pass is ~8-10m, but a single flaky-test retry tips the total past a
|
|
# 10m cap and the job is CANCELLED mid-suite while every test passed —
|
|
# a required gate going red with zero code involvement (BUG-2645
|
|
# mechanism b: marginal timeout). 15m absorbs one retry while still
|
|
# killing a genuine hang. Evidence pair: an 8m10s clean rerun vs a
|
|
# 10m18s cancel at the same SHA.
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version: "1.26"
|
|
|
|
- name: Allow Go to fetch the toolchain go.mod pins
|
|
# See the identical step in the `go` job — setup-go pins
|
|
# GOTOOLCHAIN=local, blocking the toolchain fetch go.mod requires
|
|
# (`go 1.26.5` floor, `toolchain go1.26.6` per BUG-2565; the
|
|
# "Build pad binary" step below fails without this).
|
|
run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "24"
|
|
cache: "npm"
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Install web dependencies
|
|
working-directory: web
|
|
run: npm ci
|
|
|
|
- name: Build web UI
|
|
working-directory: web
|
|
run: npm run build
|
|
|
|
- name: Build pad binary
|
|
# Web build output is embedded via //go:embed; it must exist before
|
|
# the Go build. The CI `go` job above builds against a placeholder,
|
|
# which is fine for tests — for e2e we need the real embedded UI.
|
|
run: go build -o pad ./cmd/pad
|
|
|
|
# Playwright browser binaries are downloaded from cdn.playwright.dev,
|
|
# which occasionally hangs (PR #635 hit two consecutive 10-minute
|
|
# timeouts during a CDN slow patch — see BUG-1625). Cache them per
|
|
# @playwright/test version so warm-cache runs skip the ~150 MB
|
|
# Chromium download entirely; only the apt system libraries
|
|
# (libatk, libnss, libcups, …) need a fresh install, which is
|
|
# ~10s on a healthy runner. Cache key is the resolved version from
|
|
# package-lock.json so a Playwright bump auto-invalidates.
|
|
- name: Get Playwright version
|
|
id: playwright-version
|
|
working-directory: web
|
|
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT
|
|
|
|
- name: Cache Playwright browsers
|
|
id: playwright-cache
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}-chromium
|
|
|
|
- name: Install Playwright browsers (cache miss)
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
working-directory: web
|
|
# --with-deps pulls in the Ubuntu libraries Playwright needs
|
|
# (libatk, libnss, libcups, …). Scoped to chromium to cut download
|
|
# time — the suite's mobile project uses Pixel 7, which defaults to
|
|
# Chromium, so we don't need WebKit.
|
|
run: npx playwright install --with-deps chromium
|
|
|
|
- name: Install Playwright system deps (cache hit)
|
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
working-directory: web
|
|
# When the browser binary cache hits, we still need the apt-level
|
|
# system libraries on the fresh runner — `install-deps` does just
|
|
# that without re-downloading the browser binary itself.
|
|
run: npx playwright install-deps chromium
|
|
|
|
- name: Run Playwright
|
|
working-directory: web
|
|
env:
|
|
CI: "1"
|
|
run: npx playwright test
|
|
|
|
- name: Upload Playwright report on failure
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: playwright-report
|
|
path: web/playwright-report/
|
|
retention-days: 14
|