Files
pad/.github/workflows/ci.yml
T
xarmian a88f7755c9 chore(ci): bump Node 20 actions to Node 24 ahead of June 2026 deadline (TASK-1165) (#635)
* chore(ci): bump Node 20 actions to Node 24 ahead of June 2026 deadline (TASK-1165)

GitHub deprecated Node 20 in Actions runners; the hard cutoff is
June 2nd, 2026. Pre-emptively bumps the three remaining Node 20
holdouts to their latest Node 24 versions, SHA-pinned per the
existing convention:

- actions/setup-node v4.4.0 → v6.4.0 (using: node24)
- actions/upload-artifact v4.6.2 → v7.0.1 (using: node24)
- anchore/sbom-action/download-syft v0.18.0 → v0.24.0 (using: node24)

Breaking-change review (all clear for our usage):

- setup-node v5/v6: only behavioral change is "limit automatic caching
  to npm" — we already pass cache: "npm" explicitly. node-version: "24"
  + cache-dependency-path: web/package-lock.json continue to work.
- upload-artifact v5/v6: v5 treats the Node 24 bump as breaking; v6
  requires Actions Runner ≥ 2.327.1 (GitHub-hosted runners are
  auto-updated, so no concern). Our single-fixed-name failure-only
  upload is unaffected.
- upload-artifact v7: adds optional archive: false single-file unzipped
  uploads + ESM internals. Our usage (name/path/retention-days) is
  unchanged.
- sbom-action 0.18→0.24: minor 0.x bumps; v0.24 release notes
  explicitly cite "update to node 24 + deps".

Post-audit: every uses: spec in .github/workflows/ now reports
node24 or composite. No Node 20 actions remain.

Per TASK-1165 verification: this PR touches .github/workflows/release.yml,
so the playbook's RC decision rule (PLAYB-1160 step 1) triggers — the
next release will warrant a vX.Y.Z-rc.1 to confirm the deprecation
annotation is gone before shipping stable.

* chore(ci): cap golangci-lint cache to 1 day to avoid poisoning recurrences (BUG-1624)

PR #635's first CI run failed on 30+ SA5011/SA4023 false positives
against unchanged code; local cold-cache lint reported 0 issues.
Diagnosis: golangci-lint-action's cache stores the prior pass's
resolved issue list, and once a pass writes degenerate results
(analyzer upgrade, plugin reset, sub-package drift), every downstream
restore replays that list verbatim until the cache key rotates.

The cache key hashes go.mod/go.sum/.golangci.yml plus an action-internal
prefix, so in steady state the key is stable for days and the
poisoned content propagates across PRs. Default invalidation is 7 days.

This change cuts it to 1 day. Most runs still hit warm cache (lint
runs back-to-back within hours of each other are common); we
guarantee a daily fresh full pass that overwrites any bad cached
state. Estimated cost: ~30-60s extra on one CI run per day.

Hand-mitigated the immediate occurrence by deleting the two poisoned
cache entries via the GH cache API; rerun then went green. BUG-1624
captures the full diagnosis + alternatives considered.

* chore(ci): cache Playwright browsers to dodge CDN slow-paths (BUG-1625)

PR #635 hit two consecutive 10-minute timeouts on the E2E job, both
dying inside `npx playwright install --with-deps chromium` while
downloading Chrome from cdn.playwright.dev. The apt portion completed
in ~10s; the CDN download hung for ~7 minutes before the
`timeout-minutes: 10` ceiling killed the job.

Same code earlier in the day ran E2E green in 1m07s — it's a CDN
slowness event, not a behavioral regression. But two-runs-in-a-row
timeouts mean the steady state is fragile.

Fix: cache `~/.cache/ms-playwright` per resolved @playwright/test
version. Splits the install step in two:

- Cache miss: `npx playwright install --with-deps chromium` — full
  apt + browser download (current behavior).
- Cache hit:  `npx playwright install-deps chromium` — apt system
  libraries only (~10s); browser binary is already on disk.

Cache key reads the resolved version from package-lock.json so a
Playwright bump auto-invalidates. Pinned to actions/cache v5.0.5
(node24) per the workflow's SHA-pinning convention.

After the first warm run on each Playwright version, the CDN is
out of the critical path; an outage there can only burn one CI run
before steady state recovers. BUG-1625 has the full diagnosis.
2026-05-27 17:03:34 -04:00

263 lines
10 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:
name: Go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26"
- 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@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.4
args: --timeout=5m
only-new-issues: 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.
#
# 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 env GOPATH)/bin/govulncheck" ./...
- name: Run tests
run: go test ./...
- 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 so this step stays well under the 30m budget).
#
# 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 — kept at 30m to give headroom without papering over
# an actual hang. Genuine deadlocks would still hit this and
# produce the goroutine-dump panic.
run: go test -race -timeout=30m ./...
- name: Build binary
run: go build -o pad ./cmd/pad
- name: Verify binary runs
run: ./pad --help
go-postgres:
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26"
- 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"
run: go test ./... -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.
#
# 30m 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.
run: go test -race -timeout=30m ./... -count=1
web:
name: Web
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
- 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.
run: npm audit --audit-level=high --omit=dev
- name: Build
run: npm run build
- name: Type check (svelte-check)
run: npm run check
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
# Build the binary + UI once and reuse across Playwright projects.
# The suite is small (<10s at the time of writing — see TASK-733 for
# follow-up coverage); the `timeout-minutes` cap is a sanity check.
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26"
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
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