Skip to content

Commit 6b73802

Browse files
feat(release): goreleaser + tag-driven release workflow + install.sh (#18)
Closes CLI-MCP-13R2 (BugBash QA round 2 strategic gap): the CLI had no release pipeline. `go install` was the only documented install path, which pins users to whatever HEAD happens to be and requires a Go toolchain. Every other backend service in instanode.dev auto-builds on push (CLAUDE.md rule 15) — the CLI was the odd one out. This PR closes the gap with three coordinated pieces: * `.goreleaser.yml` — cross-compiles darwin/linux × amd64/arm64 + windows amd64. Binaries are ldflag-stamped with the tag version, short commit SHA, and UTC build time (matches Makefile's `make build` so `instant --version` keeps the rule-14 build-SHA gate workable on release binaries). SBOM (CycloneDX via syft) ships alongside each archive; checksums.txt is signed by sigstore cosign via keyless OIDC. * `.github/workflows/release.yml` — fires on `v*.*.*` tag push, runs goreleaser, signs, publishes to the GitHub Release page. Third-party actions are SHA-pinned per CSO supply-chain policy (goreleaser-action / cosign-installer / sbom-action); first-party `actions/checkout` and `actions/setup-go` use `@v6` to match the rest of the repo. `GITHUB_TOKEN` is the per-job repo-scoped token — no long-lived PAT. * `install.sh` — POSIX sh (not bash) so curl-pipe-sh runs on Debian/Ubuntu (dash), macOS (bash), Alpine (busybox) without surprises. Detects OS/arch via uname, resolves latest release via GitHub API (or honors `INSTANT_VERSION=v0.2.0`), downloads the matching tar.gz, verifies SHA-256 against the release's `checksums.txt`, and installs to `/usr/local/bin` (override via `INSTANT_INSTALL_DIR`). Sudo is requested explicitly only when the target dir isn't writable. Windows users are pointed at the release page `.zip`. shellcheck-clean. README updated: `curl -sSfL https://instanode.dev/install.sh | sh` is the primary path; `go install` is the fallback. Verification (local): goreleaser check — config valid against GoReleaser v2.16 goreleaser release --snapshot --skip=sign,sbom,publish — all 5 archives build clean sh -n install.sh — POSIX syntax OK shellcheck install.sh — zero warnings make ci — green bin/instant --version — `0.0.1-next (4cd4678, …)` confirms ldflag stamping works end-to-end Follow-up (not in this PR): * Cut `v0.2.0` tag after merge to fire the first release. * Wire `https://instanode.dev/install.sh` to redirect to the raw GitHub script. Tracked separately so this PR doesn't block on instanode-web push permissions. * Homebrew / Scoop / apt taps — when there's signal that one-liner install isn't enough. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 67af56f commit 6b73802

5 files changed

Lines changed: 464 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: release
2+
3+
# CLI-MCP-13R2 — release pipeline for the `instant` CLI.
4+
#
5+
# Fires on a semver tag push (`v*.*.*`). Cross-compiles via GoReleaser,
6+
# generates SBOMs, signs the checksum file with sigstore cosign (keyless
7+
# OIDC), and publishes everything to the GitHub Release page.
8+
#
9+
# Why tag-driven instead of branch-driven: the other backend services in
10+
# instanode.dev auto-deploy on every push to `master` (CLAUDE.md rule 15),
11+
# but a CLI binary has a different shape — users install once and pin to
12+
# the latest published release. Tagging is the canonical "this is a real
13+
# release, not a transient build" signal.
14+
15+
on:
16+
push:
17+
tags:
18+
- "v*.*.*"
19+
20+
# Default permissions are read-only. Each job grants the minimum scope it
21+
# needs. `id-token: write` is required for sigstore keyless signing via
22+
# the GitHub OIDC issuer.
23+
permissions:
24+
contents: read
25+
26+
jobs:
27+
goreleaser:
28+
name: build, sign, publish
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 30
31+
permissions:
32+
contents: write # publish artifacts to the release page
33+
id-token: write # sigstore OIDC for keyless signing
34+
attestations: write # SBOM attestation
35+
steps:
36+
# Full history + tags are required so GoReleaser can read the tag
37+
# message and infer changelog scope.
38+
- name: Checkout (full history + tags)
39+
uses: actions/checkout@v6
40+
with:
41+
fetch-depth: 0
42+
fetch-tags: true
43+
44+
- name: Setup Go
45+
uses: actions/setup-go@v6
46+
with:
47+
go-version-file: go.mod
48+
49+
# Third-party actions are PINNED to a commit SHA per CSO supply-chain
50+
# policy. Renovate / Dependabot manages bumps; never use a floating
51+
# tag in this workflow.
52+
- name: Install cosign (sigstore)
53+
# pinned: tag v3.7.0
54+
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac
55+
with:
56+
cosign-release: 'v2.4.1'
57+
58+
- name: Install syft (SBOM)
59+
# pinned: tag v0.20.0
60+
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610
61+
62+
- name: Run GoReleaser
63+
# pinned: tag v6.4.0
64+
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
65+
with:
66+
distribution: goreleaser
67+
version: "~> v2"
68+
args: release --clean
69+
env:
70+
# GITHUB_TOKEN is the per-job, repo-scoped, short-lived token —
71+
# NOT a long-lived PAT. GoReleaser uses it to upload the
72+
# release artifacts to the same repo.
73+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ bin/
33

44
# Internal Claude Code skills
55
.claude/
6+
7+
# GoReleaser snapshot/release artifacts
8+
dist/

.goreleaser.yml

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# .goreleaser.yml — release pipeline for the `instant` CLI.
2+
#
3+
# CLI-MCP-13R2 (BugBash QA round 2 strategic gap): the CLI had no release
4+
# workflow. Every other backend service in instanode.dev auto-builds on push
5+
# (CLAUDE.md rule 15); the CLI's only install path was `go install`, which
6+
# requires a Go toolchain and pins the user to whatever HEAD happens to be.
7+
#
8+
# This config + .github/workflows/release.yml + install.sh together close
9+
# that gap:
10+
#
11+
# * Tagging `vX.Y.Z` on master triggers cross-compiled builds for
12+
# darwin / linux / windows × amd64 / arm64.
13+
# * Binaries are stamped with the tag's version, the commit SHA, and the
14+
# UTC build time (matches the Makefile's ldflag scheme — CLAUDE.md rule
15+
# 14 build-SHA gate still applies via `instant --version`).
16+
# * `instant_v0.2.0_darwin_arm64.tar.gz` etc. land on the GitHub release
17+
# page; `checksums.txt` is signed by sigstore cosign (keyless OIDC).
18+
# * `install.sh` curl-pipe-sh fetches the right archive for the user's
19+
# platform.
20+
#
21+
# We intentionally do NOT publish to Homebrew / Scoop / apt yet — that's a
22+
# follow-up PR. This release pipeline keeps the dependency footprint to:
23+
# - goreleaser (pinned action SHA in release.yml)
24+
# - syft (SBOM, pinned)
25+
# - cosign (signing, pinned)
26+
# Everything else is removed.
27+
28+
version: 2
29+
30+
project_name: instant
31+
32+
before:
33+
hooks:
34+
- go mod tidy
35+
36+
builds:
37+
- id: instant
38+
binary: instant
39+
main: ./
40+
env:
41+
- CGO_ENABLED=0
42+
goos:
43+
- linux
44+
- darwin
45+
- windows
46+
goarch:
47+
- amd64
48+
- arm64
49+
# No 32-bit / mips targets — the agent surface is `INSTANT_API_URL` over
50+
# HTTPS, which is hard to use from constrained devices anyway.
51+
ignore:
52+
# Windows on ARM64 ships, but it's a niche; revisit if anyone files
53+
# an issue. Linux/arm64 + Darwin/arm64 cover the GH Actions runner
54+
# matrix and Apple Silicon developer laptops.
55+
- goos: windows
56+
goarch: arm64
57+
# ldflags mirror the Makefile's `make build` target so the release
58+
# binary's `--version` line matches the source of truth. CLAUDE.md
59+
# rule 14 (build-SHA gate) reads from these via `instant --version`.
60+
ldflags:
61+
- -s -w
62+
- -X main.Version={{.Version}}
63+
- -X main.Commit={{.ShortCommit}}
64+
- -X main.BuildTime={{.Date}}
65+
66+
archives:
67+
- id: instant-archive
68+
# GoReleaser v2 renamed `name_template` keys — the format below is the
69+
# v2 canonical layout: `<project>_<version>_<os>_<arch>`. install.sh
70+
# depends on this exact pattern.
71+
name_template: >-
72+
{{ .ProjectName }}_{{ .Version }}_
73+
{{- if eq .Os "darwin" }}darwin
74+
{{- else if eq .Os "linux" }}linux
75+
{{- else if eq .Os "windows" }}windows{{ end }}_
76+
{{- .Arch }}
77+
format_overrides:
78+
- goos: windows
79+
formats:
80+
- zip
81+
formats:
82+
- tar.gz
83+
files:
84+
- LICENSE
85+
- README.md
86+
87+
checksum:
88+
name_template: "checksums.txt"
89+
algorithm: sha256
90+
91+
# Sigstore keyless signing via GitHub OIDC. The release workflow grants
92+
# `id-token: write` so cosign can mint a short-lived cert from Fulcio. No
93+
# private keys to manage. Verification:
94+
#
95+
# cosign verify-blob \
96+
# --certificate-identity-regexp 'https://github.com/InstaNode-dev/cli/.github/workflows/release.yml@.*' \
97+
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
98+
# --signature checksums.txt.sig \
99+
# --certificate checksums.txt.pem \
100+
# checksums.txt
101+
signs:
102+
- id: cosign-checksums
103+
cmd: cosign
104+
artifacts: checksums
105+
signature: "${artifact}.sig"
106+
certificate: "${artifact}.pem"
107+
args:
108+
- sign-blob
109+
- "--output-signature=${signature}"
110+
- "--output-certificate=${certificate}"
111+
- "${artifact}"
112+
- --yes
113+
output: true
114+
115+
# SBOM generation (CycloneDX via syft). Lands alongside binaries on the
116+
# release page. cyclonedx is the CSO/CISA-preferred format.
117+
sboms:
118+
- id: instant-sbom
119+
artifacts: archive
120+
121+
release:
122+
github:
123+
owner: InstaNode-dev
124+
name: cli
125+
draft: false
126+
prerelease: auto
127+
name_template: "v{{.Version}}"
128+
header: |
129+
`instant` CLI release {{.Version}}.
130+
131+
## Install (one-liner)
132+
133+
```bash
134+
curl -sSfL https://instanode.dev/install.sh | sh
135+
```
136+
137+
Or download the archive for your platform from the assets below and
138+
drop the binary on `$PATH`.
139+
140+
## Verify the release
141+
142+
Checksums are signed with sigstore cosign (keyless OIDC). To verify:
143+
144+
```bash
145+
cosign verify-blob \
146+
--certificate-identity-regexp 'https://github.com/InstaNode-dev/cli/.github/workflows/release.yml@.*' \
147+
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
148+
--signature checksums.txt.sig \
149+
--certificate checksums.txt.pem \
150+
checksums.txt
151+
```
152+
153+
snapshot:
154+
version_template: "{{ incpatch .Version }}-next"
155+
156+
changelog:
157+
sort: asc
158+
filters:
159+
exclude:
160+
- '^docs:'
161+
- '^chore:'
162+
- '^test:'
163+
- '^ci:'
164+
- Merge pull request

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,29 @@ Zero-friction infrastructure CLI for [instanode.dev](https://instanode.dev).
44

55
## Install
66

7+
Pre-built binaries for darwin / linux × amd64 / arm64 (the curl-pipe-sh
8+
script auto-detects your platform):
9+
10+
```bash
11+
curl -sSfL https://instanode.dev/install.sh | sh
12+
```
13+
14+
The installer downloads the latest release archive from
15+
[GitHub Releases](https://github.com/InstaNode-dev/cli/releases), verifies
16+
its SHA-256 against the signed `checksums.txt`, and drops the binary at
17+
`/usr/local/bin/instant`. Set `INSTANT_INSTALL_DIR=$HOME/.local/bin` to
18+
avoid sudo; set `INSTANT_VERSION=v0.2.0` to pin a specific release.
19+
20+
Or, with a Go toolchain already installed:
21+
722
```bash
823
go install github.com/InstaNode-dev/cli@latest
924
```
1025

26+
Windows users: download the `.zip` from the
27+
[releases page](https://github.com/InstaNode-dev/cli/releases) and add
28+
`instant.exe` to your `PATH`.
29+
1130
## Usage
1231

1332
Every provisioning command requires a `--name` flag. The name must be 1–64

0 commit comments

Comments
 (0)