From d90e25df2a445b08de9a2d8064d263fc272484bb Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Wed, 5 Aug 2026 15:20:30 -0400 Subject: [PATCH 1/3] Rename the module to its repo URL and derive the version from build info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module was declared as a bare `quartr-cli`, so `go install github.com/TJC-LP/quartr-cli/cmd/quartr@latest` could not resolve it — Go fetches the URL in the module path. Renaming is free right now: there are no tags, no releases, and no importers. After v0.1.0 it would be a breaking change for anyone who had imported the package. The version was also stated twice and injectable neither time: a `const Version = "0.1.0"` in cli, and a separate hardcoded `UserAgent = "quartr-cli/0.1.0"` in quartr. A const cannot be set by `-ldflags -X`, so a release build had no way to stamp itself, and the two copies could drift so that `--version` and the wire disagreed. Version resolution now lives in internal/quartr/version.go, below cli, so both readers share one value. It prefers the linker-injected string, then falls back to what the go tool already stamps into every binary: the module version for `go install pkg@v0.1.0`, or the VCS revision for a build from a checkout. "dev" only when none is available. That means an install straight from the module path reports its real version without the Makefile being involved at all. Co-Authored-By: Claude Opus 5 (1M context) --- cmd/quartr/main.go | 2 +- go.mod | 2 +- internal/cli/app.go | 7 +--- internal/cli/config.go | 2 +- internal/cli/config_test.go | 2 +- internal/cli/errors.go | 2 +- internal/cli/fetch.go | 2 +- internal/cli/handlers.go | 4 +- internal/cli/help.go | 4 +- internal/quartr/client.go | 9 ++--- internal/quartr/version.go | 66 +++++++++++++++++++++++++++++++++ internal/quartr/version_test.go | 46 +++++++++++++++++++++++ 12 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 internal/quartr/version.go create mode 100644 internal/quartr/version_test.go diff --git a/cmd/quartr/main.go b/cmd/quartr/main.go index 2a3bd93..4ef55f7 100644 --- a/cmd/quartr/main.go +++ b/cmd/quartr/main.go @@ -6,7 +6,7 @@ package main import ( "os" - "quartr-cli/internal/cli" + "github.com/TJC-LP/quartr-cli/internal/cli" ) func main() { diff --git a/go.mod b/go.mod index 43fc608..924fa0b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module quartr-cli +module github.com/TJC-LP/quartr-cli go 1.26.2 diff --git a/internal/cli/app.go b/internal/cli/app.go index a7a09fc..5136dc9 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -11,12 +11,9 @@ import ( "fmt" "io" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) -// Version is the CLI release string, surfaced via `quartr --version`. -const Version = "0.1.0" - type app struct { out io.Writer errOut io.Writer @@ -44,7 +41,7 @@ func Run(args []string, out, errOut io.Writer) int { return 2 } if globals.Version { - fmt.Fprintf(out, "quartr %s\n", Version) + fmt.Fprintf(out, "quartr %s\n", quartr.Version) return 0 } diff --git a/internal/cli/config.go b/internal/cli/config.go index bcdaeae..6f93258 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -4,7 +4,7 @@ import ( "os" "strings" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) type globalOverrides struct { diff --git a/internal/cli/config_test.go b/internal/cli/config_test.go index cb23030..695ed22 100644 --- a/internal/cli/config_test.go +++ b/internal/cli/config_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) func TestBuildConfigPrecedence(t *testing.T) { diff --git a/internal/cli/errors.go b/internal/cli/errors.go index 8334fe4..8941226 100644 --- a/internal/cli/errors.go +++ b/internal/cli/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) // usageError marks a failure caused by the command line the user typed diff --git a/internal/cli/fetch.go b/internal/cli/fetch.go index 1d15147..644d7ff 100644 --- a/internal/cli/fetch.go +++ b/internal/cli/fetch.go @@ -9,7 +9,7 @@ import ( "regexp" "strings" - "quartr-cli/internal/output" + "github.com/TJC-LP/quartr-cli/internal/output" ) // listRequest describes one list-or-paginate call. It exists so callers can diff --git a/internal/cli/handlers.go b/internal/cli/handlers.go index d7fe436..2813c1a 100644 --- a/internal/cli/handlers.go +++ b/internal/cli/handlers.go @@ -11,8 +11,8 @@ import ( "path/filepath" "strings" - "quartr-cli/internal/output" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/output" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) func (a *app) handleAuth(args []string) error { diff --git a/internal/cli/help.go b/internal/cli/help.go index ca8d479..a659d1e 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "quartr-cli/internal/quartr" + "github.com/TJC-LP/quartr-cli/internal/quartr" ) func (a *app) printRootHelp() { @@ -37,7 +37,7 @@ Examples: quartr transcripts download 432907 --output transcript.json quartr live transcripts stream 127537 --transcript-version 1.7 quartr request get /events --query tickers=AAPL --query limit=3 --format json -`, Version, quartr.DefaultBaseURL, quartr.DefaultConfigPath(), strings.Join(cmds, ", ")) +`, quartr.Version, quartr.DefaultBaseURL, quartr.DefaultConfigPath(), strings.Join(cmds, ", ")) } func (a *app) printAuthHelp() { diff --git a/internal/quartr/client.go b/internal/quartr/client.go index 62f03c9..26fc3d0 100644 --- a/internal/quartr/client.go +++ b/internal/quartr/client.go @@ -16,12 +16,9 @@ import ( "time" ) -const ( - // DefaultBaseURL is the production API endpoint used when no override is configured. - DefaultBaseURL = "https://api.quartr.com/public/v3" - // UserAgent is sent on every outbound request. - UserAgent = "quartr-cli/0.1.0" -) +// DefaultBaseURL is the production API endpoint used when no override is configured. +// Version and UserAgent live in version.go. +const DefaultBaseURL = "https://api.quartr.com/public/v3" // Client performs authenticated GET requests against the Quartr Public API. // It retries 429 and 5xx responses with backoff and honors Retry-After. diff --git a/internal/quartr/version.go b/internal/quartr/version.go new file mode 100644 index 0000000..a10e8a6 --- /dev/null +++ b/internal/quartr/version.go @@ -0,0 +1,66 @@ +package quartr + +import ( + "runtime/debug" + "strings" +) + +// buildVersion is overridden at link time by the release build: +// +// go build -ldflags "-X github.com/TJC-LP/quartr-cli/internal/quartr.buildVersion=1.2.3" +// +// It is deliberately empty by default. Everything else falls back to the build +// information the go tool stamps into the binary on its own, so a user who runs +// `go install github.com/TJC-LP/quartr-cli/cmd/quartr@v0.1.0` still gets a +// binary that reports 0.1.0 without any linker flags involved. +var buildVersion = "" + +// Version is the release string reported by `quartr --version`. It is resolved +// once, here, rather than in cli, so the version the CLI prints and the one it +// puts on the wire in User-Agent cannot drift apart. +var Version = resolveVersion() + +// UserAgent is sent on every outbound request. +var UserAgent = "quartr-cli/" + Version + +// resolveVersion reports, in order of preference: the linker-injected release +// string, the module version recorded by `go install module@version`, or the +// VCS revision stamped into a `go build` from a checkout. "dev" is the answer +// only when none of the three is available. +func resolveVersion() string { + if v := strings.TrimSpace(buildVersion); v != "" { + return strings.TrimPrefix(v, "v") + } + + bi, ok := debug.ReadBuildInfo() + if !ok { + return "dev" + } + + // Set when the binary came from `go install module@version`. A plain + // `go build` inside the module reports "" or "(devel)" instead. + if v := bi.Main.Version; v != "" && v != "(devel)" { + return strings.TrimPrefix(v, "v") + } + + var revision string + var modified bool + for _, setting := range bi.Settings { + switch setting.Key { + case "vcs.revision": + revision = setting.Value + case "vcs.modified": + modified = setting.Value == "true" + } + } + if revision == "" { + return "dev" + } + if len(revision) > 12 { + revision = revision[:12] + } + if modified { + revision += "-dirty" + } + return revision +} diff --git a/internal/quartr/version_test.go b/internal/quartr/version_test.go new file mode 100644 index 0000000..25711b7 --- /dev/null +++ b/internal/quartr/version_test.go @@ -0,0 +1,46 @@ +package quartr + +import ( + "strings" + "testing" +) + +func TestResolveVersionPrefersInjectedBuildVersion(t *testing.T) { + original := buildVersion + t.Cleanup(func() { buildVersion = original }) + + // The release build injects a bare "1.2.3", but a tag-shaped "v1.2.3" + // must not produce a doubled prefix in `quartr vv1.2.3`. + for _, injected := range []string{"1.2.3", "v1.2.3", " 1.2.3 "} { + buildVersion = injected + if got := resolveVersion(); got != "1.2.3" { + t.Errorf("resolveVersion() with buildVersion=%q = %q, want %q", injected, got, "1.2.3") + } + } +} + +// Without an injected version the value comes from the build info the go tool +// stamps in. Under `go test` that is the VCS revision, so the only invariant +// worth asserting is that the fallback chain always yields something usable. +func TestResolveVersionFallsBackToBuildInfo(t *testing.T) { + original := buildVersion + t.Cleanup(func() { buildVersion = original }) + + buildVersion = "" + got := resolveVersion() + if got == "" { + t.Fatal("resolveVersion() returned an empty string; want a revision or \"dev\"") + } + if strings.HasPrefix(got, "v") { + t.Errorf("resolveVersion() = %q; the leading v should be stripped", got) + } +} + +func TestUserAgentTracksVersion(t *testing.T) { + if want := "quartr-cli/" + Version; UserAgent != want { + t.Errorf("UserAgent = %q, want %q", UserAgent, want) + } + if strings.Contains(UserAgent, "0.1.0") && Version != "0.1.0" { + t.Error("UserAgent carries a hardcoded version that Version does not agree with") + } +} From c4237eade4b5ff11e4a4909cd031c4a1fc6ed96c Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Wed, 5 Aug 2026 15:20:37 -0400 Subject: [PATCH 2/3] Publish cross-compiled release archives from v* tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pushing a v* tag now runs the tests, cross-compiles for darwin, linux and windows on amd64 and arm64, and publishes the archives with generated notes. Tags carry the `v`; the reported version does not, so v0.1.0 ships `quartr 0.1.0`. A hyphenated tag (v0.2.0-rc1) publishes as a prerelease. `make dist` is the only build recipe — the workflow calls it rather than reimplementing the matrix in YAML, so what a maintainer builds locally and what gets uploaded cannot diverge. For the same reason ci.yml gains a dist job: a broken cross-compile should fail on the PR that caused it, not after a tag is already pushed and immutable. The workflow asserts the built binary reports the version its tag implies before it publishes anything. A release that ships a mislabeled binary is worse than one that fails to ship. Archives bundle README.md and LICENSE with the binary and are published alongside a SHA256SUMS file. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 25 ++++++++++++++++ .github/workflows/release.yml | 54 +++++++++++++++++++++++++++++++++++ Makefile | 52 ++++++++++++++++++++++++++++++--- 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1060c54..0d06ed4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,28 @@ jobs: - uses: golangci/golangci-lint-action@v7 with: version: v2.12.0 + + # Exercises the release recipe on every PR. Without this, a broken cross + # compile only shows up after a tag is pushed, which is the worst time. + dist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build release artifacts + run: make dist VERSION=ci + + - name: Smoke test the linux binary + run: | + test "$(./dist/bin/quartr-linux-amd64 --version)" = "quartr ci" + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9e1eae9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Test + run: go test ./... + + # Same recipe a maintainer runs locally, so the published archives are + # never a CI-only code path. + - name: Build release artifacts + run: make dist VERSION="${GITHUB_REF_NAME#v}" + + # A tag that ships a binary reporting a different version is worse than + # a failed release, so fail here instead. + - name: Verify the tag version was baked in + run: | + expected="quartr ${GITHUB_REF_NAME#v}" + actual="$(./dist/bin/quartr-linux-amd64 --version)" + if [ "$actual" != "$expected" ]; then + echo "version mismatch: binary says '$actual', tag implies '$expected'" >&2 + exit 1 + fi + echo "$actual" + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: | + # A tag like v0.2.0-rc1 publishes as a prerelease. + prerelease="" + case "$GITHUB_REF_NAME" in + *-*) prerelease="--prerelease" ;; + esac + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --generate-notes \ + $prerelease \ + dist/*.tar.gz dist/*.zip dist/*SHA256SUMS diff --git a/Makefile b/Makefile index e4028dd..ef24c59 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,63 @@ -.PHONY: build install test clean +.PHONY: build install test clean dist GOBIN := $(shell go env GOBIN) ifeq ($(GOBIN),) GOBIN := $(shell go env GOPATH)/bin endif +# Release string baked into the binary. Defaults to the current git +# description with any leading "v" stripped, so tag v0.1.0 reports 0.1.0. +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/^v//') +ifeq ($(VERSION),) +VERSION := dev +endif + +VERSION_LDFLAGS := -X github.com/TJC-LP/quartr-cli/internal/quartr.buildVersion=$(VERSION) +RELEASE_LDFLAGS := -s -w $(VERSION_LDFLAGS) + +# os/arch pairs published on every release. +PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 + build: - go build -o bin/quartr ./cmd/quartr + go build -ldflags "$(VERSION_LDFLAGS)" -o bin/quartr ./cmd/quartr install: - go install ./cmd/quartr + go install -ldflags "$(VERSION_LDFLAGS)" ./cmd/quartr @if [ -n "$$QUARTR_API_KEY" ]; then printf '%s' "$$QUARTR_API_KEY" | $(GOBIN)/quartr auth login --api-key-stdin; fi test: go test ./... +# dist is what the release workflow runs, so a local `make dist` and the +# published artifacts come from the same recipe. Raw binaries land in +# dist/bin for smoke tests; the archives and checksum file get uploaded. +dist: + rm -rf dist + mkdir -p dist/bin + @set -e; for platform in $(PLATFORMS); do \ + os=$${platform%/*}; arch=$${platform#*/}; ext=""; \ + if [ "$$os" = "windows" ]; then ext=".exe"; fi; \ + echo "==> quartr $(VERSION) $$os/$$arch"; \ + CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \ + go build -trimpath -ldflags "$(RELEASE_LDFLAGS)" \ + -o dist/bin/quartr-$$os-$$arch$$ext ./cmd/quartr; \ + stage=dist/stage/quartr_$(VERSION)_$${os}_$${arch}; \ + mkdir -p $$stage; \ + cp dist/bin/quartr-$$os-$$arch$$ext $$stage/quartr$$ext; \ + cp README.md LICENSE $$stage/; \ + if [ "$$os" = "windows" ]; then \ + (cd dist/stage && zip -qr ../quartr_$(VERSION)_$${os}_$${arch}.zip quartr_$(VERSION)_$${os}_$${arch}); \ + else \ + tar -czf dist/quartr_$(VERSION)_$${os}_$${arch}.tar.gz -C dist/stage quartr_$(VERSION)_$${os}_$${arch}; \ + fi; \ + done + @rm -rf dist/stage + @cd dist && if command -v sha256sum >/dev/null 2>&1; then \ + sha256sum quartr_$(VERSION)_*.tar.gz quartr_$(VERSION)_*.zip > quartr_$(VERSION)_SHA256SUMS; \ + else \ + shasum -a 256 quartr_$(VERSION)_*.tar.gz quartr_$(VERSION)_*.zip > quartr_$(VERSION)_SHA256SUMS; \ + fi + @echo "artifacts:"; ls -1 dist/*.tar.gz dist/*.zip dist/*SHA256SUMS + clean: - rm -rf bin + rm -rf bin dist From 30ea06022cf2a910444c8eb37425ee1f7dfda275 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Wed, 5 Aug 2026 15:20:44 -0400 Subject: [PATCH 3/3] Document installing from a release and correct the skill flag tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README gains an Install section: release archives with checksum verification and the Gatekeeper note, `go install ...@latest`, and the make targets. The old text claimed a plain `go build` reports "dev", which is no longer the whole story now that build info is consulted. The skill's command reference had drifted from resources.go in three places, each verified against the binary with --debug rather than by reading the map: - `--type-ids` was listed as working on audio. /audio has no typeIds param, so the CLI drops it and the caller silently gets unfiltered rows — the exact failure mode the --sort-by rejection exists to prevent. - `--transcript-version` was listed for `transcripts get` and `audio get`. Both take only `expand`; it is dropped there. It reaches the API on live and live-transcripts only. - `live transcripts` was missing its `download` operation, which the resource map has had via downloadField all along. `--expand` is now split into its two rows, because the API-side `event` expansion and the client-side `company` join apply to different resources — notably `--expand company` does work on events, which the single combined row left ambiguous. CLAUDE.md documents the release process and records that the module path must keep matching the repo URL. It also called the repo private; it is public. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/quartr/SKILL.md | 25 +++++++++--- .claude/skills/quartr/references/commands.md | 16 +++++--- CLAUDE.md | 40 +++++++++++++++++-- README.md | 42 ++++++++++++++++++-- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/.claude/skills/quartr/SKILL.md b/.claude/skills/quartr/SKILL.md index e37c6b0..a9a8a2a 100644 --- a/.claude/skills/quartr/SKILL.md +++ b/.claude/skills/quartr/SKILL.md @@ -10,16 +10,29 @@ companies, events, transcripts, reports, slides, audio, and live events. ## Binary -This skill ships inside the `quartr-cli` repo. Build the binary from the repo -root: +Check for an installed binary first — `command -v quartr` — and use it if it is +on `PATH`. `quartr --version` prints the release it came from. + +If it is not installed, install a prebuilt one: + +```bash +go install github.com/TJC-LP/quartr-cli/cmd/quartr@latest +``` + +Or download an archive for the platform from + (macOS, Linux, and Windows; +amd64 and arm64), each published with a `SHA256SUMS` file. + +This skill also ships inside the `quartr-cli` repo, so when working from a +clone, build from the repo root instead: ```bash -go build -o bin/quartr ./cmd/quartr +make build # ./bin/quartr, with the version baked in ``` -Then invoke it as `./bin/quartr` from the repo root, or `$(git rev-parse --show-toplevel)/bin/quartr` -from anywhere inside the worktree. Use `go install ./cmd/quartr` instead to -install it on `$GOBIN/quartr` for PATH-style invocation. +Then invoke it as `./bin/quartr` from the repo root, or +`$(git rev-parse --show-toplevel)/bin/quartr` from anywhere inside the +worktree. `make install` puts it on `$GOBIN/quartr` for PATH-style invocation. ## Auth diff --git a/.claude/skills/quartr/references/commands.md b/.claude/skills/quartr/references/commands.md index 92e9bd5..cfd8f31 100644 --- a/.claude/skills/quartr/references/commands.md +++ b/.claude/skills/quartr/references/commands.md @@ -17,7 +17,7 @@ specific flag or endpoint at hand. | `audio` | `list`, `get`, `chapters`, `download` | `/audio` | `list` may be tier-restricted; `fileUrl` | | `live` | `list`, `get` | `/live` | Honors `transcriptVersion` | | `live audio` | `list`, `get`, `download` | `/live/audio` | Download field is `audio` | -| `live transcripts` | `list`, `get`, `stream` | `/live/transcripts` | `list` may be tier-restricted; stream field is `transcript` | +| `live transcripts` | `list`, `get`, `download`, `stream` | `/live/transcripts` | `list` may be tier-restricted; download and stream both read the `transcript` field | | `event-types` | `list`, `get` | `/event-types` | Lookup table; `list` returns the whole catalog | | `document-types` | `list`, `get` | `/document-types` | Lookup table; `list` returns the whole catalog | | `request` | `get` | (any path) | Escape hatch; `--query k=v --paginate` | @@ -62,12 +62,18 @@ Auth precedence: flags > env > config file > defaults. --end-date 2024-12-31 ISO 8601 --updated-after 2024-01-01 incremental sync lower bound --updated-before 2024-12-31 ---expand event,company event is expanded by the API; company is joined client-side ---type-ids 1,2,3 events / documents* / transcripts* / reports* / slides* / audio* ---event-ids 128301 documents* / transcripts* / reports* / slides* / audio* / live* +--expand event API-side; documents / reports / slides / transcripts / audio. + Dropped on events (a self-expansion) and companies. +--expand company client-side join; works on any resource whose rows carry a + companyId, including events. Only `companies` rejects it, + as redundant. +--type-ids 1,2,3 events / documents / transcripts / reports / slides. + NOT audio — /audio has no typeIds param and drops it. +--event-ids 128301 documents / transcripts / reports / slides / audio / live* --document-group-ids foo documents* / transcripts* / reports* / slides* --states live,willBeLive live, live-transcripts, live-audio ---transcript-version 1.7 live, live-transcripts, transcripts (get only), audio (get only) +--transcript-version 1.7 live, live-transcripts (list and get) only; silently + dropped on transcripts/audio, which have no such param --sort-by id|date events list ONLY; rejected with exit 2 everywhere else --levels 1,2 chapters subcommand on reports/slides/transcripts/audio ``` diff --git a/CLAUDE.md b/CLAUDE.md index a4074e6..400f4e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,12 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands ```bash -make build # build ./bin/quartr +make build # build ./bin/quartr, version baked in via -ldflags make test # go test ./... make install # go install ./cmd/quartr; if QUARTR_API_KEY is in env, # also runs `quartr auth login` to persist the key # to ~/.config/quartr/config.json (mode 0600) -make clean # remove ./bin +make dist # cross-compile release archives + checksums into ./dist +make clean # remove ./bin and ./dist go test ./internal/cli -run TestBuildConfigPrecedence # run a single test @@ -55,9 +56,40 @@ Three internal packages, no external deps (Go stdlib only): ## Repo / branch hygiene -- Remote: `git@github.com:TJC-LP/quartr-cli.git` (private). +- Remote: `git@github.com:TJC-LP/quartr-cli.git` (public). +- Module path is `github.com/TJC-LP/quartr-cli`, which must keep matching the + repo URL — that is what makes `go install github.com/TJC-LP/quartr-cli/cmd/quartr@latest` + resolve. Renaming the repo means renaming the module. - `main` is protected — push to a feature branch and open a PR with `gh pr create`. Direct pushes to `main` are rejected. -- `.env` and `bin/` are gitignored; the `.env.example` exception is preserved. +- `.env`, `bin/`, and `dist/` are gitignored; the `.env.example` exception is preserved. + +## Releases + +Cutting a release is one push: + +```bash +git tag -a v0.2.0 -m "v0.2.0" +git push origin v0.2.0 +``` + +`.github/workflows/release.yml` fires on `v*` tags, runs the tests, calls +`make dist VERSION=${tag#v}`, and publishes the archives with `gh release +create --generate-notes`. A tag containing a hyphen (`v0.2.0-rc1`) publishes as +a prerelease. + +- **Tags carry the `v`; the reported version does not.** `v0.1.0` produces + `quartr 0.1.0`. The workflow asserts this before publishing, so a mismatch + fails the release rather than shipping a mislabeled binary. +- **`make dist` is the single build recipe** — CI calls it rather than + reimplementing the matrix, and the `dist` job in `ci.yml` runs it on every PR + so a broken cross-compile surfaces before a tag exists. +- **Version resolution lives in `internal/quartr/version.go`**, not in `cli`, + so `--version` and the `User-Agent` header cannot drift. Order: `-ldflags + -X ...quartr.buildVersion`, then `debug.ReadBuildInfo` (module version for + `go install pkg@version`, VCS revision for a checkout build), then `dev`. + If you move or rename `buildVersion`, update `VERSION_LDFLAGS` in the Makefile. +- Release archives bundle `README.md` and `LICENSE` alongside the binary and + ship with a `SHA256SUMS` file. ## Skill diff --git a/README.md b/README.md index e5e9ec2..8d72a4c 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,52 @@ It is designed for API subscribers who want a terminal-friendly interface for co - Retries transient `429` and `5xx` responses with short backoff and honors `Retry-After` when present. - Uses only the Go standard library. -## Build +## Install + +### From a release + +Prebuilt binaries for macOS, Linux, and Windows (amd64 and arm64) are attached +to every [release](https://github.com/TJC-LP/quartr-cli/releases). Download the +archive for your platform, verify it, and put `quartr` on your `PATH`: ```bash -go build -o bin/quartr ./cmd/quartr +VERSION=0.1.0 +OS=$(uname -s | tr '[:upper:]' '[:lower:]') # darwin | linux +ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') +BASE="https://github.com/TJC-LP/quartr-cli/releases/download/v$VERSION" + +curl -fsSLO "$BASE/quartr_${VERSION}_${OS}_${ARCH}.tar.gz" +curl -fsSLO "$BASE/quartr_${VERSION}_SHA256SUMS" +shasum -a 256 -c quartr_${VERSION}_SHA256SUMS --ignore-missing + +tar -xzf "quartr_${VERSION}_${OS}_${ARCH}.tar.gz" +sudo install "quartr_${VERSION}_${OS}_${ARCH}/quartr" /usr/local/bin/quartr +quartr --version ``` -Or install from the project directory: +macOS Gatekeeper quarantines binaries downloaded with a browser. If you get +"cannot be opened because the developer cannot be verified", clear the +attribute: `xattr -d com.apple.quarantine /usr/local/bin/quartr`. + +### From source ```bash -go install ./cmd/quartr +go install github.com/TJC-LP/quartr-cli/cmd/quartr@latest ``` +Or from a clone of this repo: + +```bash +make build # ./bin/quartr +make install # $GOBIN/quartr +``` + +`make` bakes the version in via `-ldflags`, so `quartr --version` reports the +tag. A plain `go build ./cmd/quartr` works too and falls back to whatever the +go tool stamped: the module version for `go install ...@v0.1.0`, a +`0.0.0--` pseudo-version when building from a checkout, or +`dev` when no build information is available at all. + ## Configure ```bash