Skip to content

feat: release with GoReleaser and streamline installation - #2

Open
cardoe wants to merge 4 commits into
mainfrom
claude/understackctl-repo-migration-xiky48
Open

feat: release with GoReleaser and streamline installation#2
cardoe wants to merge 4 commits into
mainfrom
claude/understackctl-repo-migration-xiky48

Conversation

@cardoe

@cardoe cardoe commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Now that understackctl lives in its own repository, this replaces the hand-rolled Makefile release path with GoReleaser and makes installing the binary a one-liner on Linux, macOS and Windows.

Why

The Makefile reimplemented cross-compilation, per-OS archive formats and checksums in shell, and the result had some rough edges for anyone installing a release:

  • Archives contained a nested directory (understackctl_linux_amd64/understackctl), so tar xzf handed you a folder rather than something runnable, and shipped no LICENSE.
  • No -trimpath, so absolute build paths were baked into every binary.
  • go install builds reported version dev (unknown), which is useless in a bug report.
  • macOS users — most of the audience — had no brew path and had to know their own CPU architecture.
  • The release job requested id-token: write without using it, and apt-get installed sed and grep, which are already on every runner.
  • Nothing verified the release pipeline until tag time, when a break can only be fixed with another tag.

What's in it

Distribution.goreleaser.yaml builds linux/darwin/windows on amd64/arm64, drops the 386 targets, and adds a darwin_all universal binary so macOS users cannot pick the wrong archive. Archives now hold the binary at their root alongside LICENSE, README.md and generated shell completions. -trimpath plus a commit-pinned mod_timestamp make builds reproducible.

Installationinstall.sh detects OS/arch, verifies the SHA-256 checksum before installing, picks a writable install directory, and warns when it is not on PATH. BASE_URL points it at an internal mirror for restricted-egress environments. The README now documents every install path, checksum and provenance verification, completion setup per shell, and which external tools each command group needs.

Version reporting — new internal/version resolves the version from linker flags, falling back to debug.ReadBuildInfo() so go install ...@v0.1.0 reports a real version. It also normalises the v prefix that GoReleaser's .Version strips, so every install method reports the same string. Adds understackctl version [-o json]; --version keeps working.

CI — the release workflow uses goreleaser-action and attests build provenance via actions/attest, so archives can be checked with gh attestation verify. It accepts pre-release tags (v0.1.0-rc.1), which prerelease: auto keeps from being marked "latest". A new Release Build workflow validates the config, cross-compiles every published target and asserts the archive layout on each pull request. Go Tests now runs on Linux, macOS and Windows — we publish binaries for three platforms, and testing on one made that an aspirational claim.

Also reduces the Makefile to a developer wrapper (make build|test|lint|fmt-check|snapshot|check), stops .editorconfig from imposing spaces on Go sources, and gofmts the two files that drift had already broken.

Verification

The full pipeline was run locally with GoReleaser v2.18.1 — 6 cross-compiled targets, universal binary, 7 archives, checksums, Homebrew cask and Scoop manifest, all inspected.

install.sh was exercised end-to-end against those artifacts over a local HTTP server, including the failure paths:

Case Result
Happy path downloads, verifies checksum, installs, reports version
Tampered archive (appended bytes) aborts on digest mismatch, installs nothing
Nonexistent version actionable 404 message
BASE_URL without VERSION rejected up front
Unlisted in checksums.txt rejected

Every pinned action SHA was resolved with git ls-remote rather than written from memory. That turned up two things worth noting: goreleaser-action is on v7 (inputs unchanged, so the bump is safe), and attest-build-provenance v4 is now a thin wrapper whose README directs new implementations at actions/attest — so this uses actions/attest with subject-checksums: dist/checksums.txt, attesting every artifact in one step. artifact-metadata: write is deliberately not granted; it is only needed for the storage record, which requires push-to-registry.

go vet, go test ./..., make fmt-check and goreleaser check are all clean.

Action needed before this helps macOS and Windows users

The Homebrew cask and Scoop manifest are generated but not published (skip_upload: true), because publishing needs repositories that do not exist yet. A release cannot break on missing infrastructure, but the brew and scoop install paths stay unavailable until someone:

  1. Creates rackerlabs/homebrew-tap and rackerlabs/scoop-bucket.
  2. Adds PATs with contents: write on those repos as HOMEBREW_TAP_TOKEN / SCOOP_BUCKET_TOKEN — the default GITHUB_TOKEN cannot push to another repository.
  3. Uncomments the matching env: lines in .github/workflows/release.yaml.
  4. Sets skip_upload: false.

