Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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
Expand All @@ -12,11 +22,16 @@ permissions:
# 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:
Expand All @@ -32,11 +47,61 @@ jobs:
- 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:
Expand All @@ -47,6 +112,7 @@ jobs:
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
Expand All @@ -73,7 +139,7 @@ jobs:
registry-url: "https://registry.npmjs.org"

- name: Build npm packages
run: node packaging/npm/build.mjs "${GITHUB_REF_NAME}" artifacts dist/npm
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
Expand All @@ -96,7 +162,7 @@ jobs:

- name: Build PyPI wheels
if: ${{ !cancelled() }}
run: python3 packaging/pypi/build.py "${GITHUB_REF_NAME}" artifacts dist/pypi
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
Expand Down
26 changes: 22 additions & 4 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ archives:
checksum:
name_template: "checksums.txt"

release:
# Re-running a release must be safe. The default `keep-existing` leaves a
# half-published release from a failed run in place, so the retry cannot
# correct it; `replace` makes the tag's assets match whatever this run built.
mode: replace

snapshot:
version_template: "{{ incpatch .Version }}-next"

Expand All @@ -50,18 +56,30 @@ changelog:
- "Merge pull request"

homebrew_casks:
- repository:
# skip_upload is driven by the workflow's tap-credential probe.
#
# goreleaser treats a failed cask push as fatal, and the tap PATs expired
# seven months after they were minted. On v0.2.0 that 401 took the ENTIRE
# release with it: npm, PyPI and every step after goreleaser never ran, over
# a credential that only affects two optional install channels. A dead tap
# token now degrades the release instead of ending it.
#
# envOrDefault, not .Env: an unset secret must not fail config loading.
- skip_upload: '{{ envOrDefault "SKIP_HOMEBREW" "false" }}'
repository:
owner: ModelsLab
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
token: '{{ envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "unset" }}'
homepage: https://modelslab.sh
description: "ModelsLab CLI — AI generation and account management from the terminal"

scoops:
- repository:
# See the note on homebrew_casks above.
- skip_upload: '{{ envOrDefault "SKIP_SCOOP" "false" }}'
repository:
owner: ModelsLab
name: scoop-bucket
token: "{{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }}"
token: '{{ envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "unset" }}'
homepage: https://modelslab.sh
description: "ModelsLab CLI — AI generation and account management from the terminal"
license: MIT
Expand Down
Loading