From 0b56e29c33841506764f024a5032e00f01c7adab Mon Sep 17 00:00:00 2001 From: jferrl Date: Sat, 19 Sep 2026 13:24:08 +0200 Subject: [PATCH 1/3] feat: publish prebuilt CLI binaries with GoReleaser 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. --- .github/workflows/release.yml | 38 ++++++++++++++++ .gitignore | 3 ++ .goreleaser.yaml | 83 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 10 +++++ README.md | 4 +- cmd/githubauth/main.go | 40 +++++++++++++++-- 6 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5ade12e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: release + +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + goreleaser: + # The /v2 module path was published by accident and is permanently + # retracted (see CHANGELOG). The v2.0.1 and v2.0.2 tags have to stay in the + # repository because v2.0.2 is what carries the retract block, so a v2 tag + # can always be re-pushed by accident. Never build a release from one. + if: ${{ !startsWith(github.ref_name, 'v2.') }} + runs-on: ubuntu-latest + permissions: + # goreleaser creates the GitHub release and uploads the archives. + contents: write + steps: + - uses: actions/checkout@v7 + with: + # goreleaser builds the changelog from history, so a shallow + # checkout would produce an empty one. + fetch-depth: 0 + + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + + - uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 99214c6..26a0c48 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # Claude Code local settings and scratch worktrees .claude/ + +# GoReleaser build output +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..974875f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,83 @@ +# Release config for the githubauth CLI. The library is consumed with go get and +# is unaffected by anything here. +# +# Run "goreleaser release --snapshot --clean" to build locally without publishing. +version: 2 + +project_name: githubauth + +before: + hooks: + - go mod tidy + +builds: + - id: githubauth + main: ./cmd/githubauth + binary: githubauth + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + flags: + - -trimpath + ldflags: + # buildVersion is the CLI's own variable; without it a release binary + # would fall back to the build info and report a pseudo-version. + - -s -w -X main.buildVersion={{ .Version }} + +archives: + - id: githubauth + formats: + - tar.gz + format_overrides: + - goos: windows + formats: + - zip + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + files: + - README.md + - LICENSE + +checksum: + name_template: checksums.txt + +snapshot: + version_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" + - "^ci:" + - "Merge pull request" + +release: + footer: | + ## Installing the CLI + + Download the archive for your platform above, or: + + ```bash + go install github.com/jferrl/go-githubauth/cmd/githubauth@{{ .Tag }} + ``` + + Verify a download against `checksums.txt`: + + ```bash + sha256sum -c checksums.txt --ignore-missing + ``` + + The library is unaffected by these artifacts: + + ```bash + go get github.com/jferrl/go-githubauth@{{ .Tag }} + ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ab6ad..40b2c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 It adds no module dependencies. The CLI is built on the standard library's `flag`, so the two-dependency footprint is unchanged for anyone importing the library. +- Prebuilt `githubauth` binaries on each release, for Linux, macOS and Windows on amd64 + and arm64, with a `checksums.txt` to verify them. A tagged release builds them with + GoReleaser; the library is unaffected and is still consumed with `go get`. + +### Fixed + +- `githubauth version` reported a pseudo-version for a binary that was not installed with + `go install`. A release build now carries its tag, and a build from a checkout falls + back to the stamped commit. + ## [v1.8.0] - 2026-09-18 A correctness release. Two changes alter behaviour callers may depend on; both diff --git a/README.md b/README.md index 485137c..ef1a939 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,9 @@ Full list on [pkg.go.dev](https://pkg.go.dev/github.com/jferrl/go-githubauth?tab ## Command line -The same credentials, without writing Go: +The same credentials, without writing Go. Download a binary from the +[latest release](https://github.com/jferrl/go-githubauth/releases/latest) — Linux, macOS +and Windows, on amd64 and arm64 — or build it yourself: ```bash go install github.com/jferrl/go-githubauth/cmd/githubauth@latest diff --git a/cmd/githubauth/main.go b/cmd/githubauth/main.go index 59db2b2..fbb1cc1 100644 --- a/cmd/githubauth/main.go +++ b/cmd/githubauth/main.go @@ -317,14 +317,46 @@ func envInt64(key string) int64 { return n } -// version reports the module version go install stamped, so a built binary -// can say where it came from without a linker flag. +// buildVersion is set by the release build with -ldflags. It stays empty for +// a binary built any other way, which then falls back to the build info. +var buildVersion string + +// version reports where the binary came from. A release build carries the tag +// in buildVersion; go install stamps the module version into the build info; +// a local build has neither and reports the revision instead. func version() string { + if buildVersion != "" { + return buildVersion + } + info, ok := debug.ReadBuildInfo() - if !ok || info.Main.Version == "" { + if !ok { + return "(unknown)" + } + if v := info.Main.Version; v != "" && v != "(devel)" { + return v + } + + // A build from a checkout: report the commit Go stamped, if any. + var revision, modified string + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + case "vcs.modified": + modified = s.Value + } + } + if revision == "" { return "(devel)" } - return info.Main.Version + if len(revision) > 12 { + revision = revision[:12] + } + if modified == "true" { + revision += "-dirty" + } + return "(devel) " + revision } // nearest suggests a command for a typo, matching on a shared prefix or a From 1240d11b08184cc6f605a56f70b9cb4ad36767ab Mon Sep 17 00:00:00 2001 From: jferrl Date: Sat, 19 Sep 2026 13:28:28 +0200 Subject: [PATCH 2/3] feat: publish a Homebrew cask for the CLI 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. --- .github/workflows/release.yml | 4 ++++ .goreleaser.yaml | 24 +++++++++++++++++++++++- CHANGELOG.md | 9 +++++++++ README.md | 8 +++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ade12e..0a387e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,3 +36,7 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Pushing the Homebrew cask writes to jferrl/homebrew-tap, which the + # job's GITHUB_TOKEN cannot reach. Without this secret the release + # still publishes; only the cask is skipped. + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 974875f..e1ed9ed 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -13,7 +13,6 @@ before: builds: - id: githubauth main: ./cmd/githubauth - binary: githubauth env: - CGO_ENABLED=0 goos: @@ -81,3 +80,26 @@ release: ```bash go get github.com/jferrl/go-githubauth@{{ .Tag }} ``` + +homebrew_casks: + - name: githubauth + # A failed tap push fails the whole release, binaries included. If the + # token is not set, publish everything else and skip only the cask. + skip_upload: '{{ if eq (index .Env "HOMEBREW_TAP_GITHUB_TOKEN") "" }}true{{ else }}false{{ end }}' + repository: + owner: jferrl + name: homebrew-tap + token: '{{ index .Env "HOMEBREW_TAP_GITHUB_TOKEN" }}' + homepage: https://github.com/jferrl/go-githubauth + description: GitHub App authentication for your shell - installation tokens, JWTs and PATs + license: MIT + commit_author: + name: jferrl + email: jorgeferrerolinacero@gmail.com + hooks: + post: + # The binary is not notarized, so Gatekeeper would quarantine it. + install: | + if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/githubauth"] + end diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b2c70..5f469cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and arm64, with a `checksums.txt` to verify them. A tagged release builds them with GoReleaser; the library is unaffected and is still consumed with `go get`. +- A Homebrew cask, so the CLI installs without a Go toolchain: + + ```bash + brew install jferrl/tap/githubauth + ``` + + The cask is generated into [jferrl/homebrew-tap](https://github.com/jferrl/homebrew-tap) + when a release is tagged. + ### Fixed - `githubauth version` reported a pseudo-version for a binary that was not installed with diff --git a/README.md b/README.md index ef1a939..ce784fc 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,13 @@ Full list on [pkg.go.dev](https://pkg.go.dev/github.com/jferrl/go-githubauth?tab ## Command line -The same credentials, without writing Go. Download a binary from the +The same credentials, without writing Go: + +```bash +brew install jferrl/tap/githubauth +``` + +Or download a binary from the [latest release](https://github.com/jferrl/go-githubauth/releases/latest) — Linux, macOS and Windows, on amd64 and arm64 — or build it yourself: From fc7fad3dc7380998f43a98967ce96512e6783117 Mon Sep 17 00:00:00 2001 From: jferrl Date: Sat, 19 Sep 2026 13:37:20 +0200 Subject: [PATCH 3/3] ci: say in the log whether the Homebrew cask will publish 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. --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a387e3..a4589f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,19 @@ jobs: with: go-version-file: go.mod + # A missing or empty tap token skips the cask instead of failing the + # release, which is the right trade but is otherwise invisible. Say so in + # the log, because a secret cannot be read back to check it. + - name: Report whether the Homebrew cask will publish + env: + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + run: | + if [ -z "$HOMEBREW_TAP_GITHUB_TOKEN" ]; then + echo "::warning::HOMEBREW_TAP_GITHUB_TOKEN is unset or empty. Archives and checksums will publish; the Homebrew cask will be skipped." + else + echo "HOMEBREW_TAP_GITHUB_TOKEN is present. The cask will be pushed to jferrl/homebrew-tap." + fi + - uses: goreleaser/goreleaser-action@v6 with: version: "~> v2"