Render the tap tokens the only way goreleaser allows (#11) #11
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*" | |
| # A release that fails halfway must be retryable without moving a published | |
| # tag. v0.2.0 published its GitHub assets and then lost npm, PyPI and both | |
| # taps to an expired credential, and there was no way to finish it: the only | |
| # trigger was the tag push that had already happened. | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Existing tag to (re)publish, e.g. v0.2.0" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| # Required by `npm publish --provenance`: npm mints a signed provenance | |
| # attestation from a GitHub OIDC token, and without this it fails the publish | |
| # outright rather than degrading to an unsigned one. | |
| id-token: write | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag || github.ref_name }} | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.tag || github.ref_name }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.26" | |
| - name: Unit Tests | |
| run: go test ./internal/... -count=1 | |
| release: | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ inputs.tag || github.ref_name }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.26" | |
| # Probe the tap credentials BEFORE goreleaser runs. | |
| # | |
| # goreleaser treats a failed cask/scoop push as fatal, so a PAT that | |
| # expires silently takes down npm, PyPI and every downstream step along | |
| # with the two install channels it actually governs. Checking first turns | |
| # that into a warning and a skipped tap. Reads .goreleaser.yml's | |
| # SKIP_HOMEBREW / SKIP_SCOOP. | |
| - name: Check tap credentials | |
| id: taps | |
| env: | |
| HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | |
| SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| probe() { | |
| local name="$1" repo="$2" token="$3" | |
| if [ -z "$token" ]; then | |
| echo "::warning title=$name skipped::no token configured for $repo" | |
| return 1 | |
| fi | |
| local status | |
| status="$(curl -sS -o /dev/null -w '%{http_code}' \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/$repo")" | |
| if [ "$status" != "200" ]; then | |
| echo "::warning title=$name skipped::token cannot read $repo (HTTP $status) — rotate the PAT and re-run this workflow with the same tag" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| if probe Homebrew ModelsLab/homebrew-tap "$HOMEBREW_TAP_GITHUB_TOKEN"; then | |
| echo "SKIP_HOMEBREW=false" >> "$GITHUB_ENV" | |
| else | |
| echo "SKIP_HOMEBREW=true" >> "$GITHUB_ENV" | |
| fi | |
| if probe Scoop ModelsLab/scoop-bucket "$SCOOP_BUCKET_GITHUB_TOKEN"; then | |
| echo "SKIP_SCOOP=false" >> "$GITHUB_ENV" | |
| else | |
| echo "SKIP_SCOOP=true" >> "$GITHUB_ENV" | |
| fi | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@v6 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | |
| SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }} | |
| # SKIP_HOMEBREW and SKIP_SCOOP reach goreleaser through $GITHUB_ENV. | |
| # Both registries package the SAME binaries goreleaser just built, taken | |
| # from dist/ rather than rebuilt, so npm, PyPI, Homebrew and Scoop can | |
| # never ship different bytes for one tag. | |
| - name: Collect release binaries | |
| run: | | |
| set -euo pipefail | |
| # Read goreleaser's own manifest rather than parsing dist/ directory | |
| # names: those carry microarchitecture suffixes (_v1, _v8.0) that move | |
| # between goreleaser versions, and artifacts.json states goos/goarch | |
| # outright. | |
| jq -r '.[] | select(.type == "Binary") | "\(.goos)_\(.goarch)\t\(.path)"' \ | |
| dist/artifacts.json | | |
| while IFS=$'\t' read -r target path; do | |
| mkdir -p "artifacts/$target" | |
| cp "$path" "artifacts/$target/" | |
| done | |
| find artifacts -type f | sort | |
| test "$(find artifacts -type f | wc -l)" -eq 6 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Build npm packages | |
| run: node packaging/npm/build.mjs "${RELEASE_TAG}" artifacts dist/npm | |
| # `secrets` is not an available context in a step-level `if`, so the token | |
| # is mapped to env and the guard reads that. Without the guard, a fork or a | |
| # repo that has not configured the token fails the whole release. | |
| - name: Publish to npm | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| if: ${{ env.NODE_AUTH_TOKEN != '' }} | |
| run: bash packaging/npm/publish.sh dist/npm | |
| # `!cancelled()` on every PyPI step, not just the upload. v0.1.3 had the | |
| # guard on the upload alone: npm failed, the build step was skipped as a | |
| # normal downstream skip, and the upload then ran and died on | |
| # "Cannot find file dist/pypi/*.whl". A guard on the last step of a chain | |
| # protects nothing. | |
| - uses: actions/setup-python@v5 | |
| if: ${{ !cancelled() }} | |
| with: | |
| python-version: "3.12" | |
| - name: Build PyPI wheels | |
| if: ${{ !cancelled() }} | |
| run: python3 packaging/pypi/build.py "${RELEASE_TAG}" artifacts dist/pypi | |
| # `if: always()` because npm and PyPI are independent registries and a | |
| # failure at one is not a reason to skip the other. v0.1.2 published five | |
| # npm packages, tripped npm's spam heuristic on the sixth, and PyPI never | |
| # ran at all — one registry's flakiness took the whole release with it. | |
| - name: Publish to PyPI | |
| env: | |
| TWINE_USERNAME: __token__ | |
| TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} | |
| # `secrets` is not an available context in a step-level `if` — hence env. | |
| if: ${{ !cancelled() && env.TWINE_PASSWORD != '' }} | |
| run: | | |
| set -euo pipefail | |
| python3 -m pip install --quiet twine | |
| python3 -m twine check dist/pypi/*.whl | |
| # --skip-existing so a re-run after a partial failure is safe; PyPI | |
| # rejects a repeated version outright and would fail the retry. | |
| python3 -m twine upload --skip-existing dist/pypi/*.whl |