Skip to content

fix(release): publish with relative paths, and stop skipping the whee… #5

fix(release): publish with relative paths, and stop skipping the whee…

fix(release): publish with relative paths, and stop skipping the whee… #5

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
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
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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
- uses: actions/setup-go@v5
with:
go-version: "1.26"
- 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 }}
# 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 "${GITHUB_REF_NAME}" 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 "${GITHUB_REF_NAME}" 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