Files
pad/.github/workflows/release.yml
T
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

150 lines
7.2 KiB
YAML

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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26"
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.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@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
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 }}
# Force-set the release tag from the triggering ref to bypass
# goreleaser's git-describe-based auto-detection. When two
# lightweight tags point at the same commit (e.g. v0.4.0 cut
# right on top of v0.4.0-rc.1 with no intervening commits),
# git-describe's tiebreaker is non-deterministic across hosts
# — locally it picked v0.4.0, the CI runner picked v0.4.0-rc.1
# during the v0.4.0 ship and stamped artifacts with the RC
# version. github.ref_name is unambiguous: it's exactly the
# tag that triggered the workflow. See PLAYB-1160 failure modes.
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
# 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"