* 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.
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).
* 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
* 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+"
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.
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.
- README: update agent integration section to show pad install with all supported tools
- README: add pad install step to Quick Start
- Release workflow: add web build placeholder before go test to prevent embed failure
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