#896 raised the go.mod floor to `go 1.26.5`, but actions/setup-go's
`go-version: "1.26"` resolves to the newest patch in its manifest
(1.26.4) and unconditionally exports GOTOOLCHAIN=local, so every Go
command fails with "go.mod requires go >= 1.26.5 (running go 1.26.4)".
This broke `go vet`, the PostgreSQL test job, and the e2e "Build pad
binary" step on main and every PR since #896.
Override GOTOOLCHAIN=auto via a step that writes $GITHUB_ENV *after*
setup-go (last-write wins, since setup-go's export is unconditional),
so Go downloads the required toolchain on demand. Applied to all three
Go jobs in ci.yml plus release.yml.
Claude-Session: https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF
BUG-2084. Two parts.
RAM fix: `make vuln` and CI's Go job now run govulncheck in BINARY mode
(`-mode binary` against a freshly-built pad binary) instead of source mode
(`govulncheck ./...`). Source mode builds an SSA call-graph over the whole
dependency tree (BigQuery/OTel/gRPC/Cloud) and balloons to multiple GB of
RAM, which was locking up a memory-constrained host. Binary mode reads the
binary's symbol table — ~99 MB peak here — while staying call-graph-precise
and still detecting stdlib vulns from the Go version stamped in the binary.
The scan binary is written to the repo root (real disk, gitignored), never
/tmp, since some hosts mount /tmp as a small RAM-backed tmpfs where a large
embedded binary can hit "no space left" and consume the RAM we're sparing.
Vuln fix (govulncheck binary mode: 0 vulnerabilities after):
- go 1.26.4 -> 1.26.5: clears the only CALLED vuln GO-2026-5856 (crypto/tls)
plus not-called os GO-2026-4970.
- golang.org/x/crypto v0.51.0 -> v0.52.0: clears 13 not-called advisories.
- google.golang.org/grpc v1.59.0 -> v1.79.3: clears GO-2026-4762 (gRPC
authorization bypass). pad runs no gRPC server, but grpc.Server.Serve ships
transitively (OTel/ory/grpc-gateway) so binary mode flags the symbol.
Contained 12-line go.mod bump (genproto/protobuf/oauth2 family), no cascade.
Remaining not-called advisories deferred to a follow-up dependency sweep:
GO-2026-4985 (otel otlptracehttp) and GO-2026-5932 (x/crypto, Fixed in: N/A).
CLAUDE.md mandates exact-pinning the coordinated Tiptap packages so npm
cannot silently slide the shared Y.Doc/ProseMirror schema out of sync.
@tiptap/core (^3.22.5), @tiptap/y-tiptap (^3.0.3) and @tiptap/pm (^3.20.4)
were still floating on caret ranges; only @tiptap/extension-collaboration
was exact.
- Exact-pin @tiptap/core, @tiptap/y-tiptap and @tiptap/pm to their
currently-resolved versions (3.22.5 / 3.0.3 / 3.22.5); refresh lockfile.
- Add web/scripts/check-tiptap-pins.mjs + check:tiptap-pins npm script:
fails if any coordinated package loses its exact pin in package.json or
resolves to more than one version in the lockfile.
- Wire the guard into the CI web job.
The 128-test vitest suite (7 files, incl. the WebMCP dispatch/descriptor
tests backing PLAN-1888) ran nowhere in CI. Add a "Run web unit tests"
step to the Web job after the build/check steps, a `web-test` Makefile
target wired into the `check` chain, and a CLAUDE.md Testing note.
Fixes TASK-1999.
Claude-Session: https://claude.ai/code/session_01BoPkYhKqMiWPYmxQigeWsA
The internal/server suite's aggregate runtime under -race crossed the
30m budget (~30.3m on a fast local machine, 734 tests, none slower than
14s — growth, not a hang), making the Go jobs fail most main runs with
'panic: test timed out after 30m0s' on whichever test happened to be
running. 45m restores headroom; genuine deadlocks still produce the
goroutine-dump panic, up to 15m later. Real fix (cheaper suite) stays
tracked on BUG-1913.
Claude-Session: https://claude.ai/code/session_01CL1pBjNpPUX6SWkuAuYXHS
* 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.
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.
Bumps all four Node version pins from "22" to "24" together so CI ↔
production stay aligned:
• Dockerfile (production image): node:22-alpine → node:24-alpine
• .github/workflows/ci.yml — Web job + E2E job
• .github/workflows/release.yml — release pipeline
Going to LTS-bound 24 instead of dependabot's proposed 25-alpine,
which hits EOL on 2026-06-01 (~3 weeks from this commit). Going to
24 instead of waiting for 26-LTS (Oct 2026) because 5 months is too
long to sit on the deferred-bumps backlog and 24 is already a year
into LTS-tested production use. Worst case follow-up is one more
trivial Dockerfile bump in October.
Verified:
• Local `docker build --target web-builder` on node:24-alpine
(npm ci + npm run build) — clean, 24s end-to-end
• `make check` — golangci-lint + go test + npm run build +
svelte-check, 0 errors
Closes dependabot/docker/node-25-alpine (PR #209) — closing rather
than rebasing because we're going to 24, not 25.
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
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.
* 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).
* 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+"
* 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.
* 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.
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.
* 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.
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).
- 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
- 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
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.
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.
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