mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
504d348917c2fb8ed2c139bbbc352e07fccae19a
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
756d91acad |
fix(ci): gofmt + bump race-detector timeout to 30m (#299)
CI on main has been failing since the PLAN-866 attachment work
landed. Two independent issues:
1. gofmt failures (golangci-lint) — seven files in the attachments
path had trailing-comment alignment that gofmt wanted nudged a
column. Pure whitespace; ran `gofmt -w` across the affected
files. golangci-lint's gofmt linter caught it on every PR /
push since TASK-870 but we hadn't been watching those signals.
Files cleaned: internal/attachments/{fs_store_test,mime,
mime_test,processor_test}.go, internal/server/{
handlers_attachments_download_test,handlers_attachments_transform,
render/attachments_test}.go.
Local guard: `gofmt -l ./...` now exits clean.
2. Race-detector tests timed out at 20m on the GitHub-hosted runner.
Two contributors:
- PostgreSQL adds latency on every CREATE/DROP plus on the
bcrypt hash inside auth/bootstrap (~3s per call under -race
on the runner). Tests that bootstrap a fresh user (e.g.
TestSessionIPChange_*) pay the full cost each time.
- The PLAN-866 image-processing tests (thumbnail derivation,
rotate / crop transform) added ~2-3 minutes of decode/encode
work on top of the existing suite.
The previous "20m gives margin without papering over a hang"
comment was right at the time it was written; we now genuinely
need more headroom. Bumped to 30m on both the SQLite and
PostgreSQL race steps. Genuine deadlocks would still trip this
and produce the goroutine-dump panic — we just stop confusing
"slow but progressing" with "permanently hung".
Reference points before / after:
- TASK-875 main run #294: Go (PostgreSQL) finished in 17m48s ✓
- TASK-880 main run #298: Go (PostgreSQL) hit 20m timeout ✗
- Local: my new tests under -race add ~63s on a developer laptop
(TestThumbnails + TestTransform + TestProcessor combined).
Verification:
go test ./... — pass
go vet ./... — clean
gofmt -l (recursively) — clean
|
||
|
|
715ec70e94 |
fix(server): drain ipRateLimiter cleanup goroutines on Stop() (BUG-851) (#276)
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.
|
||
|
|
e5e2bd7b86 |
chore: flip CI only-new-issues=false + scope lint policy (TASK-771) (#253)
* chore: gate CI on full lint, scoped to checks we enforce (TASK-771) Flip golangci-lint-action's only-new-issues from true to false so CI fails on ANY linter finding, not just findings on PR-changed lines. This catches lint regressions on the next push instead of letting them drift into main. The gate flip is paired with a deliberate scope-down of .golangci.yml: 1. errcheck is disabled. The codebase has 325 pre-existing unchecked- error sites where the error is intentionally discarded (best-effort logging writes, defensive parses with zero-valued fallbacks, etc.). Auditing every site is its own project — bigger than IDEA-732 by an order of magnitude. Tracked as a follow-up if/when we want the safety net back. 2. staticcheck is restricted to the SA* check family (real-bug detectors). The ST*/QF*/S* families are stylistic/quick-fix suggestions we don't gate CI on yet — they would have re-flooded the lint output with capitalized error strings, De Morgan's law simplification suggestions, etc., that aren't bug-finding signals. Re-enable selectively if the team wants them. After scoping, the live linters are: govet, ineffassign, staticcheck (SA*), unused, gofmt — exactly the set that IDEA-732 cleaned up. Other changes in this PR: - Drop pull-requests:read permission. It was only required by the golangci-lint-action when only-new-issues=true (the action used it to fetch PR diff metadata). Not needed any more. - Update the Run-golangci-lint comment block to explain the new policy and reference the IDEA-732 cleanup PRs (#247/#249/#251/#252). - Replace the SA4017 //lint:ignore directive in cmd/pad/main.go:4631 with an inline //nolint:staticcheck — the multi-line //lint:ignore block was too far from the if statement for staticcheck's proximity rule, so the directive wasn't taking effect. - Apply gofmt -w on three files where post-deletion blank-line artifacts had drifted (cmd/pad/main.go imports, two trailing newline fix-ups in handlers_items.go and middleware_ratelimit.go). Verified: - `golangci-lint run ./...` reports 0 issues. - `go build ./...` clean. - `go vet ./...` clean. - `go test ./...` all pass. Parent: PLAN-644. * chore: address Codex round 1 on PR #253 (TASK-771) Two LOWs from Codex on the gate-flip PR: 1. //nolint:staticcheck was broader than necessary (suppressed any future staticcheck diagnostic on the line) and didn't self-report when the underlying false positive gets fixed upstream. Codex suggested swapping back to a tightly-placed //lint:ignore SA4017. I tried that, but golangci-lint v2's staticcheck integration does not honour //lint:ignore the way direct staticcheck does — the directive was silently no-op'd via golangci-lint while the same directive worked when staticcheck was invoked directly. So instead of fighting the linter wrapper, sidestep the false positive entirely: rewrite the keepalive check from `strings.HasPrefix(line, ":")` to `len(line) > 0 && line[0] == ':'`. Same observable behaviour for a single-byte ASCII prefix, no suppression directive needed at all, no exposure when staticcheck eventually fixes the false positive. 2. The new lint-step comment in ci.yml said main is "clean of staticcheck SA*/U1000" — but U1000 is reported by the standalone `unused` linter in .golangci.yml, not by staticcheck.checks. Tighten the comment to attribute each enforced check correctly. Verified: - `golangci-lint run ./...` reports 0 issues - `go test ./cmd/pad/...` passes (the SSE watch loop is exercised by reconcile_test.go and the broader integration tests). |
||
|
|
157ca4e88f |
chore: bump Go toolchain to 1.26 (TASK-763) (#247)
* chore: bump Go toolchain to 1.26 (TASK-763) Bump Go from 1.25 to 1.26 across all toolchain pins: - go.mod — go 1.25.0 → go 1.26.0 - Dockerfile — golang:1.25-alpine → golang:1.26-alpine - .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs) - .github/workflows/release.yml — release pipeline No `toolchain` directive: the repo is pre-launch with no external contributors yet, so we set the floor where we want it (hard requirement). Verified locally before commit: - golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI) - golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub - go build ./... clean - go vet ./... clean - go test ./... all pass Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish). * chore: gofmt -w under Go 1.26 (TASK-763) Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all struct-tag whitespace realignment — no semantic changes. Verified: - gofmt -l ./cmd ./internal returns empty after - go build ./... still clean - go test ./... still passes (run before commit) Bundling the gofmt diff with the toolchain bump in the same PR because the formatting drift is a direct consequence of moving from 1.25 to 1.26; splitting them creates a mandatory two-PR ordering for no value. Parent: PLAN-644. * docs: bump documented Go floor to 1.26 (TASK-763) Match go.mod's hard 1.26.0 requirement in the source-build instructions. Caught by Codex review round 1 on PR #247. - README.md:158 — "Go 1.25+" → "Go 1.26+" - CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+" |
||
|
|
c8601a2031 |
test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689) (#225)
* test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689) Option A of TASK-689: land the test infrastructure and a minimal smoke test on both mobile and desktop viewports. Broader flow coverage (board view drag, item detail, comments, mobile hamburger, BottomSheet regression guard) is tracked as TASK-733. Infrastructure -------------- - web/playwright.config.ts: two projects (desktop-chromium, mobile- chromium via Pixel 7), reporter list+html, trace/video/screenshot retained on failure, webServer that wipes + recreates the data dir then runs the pad binary. Paths anchored to the config file's directory so runs are cwd-invariant. - web/e2e/global-setup.ts: bootstraps admin via POST /auth/bootstrap, logs in, creates the e2e workspace, mints a user-scoped API token, and persists the token + resolved admin username to fixture.json. - web/e2e/fixtures.ts: extends base test so every BrowserContext automatically gets Authorization: Bearer <token>. Uses a token rather than a session cookie because sessions are User-Agent bound in middleware_auth.go and a node-minted session would be rejected by a Chromium UA. Tests ----- - web/e2e/dashboard.spec.ts: a logged-in user lands on the seeded workspace, no login form is rendered, and the workspace name appears on the page. Runs in both project viewports. CI -- - New `e2e` job in .github/workflows/ci.yml: builds web UI + binary, installs Playwright chromium with OS deps, runs the suite, and uploads the HTML report as an artifact on failure. Timeout capped at 10 minutes (suite itself runs in ~4s today). Local run (in mcr.microsoft.com/playwright:v1.59.1-noble): 2 passed in 4.1s. Parent: PLAN-644. Follow-up: TASK-733 for broader flow coverage (Option B in the original ship plan). * fix(e2e): persist server-returned workspace slug instead of the constant (TASK-689) Addresses Codex P2 on PR #225. When Playwright's `reuseExistingServer: true` (local dev), a re-run of globalSetup hits `POST /api/v1/workspaces` against a DB that already has `e2e`. The server uniquifies the slug (`e2e` → `e2e-2` → …) and returns the uniquified value, but the old code wrote `WORKSPACE_SLUG` (the constant) to fixture.json. Tests then navigated to /e2e-admin/e2e — which might still exist from a previous run with stale state — instead of /e2e-admin/e2e-2, missing regressions in freshly-seeded content. Fix: read `slug` back from the workspace-create response and use that when writing fixture.json. Local re-runs now always point at the workspace this run actually created. Parent: PLAN-644. * fix(e2e): cross-platform webServer bootstrap via Node wrapper (TASK-689) Addresses Codex P2 on PR #225: `rm -rf && mkdir -p && pad server start` in webServer.command is POSIX-only. Windows contributors on cmd.exe or PowerShell can't run `npm run test:e2e` at all — the e2e suite becomes Linux/macOS-only, defeating the "CI parity" goal. Fix: extract the wipe-and-exec logic into web/e2e/run-pad.mjs. Node's fs.rmSync / mkdirSync / child_process.spawn are uniform across platforms, and the wrapper forwards SIGTERM/SIGINT so Playwright's teardown still cleanly kills the child on suite exit. Local re-run in mcr.microsoft.com/playwright:v1.59.1-noble: 2 passed. Parent: PLAN-644. |
||
|
|
04db0c1a67 |
chore: add .editorconfig, golangci-lint, pre-commit (TASK-682) (#220)
* chore: add .editorconfig, golangci-lint, pre-commit (TASK-682) Prevents style churn from first-time external contributors by codifying the project's formatting and lint rules into shared config. Changes: - .editorconfig: tabs for code, 2-space for YAML/JSON/Markdown, LF everywhere, UTF-8; Makefile overrides enforce tab (syntactic). - .golangci.yml: enables gofmt, govet, errcheck, ineffassign, staticcheck, unused. Scoped to Go sources; excludes web/, docs/, deploy/, skills/. - .pre-commit-config.yaml: repo-local hygiene (trailing whitespace, EOF, YAML/JSON checks, merge-conflict markers, 500KB file cap, LF line endings), Go formatting (go-fmt, go-imports), and prettier for YAML/JSON/Markdown only (Svelte intentionally excluded — no Svelte prettier config yet). - CI: wires golangci-lint into .github/workflows/ci.yml via the official pinned action (v6.5.2 → SHA 55c2c144...). Uses only-new-issues: true so this PR is not blocked by the 17 pre-existing findings on main, which are tracked as IDEA-732 and will flip to strict enforcement after they're resolved. Verified: - golangci-lint run with this config locally; config parses and --new-from-rev=HEAD is clean - All YAML parses via yq - go build/vet/test + web build all green Parent: PLAN-644. Follow-up: IDEA-732 (fix legacy lint findings, flip to strict mode). * fix(ci): upgrade golangci-lint to v2 for Go 1.25 support (TASK-682) Per Codex review on PR #220: golangci-lint v1.x (including v1.64.8 which I originally pinned) is capped at Go 1.24 support. Running v1 binaries against Go 1.25 source can silently drop/misreport findings, defeating the purpose of a lint gate. Switch to: - Action: golangci/golangci-lint-action@v9.2.0 (pinned SHA 1e7e51e7...) - Lint version: v2.11.4 (latest stable v2) - Config rewritten to v2 YAML format (version: "2", linters.default, formatters section for gofmt, exclusions.paths) Verified: - `golangci-lint config verify` clean - `golangci-lint run --new-from-rev=HEAD` reports 0 issues on the current diff (still well under the safety cap since we're using only-new-issues: true) - Full run against main surfaces the expected legacy findings, which remain tracked in IDEA-732 Parent: PLAN-644. * fix(ci): anchor golangci-lint exclusion paths + add PR read perm (TASK-682) Addresses two follow-up comments from Codex on PR #220: P1 — golangci-lint v2 uses regex path matching for exclusions, so the bare patterns "web" and "skills" would also match unrelated Go files whose path contains those substrings (e.g. "internal/websocket"). Anchor with a leading "^" and trailing "/" so only the intended directory trees are skipped. P2 — The workflow already grants "contents: read" at top level, but golangci-lint-action with only-new-issues: true also fetches PR diff metadata from the GitHub API; the action docs list "pull-requests: read" as required for that path. Add it explicitly so the action doesn't fall back to scanning all code and defeating the purpose of scoped issue reporting. Parent: PLAN-644. |
||
|
|
1052be7282 |
security(ci): pin all GitHub Actions to commit SHAs (TASK-677) (#201)
Every third-party Action in .github/workflows/ was using a floating tag (@v4, @v5, @v6). A compromised maintainer — or a tag that gets re-pointed at a malicious commit — could execute attacker code in CI with contents:write, packages:write, and the GHCR token in scope. Release.yml is especially exposed: a compromised step there could publish tampered binaries to GitHub Releases and GHCR. All 12 'uses:' references now pin to a 40-char commit SHA with a trailing '# vX.Y.Z' comment (the comment is what humans read during review; the SHA is what GitHub actually resolves): actions/checkout@34e114876b # v4.3.1 actions/setup-go@40f1582b24 # v5.6.0 actions/setup-node@49933ea528 # v4.4.0 docker/setup-buildx-action@8d2750c68a # v3.12.0 docker/login-action@c94ce9fb46 # v3.7.0 goreleaser/goreleaser-action@e435ccd777 # v6.4.0 Version bumps: pin to the newest release within the same major that was previously in use (so behavior stays the same — no major version jumps hidden inside a security PR). Dependabot (incoming in TASK-678) will track the commit-pinned refs and open PRs that update both the SHA and the version comment together. Parent: PLAN-644. |
||
|
|
4f298db4d0 |
docs(readme): add 'Hardening for public deployments' section + govulncheck CI gate (PLAN-643 exit) (#195)
* docs(readme): add 'Hardening for public deployments' + govulncheck CI gate (PLAN-643 exit criteria) Closes the last two exit criteria of PLAN-643 (OSS Security Hardening): - README.md gains a full "Hardening for public deployments" section walking operators through the network boundary (bind addr, TLS, trusted proxies), secrets (PAD_ENCRYPTION_KEY, token scopes, bootstrap window), auth hardening (PAD_IP_CHANGE_ENFORCE, password strength UI messaging, PAD_CORS_ORIGINS), observability (PAD_METRICS_ TOKEN, audit-log shipping), and a deploy-day checklist. Cross- references every relevant env var documented elsewhere. - CI workflow gains a govulncheck step on the Go job, mirroring the existing `npm audit --audit-level=high --omit=dev` gate on the web job. Locally `govulncheck ./...` reports "No vulnerabilities found", so the first run on main should pass. Exit criteria for PLAN-643: [x] All CRITICAL + HIGH + MEDIUM findings closed and verified [x] `npm audit --audit-level=high --production` clean in web/ [x] CSP denies inline event handlers (script-src-attr 'none') [x] Docker default compose publishes to 127.0.0.1 only [x] README has a "Hardening for public deployments" section [x] `govulncheck ./...` clean * fix(ci): pin govulncheck to v1.2.0 instead of @latest (PLAN-643) Addresses Codex P2 on PR #195: tracking @latest on every CI run makes the gate non-deterministic — a future upstream release could change behavior or require a newer Go toolchain than the workflow's pinned `go-version: 1.25` and break unrelated PRs. Pin to the currently- released v1.2.0 (Go 1.26.2 toolchain) and update intentionally. * docs(readme): recommend pinned govulncheck install in hardening section Follows Codex P2 on PR #195: the earlier commit pinned the CI workflow to v1.2.0 but the README's hardening-checklist bullet still told operators to install @latest. Teams copying that into their own CI would re-introduce the non-determinism the pin was meant to fix. Update the docs to recommend a pinned tag (matching the workflow's v1.2.0) and note that the pin should be bumped intentionally. |
||
|
|
3544e42de1 |
chore(web): npm audit fix + CI audit gate (TASK-654) (#181)
web/package-lock.json had 11 advisories (1 low, 1 moderate, 9 high) before this PR: @sveltejs/kit (redirect/body-size), cookie<0.7.0, dompurify<=3.3.3, vite 7.0.0-7.3.1, lodash-es, picomatch, chevrotain, etc. All required a mix of `npm audit fix` and targeted upgrades. Changes: - web/package.json: upgrade @sveltejs/kit to ^2.57.1. Add `overrides` map pinning cookie to ^0.7.2 (upstream @sveltejs/kit@2.57.1 still ships cookie@0.6.0 which is LOW severity but trivially fixable). - web/package-lock.json: regenerated via `npm install` + `npm audit fix`. - .github/workflows/ci.yml: add `npm audit --audit-level=high --omit=dev` step after `npm ci`. Fails the build on any HIGH+ advisory in production deps; dev-only issues stay informational so CI isn't held hostage by unfixable upstream chevrotain/vite dev-server advisories. `npm audit --audit-level=high --omit=dev` now reports 0 vulnerabilities locally. `npm run build` and `go test ./...` remain green. Parent: PLAN-643 (OSS Security Hardening). |
||
|
|
b2b4feecb9 |
feat: console navigation, PostgreSQL CI, and operational improvements
- Route root (/) to /console for centralized workspace management - Update TopBar user dropdown with console nav links (workspaces, settings, billing, admin) - Move account settings (profile, password, tokens) from workspace settings to /console/settings - Enhance admin page with email configuration UI and CSRF-protected writes - Add PostgreSQL CI job to GitHub Actions with race detector on main - Add `make test-pg` for local PostgreSQL testing via docker-compose - Expand health/ready endpoint with DB connection pool stats - Increase item number retry limit for high-concurrency environments - Add concurrent store benchmarks and FTS search quality tests - Add AGENTS.md for multi-agent development guidance |
||
|
|
239ce19d1f |
ci: reduce CI usage by ~60-70% with optimized workflow
- Add concurrency groups to cancel superseded in-progress runs - Move race detector to main-only (saves ~9.5 min per PR run) - Merge go-vet into go job (eliminates separate VM) - Remove redundant full-build job (release.yml handles real builds) - Add binary build+verify as steps in go job for smoke testing |
||
|
|
14fe7fd631 |
Fix CI: create web build placeholder for Go embed in test and vet jobs
The go:embed directive requires web/build/* to exist. Go test and vet jobs run without building the web UI, so create a minimal placeholder directory to satisfy the embed pattern. |
||
|
|
7ba69abb88 |
Misc improvements: CLI field summaries, editor enhancements, CI and UI polish
Show field summary after create/update CLI commands. Make svelte-check blocking in CI. Improve editor block handling, field editor layout, conventions page, and minor UI consistency fixes across pages. |
||
|
|
81579847c6 |
Initial release
Pad — project management for developers and AI agents. Single Go binary with embedded SvelteKit web UI, SQLite storage, CLI, and Claude Code /pad skill integration. https://getpad.dev |