Release #95
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |