Skip to content

feat(release): GitHub Release workflow + install scripts + Homebrew formula (#153)#11

Merged
saadqbal merged 4 commits into
developfrom
feat/153-release-distribution
May 25, 2026
Merged

feat(release): GitHub Release workflow + install scripts + Homebrew formula (#153)#11
saadqbal merged 4 commits into
developfrom
feat/153-release-distribution

Conversation

@saadqbal
Copy link
Copy Markdown
Collaborator

@saadqbal saadqbal commented May 22, 2026

Summary

Closes the v0.1 epic (tracebloc/client#147). Phase 5 is the distribution infrastructure that makes the CLI installable without go install. After this merges, tagging v0.1.0 triggers the full release pipeline.

What lands

File Purpose
.github/workflows/release.yml Tag-triggered build/sign/publish workflow. 5-platform matrix + cosign keyless OIDC + SHA256SUMS + GH Release creation. Third job (bump-homebrew-tap) wired but gated if: false until the tap repo + HOMEBREW_TAP_TOKEN are set up.
scripts/install.sh POSIX-compatible (dash/busybox/bash) installer. OS/arch detection, SHA256 + cosign verification, /usr/local/bin install with $HOME/.local/bin fallback.
scripts/install.ps1 PowerShell 5.1+ Windows installer. Same verification flow + user-scope PATH update.
scripts/homebrew-formula.rb.tmpl Per-platform bottle-style formula; release workflow sed-renders it per tag. Ships pre-built binaries (k8s.io dep tree is too heavy for source builds on customer laptops).
scripts/RELEASE_CHECKLIST.md On-call doc separating "automated" from "one-time setup" from "manual per release."

Install one-liners (post v0.1.0 tag)

# Linux + macOS:
curl -fsSL https://github.com/tracebloc/cli/releases/latest/download/install.sh | sh

# Windows (PowerShell):
irm https://github.com/tracebloc/cli/releases/latest/download/install.ps1 | iex

# macOS via Homebrew (once tap is set up):
brew install tracebloc/tap/tracebloc

Hosting choice

GitHub Release raw URLs (per pre-build user decision). Zero infrastructure setup; the bootstrap URL works the day v0.1.0 ships. install.tracebloc.io vanity URL is documented as a v0.2 follow-up.

Verification surface

Each binary is signed with cosign keyless OIDC against this workflow's identity. install.sh / install.ps1 download .sig + .cert and verify if cosign is on PATH. SHA256SUMS is the unconditional baseline check.

cosign verify-blob \
  --certificate-identity-regexp \
    'https://github.com/tracebloc/cli/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  --certificate tracebloc-v0.1.0-linux-amd64.cert \
  --signature tracebloc-v0.1.0-linux-amd64.sig \
  tracebloc-v0.1.0-linux-amd64

Out of this PR (separate setup, documented in RELEASE_CHECKLIST.md)

  • Creating the tracebloc/homebrew-tap repo
  • HOMEBREW_TAP_TOKEN secret + flipping bump-homebrew-tap's if: gate
  • install.tracebloc.io DNS (v0.2 follow-up)
  • Tagging v0.1.0 itself (post-merge, after Phase 6 dogfood)

Test plan

  • go test -race ./... — green
  • gofmt -s -l . — no drift
  • errcheck ./... — green
  • install.sh syntax check: sh -n scripts/install.sh clean
  • Real release dry-run: push a v0.1.0-rc1 tag, verify all 5 binaries + sigs + checksums attach to the Release
  • Verify install.sh works against the rc release on a clean Linux + macOS host
  • Verify install.ps1 works against the rc release on a clean Windows host

After this merges, the v0.1 epic (#147) is mechanically complete — a git tag v0.1.0 && git push produces the full distribution.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


Note

Medium Risk
New supply-chain surface (release workflow, install scripts, signing); mistakes could ship bad binaries or weaken verification, but changes are CI/scripts only and don't alter runtime CLI logic.

Overview
Adds tag-driven distribution so customers can install the CLI without go install. Pushing a v*.*.* tag (or manual workflow_dispatch) runs a new Release workflow: five-way cross-compile, cosign keyless .sig/.cert per binary, aggregated SHA256SUMS, and a GitHub Release that bundles binaries, checksums, and install.sh / install.ps1 at releases/latest/download/. Prerelease is set when the tag contains -.

New POSIX install.sh and PowerShell install.ps1 resolve latest (or pinned) release, verify SHA256 (refuse install without a hasher), optionally verify cosign against the workflow identity, then install with sane prefix/PATH behavior. A Homebrew formula template plus a bump-homebrew-tap job (currently if: false) render per-platform SHAs and push to tracebloc/homebrew-tap once secrets exist. RELEASE_CHECKLIST.md documents automated vs one-time vs per-release steps.

Reviewed by Cursor Bugbot for commit 0d1aaf8. Bugbot is set up for automated code reviews on this repo. Configure here.

…mula (#153)

Closes the v0.1 epic (#147). Phase 5 is the distribution
infrastructure that makes the CLI installable without `go install`.

## What lands in this PR

### .github/workflows/release.yml

Triggered by `v*.*.*` tags (and workflow_dispatch for manual
re-runs). Two-job pipeline:

  - `release` (matrix): cross-compiles linux/{amd64,arm64},
    darwin/{amd64,arm64}, windows/amd64. Each binary gets a
    cosign keyless OIDC signature (.sig + .cert), per-platform
    SHA256, and a stamped version string via -ldflags.

  - `publish`: aggregates SHA256SUMS, stages the install scripts,
    creates the GitHub Release with auto-generated notes +
    every artifact attached. prerelease=true when the tag
    contains a hyphen (e.g. v0.1.0-rc1).

A third `bump-homebrew-tap` job is wired but gated `if: false`
until the tracebloc/homebrew-tap repo + HOMEBREW_TAP_TOKEN secret
are set up. The path is: flip the gate, secret is added,
formula gets updated automatically on each tag. RELEASE_CHECKLIST
documents the one-time setup.

### scripts/install.sh

POSIX-compatible (tested against dash, busybox sh, bash) — the
customer's distro might not have bash. Detects OS+arch,
resolves the latest tag via the /releases/latest redirect-trail
(no rate-limited API call), downloads binary + SHA256SUMS,
verifies the checksum, optionally verifies cosign signature if
cosign is on PATH, installs to /usr/local/bin (falls back to
~/.local/bin with PATH advice if not writable).

One-liner: `curl -fsSL https://github.com/tracebloc/cli/releases/latest/download/install.sh | sh`

### scripts/install.ps1

PowerShell 5.1+ (ships with Windows 10 21H1+). amd64 only for
v0.1; arm64 errors with a clear "file an issue" pointer.
Strict-mode + halt-on-error so half-installs don't happen.
Same SHA256 + cosign verification surface as install.sh.
Installs to $LOCALAPPDATA\Programs\tracebloc and PATH-adds at
user scope.

One-liner: `irm https://github.com/tracebloc/cli/releases/latest/download/install.ps1 | iex`

### scripts/homebrew-formula.rb.tmpl

Formula template the bump-homebrew-tap job renders per release.
Substitutes __VERSION__, __TAG__, and four per-platform
__SHA_*__ placeholders via sed (no Ruby in the runner needed).
Ships pre-built binaries rather than building from source — the
k8s.io transitive dep tree is too heavy to build on customer
laptops.

### scripts/RELEASE_CHECKLIST.md

Per-release on-call doc. Distinguishes "what's automated"
(everything in the workflow) from "what needs one-time setup"
(homebrew-tap repo, eventually install.tracebloc.io DNS) from
"what's manual per release" (the actual tag push + sanity-test
of each install path).

## Hosting choice

GitHub Release raw URLs (per user decision). Zero infrastructure
needed; the bootstrap URL works the day v0.1.0 ships. install.
tracebloc.io is a v0.2 follow-up via CNAME / Cloudflare worker.

## Out of this PR

- Creating the tracebloc/homebrew-tap repo (separate; documented
  in RELEASE_CHECKLIST.md)
- DNS for install.tracebloc.io (v0.2)
- Tagging v0.1.0 (post-merge, after the Phase 6 dogfood)

Local: vet, test -race -cover, gofmt -s, errcheck — all green.

After this merges, the v0.1 epic (#147) is mechanically complete
— `tag v0.1.0` produces the full release pipeline output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LukasWodka
Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 20 / 8

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

Comment thread scripts/install.sh
Bugbot PR #11 round 1: the previous flow printed "✓ checksum
matches" unconditionally after the verify block, even when
neither sha256sum nor shasum was on PATH (the no-tool branch
set actual="" and the mismatch check guarded on -n "$actual",
so it was silently skipped). Two failures in one:

  1. Misleading log: claimed verification succeeded when it
     didn't run.
  2. Security regression: an attacker could MITM the binary and
     the customer would never notice on a tool-missing host.

Fix: refuse to install when no SHA256 tool is available. Error
message lists the per-distro install commands so the customer
knows the fix.

Trade-off: a "warn-but-continue" mode would be friendlier on
exotic hosts, but the whole point of this script is to verify a
binary the customer pulls off the internet. "Friendly" defaults
that skip the security check are exactly the install-script
anti-pattern that's bitten the industry repeatedly. Hard error
is the right call.

`sh -n` syntax check clean.
Comment thread scripts/install.sh
Comment thread scripts/install.ps1
Two Bugbot PR #11 r2 findings, both real:

## Medium — install.ps1: failed cosign verify treated as "skip"

The previous flow's try-block contained BOTH the .sig/.cert
download AND the cosign verify + Write-Error. With
$ErrorActionPreference = 'Stop', Write-Error throws an exception
caught by the SAME catch that handled missing-sig downloads — so
a verified-failed binary went through the "couldn't download
.sig/.cert — release may pre-date signing" branch and continued
to install.

Fix: separate the two concerns. Download in a try/catch (failures
= "predates signing, skip"). Verify OUTSIDE the try via & cosign
(external process, doesn't interact with $ErrorActionPreference);
$LASTEXITCODE check + Write-Host + explicit exit 1 if non-zero.
A failed signature now correctly refuses to install.

## Medium — install.sh: custom --prefix silently ignored if missing

POSIX `test -w` returns false for nonexistent paths. The previous
flow used `[ ! -w "$PREFIX" ]` to decide whether to fall back to
~/.local/bin — but that meant a legitimate `--prefix /opt/tracebloc`
(a dir that doesn't exist yet) silently triggered the fallback,
overriding the customer's explicit choice.

Fix: try `mkdir -p "$PREFIX"` first; if THAT succeeds AND the
resulting dir is -w, use it. Only fall back if mkdir fails (no
write perms on parent) OR the existing dir is unwriteable. The
dominant default-prefix case (`/usr/local/bin` without sudo)
still routes to ~/.local/bin; custom prefixes get respected.

Local: `sh -n scripts/install.sh` clean; full Go test suite +
gofmt + errcheck green.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e5639d6. Configure here.

Comment thread scripts/install.ps1 Outdated
Comment thread scripts/install.ps1 Outdated
@saadqbal saadqbal self-assigned this May 25, 2026
…bot r3)

Two more PowerShell-specific Bugbot findings on PR #11:

## Medium — Write-Error + exit 1 = dead exit, ugly UX

With $ErrorActionPreference = 'Stop', every Write-Error call
throws a terminating error BEFORE any subsequent statement runs,
so the `exit 1` lines after them were dead code. The result: a
customer hitting an error saw PowerShell's full error record
(stack trace + script line numbers + ErrorCategoryInfo) instead
of a clean one-line message.

The cosign branch was fixed in r2 with Write-Host + explicit
exit. The earlier error paths (Get-Arch, Resolve-Tag, SHA256
mismatch, missing SHA256SUMS entry) still used the broken
Write-Error pattern.

Fix: add a Fail helper that does `Write-Host "Error: $msg" -ForegroundColor Red; exit 1` and replace every Write-Error call
site with `Fail "..."`. DRY + consistent UX.

## Medium-Security — Null user PATH = leading semicolon = PATH-injection

GetEnvironmentVariable('Path', 'User') returns $null on fresh
Windows installs (or accounts that never set a user-scope PATH).
The naive `"$userPath;$InstallPrefix"` interpolation then
produced `";C:\Users\...\Programs\tracebloc"` — a leading
semicolon = empty PATH entry, which Windows resolves as the
CURRENT WORKING DIRECTORY. That's a well-known PATH-injection
vector: any binary planted in the user's cwd runs ahead of real
PATH entries.

Fix: null-guard $userPath before concatenation. If $userPath is
falsy (null or empty), use just $InstallPrefix as the new value.
The `$existingEntries -notcontains` check now also handles the
null case correctly via the `if ($userPath) { ... } else { @() }`
fallback.

Local: go test green, gofmt + errcheck clean.

This is r3 on PR #11 — the install scripts have surfaced the
most findings because they're shell+PowerShell where bugbot's
language-specific coverage is sharpest. Each finding has been
real.
@saadqbal saadqbal merged commit 02cd3f3 into develop May 25, 2026
9 checks passed
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.

3 participants