Files
pad/.github/workflows/ci.yml
T
xarmian c2351d861d fix(ci): lower test-only bcrypt cost + re-enable -race on PRs (BUG-1371) (#513)
The full `internal/server` test suite under `-race` had grown past the
30m CI timeout, failing every push to main since ~TASK-1354. Diagnosis:
bcrypt at the production cost (12) takes ~3s per call under the race
detector, and dozens of tests now bootstrap a user via the loopback
HTTP path (`bootstrapFirstUser` → `store.CreateUser` →
`bcrypt.GenerateFromPassword`). Cumulative cost dominated the budget.

Two coordinated changes:

1. Lower bcrypt cost in test binaries. `bcryptCost` becomes a package
   var (still package-private), and a new `SetBcryptCostForTesting`
   helper lets each test binary's `TestMain` drop it to
   `bcrypt.MinCost`. Production stays at 12 — only the test process
   ever mutates the value.

2. Re-enable `-race` on pull requests. The `if: github.ref ==
   'refs/heads/main'` gate was originally a GitHub Actions minutes
   cost-control; the repo is public now, so PR minutes are free, and
   we'd rather catch race regressions on the contributing branch than
   after merge.

Measured impact:
- `go test -race ./internal/server`: 1800s timeout → 830s (13m51s).
- `go test ./internal/store`: 808s → 35s.
- `go test ./internal/server`: 192s → 60s.

The 30m timeout stays — it's headroom for genuine deadlocks, which
would still hit the goroutine-dump panic the way BUG-851 did.

Prior art: BUG-851 (10m → 30m bump, ipRateLimiter goroutine drain).
This is a different cause (bcrypt cumulative time) so the fix is
different.
2026-05-12 10:01:14 -04:00

224 lines
8.0 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
- 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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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
- name: Install Playwright browsers
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: Run Playwright
working-directory: web
env:
CI: "1"
run: npx playwright test
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-report
path: web/playwright-report/
retention-days: 14