mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
715ec70e94
NewRateLimiters spawned 9 ipRateLimiter cleanup goroutines per Server,
each in an unbounded `for { time.Sleep(5*time.Minute); ... }` loop with
no exit signal (middleware_ratelimit.go:78-89). Every testServer(t)
call leaked all 9, accumulating across the 210-test internal/server
suite. Under -race the goroutine count + sync overhead pushed the run
past the default 10m timeout, which is why the `Run tests with race
detector` step (gated to main pushes) has been failing on every main
run since the step was added on 2026-04-13.
This is the same flavor as BUG-842 part 2 (request-handler
fire-and-forget goroutines drained via Server.bg WaitGroup). The
rate-limiter case wasn't in BUG-842's scope: those goroutines are
spawned at construction time, not at request time, so they need a
different drain primitive.
Changes:
- ipRateLimiter gains stopCh + stopOnce + stopWg. cleanup() rewrites
its loop as a select over stopCh and a 5-minute ticker, deferring
stopWg.Done(). New Stop() closes stopCh once and waits for the
cleanup goroutine to return.
- RateLimiters gains a Stop() that walks all 9 limiters (nil-safe
via the (*ipRateLimiter).Stop receiver guard).
- Server.Stop() now also calls s.rateLimiters.Stop() after
s.bg.Wait(). Test cleanups already call Server.Stop() (added in
BUG-842), so no test-helper changes needed.
- New TestServer_Stop_DrainsRateLimiterCleanup pins the contract:
construct + Stop N servers, assert runtime.NumGoroutine() returns
to baseline ±3.
- .github/workflows/ci.yml: bump the -race timeout from the default
10m to 20m. The full server suite under -race takes ~13m on a dev
laptop after the leak fix; 20m gives margin without papering over
an actual hang. Both `Run tests with race detector` (SQLite) and
`Run tests with race detector against PostgreSQL` are bumped.
Verified locally: go test -race -timeout=1500s ./internal/server/
finishes ok in 776s (12m57s). Without the leak fix, the same command
times out at 600s (10m) with a goroutine dump showing hundreds of
ipRateLimiter.cleanup frames.
209 lines
7.1 KiB
YAML
209 lines
7.1 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.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
|
|
if: github.ref == 'refs/heads/main'
|
|
# 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) — 20m gives margin for the
|
|
# GitHub-hosted runner without papering over an actual hang.
|
|
run: go test -race -timeout=20m ./...
|
|
|
|
- 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.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
|
|
if: github.ref == 'refs/heads/main'
|
|
env:
|
|
PAD_TEST_POSTGRES_URL: "postgres://pad:pad@localhost:5432/pad?sslmode=disable"
|
|
# See SQLite race-detector step: 20m headroom over the default 10m
|
|
# because the full suite under -race exceeds 10m post-BUG-851 fix.
|
|
run: go test -race -timeout=20m ./... -count=1
|
|
|
|
web:
|
|
name: Web
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: web
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
|
with:
|
|
go-version: "1.26"
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
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
|