Skip to content

Release

Release #95

Workflow file for this run

name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
preflight:
# Fail within seconds if WINGET_GITHUB_TOKEN is expired or revoked,
# instead of 14 minutes into GoReleaser — after it has already created
# the draft release and opened the Homebrew/Scoop PRs — which strands
# the release in a half-published state (see the v0.18.2 run). The
# winget publisher is the only one on a long-lived PAT: the release-bot
# app token cannot open PRs on microsoft/winget-pkgs, where the app is
# not installed.
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check winget token validity and expiry
env:
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
run: |
set -euo pipefail
headers_file="$(mktemp)"
status="$(curl -sS -o /dev/null -D "$headers_file" -w '%{http_code}' \
-H "Authorization: Bearer ${WINGET_GITHUB_TOKEN}" \
https://api.github.com/repos/bomly-dev/winget-pkgs)"
if [ "$status" != "200" ]; then
echo "::error::WINGET_GITHUB_TOKEN cannot read bomly-dev/winget-pkgs (HTTP ${status}). The PAT has likely expired or been revoked — mint a new one (public_repo scope, it must be able to open PRs on microsoft/winget-pkgs) and update the secret: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli"
exit 1
fi
expiry="$(grep -i '^github-authentication-token-expiration:' "$headers_file" | cut -d' ' -f2- | tr -d '\r' || true)"
if [ -n "$expiry" ]; then
expiry_epoch="$(date -u -d "$expiry" +%s)"
days_left=$(( (expiry_epoch - $(date -u +%s)) / 86400 ))
echo "WINGET_GITHUB_TOKEN expires ${expiry} UTC (${days_left} days from now)."
if [ "$days_left" -lt 14 ]; then
echo "::warning::WINGET_GITHUB_TOKEN expires in ${days_left} days (${expiry} UTC). Rotate it soon: gh secret set WINGET_GITHUB_TOKEN -R bomly-dev/bomly-cli"
fi
else
echo "WINGET_GITHUB_TOKEN has no expiration date."
fi
validate:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: |
go.sum
- name: Run test suite
run: make test
- name: Run go vet
run: go vet ./...
release:
needs: [preflight, validate]
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
packages: write
id-token: write # required for cosign keyless signing
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: |
go.sum
- name: Mint package-manager token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: package-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: |
homebrew-tap
scoop-bucket
permission-contents: write
permission-pull-requests: write
- name: Mint release token
# Used as GoReleaser's GITHUB_TOKEN to create the draft release on this
# repo and upload its assets (archives, packages, checksums, signature).
# The release stays a draft here (see .goreleaser.yaml) — actual
# publishing happens in the `publish` job below, after the SLSA
# provenance file is also attached.
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: release-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: bomly-cli
permission-contents: write
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.16.0
args: release --clean
env:
GITHUB_TOKEN: ${{ steps.release-token.outputs.token }}
TAP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }}
SCOOP_GITHUB_TOKEN: ${{ steps.package-token.outputs.token }}
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
- name: Generate artifact hashes for SLSA provenance
id: hash
run: echo "hashes=$(base64 -w0 < dist/SHA256SUMS)" >> "$GITHUB_OUTPUT"
provenance:
needs: release
permissions:
actions: read # detect the workflow that triggered this run
id-token: write # sign the provenance
contents: write # upload the provenance to the release
# The SLSA generator MUST be pinned by a full vX.Y.Z tag, not a commit SHA or
# shorter tag — slsa-verifier resolves the builder identity from this exact ref,
# and SHA-pinning breaks that resolution. See .github/pinact.yaml, which excludes
# this line from automated re-pinning.
#
# upload-assets is deliberately false: its underlying action
# (softprops/action-gh-release) finds a release by tag, and GitHub's
# "get release by tag" API does not return drafts. Against our still-draft
# release, it can't find one — so it creates a *second*, non-draft release
# for the same tag instead, which immediately and permanently marks that
# tag as immutable (even if the bad release is later deleted). The
# `publish` job uploads this provenance file itself, by release ID, to
# avoid that entirely.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.release.outputs.hashes }}
upload-assets: false
publish:
needs: [release, provenance]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Mint release token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: release-token
with:
client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: bomly-dev
repositories: bomly-cli
permission-contents: write
- name: Download SLSA provenance
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.provenance.outputs.provenance-name }}
- name: Attach provenance and publish release
# Uploads the provenance file directly to the draft release by ID
# (not by tag — see the `provenance` job comment for why), then
# finalizes the draft into a published release now that every asset
# is attached. Must run last: immutable releases reject new assets
# once published.
#
# Uses the release-bot app token, not the default GITHUB_TOKEN: GitHub
# does not start new workflow runs from events triggered by the
# default token, so a release published with it would never fire the
# `release: published` trigger on "Release lifecycle sync"
# (notify-landing-yank.yml) — and the landing-page docs sync would
# silently never run. An app token is attributed to the app, so the
# publish event cascades.
env:
GH_TOKEN: ${{ steps.release-token.outputs.token }}
RELEASE_TAG: ${{ github.ref_name }}
PROVENANCE_FILE: ${{ needs.provenance.outputs.provenance-name }}
run: |
set -euo pipefail
release_id="$(gh api "repos/bomly-dev/bomly-cli/releases?per_page=100" | \
jq -r --arg tag "$RELEASE_TAG" '.[] | select(.tag_name == $tag and .draft == true) | .id' | head -n1)"
if [ -z "$release_id" ]; then
echo "::error::No draft release found for tag ${RELEASE_TAG}"
exit 1
fi
curl -fsSL \
-X POST \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${PROVENANCE_FILE}" \
"https://uploads.github.com/repos/bomly-dev/bomly-cli/releases/${release_id}/assets?name=${PROVENANCE_FILE}" \
> /dev/null
gh release edit "$RELEASE_TAG" --draft=false --repo bomly-dev/bomly-cli