diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a4589f1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +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 + + # 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" + 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/.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..e1ed9ed --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,105 @@ +# 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 + 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 }} + ``` + +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 c4ab6ad..5f469cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,25 @@ 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`. + +- 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 + `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..ce784fc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,14 @@ Full list on [pkg.go.dev](https://pkg.go.dev/github.com/jferrl/go-githubauth?tab 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: + ```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