Commit Graph

58 Commits

Author SHA1 Message Date
xarmian a1716d8170 ci(web): decide the npm audit gate from the report, not the exit code, and run it last (BUG-2881) (#1247)
* ci(web): decide the npm audit gate from the report, not the exit code, and run it last (BUG-2881)

`npm audit` exits non-zero identically for "a HIGH/CRITICAL advisory
exists" and "the advisory service was unreachable". The Web job ran it
before Build / Type check / vitest under `bash -e`, so a registry
timeout (main, 03:50Z) and a 503 (#1246, 04:33Z) on 2026-09-04 each
produced a red row with every frontend verification step SKIPPED — a
lane that read like a failure and had asked nothing.

scripts/ci-audit.mjs runs the audit in --json mode and decides from the
report: metadata.vulnerabilities present → fail iff high+critical > 0,
naming the advisories; an error envelope or unparseable output → a
GitHub warning annotation saying the gate did not run, exit 0. The step
moves to the end of the job so the frontend's own verdict always exists
whatever the audit does.

Verified locally against five report shapes (transport timeout envelope,
E503 envelope, one high advisory, clean, garbage) and two live runs (the
real registry: clean; a dead registry: warning, exit 0). `--input <file>`
is the seam those checks use.

Fixes BUG-2881

* ci(web): the audit gate fails closed — retry an unreachable advisory service, then fail under its own title

Codex round 1 on #1247: the first draft warned and exited 0 when the
advisory service could not be asked, which made the only supply-chain
gate pass exactly when it had not run. A gate that passes when it cannot
run is not a gate.

Now: up to three attempts with backoff (registry blips are usually
seconds long), then `::error title=npm audit did not run` and exit 1.
The title is distinct from `::error title=npm audit` (a real advisory)
so the checks tab tells the two apart without opening the log; re-running
is the remedy for the first and never for the second. Because the step
runs last, Build / Type check / vitest have already produced their result
either way — the original blindness is gone regardless of which way this
step fails.

Verified against the same five saved shapes (transport and E503 envelopes
and garbage now exit 1 under the did-not-run title; a high advisory exits
1 under the advisory title; clean exits 0) and two live runs (real
registry: clean; dead registry: three attempts logged, exit 1).

Refs BUG-2881

* ci(web): the audit gate refuses counts it cannot read, and refuses bad tuning without crashing

Codex round 2 on #1247. (1) metadata.vulnerabilities was checked for
presence, not for shape: Number("x") + Number(null) > 0 is false, so a
malformed count read as a clean audit — a second fail-open, one layer
deeper than round 1's. high/critical must now be non-negative integers
or the report is unreadable, which is the fail-closed path. (2) The two
env knobs are operator-set, but CI_AUDIT_ATTEMPTS=NaN left the retry loop
unexecuted and threw a TypeError, and CI_AUDIT_BACKOFF_MS=Infinity parked
Atomics.wait forever; both now fall back to the default with a line
saying so.

Refs BUG-2881

* build: the local preflight runs the same audit gate CI does, and runs it last

Codex round 3 on #1247 (blast radius): `make web-check` still chained
bare `npm audit && npm run check`, so a registry blip stopped svelte-check
locally exactly as it had in CI, and CONTRIBUTING documented the bare
command as the way to reproduce the gate. New `web-audit` target runs
`npm run audit:ci`; `check` runs it after web-check and web-test, mirroring
the Web job's order. CONTRIBUTING and docs/architecture.md say so.

Refs BUG-2881

* build: web-audit stands alone — no `web` prerequisite, so `check` runs npm ci once and no new target reaches it

Codex round 4 on #1247: `web-audit: web` made `check` run `npm ci` twice
(`web` is .PHONY) and added a target CLAUDE.md's worktree rule did not
list as reaching `npm ci`. `npm audit` reads the lockfile and needs
neither node_modules nor a build — verified by running it with
node_modules removed — so the prerequisite goes; CLAUDE.md's safe list
gains `web-audit`.

Refs BUG-2881
2026-09-04 10:45:21 -04:00
dependabot[bot] 99ec711813 chore(ci)(deps): bump anchore/sbom-action/download-syft (#1222)
Bumps the actions-minor-and-patch group with 1 update: [anchore/sbom-action/download-syft](https://github.com/anchore/sbom-action).


Updates `anchore/sbom-action/download-syft` from 0.24.0 to 0.24.2
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/e22c389904149dbc22b58101806040fa8d37a610...3ad7283483fc7af8ff2b4ea19663c2d5ca935e26)

---
updated-dependencies:
- dependency-name: anchore/sbom-action/download-syft
  dependency-version: 0.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 17:15:42 -04:00
dependabot[bot] 55fce493f1 chore(ci)(deps): bump docker/setup-buildx-action (#1189)
Bumps the actions-minor-and-patch group with 1 update: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action).


Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 11:09:26 -04:00
xarmian b62c6692c8 ci: stabilize two required gates that go red with no code involvement (BUG-2645 opt 1, BUG-2568 opt 1) (#1158)
Two self-inflicting required gates, one reviewed workflow unit (CONVE-2438: its own PR, never a rider; minimal diff, action pins untouched).

- E2E (Playwright) timeout-minutes 10 -> 15 (BUG-2645 mechanism b). The 10m cap covers build-web + build-binary + install-playwright + ~190 tests; a clean pass is ~8-10m, so a single flaky-test retry tips the total past the cap and the job is CANCELLED mid-suite while every test passed. 15m absorbs one retry and still kills a genuine hang. Right-sizing evidence: an 8m10s clean rerun vs a 10m18s cancel at the SAME SHA (PLAN-2636 unit 2, #1157).

- golangci-lint-action verify: false (BUG-2568). The action's pre-lint `config verify` fetches its JSON schema over the network from golangci-lint.run and fails the required Go gate when that host is slow/unreachable — no lint finding, no code involvement (once skipped govulncheck entirely). A malformed .golangci.yml is still caught by the lint run itself, with a worse message.

Scope: this closes BUG-2645 mechanism (b) only. The other two 2645 mechanisms remain, documented as known and out of scope for this unit: (a) the CI/Nix twin-workflow concurrency race that cancels an already-passed run, and (c) a job-rerun expiring inside a parent run already terminal-cancelled. Both are concurrency/rerun-config issues, not a timeout bump.

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
2026-08-18 18:06:32 -04:00
dependabot[bot] a8989a0ad3 chore(ci)(deps): bump actions/attest-build-provenance (#1071)
Bumps the actions-minor-and-patch group with 1 update in the / directory: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 4.1.1 to 4.2.2
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/0f67c3f4856b2e3261c31976d6725780e5e4c373...4d101475d8b20a2381f78447822ac1eab6504dd8)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 11:35:24 -04:00
dependabot[bot] f1bd144b02 chore(ci)(deps): bump actions/checkout from 6.0.2 to 7.0.1 (#1073)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.2...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 11:25:36 -04:00
xarmian cfad8d989e ci(nix): gate the Nix-built binary with govulncheck (BUG-2567) (#1095)
* ci(nix): gate the Nix-built binary with govulncheck (BUG-2567)

The main CI govulncheck job scans a go-built binary, which honours
go.mod's toolchain line — so the Nix artifact (GOTOOLCHAIN=local in
nixpkgs, go 1.26.5 until nixos-26.05 backports 1.26.6) shipped with no
vulnerability gate over it at all.

Add nix/vulnscan.sh: binary-mode govulncheck against result/bin/pad,
compared to nix/accepted-advisories.txt. Known advisories stay green
and recorded in-repo; any NEW advisory fails the Nix job; a cleared
advisory emits a warning annotation so the list gets pruned and
BUG-2567 closed when the backport lands.

The accepted list carries 11 entries, measured against a
build-faithful proxy (GOTOOLCHAIN=go1.26.5, CGO_ENABLED=0,
ldflags "-s -w"): the 8 reachable stdlib advisories from BUG-2565,
plus 3 module-level entries that only appear because -s -w strips the
symbols govulncheck needs for call-graph precision — a symbol-precise
scan of the same source shows all three uncalled.

Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt

* ci(nix): guard vulnscan against empty or non-binary govulncheck output

Codex round 2: an exit-0 govulncheck run that produced empty, truncated,
or garbled JSON — or silently ran in a mode other than binary — was
indistinguishable from a clean scan. Assert the stream's config message
reports scan_mode=binary and make both jq extractions fail closed
(exit 2, operational error).

Also sharpen the accepted-list comment on the three module-level
entries: on the stripped artifact govulncheck reports them as affected
with symbol frames (it cannot prune the call graph, so every vulnerable
symbol of an imported package counts as potentially called); the
round-2 reading of "degrades to module-level reporting" as functionless
findings was wrong, verified against the actual JSON stream.

Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt
2026-08-14 20:39:42 -04:00
xarmian 8af62d1c4e fix(ci): build with go1.26.6 to clear the govulncheck gate (BUG-2565) (#1093)
* fix(ci): build with go1.26.6 to clear the govulncheck gate (BUG-2565)

main has been red since da6ce642 on CI's Go job, at the govulncheck
step — not a test, lint or build failure, and not caused by that commit,
which touches no dependency or toolchain pin. govulncheck reports 8
reachable Go standard-library advisories, all "Found in: net@go1.26.5 /
Fixed in: net@go1.26.6" (GO-2026-5942 via net.Resolver.LookupCNAME,
GO-2026-5026 via http.Client.Do, and friends). A vuln-DB entry published
in the window turned the gate red with nobody committing anything, which
is the failure mode a govulncheck gate has by design. No dependency bump
fixes a stdlib advisory; the toolchain has to move.

Adds `toolchain go1.26.6` rather than raising the `go 1.26.5` directive.
The distinction is load-bearing: every GitHub Actions job here exports
GOTOOLCHAIN=auto explicitly, so all of them fetch 1.26.6 and stamp it
into the binary govulncheck scans, while builders pinned to
GOTOOLCHAIN=local ignore the line and keep satisfying the 1.26.5 floor.
nixpkgs nixos-26.05 still ships go 1.26.5 (verified against the channel's
go/1.26.nix), so raising the floor would have failed the Nix build
outright to buy nothing.

That leaves the Nix-packaged binary on the 1.26.5 stdlib. Not silently:
BUG-2567 tracks it, with the two heavier options (move the channel,
override go in the package) written down and deliberately not taken here.

The GOTOOLCHAIN=auto comments in both workflows now say what they are
protecting — under `local` the build silently falls back to setup-go's
patch and the gate goes red again, and released binaries would ship the
vulnerable stdlib.

Verified: make vuln goes from "affected by 8 vulnerabilities" to "No
vulnerabilities found" (exit 0), and `go version -m` on the built binary
reads go1.26.6, so the mechanism is the stamp and not a scanner quirk.
Build, make lint (0 issues) and go test ./... all green on 1.26.6.

Fixes BUG-2565. Refs BUG-2567.

* fix(ci): give native-smoke the GOTOOLCHAIN override too, per Codex review

Codex round 1, P2, and correct: native-smoke resolves its Go via
`go-version-file: go.mod`, which reads the `go` directive (1.26.5), so
setup-go's GOTOOLCHAIN=local previously had nothing to block and the
override was unnecessary. Adding `toolchain go1.26.6` changes that —
without it this job keeps smoke-testing a 1.26.5 binary on macOS and
Windows while every other job and every release artifact moves to
1.26.6.

Worth fixing precisely because it is not a gate failure. Nothing goes
red; the platform smoke coverage just quietly stops matching what ships,
which is the pins-disagree failure mode this PR exists to avoid rather
than introduce.

Uses the documented Out-File form rather than `>>`: this job runs pwsh,
where redirection encoding is version-dependent and a UTF-16 line in
$GITHUB_ENV is silently ignored.

* fix(ci): drop the redundant native-smoke override, document why (round 2)

Codex round 2 flagged the comment I added in round 1 as stale, and
checking the pinned setup-go source settles it — but not quite the way
the note said, so recording what the code actually does:
parseGoVersionFile (installer.ts) prefers go.mod's `toolchain` directive
over the `go` directive, CONDITIONALLY — only when GOTOOLCHAIN is not
already `local` in the environment at parse time.

In this job it isn't, so setup-go installs go1.26.6 itself and the
override I added does nothing. Removing it rather than keeping a no-op
step whose comment asserts a mechanism that isn't operative — a wrong
explanation in the tree is worse than no explanation, since it is the
part the next person reuses without re-deriving.

Replaced with a comment on the setup-go step covering both halves: why
this job needs no override where its siblings do, and the one way it
regresses silently (a job- or workflow-level `env: GOTOOLCHAIN: local`
added above it would flip the parser back to the 1.26.5 floor while
everything else ships 1.26.6 — no red, just smoke coverage that stops
matching the artifact).

The round-1 finding was still right: before checking, "native-smoke
builds 1.26.5" was the reasonable read.
2026-08-14 17:49:47 -04:00
xarmian f9195c5b09 ci: make the go test timeout explicit everywhere (TASK-2545) (#1089)
* ci: make the go test timeout explicit everywhere (TASK-2545)

The v0.13.0 release pre-flight died on `panic: test timed out after
10m0s` in internal/store, on a commit whose Go tree was identical to a
green run an hour earlier. Nothing hung — the package's runtime simply
crossed a budget nobody had chosen.

`go test` without -timeout uses a 10m per-test-binary default. This repo
raised the two RACE steps to 45m twice as the suite grew (BUG-1371 30m,
BUG-1913 30m→45m), each time with a careful comment — and each time left
their non-race siblings on the silent default. Three steps were still
running on it, including the release gate:

  ci.yml       "Run tests"                    (SQLite)
  ci.yml       "Run tests against PostgreSQL" (the one that panicked)
  release.yml  "Run tests"                    (the release gate itself)

All three now carry -timeout=45m, matching the race legs so the file has
one number, with comments saying it is a hang-catcher rather than a
performance budget and that job wall-clock is the signal for "the suite
got slow".

Measured at 212d59e7 on a dev box, both drivers, before and after:

  PostgreSQL  whole suite 4m43s wall; internal/store 280s; server 103s
  SQLite      whole suite 1m52s wall; internal/server 107s; store 64s

CI runners are roughly 2x slower, which is what put store's PG binary
over 10m. 45m is ~4.5x current CI headroom.

This raises the ceiling; it does not change the slope. internal/store on
PG costs ~0.43s per test in database setup alone (CREATE DATABASE plus a
full migration replay, where the SQLite harness copies a pre-migrated
template — IDEA-1914), so every test added costs PG CI ~0.43s forever
and that package is 99% of the job's critical path. Measured and filed
as IDEA-2550 rather than fixed here: it changes shared test
infrastructure that gates every merge and deserves its own review.

Verified by running the exact post-change commands on both drivers: PG
green in 4m42s, SQLite green in 1m52s, 25 packages each.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* ci: time the Makefile's go test targets too (TASK-2545)

The previous commit said the timeout was explicit "everywhere" and it
wasn't — `make test`, `make test-pg`, and `make check` were all still on
the 10m default. That matters twice over: it's the same trap the commit
is about, and `make test-pg` is the local mirror of the CI leg that
actually panicked, so a developer reproducing the failure would have hit
a different budget than the one they were debugging.

Found by sweeping every `go test` in the repo rather than only the
workflows — which is what the commit message's own claim required and I
hadn't done when I wrote it.

Verified: `make test` green, 25 packages.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* ci: time the nix checkPhase, cap the Go jobs, correct two claims (TASK-2545)

Codex review. No P1s; the two P2s were both right and one of them
catches me stating an explanation I had not checked.

COVERAGE. `nix/package.nix`'s checkPhase runs `go test ./...` on the
default too, and .github/workflows/nix.yml exercises it — a fourth site
after the three workflow steps and the three Makefile targets. Now
timed. Every `go test` invocation in the repo carries an explicit
-timeout; the sweep is `grep -rn "go test"` over workflows, Makefile and
nix, not just the workflows I happened to be looking at.

JOB CAPS. Codex objected that 45m lets a hung binary burn a
release-gating job. Fair, and the real hole was worse: `go` and
`go-postgres` had NO `timeout-minutes`, so they inherit GitHub's 6-HOUR
default. Both now capped at 100m — deliberately above the two 45m test
steps so the per-binary timeout always fires first, because that is the
one that prints the goroutine dump naming the hung test. The cap only
catches a runaway that isn't a single test (wedged service container,
stuck download).

CORRECTIONS to 496f521f's message:

- It said the race steps were raised "twice (BUG-1371 30m, BUG-1913
  30m→45m)". BUG-1371 kept 30m and fixed the bcrypt cost that had blown
  past it; BUG-1913 made the only 30m→45m change. One raise, not two.
- It said CI runners are "roughly 2x slower, which is what put store's
  PG binary over 10m". That does not survive its own arithmetic: 280s
  local x 2 is 9m20s, under the budget. What is actually known is that
  the CI binary exceeded 10m and the local one takes 280s, so CI is
  >2.14x slower on that binary — a lower bound derived from the failure,
  not an explanation of it. I have not measured CI's runtime and should
  not have written a factor as if I had.
- "Nothing hung" and "cost the cut ~40 minutes" are TASK-2545's findings
  from the goroutine dump and the release timeline, not mine. Attributed
  rather than restated as my own observation.

The 0.43s per-test setup figure and both driver runtimes are mine, taken
on this box at 212d59e7 and reproducible with the commands in IDEA-2550.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag

* ci: put the corrections in the file, not only in a commit message (TASK-2545)

Codex's re-review came back with no P1s or P2s and four nits, all the
same shape: the claims I retracted in 4623cae9's COMMIT MESSAGE were
still sitting in the workflow comments. That's the half that matters —
nobody reads a commit message while editing a CI file, and a correction
that lives only in git log is a correction almost nobody receives.

Fixed in place:

- The raise history: BUG-1913 raised 30m→45m once. BUG-1371 kept 30m and
  dropped the test-only bcrypt cost that had blown past it. My comment
  said "raised twice (BUG-1371, BUG-1913)".
- The pre-existing race-step comment claiming BUG-1371 kept the step
  "well under the 30m budget" — contradicted by BUG-1913 having to raise
  it later. Reworded to say what each change actually did. Not my text,
  but it is wrong in the file I am editing and the next reader inherits
  it either way.
- The "~2x slower, which put store over 10m" line, which its own
  arithmetic refutes (280s x 2 = 9m20s). Now states the lower bound the
  failure actually supports — CI's store binary exceeded 10m, so >2.14x
  this box — and names the retracted claim so a reader who saw the old
  version knows it was withdrawn rather than lost.
- "so it never fires before they do" on the job caps, which a job-level
  timeout cannot promise: it covers setup and every step, not just the
  two 45m ones. Now says "in practice", not a guarantee.

Attribution of TASK-2545's own findings (the ~40 minutes, the goroutine
dump showing nothing hung) moved into the comment too.

Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
2026-08-13 17:15:30 -04:00
xarmian 5320eebe9b Merge pull request #927 from PerpetualSoftware/dependabot/github_actions/actions/cache-6.1.0
chore(ci)(deps): bump actions/cache from 5.0.5 to 6.1.0
2026-08-06 14:24:59 -04:00
xarmian 0cdd4620fa Merge pull request #982 from PerpetualSoftware/dependabot/github_actions/actions/setup-go-7.0.0
chore(ci)(deps): bump actions/setup-go from 6.5.0 to 7.0.0
2026-08-06 14:24:57 -04:00
xarmian 08732f2653 Merge pull request #983 from PerpetualSoftware/dependabot/github_actions/actions/setup-node-7.0.0
chore(ci)(deps): bump actions/setup-node from 6.4.0 to 7.0.0
2026-08-06 14:24:55 -04:00
xarmian 6be16e33db Merge pull request #1040 from PerpetualSoftware/dependabot/github_actions/actions/checkout-7.0.1
chore(ci)(deps): bump actions/checkout from 6.0.2 to 7.0.1
2026-08-06 14:24:52 -04:00
xarmian 10c7b9a8b0 Merge pull request #1039 from PerpetualSoftware/dependabot/github_actions/actions-minor-and-patch-dee7dd5463
chore(ci)(deps): bump docker/login-action from 4.4.0 to 4.6.0 in the actions-minor-and-patch group across 1 directory
2026-08-06 14:24:50 -04:00
dependabot[bot] 2ab5b1033f chore(ci)(deps): bump actions/setup-go from 6.5.0 to 7.0.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/924ae3a1cded613372ab5595356fb5720e22ba16...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 17:31:09 +00:00
xarmian 71c18200c5 ci(nix): pin nix-installer-action to the actual v22 commit
The pinned SHA was a genuine ancestor commit from the upstream repo but
~68 commits behind the v22 tag it claimed; the workflow's own convention
requires SHA and version comment to move together.

Claude-Session: https://claude.ai/code/session_01RNcrc3CtXwJwreubtHTgN6
2026-08-06 04:43:46 +00:00
dependabot[bot] 38e1042fee chore(ci)(deps): bump docker/login-action
Bumps the actions-minor-and-patch group with 1 update in the / directory: [docker/login-action](https://github.com/docker/login-action).


Updates `docker/login-action` from 4.4.0 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...dbcb813823bdd20940b903addbd779551569679f)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-03 13:09:23 +00:00
dependabot[bot] 1dafa37c1d chore(ci)(deps): bump actions/checkout from 6.0.2 to 7.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 13:10:09 +00:00
Claude 02b302519e feat(nix): add flake packaging for pad with CI build
Adds a Nix flake exposing the pad binary as packages.default (buildGoModule
+ importNpmLock for the embedded SvelteKit UI), a devShell, and flake
checks (package build with `go test ./...`, plus a `pad --version` smoke
test). nix/package.nix is written nixpkgs-submission-ready (no
flake-specific inputs) so it can later be adapted for pkgs/by-name.

Also adds a GitHub Actions workflow that runs `nix flake check` and
`nix build` on push/PR, and documents `nix run` / `nix profile install`
/ `nix develop` in the README.
2026-07-26 22:49:10 +00:00
dependabot[bot] b5d0d5a028 chore(ci)(deps): bump actions/setup-node from 6.4.0 to 7.0.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 13:10:24 +00:00
dependabot[bot] 0f69e7bbb8 chore(ci)(deps): bump actions/cache from 5.0.5 to 6.1.0
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-13 13:09:57 +00:00
dependabot[bot] f528190de4 chore(ci)(deps): bump sigstore/cosign-installer from 3.10.1 to 4.1.2 (#498)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.10.1 to 4.1.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/7e8b541eb2e61bf99390e1afd4be13a184e9ebc5...6f9f17788090df1f26f669e9d70d6ae9567deba6)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 01:51:24 -04:00
dependabot[bot] 86772b5e9e chore(ci)(deps): bump the actions-minor-and-patch group across 1 directory with 6 updates (#774)
Bumps the actions-minor-and-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.4.0` | `6.5.0` |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `9.2.0` | `9.3.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.0.0` | `4.2.0` |
| [docker/login-action](https://github.com/docker/login-action) | `4.1.0` | `4.4.0` |
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `7.2.1` | `7.2.3` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.0` | `4.1.1` |



Updates `actions/setup-go` from 6.4.0 to 6.5.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...924ae3a1cded613372ab5595356fb5720e22ba16)

Updates `golangci/golangci-lint-action` from 9.2.0 to 9.3.0
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a)

Updates `docker/setup-buildx-action` from 4.0.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c)

Updates `docker/login-action` from 4.1.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...af1e73f918a031802d376d3c8bbc3fe56130a9b0)

Updates `goreleaser/goreleaser-action` from 7.2.1 to 7.2.3
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8...f06c13b6b1a9625abc9e6e439d9c05a8f2190e94)

Updates `actions/attest-build-provenance` from 4.1.0 to 4.1.1
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32...0f67c3f4856b2e3261c31976d6725780e5e4c373)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-and-patch
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-and-patch
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 01:38:34 -04:00
raj kumar 694497948b ci: add Windows and macOS smoke tests (#911) 2026-07-11 01:22:41 -04:00
xarmian 2ff8ac9c15 fix(ci): let Go fetch the 1.26.5 toolchain go.mod pins (unbreak CI) (#901)
#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
2026-07-10 19:40:27 -04:00
xarmian d6c6dfe682 build(deps): govulncheck binary mode + Go 1.26.5 / x-crypto / gRPC security bumps (#896)
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).
2026-07-10 12:02:20 -04:00
xarmian 982f534b4a fix(web): exact-pin coordinated Tiptap packages + CI drift guard (BUG-2009) (#838)
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.
2026-07-07 16:11:44 -04:00
xarmian 88c5771aed ci(web): run vitest unit tests in CI + Makefile web-test target (#835)
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
2026-07-07 15:12:19 -04:00
xarmian 4e4d4edc86 fix(ci): raise race-step go test timeout to 45m (BUG-1913) (#787)
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
2026-07-03 02:45:12 -04:00
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
xarmian 5673522608 fix(release): force GORELEASER_CURRENT_TAG from github.ref_name
When two lightweight tags point at the same commit (v0.4.0 cut on top
of v0.4.0-rc.1 with no intervening commits, per PLAYB-1160), goreleaser's
git-describe-based auto-detection picked the wrong one on the CI runner
and stamped v0.4.0 artifacts with version 0.4.0-rc.1 — they then collided
with the existing RC release-page assets and the workflow aborted with
422 already_exists. Setting GORELEASER_CURRENT_TAG to ${{ github.ref_name }}
bypasses the auto-detection: it's exactly the tag that triggered the run.

Root-cause-fixes the v0.4.0 ship failure and prevents recurrence for any
future RC → stable sequence where the stable tag sits on top of the RC
without an intervening commit.
2026-05-12 18:17:47 +00:00
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
xarmian e380b4e660 chore(deps): bump Node 22 → 24 (Dockerfile + CI workflows) (TASK-1235) (#443)
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.
2026-05-08 07:32:43 -04:00
dependabot[bot] 5026aa529d chore(ci)(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#207)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:53:20 -04:00
dependabot[bot] b780c2a1fe chore(ci)(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#206)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:52:14 -04:00
dependabot[bot] 90dcc3d4ce chore(ci)(deps): bump actions/checkout from 4.3.1 to 6.0.2 (#204)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/34e114876b0b11c390a56381ad16ebd13914f8d5...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:51:15 -04:00
dependabot[bot] 8b9a4d63d5 chore(ci)(deps): bump actions/setup-go from 5.6.0 to 6.4.0 (#203)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.6.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/40f1582b2485089dde7abd97c1529aa768e1baff...4a3601121dd01d1626a1e23e37211e3254c1c06c)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:50:08 -04:00
dependabot[bot] b1f68818f1 chore(ci)(deps): bump goreleaser/goreleaser-action from 6.4.0 to 7.2.1 (#268)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6.4.0 to 7.2.1.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/e435ccd777264be153ace6237001ef4d979d3a7a...1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:47:25 -04:00
xarmian 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
2026-04-29 15:41:12 -04:00
xarmian 2b752ba194 feat(release): sign + notarize macOS binaries (IDEA-830) (#278)
* feat(release): sign + notarize macOS binaries (IDEA-830)

Adds Developer ID code-signing and Apple notarization to the release
pipeline so users installing via `brew install perpetualsoftware/tap/pad`
or downloading binaries directly no longer hit Gatekeeper's "cannot
verify the developer" warning.

Uses GoReleaser v2's built-in `notarize:` block (Anchore/Quill backend),
which signs and notarizes in-process from the existing ubuntu-latest
runner — no rcodesign install, no macOS runner needed.

Both the .p12 cert and the .p8 App Store Connect notary key are stored
as base64-encoded repo secrets; Quill decodes them in-process. The
notarize block is gated on `MACOS_CERT_P12` being set, so snapshot
builds, fork PRs, and any context where the cert isn't available skip
cleanly without failing.

Verification plan: tag v0.0.1-rc.3, confirm Action goes green end-to-end,
then on a clean Mac run `brew install perpetualsoftware/tap/pad` and
verify `codesign -dv $(which pad)` shows the Developer ID signature and
`spctl -a -t exec -vv $(which pad)` reports "accepted" with the notary
ticket stapled.

* harden(release): isolate npm build from macOS secrets + pin goreleaser

Addresses Codex review findings on PR #278:

MEDIUM — Apple signing secrets were exposed to the npm web build.
The previous `before.hooks` block ran `npm ci && npm run build` inside
the GoReleaser process, which means npm lifecycle scripts and the
SvelteKit build inherited the Developer ID .p12 cert, cert password,
and .p8 notary key from the goreleaser-action's env. Adding a 5-year
signing cert to that environment meaningfully widened the blast radius
of any npm supply-chain compromise.

Fix: move the web build into a dedicated workflow step before the
goreleaser-action invocation. The MACOS_* secrets are scoped only to
the goreleaser env block, so the npm phase no longer sees them.

LOW — GoReleaser binary version was floated as `~> v2`, while every
third-party Action in this workflow is SHA-pinned per the policy at
the top of the file. With Apple signing credentials now in the env, a
compromised or regressed GoReleaser release would carry meaningful
blast radius. Pinned to v2.15.4 (current latest) so bumps go through
explicit review like the rest of the action pins.

No functional change to the signing/notarization itself — same schema,
same gating, same secrets.

* docs(release): document web/build prereq for local snapshot builds

Addresses second-pass Codex review finding on PR #278.

Removing the goreleaser `before.hooks` web build (done in 6f4a52c to
isolate npm from MACOS_* secrets) means `goreleaser release --snapshot`
from a clean checkout now fails with "embed: no matching files found"
because embed.go declares `//go:embed all:web/build` and there's no
longer a hook that auto-populates that directory.

CI is unaffected (the workflow has a dedicated `Build web UI` step), but
contributors testing goreleaser config locally would hit a confusing
error before reaching the part they wanted to validate.

Doc-only fix: extend the existing comment block at the top of
.goreleaser.yaml to point contributors at `make web` (or the explicit
npm commands) as the prerequisite for local snapshot builds. Note that
`make build` and `make install` already handle this automatically.

* harden(release): add concurrency guard + bump goreleaser timeout

Addresses third-pass Codex review findings on PR #278.

Both findings were LOW (no ship blockers) and pre-existing concerns
that the macOS notarize block makes slightly more visible. Folded into
this PR rather than deferring because both relate directly to the
goreleaser invocation we already touched.

LOW #1 — GoReleaser overall timeout was the default 1h, while the
notarize block now allows up to 20m of Apple notary wait time on top
of build + cosign blob-sign + SBOM + multi-arch docker. On a slow
notary day (or first-cert-use latency), that could come close to or
hit the default ceiling. Bumped `release --clean` to
`release --clean --timeout=2h` for comfortable headroom without
burning Action minutes on the happy path (worker exits as soon as
Apple replies).

LOW #2 — No workflow-level concurrency guard. If two `v*` tags landed
close together (rc.3 then rc.4 within a minute), runs would race on
shared mutable outputs: GHCR `:latest`, the homebrew cask in the
separate tap repo, the GitHub Releases page. Added a top-level
concurrency block that serializes all release runs.

Group is intentionally NOT keyed by `github.ref` — different tag names
share the same mutable infrastructure, so we want all release tags to
serialize, not just repeat pushes of the same tag. cancel-in-progress
is false so a queued tag never aborts a release mid-publish, which
could leave GHCR and the brew tap in inconsistent states.

No functional change to signing/notarization itself.
2026-04-28 20:24:38 -04:00
xarmian 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.
2026-04-28 17:20:03 -04:00
xarmian 7cda0d7896 feat: rebrand to Perpetual Software + new tagline (IDEA-832) (#273)
Migrates from xarmian/pad to PerpetualSoftware/pad across the entire
repo and updates the product subtitle to "Collaborate with your AI
agents".

Go module rename
- go.mod: github.com/xarmian/pad → github.com/PerpetualSoftware/pad
- All Go imports updated across cmd/pad, internal/{cli,server,store,
  models,collections,items,events,metrics,webhooks} (~130 files)
- Test fixtures with the literal repo slug ("xarmian/pad" in JSON
  shapes, SSH/HTTPS git URL strings, workspace_context fixtures)
  also updated, including the secondary repo entry
  (xarmian/pad-web → PerpetualSoftware/pad-web — pad-web was also
  moved to the org per branch context)

Docs / config
- README badges, install instructions, brew tap, Docker image, source
  build path, sponsor link (sponsor link kept as personal @xarmian)
- Subtitle: "Project management for developers and AI agents." →
  "Collaborate with your AI agents." (README, manifests, web layout
  meta, .goreleaser homebrew description)
- CONTRIBUTING.md, SECURITY.md, skills/INSTALL.md
- .goreleaser.yaml: homebrew_casks owner, GHCR image, release github
  owner, cosign cert-identity regex, comments
- .github/workflows/release.yml: tap/release comments
- deploy/k8s/deployment.yaml: container image
- docs/deployment.md: clone URL
- web/static/{site.webmanifest,manifest.json}: description
- web/src/routes/+layout.svelte: meta description + og:description

Brew tap path is PerpetualSoftware/tap/pad (CamelCase, matches
GitHub user case). GHCR image is ghcr.io/perpetualsoftware/pad
(lowercased per GHCR's URL normalization). CODEOWNERS @xarmian and
FUNDING.yml github: xarmian intentionally retained — those are the
personal maintainer / sponsor account, separate from the org repo.

Verification: go build ./..., go test ./... (all pkgs pass), web
build, and make install all clean (TASK-844, TASK-845).
2026-04-28 12:26:39 -04:00
xarmian e57da62917 chore: modernize goreleaser config + wire homebrew-tap token (#256)
* chore: wire HOMEBREW_TAP_GITHUB_TOKEN into release pipeline

The brews block in .goreleaser.yaml targets the separate xarmian/homebrew-tap
repo. Without a token override, goreleaser falls back to the workflow's
GITHUB_TOKEN — which is scoped to xarmian/pad only and cannot push to the
tap repo. At first real tag time the brew publish step would fail with a
permission error.

- Add `repository.token` to the brews block, referencing
  `{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}`
- Export `HOMEBREW_TAP_GITHUB_TOKEN` from the workflow `secrets` into the
  goreleaser step env, alongside the existing `GITHUB_TOKEN`
- Comment both edits with the rationale + the fine-grained PAT permissions
  the secret needs (`Contents: write` + `Metadata: read` on the tap repo)

The secret itself is created on the human side (HT-780). Snapshot mode
skips publishing so this isn't testable locally — the gate is HT-782's
v0.0.1-rc.1 dress rehearsal.

Refs: TASK-806, TASK-778 (audit), HT-780 (operator step)

* chore: migrate goreleaser deprecations (archives.formats, dockers_v2, homebrew_casks)

Three v2 deprecations were flagged by `goreleaser check` while wiring the
tap token. Migrating them now (instead of filing tech debt) because we're
already touching the file and these are part of the same release pipeline
that ships at v0.1.0 — no point landing a "wire the token" commit that
still trips deprecation warnings on the very next CI run.

Changes:

- archives: `format: tar.gz` + `format_overrides[].format: zip` →
  `formats: ["tar.gz"]` + `format_overrides[].formats: ["zip"]`
  (single-string is still accepted but the list form is the new spec)

- dockers + docker_manifests → dockers_v2:
  Single block with `images:` + `tags:` + `platforms:` collapses the
  prior per-architecture builds plus separate manifest declarations into
  one declaration. buildx + multi-platform are implicit. Snapshot
  validates: amd64 + arm64 images both build, manifest list assembled,
  binary runs inside the cross-built image.

  goreleaser flags dockers_v2 as "experimental and subject to change" —
  it's the documented forward path for v2 and the project is already
  pinned to `version: "~> v2"`, so we're committed to the roadmap.

- Dockerfile.goreleaser: add `ARG TARGETPLATFORM` and update the COPY to
  `${TARGETPLATFORM}/pad`. dockers_v2 organizes pre-built binaries under
  `linux/amd64/pad`, `linux/arm64/pad` etc; buildx populates
  TARGETPLATFORM per platform during the build.

- brews → homebrew_casks: `directory: Formula` → `directory: Casks`.
  The `brews` keyword is fully phased out in v2.10+; goreleaser's
  homebrew_casks now natively handles pre-compiled binaries (which used
  to require workarounds with the old brews block). End-user UX is
  unchanged: `brew install xarmian/tap/pad` works identically because
  modern Homebrew auto-detects whether a tap entry is a formula or a
  cask. Removed the no-op `test:` stanza that doesn't apply to casks.

Validation: `goreleaser check` clean (zero warnings), `goreleaser
release --snapshot --clean --skip=publish,sign,sbom` builds all six
binaries, six archives, one cask, two cross-platform docker images.
`docker run --rm ghcr.io/xarmian/pad:latest-amd64 --version` returns
the snapshot version as expected.

Refs: TASK-806
2026-04-26 16:38:45 -04:00
xarmian 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).
2026-04-25 12:36:05 -04:00
xarmian 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+"
2026-04-25 11:35:19 -04:00
xarmian 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.
2026-04-22 22:35:08 -04:00
xarmian 9f0f168c6b chore(release): cosign signing + SBOM + SLSA provenance (TASK-684) (#224)
Matches the security-posture promise in SECURITY.md and README. Every
GA release will now publish:

- Signed checksums.txt (Sigstore keyless cosign via GitHub OIDC)
- Signed container manifests for ghcr.io/xarmian/pad (cosign sign)
- SPDX SBOM (.spdx.json) next to each archive (syft via goreleaser
  sboms: section)
- SLSA v1 build provenance attestation for every archive via
  actions/attest-build-provenance

.goreleaser.yaml:
- Add sboms: (artifacts: archive, SPDX JSON)
- Add signs: for checksum artifact with --output-certificate /
  --output-signature
- Add docker_signs: for container manifests
- Fix pre-existing v2 incompatibility in before.hooks (map-with-cmd
  form is rejected in v2; switched to the plain-string form required
  by the v2 schema — the existing config never validated)

.github/workflows/release.yml:
- Grant id-token: write (required for cosign keyless OIDC) and
  attestations: write (required by attest-build-provenance)
- Install cosign (sigstore/cosign-installer v3.10.1) before goreleaser
- Install syft (anchore/sbom-action/download-syft v0.18.0) before
  goreleaser
- Add actions/attest-build-provenance (v4.1.0) step after goreleaser
  to mint SLSA provenance for all built archives

All new actions pinned to 40-char SHAs per the existing pinning
convention. Verified with:
  docker run goreleaser/goreleaser:v2.15.4 check  # config valid
  yq -e '.' .github/workflows/release.yml         # YAML valid

Parent: PLAN-644.
2026-04-22 21:16:33 -04:00
xarmian 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.
2026-04-22 20:44:52 -04:00
xarmian 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.
2026-04-22 15:27:37 -04:00
xarmian 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.
2026-04-22 13:46:32 -04:00