Skip to content

Release v0.36.0 (#308) #91

Release v0.36.0 (#308)

Release v0.36.0 (#308) #91

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
# Serialize runs for the same tag. The intake-time run (gate skips) and the
# post-merge re-tag run are far apart in time, so this never queues in practice;
# it makes a manual "re-run all jobs" deterministic instead of racing GoReleaser.
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
# Gate: only cut a release when the tag is on main (i.e. it was created on the
# squash commit by post-merge-release after the dev->main PR merged). A v* tag
# that is not reachable from main is NOT released, which neutralizes a stray or
# legacy direct tag push on a dev commit. A tag pushed directly onto a commit
# already on main (legacy escape hatch) passes the gate immediately.
gate:
name: gate
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
on_main: ${{ steps.check.outputs.on_main }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
persist-credentials: false
- name: Check tag is on main
id: check
run: |
set -euo pipefail
git fetch --force origin main
TAG_SHA="$(git rev-list -n 1 "${GITHUB_REF#refs/tags/}")"
if git merge-base --is-ancestor "$TAG_SHA" origin/main; then
echo "on_main=true" >> "$GITHUB_OUTPUT"
echo "Tag is on main; proceeding with release."
else
echo "on_main=false" >> "$GITHUB_OUTPUT"
echo "::notice::Tag is not on main; skipping release. A release fires only after the squash-merge creates the tag on main."
fi
release:
needs: gate
if: needs.gate.outputs.on_main == 'true'
runs-on: ubuntu-latest
permissions:
contents: write # Create the GitHub release
id-token: write # Required for OIDC / attestations
attestations: write # Required for actions/attest-build-provenance
actions: read # Required to read run metadata
steps:
########################################
# CHECKOUT (fetch-depth 0 for changelog and GoReleaser)
########################################
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
# No git write happens in this job: GoReleaser publishes over the API with an
# explicit token, so there is nothing here that needs the credential left behind
# in .git/config. Matches the checkout in the gate job above.
persist-credentials: false
########################################
# VALIDATE TAG
########################################
- name: Validate SemVer tag
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "Current tag: $TAG"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z\.-]+)?$ ]]; then
echo "❌ Tag '$TAG' is not a valid SemVer (expected vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc1/beta1)"
exit 1
fi
echo "✔ Tag '$TAG' is valid SemVer"
########################################
# SETUP GO
########################################
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version-file: 'go.mod'
# Privileged job (contents: write, id-token: write): opt out of the
# default Go cache to avoid cache-poisoning from lower-privileged
# workflows, matching codecov/codeql/security-ultimate.
cache: false
- name: Show Go version
run: go version
########################################
# VERIFY MODULES
########################################
- name: Verify Go modules
run: go mod verify
########################################
# INSTALL SYFT (for CycloneDX SBOM via GoReleaser)
########################################
- name: Install Syft (for SBOM generation)
uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
with:
syft-version: v1.19.0
########################################
# VALIDATE THE SIGNING KEY *BEFORE* PUBLISHING
# GoReleaser publishes the release (incl. SHA256SUMS) before the signing step
# below; if signing then failed, the tag would go live without SHA256SUMS.sig
# and both install.sh and the in-app upgrade hard-fail on the missing file,
# blocking every install/upgrade. Fail here, before anything is published, if
# the signing secret is missing or does not match the pinned public key.
########################################
- name: Validate release signing key before publishing
env:
PROXSAVE_KEY_SIGNATURE: ${{ secrets.PROXSAVE_KEY_SIGNATURE }}
run: |
set -euo pipefail
if [ -z "${PROXSAVE_KEY_SIGNATURE:-}" ]; then
echo "::error::PROXSAVE_KEY_SIGNATURE is empty; refusing to publish a release that cannot be signed." >&2
exit 1
fi
KEY_FILE="$(mktemp)"
trap 'rm -f "$KEY_FILE"' EXIT
printf '%s\n' "$PROXSAVE_KEY_SIGNATURE" > "$KEY_FILE"
# Derive the public key from the signing secret and compare its base64 body
# to the pinned key install.sh ships, so a missing/wrong/corrupt secret fails
# HERE, before GoReleaser publishes - otherwise the tag would go live without a
# usable SHA256SUMS.sig and block every install/upgrade. We compare base64
# bodies (not whole PEM files): install.sh stores the key as a shell-quoted
# value (PUBKEY_PEM='-----BEGIN...'), so a naive PEM extraction is not openssl-
# readable; and the dash-less awk patterns never look like a PEM block to
# TestReleaseSigningKeyNoDrift, so no second hard-coded copy is introduced.
derived="$(openssl pkey -in "$KEY_FILE" -pubout 2>/dev/null | awk '/BEGIN PUBLIC KEY/{f=1;next} /END PUBLIC KEY/{f=0} f{print}' | tr -d '\r\n ')" || true
pinned="$(awk '/BEGIN PUBLIC KEY/{f=1;next} /END PUBLIC KEY/{f=0} f{print}' install.sh | tr -d '\r\n ')"
if [ -z "$derived" ]; then
echo "::error::could not derive a public key from PROXSAVE_KEY_SIGNATURE (corrupt or wrong-format signing secret)." >&2
exit 1
fi
if [ -z "$pinned" ]; then
echo "::error::could not read the pinned public key from install.sh." >&2
exit 1
fi
if [ "$derived" != "$pinned" ]; then
echo "::error::PROXSAVE_KEY_SIGNATURE does not match the pinned public key; refusing to publish." >&2
exit 1
fi
echo "Release signing key validated against the pinned public key."
########################################
# GORELEASER
########################################
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with:
version: latest
workdir: ${{ github.workspace }}
args: release --clean --config .github/.goreleaser.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Baked into the binary via ldflags (support.supportEmail) - the maintainer
# support-email recipient. Empty (secret unset) just disables the support email.
EMAIL_SUPPORT: ${{ secrets.EMAIL_SUPPORT }}
########################################
# SIGN SHA256SUMS (ECDSA P-256) + upload as a release asset
########################################
- name: Sign and publish SHA256SUMS signature
env:
PROXSAVE_KEY_SIGNATURE: ${{ secrets.PROXSAVE_KEY_SIGNATURE }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
test -f build/SHA256SUMS
KEY_FILE="$(mktemp)"
trap 'rm -f "$KEY_FILE"' EXIT
printf '%s\n' "$PROXSAVE_KEY_SIGNATURE" > "$KEY_FILE"
# Sign the checksum file with the project's ECDSA P-256 private key.
openssl dgst -sha256 -sign "$KEY_FILE" -out build/SHA256SUMS.sig build/SHA256SUMS
# Defense in depth: verify the signature against the SAME public key that
# install.sh pins, so a wrong/corrupt signing secret fails the release here
# instead of shipping an unverifiable artifact.
printf '%s\n' \
'-----BEGIN PUBLIC KEY-----' \
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElks05mPtm1vm0YtHlSGX1HlgdXjn' \
'liDJEnB+RgiWOQR+6xLWeX7PyauuMxUh/HNnvBQAokK91fLWes4r9Xlwzw==' \
'-----END PUBLIC KEY-----' > pinned_pub.pem
openssl dgst -sha256 -verify pinned_pub.pem -signature build/SHA256SUMS.sig build/SHA256SUMS
# Upload the signature to the release GoReleaser just created.
gh release upload "${GITHUB_REF#refs/tags/}" build/SHA256SUMS.sig --clobber
########################################
# BUILD PROVENANCE ATTESTATION
########################################
- name: Attest Build Provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8
with:
subject-path: build/proxsave_*