The full checklist is inline in .goreleaser.yaml. The README marks both paths as not-yet-available so nobody follows instructions that cannot work.

Per the earlier discussion, the first standalone tag is intended to be v0.1.0. Linux .deb/.rpm packages were deliberately skipped, as were man pages (they would pull in go-md2man).

Also included: AUDIT.md

The first commit adds the repo-split audit that prompted this work. It is a working document meant to be triaged into issues and then deleted, not permanent documentation — happy to drop it from this PR if you would rather it lived somewhere else.

Its remaining findings are not addressed here. The most important one is that device-type validate|add and flavor validate|add resolve their JSON schemas via relative paths that only exist inside the monorepo checkout, so those four commands cannot work from any binary this pipeline produces. That plus the KUBECONFIG handling and the password-rotation bug in loadOrGenSecret are the natural next PR.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Tr9ob3LqrbsZL2rpJehEr3


Generated by Claude Code

cardoe and others added 4 commits September 6, 2026 03:29
Audit of understackctl as a standalone CLI, covering migration blockers,
cross-platform distribution and installation, platform correctness, CLI
usability, code quality and repo hygiene.

Intended to be triaged into issues and then removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr9ob3LqrbsZL2rpJehEr3
Replaces the hand-rolled Makefile release path with GoReleaser and makes
installing understackctl a one-liner on Linux, macOS and Windows.

Distribution:
- .goreleaser.yaml builds linux/darwin/windows on amd64/arm64, plus a
  darwin_all universal binary so macOS users cannot pick the wrong archive.
  Drops the 386 targets nobody runs.
- Archives now hold the binary at their root alongside LICENSE, README and
  generated shell completions. Previously `tar xzf` produced a directory and
  shipped no license.
- Adds -trimpath and a commit-pinned mod_timestamp for reproducible builds.
- Homebrew cask and Scoop manifest are generated but not published
  (skip_upload: true) until rackerlabs/homebrew-tap and
  rackerlabs/scoop-bucket exist; the enablement checklist is inline.

Installation:
- install.sh detects OS/arch, verifies the SHA-256 checksum before
  installing, picks a writable install dir, and warns when it is not on
  PATH. BASE_URL points it at an internal mirror for restricted-egress
  environments.
- README documents every install path, checksum and provenance
  verification, shell completion setup, and the external tools each command
  group needs.

Version reporting:
- New internal/version resolves the version from linker flags, falling back
  to debug.ReadBuildInfo so `go install ...@v0.1.0` no longer reports
  "dev". Normalizes the v prefix that GoReleaser's .Version strips so all
  install methods report the same string.
- New `understackctl version [-o json]` for bug reports; --version keeps
  working.

CI:
- Release workflow uses goreleaser-action and attests build provenance, so
  archives can be checked with `gh attestation verify`. It accepts
  pre-release tags, which prerelease: auto keeps from being marked latest,
  and no longer apt-installs sed and grep.
- New Release Build workflow validates the config, cross-compiles every
  published target and asserts the archive layout on each pull request, so
  release breakage surfaces before tagging.
- Go Tests now runs on Linux, macOS and Windows rather than Linux alone.

Also reduces the Makefile to a developer wrapper, stops .editorconfig from
imposing spaces on Go sources, and gofmts the two files that drift had
already broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr9ob3LqrbsZL2rpJehEr3
As of v4, actions/attest-build-provenance is a thin wrapper over
actions/attest and its README directs new implementations at the latter.

Switches to actions/attest and feeds it dist/checksums.txt via
subject-checksums, which attests every published archive in one step
instead of globbing paths. Default behaviour is still SLSA build
provenance, so `gh attestation verify` works unchanged.

artifact-metadata: write is deliberately not granted; it is only needed for
the storage record, which requires push-to-registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr9ob3LqrbsZL2rpJehEr3
golangci-lint's errcheck flagged the text branch of `understackctl
version`, which discarded the fmt.Fprint error while the json branch
already checked its encoder error. Reproduced with the pinned
golangci-lint v2.1.2 and confirmed clean after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tr9ob3LqrbsZL2rpJehEr3
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