name: Release on: push: tags: - "v*" # Serialize all release runs. If two v* tags land close together (e.g. # rc.3 then rc.4 within a minute), queue rather than race — they share # mutable outputs (the GHCR `:latest` tag, the homebrew cask in the # separate tap repo, the GitHub Releases page) and parallel runs would # interleave nondeterministically. Group is intentionally NOT keyed by # `github.ref`: we want different tag names to serialize too, not just # repeat pushes of the same tag. cancel-in-progress=false so a queued # tag never aborts a release mid-publish (which could leave GHCR and the # brew tap in inconsistent states). concurrency: group: release cancel-in-progress: false permissions: contents: write packages: write # id-token: write is required for keyless cosign signing (GitHub OIDC # exchanges this workflow's identity token for a short-lived Fulcio # certificate) and for actions/attest-build-provenance to mint SLSA # v1 provenance statements. id-token: write # attestations: write is required by actions/attest-build-provenance so # the resulting provenance bundles can be stored against the repo. attestations: write # All third-party Actions are pinned to a 40-char commit SHA with a trailing # '# vX.Y.Z' comment so a compromised maintainer or moved tag cannot silently # execute attacker code in the release pipeline (this workflow has # contents:write + packages:write + the GHCR token, so a malicious action # here could publish tampered binaries). Bump the SHA + comment together. jobs: release: name: Build & Release runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: "1.26" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22" cache: "npm" cache-dependency-path: web/package-lock.json - name: Create web build placeholder for tests run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep - name: Run tests run: go test ./... - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Login to GitHub Container Registry uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # cosign + syft need to be on PATH before goreleaser runs — goreleaser # shells out to both for the signs/docker_signs/sboms sections. - name: Install cosign uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - name: Install syft (for SBOM generation) uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 # Build the SvelteKit web UI before GoReleaser so the static assets # get embedded into the Go binary. Done as a dedicated step (instead # of a goreleaser `before:` hook) so the npm install/build does NOT # inherit the MACOS_* signing secrets — those are scoped only to the # `Run GoReleaser` step's env block below. This isolates the 5-year # Developer ID cert from any npm supply-chain compromise during # dependency install. - name: Build web UI run: cd web && npm ci && npm run build - name: Run GoReleaser id: goreleaser # GoReleaser binary is pinned to an exact version (not "~> v2") to # match the SHA-pinning policy applied to the Actions themselves — # see the comment at the top of this file. With Apple signing # credentials now flowing through this step, a compromised or # regressed GoReleaser release would carry meaningful blast radius; # pinning forces an explicit, reviewed bump. uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: "v2.15.4" # --timeout=2h overrides GoReleaser's 1h default. With Apple # notarization (`wait: true`, up to 20m per the .goreleaser.yaml # notarize block) layered on top of build + cosign blob-sign + # SBOM + multi-arch docker manifest, slow notary days could push # close to the default ceiling. 2h gives comfortable headroom # without burning excessive Action minutes when notarization # actually fails fast (the worker exits as soon as Apple replies). args: release --clean --timeout=2h env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Cross-repo PAT for pushing the brew formula to PerpetualSoftware/homebrew-tap. # The default GITHUB_TOKEN above only has access to PerpetualSoftware/pad. # Add this secret in repo settings before tagging a release that ships # a brew formula — without it goreleaser fails at the brew publish step. HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} # macOS code-signing + Apple notarization (per IDEA-830). The # `notarize:` block in .goreleaser.yaml is gated on MACOS_CERT_P12 # being set, so PR builds + snapshot mode skip cleanly when these # are absent. The .p12 cert and .p8 notary key are stored # base64-encoded; GoReleaser's Quill backend decodes them in-process, # so no external signing tool needs to be installed on the runner. MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} MACOS_NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_KEY_P8 }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} # SLSA build provenance for every archive GoReleaser produced. # Writes a Sigstore-backed attestation to the repo so downstream # consumers can verify this binary was actually built by this # workflow from this commit, e.g.: # gh attestation verify pad_v1.0.0_linux_amd64.tar.gz \ # --repo PerpetualSoftware/pad - name: Generate build provenance for archives uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: "dist/pad_*_*_*.tar.gz,dist/pad_*_*_*.zip"