Files
pad/.github/workflows/release.yml
T
xarmian 7c9f29b2f3 fix(ci): Go toolchain 1.26.10 (GO-2026-6809) and markdown-it 14.3.3 (TASK-4007) (#2267)
* fix(ci): Go toolchain 1.26.10 for GO-2026-6809, markdown-it 14.3.3 for npm audit (TASK-4007)

Main CI is red on both gates.
- govulncheck: GO-2026-6809 (crypto/x509) is fixed in go1.26.10. The
  go.mod toolchain line and the Dockerfile's golang image move to
  1.26.10. The workflow comments that name the toolchain value follow
  it, and go.mod's comment records the raise.
- Nix: its artifact is built with nixos-26.05's go, which predates
  1.26.10, so GO-2026-6809 joins nix/accepted-advisories.txt the way the
  1.26.9 block did, with the same removal condition.
- npm audit: markdown-it up to 14.3.2 has two high quadratic-time DoS
  advisories (GHSA-vfgh-jcmw-g37q, GHSA-gfr2-rv44-r23c). The overrides
  pin goes ^14.3.0 -> ^14.3.3; the lock moves markdown-it 14.3.2 ->
  14.3.3 and nothing else.

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

* ci(deps): allow markdown-it 14.3.3 under the 7-day age check, a security fix (TASK-4007)

Dependency age (advisory) flagged only markdown-it@14.3.3, published
2026-10-16: the fix for GHSA-vfgh-jcmw-g37q and GHSA-gfr2-rv44-r23c.
pad-merge-pr requires every check at the head to be SUCCESS, so the
advisory red blocked the merge. A security fix cannot wait out the
window. The allow line is the file's own remedy for this case; its
removal after the version ages past 7 days is a filed follow-up.

Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
2026-10-21 00:07:12 -04:00

241 lines
12 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26"
- name: Allow Go to fetch the toolchain go.mod pins
# actions/setup-go pins GOTOOLCHAIN=local, which blocks the toolchain
# go.mod requires (`go 1.26.5` floor, `toolchain go1.26.10`) from being
# fetched. `auto`, written after setup-go so it wins the $GITHUB_ENV
# last-write, lets Go pull it on demand. Mirrors the CI workflow; see
# #896. As of BUG-2565 this also decides which stdlib the RELEASED
# binaries carry — under `local` they would ship the 1.26.5 stdlib and
# its 8 reachable advisories.
run: echo "GOTOOLCHAIN=auto" >> "$GITHUB_ENV"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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
# Built for real, not stubbed: internal/materialize's tests run the
# materializer bundle (TASK-2198). The "Build web UI" step below
# rebuilds it, with the rest of the web build, for the release binaries.
- name: Build the materializer bundle for tests
working-directory: web
run: npm ci && node scripts/build-materializer.mjs
- name: Run tests
# Explicit -timeout, same reasoning as ci.yml's steps (TASK-2545):
# `go test` defaults to 10m per test binary, and this is the RELEASE
# gate — the one place an unchosen default is most expensive. SQLite
# only here (no PAD_TEST_POSTGRES_URL), so it is the faster driver,
# but it grows on the same curve.
run: go test -timeout=45m ./...
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Install syft (for SBOM generation)
uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
# 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
# The changelog's start: the highest stable tag below this one, by
# version and whatever branch it is on. GoReleaser's own default is git
# describe on the tag's parent, which cannot see a tag cut on a
# release-X.Y.x branch; from main it chose v0.15.0-rc.1 while v0.16.0
# existed (TASK-3268). Fails the run rather than falling back.
- name: Resolve the previous release tag
id: previous_tag
# Assigned first: a failing $(...) inside echo would not fail the step.
run: |
previous="$(scripts/release-previous-tag.sh "$GITHUB_REF_NAME")"
echo "Changelog base: $previous..$GITHUB_REF_NAME"
echo "tag=$previous" >> "$GITHUB_OUTPUT"
# The release gate (TASK-2035, scripts/release-smoke.sh, called from a
# .goreleaser.yaml build hook) BOOTS the linux/arm64 binary as well as
# linux/amd64, which needs qemu-user registered with binfmt_misc.
# Ubuntu's own package rather than a third-party action: this job holds
# contents:write and the GHCR token, and the qemu here only ever runs
# our own freshly built binary.
- name: Install qemu-user for the arm64 release smoke
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq qemu-user-static
test -e /proc/sys/fs/binfmt_misc/qemu-aarch64
- 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@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
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 }}
GORELEASER_PREVIOUS_TAG: ${{ steps.previous_tag.outputs.tag }}
# 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@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: "dist/pad_*_*_*.tar.gz,dist/pad_*_*_*.zip"
# TASK-3487: point the Claude Code plugin marketplace at this release.
# `.claude-plugin/marketplace.json` on main is what `/plugin marketplace add
# PerpetualSoftware/pad` reads; the script pins its pad entry to this tag
# (git-subdir, path plugin) and pushes to main. STABLE TAGS ONLY: the brew
# cask never gets an rc, so the plugin pairs with the CLI people have.
#
# Its own job, after the release, and it never fails the run: the release is
# the product and the pin is bookkeeping. A pin that cannot land opens an
# issue instead, so a maintainer can run the script by hand.
pin-plugin:
name: Pin the plugin marketplace to this release
needs: release
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
# One pin at a time; the script also refuses to move the pin backwards,
# for two releases that finish out of order.
concurrency:
group: pin-plugin
cancel-in-progress: false
permissions:
contents: write
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
- name: Pin the marketplace to ${{ github.ref_name }}
id: pin
continue-on-error: true
run: scripts/pin-plugin-marketplace.sh --push "$GITHUB_REF_NAME"
- name: Report a pin that did not land
if: ${{ steps.pin.outcome == 'failure' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::warning::The plugin marketplace was not pinned to $GITHUB_REF_NAME; see the opened issue."
gh issue create --repo "$GITHUB_REPOSITORY" \
--title "Plugin marketplace not pinned to $GITHUB_REF_NAME" \
--body "The release workflow's pin step failed (run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID). The release itself is published. Pin by hand from an up-to-date main: \`scripts/pin-plugin-marketplace.sh --push $GITHUB_REF_NAME\` (TASK-3487)."