Skip to content

feat: publish prebuilt CLI binaries with GoReleaser - #72

Merged
jferrl merged 3 commits into
mainfrom
feat/goreleaser
Sep 19, 2026
Merged

jferrl merged 3 commits into
mainfrom
feat/goreleaser

Conversation

@jferrl

@jferrl jferrl commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Description

The CLI could only be installed with a Go toolchain, which excludes the people it was
written for: a CI step or a shell script that wants an installation token without building
anything.

A tagged release now produces archives for Linux, macOS and Windows on amd64 and arm64,
plus checksums.txt. Verified with a local snapshot build before opening this:

dist/githubauth_1.8.1-next_darwin_amd64.tar.gz    3.0 MB
dist/githubauth_1.8.1-next_darwin_arm64.tar.gz    2.8 MB
dist/githubauth_1.8.1-next_linux_amd64.tar.gz     3.0 MB
dist/githubauth_1.8.1-next_linux_arm64.tar.gz     2.7 MB
dist/githubauth_1.8.1-next_windows_amd64.zip      3.1 MB
dist/githubauth_1.8.1-next_windows_arm64.zip      2.7 MB
dist/checksums.txt

The extracted binary reports its stamped version and mints a JWT; shasum -c checksums.txt passes; goreleaser check validates the config.

v2 tags cannot trigger a release

The /v2 module path was published by accident and is permanently retracted, but
v2.0.1 and v2.0.2 have to stay in the repository because v2.0.2 is what carries the
retract block. That means a v2 tag can always be re-pushed by mistake, and a release
built from one would publish binaries for a module path go get cannot resolve.

The workflow skips them:

Tag pushed Result
v1.9.0 release
v2.0.1, v2.0.2, v2.1.0 skipped
v3.0.0 release

Guarding on v2. rather than allowing only v1. means a future major version works
without editing the workflow, while the retracted path stays blocked.

The version fix

githubauth version read the module version out of the build info. A binary built from a
checkout — which is what GoReleaser produces — does not carry one, so every downloaded
release would have reported a pseudo-version.

Build Reports
Release build v1.9.0 (the tag, via -ldflags)
go install ...@v1.9.0 v1.9.0 (module version from build info)
go build from a checkout v1.8.1-0.2026...-69b5dd9+dirty
No build info at all (devel)

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)

Testing

  • New and existing unit tests pass locally with my changes
  • I have tested this change manually

go test ./... passes, golangci-lint run ./... reports 0 issues, goreleaser check
validates, and a full goreleaser release --snapshot --clean was run and its output
exercised.

Documentation

  • I have updated the documentation accordingly
  • I have updated the CHANGELOG.md file
  • I have added/updated code comments where necessary

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have checked my code and corrected any misspellings
  • My changes are compatible with the Go versions in go.mod and the CI matrix

Homebrew

brew install jferrl/tap/githubauth

A cask, not a formula. brews: is deprecated in GoReleaser v2, and a Homebrew formula
is expected to build from source, which is the opposite of shipping a prebuilt binary. The
cask carries a postflight hook clearing the quarantine attribute, since the binary is
not notarized and Gatekeeper would otherwise refuse to run it.

jferrl/homebrew-tap has been created and seeded
with a README, so it has a default branch for GoReleaser to push to.

One thing needed before the first release

Pushing the cask writes to that other repository, which this workflow's GITHUB_TOKEN
cannot reach. It needs a secret named HOMEBREW_TAP_GITHUB_TOKEN on this repository — a
token with contents: write on jferrl/homebrew-tap.

A failed tap push fails the entire release, binaries included, so skip_upload is
templated on the token being present:

skip_upload: '{{ if eq (index .Env "HOMEBREW_TAP_GITHUB_TOKEN") "" }}true{{ else }}false{{ end }}'

Without the secret the release still publishes the archives and checksums and skips only
the cask. Verified: goreleaser check passes both with and without the variable set, and
a snapshot build generates the cask either way.

The CLI could only be installed with a Go toolchain, which excludes
the people it was written for: a CI step or a shell script that wants
an installation token without building anything.

A tagged release now produces archives for Linux, macOS and Windows on
amd64 and arm64, plus checksums.txt. Verified locally with a snapshot
build: six archives, the extracted binary mints a JWT, and the
checksums check out.

The release workflow skips any v2 tag. The /v2 module path was
published by accident and is permanently retracted, but v2.0.1 and
v2.0.2 have to stay in the repository because v2.0.2 carries the
retract block, so those tags can always be re-pushed by mistake. A
release built from one would publish binaries for a module path that
go get cannot resolve.

Also fixes the version the CLI reports. It read the module version
from the build info, which a release binary does not carry, so every
downloaded binary would have claimed a pseudo-version. The release
build now stamps the tag through -ldflags, and a build from a checkout
falls back to the commit.
@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 41.17647% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.96%. Comparing base (69b5dd9) to head (fc7fad3).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
cmd/githubauth/main.go 41.17% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #72      +/-   ##
==========================================
- Coverage   98.88%   96.96%   -1.92%     
==========================================
  Files           5        5              
  Lines         447      462      +15     
==========================================
+ Hits          442      448       +6     
- Misses          5       14       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

brew install is what reaches the people the CLI was written for: the
ones who will never run go install. A tagged release now writes a cask
into jferrl/homebrew-tap.

    brew install jferrl/tap/githubauth

A cask rather than a formula: brews is deprecated in GoReleaser v2,
and Homebrew expects a formula to build from source, which is the
opposite of shipping a prebuilt binary. The cask carries a postflight
hook that clears the quarantine attribute, because the binary is not
notarized and Gatekeeper would otherwise refuse to run it.

Pushing the cask writes to another repository, which the workflow's
GITHUB_TOKEN cannot reach, so it needs HOMEBREW_TAP_GITHUB_TOKEN. A
failed tap push fails the entire release, binaries included, so
skip_upload is templated on the token being present: without the
secret the release still publishes everything else and skips only the
cask.
A missing or empty HOMEBREW_TAP_GITHUB_TOKEN skips the cask rather
than failing the release, which is the right trade: archives and
checksums still publish. It is also invisible, and a secret cannot be
read back to check it, so "brew install" quietly staying on an old
version would be the first symptom.

The run now states which of the two happened, and annotates a warning
when the token is missing.
@jferrl
jferrl merged commit 2d94e7b into main Sep 19, 2026
8 of 10 checks passed
@jferrl
jferrl deleted the feat/goreleaser branch September 19, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant