Skip to content

Commit b660a76

Browse files
author
gluck
committed
Merge branch 'feat/ship'
2 parents 4369b3a + c6d3234 commit b660a76

7 files changed

Lines changed: 825 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ci.yml: the same gate scripts/release.sh runs locally before it will cut a
2+
# tag (go build / vet / test), run on every push and PR so the gate is never
3+
# the first place a break is discovered.
4+
name: ci
5+
6+
on:
7+
push:
8+
branches: ["**"]
9+
pull_request:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-go@v5
21+
with:
22+
go-version-file: go.mod
23+
cache: true
24+
25+
- name: build
26+
# CGO off everywhere, so CI cannot pass on a binary the release path
27+
# would never produce (agents.md invariant #11).
28+
env:
29+
CGO_ENABLED: "0"
30+
run: go build ./...
31+
32+
- name: vet
33+
env:
34+
CGO_ENABLED: "0"
35+
run: go vet ./...
36+
37+
- name: test
38+
env:
39+
CGO_ENABLED: "0"
40+
run: go test ./...

‎.github/workflows/release.yml‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# release.yml: turn a pushed v* tag into downloadable binaries.
2+
#
3+
# scripts/release.sh runs on a human's machine: it moves flake.nix, writes the
4+
# annotated tag, pushes it, and creates the GitHub release with hand-written
5+
# notes. This workflow is what the tag push wakes up, and it does one job:
6+
# build the archives and attach them. The .goreleaser.yaml `release.mode:
7+
# keep-existing` is what keeps it from stomping the notes.
8+
name: release
9+
10+
on:
11+
push:
12+
tags:
13+
- "v*"
14+
15+
permissions:
16+
contents: write
17+
18+
jobs:
19+
goreleaser:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
# goreleaser derives the version from the tag and the changelog from
25+
# history. A shallow clone with no tags makes it guess, and it guesses
26+
# wrong (0.0.1-next).
27+
fetch-depth: 0
28+
fetch-tags: true
29+
30+
- uses: actions/setup-go@v5
31+
with:
32+
# go.mod is the single declaration of the toolchain. Naming a version
33+
# here would be a second one, free to drift.
34+
go-version-file: go.mod
35+
cache: true
36+
37+
- uses: goreleaser/goreleaser-action@v6
38+
with:
39+
version: "~> v2"
40+
args: release --clean
41+
env:
42+
# Uploads the assets to the release in THIS repo.
43+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
# Pushes the cask to jack-work/homebrew-tap, a DIFFERENT repo, which
45+
# the automatic GITHUB_TOKEN cannot write to. This secret must be
46+
# created by hand: a fine-grained PAT (or classic PAT with `repo`)
47+
# scoped to jack-work/homebrew-tap with contents:write, stored as
48+
# the HOMEBREW_TAP_TOKEN repository secret. Until it exists, the tap
49+
# step is the only thing that fails; the release assets still land.
50+
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ teams.log
1010
*.pem
1111
.env
1212
.env.*
13+
/dist

‎.goreleaser.yaml‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# .goreleaser.yaml: build the release artifacts and hang them off the GitHub
2+
# release that scripts/release.sh already created.
3+
#
4+
# The division of labour matters. release.sh owns the version (flake.nix), the
5+
# annotated tag, and the hand-written notes; it pushes the tag and creates the
6+
# release. This file owns nothing but bytes: cross-compiled binaries, archives,
7+
# checksums, and the Homebrew formula. Everything below is downstream of a tag
8+
# that already exists.
9+
#
10+
# goreleaser check # validate this file
11+
# goreleaser build --snapshot --clean # local binaries, no tag needed
12+
# goreleaser release --clean # what CI runs on a v* tag push
13+
version: 2
14+
15+
project_name: figaro
16+
17+
before:
18+
hooks:
19+
- go mod download
20+
21+
builds:
22+
- id: figaro
23+
main: ./cmd/figaro
24+
binary: figaro
25+
env:
26+
# One static binary (agents.md invariant #11). CGO would drag in the
27+
# host libc and make the linux archives useless on musl systems.
28+
- CGO_ENABLED=0
29+
goos: [linux, darwin, windows]
30+
goarch: [amd64, arm64]
31+
# Reproducibility: without this the linker stamps the wall clock into
32+
# every binary, so two builds of the same tag hash differently and a
33+
# user cannot check our checksums against their own build.
34+
mod_timestamp: "{{ .CommitTimestamp }}"
35+
flags:
36+
- -trimpath
37+
ldflags:
38+
- -s -w
39+
# The same three symbols flake.nix stamps, so a brew/curl figaro
40+
# reports exactly what a nix figaro reports. semver carries NO leading
41+
# v: goreleaser's .Version is already stripped, trimprefix is belt and
42+
# braces for the snapshot/nightly paths where it is not.
43+
- -X github.com/jack-work/figaro/internal/cli.semver={{ trimprefix .Version "v" }}
44+
- -X github.com/jack-work/figaro/internal/cli.commit={{ .FullCommit }}
45+
# Release builds run on a clean checkout, so the honest value is empty.
46+
# A dirty tree only ever happens under --snapshot, and saying so keeps
47+
# a locally-built snapshot from impersonating a release.
48+
- -X github.com/jack-work/figaro/internal/cli.commitDirty={{ if .IsGitDirty }}true{{ end }}
49+
50+
archives:
51+
- id: figaro
52+
ids: [figaro]
53+
# No share/figaro/skills tree: the first-party skills are embedded in the
54+
# binary, so an archive is one executable plus the paperwork. Single file
55+
# install is the whole point of this distribution path.
56+
name_template: figaro_{{ .Version }}_{{ .Os }}_{{ .Arch }}
57+
formats: [tar.gz]
58+
format_overrides:
59+
# Windows has no tar in the shells people actually use to unpack this.
60+
- goos: windows
61+
formats: [zip]
62+
files:
63+
- LICENSE
64+
- README.md
65+
66+
checksum:
67+
name_template: checksums.txt
68+
algorithm: sha256
69+
70+
snapshot:
71+
# Snapshot versions must not look like a real release when they land in
72+
# `figaro --version`. The -snapshot suffix is the tell.
73+
version_template: "{{ incpatch .Version }}-snapshot"
74+
75+
changelog:
76+
# release.sh writes the notes by hand from the annotated tag message.
77+
# Nothing here should generate a competing changelog.
78+
disable: true
79+
80+
release:
81+
github:
82+
owner: jack-work
83+
name: figaro
84+
# THE IMPORTANT LINE. release.sh has already created the release with prose
85+
# notes taken from the tag. The default mode (replace) would overwrite that
86+
# body with goreleaser's own; `keep-existing` uploads the assets and leaves
87+
# the notes exactly as written. If the release does not exist yet (someone
88+
# pushed a tag without release.sh), goreleaser still creates it.
89+
mode: keep-existing
90+
91+
# A cask, not a formula. `brews:` is deprecated as of goreleaser 2.10 and
92+
# `goreleaser check` now FAILS on it, so a formula is no longer a validated
93+
# option here. The cost, stated plainly: casks are macOS-only, so Linux
94+
# Homebrew users take install.sh or nix instead of `brew install`.
95+
homebrew_casks:
96+
- name: figaro
97+
ids: [figaro]
98+
binaries: [figaro]
99+
repository:
100+
owner: jack-work
101+
name: homebrew-tap
102+
branch: main
103+
# A token with write access to the tap repo, created by hand and stored
104+
# as the HOMEBREW_TAP_TOKEN secret. The workflow's default GITHUB_TOKEN
105+
# is scoped to this repo and cannot push to jack-work/homebrew-tap.
106+
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
107+
homepage: https://figar.org
108+
description: A coding agent with few opinions, firmly held.
109+
license: MIT
110+
commit_author:
111+
name: figaro-release
112+
email: releases@figar.org
113+
commit_msg_template: "figaro {{ .Version }}"
114+
# Completions are generated by running the binary we just staged, so they
115+
# can never drift from it. Same trick flake.nix pulls in postInstall.
116+
generate_completions_from_executable:
117+
executable: figaro
118+
args: [completion]
119+
shells: [bash, zsh, fish]
120+
hooks:
121+
post:
122+
# `fig` is a pure rename, not an argv rewrite: main.go reads
123+
# filepath.Base(os.Args[0]), so help and completions come out under
124+
# whichever name was typed. A cask has no bin.install_symlink, so the
125+
# alias is made by hand next to the binary brew just linked.
126+
install: |
127+
# The binary is not signed or notarized, and Homebrew quarantines
128+
# what it downloads. On Apple Silicon a quarantined unsigned binary
129+
# is killed on sight, so the xattr has to come off before the first
130+
# run rather than after the first confusing crash.
131+
system_command "/usr/bin/xattr",
132+
args: ["-dr", "com.apple.quarantine", "#{staged_path}/figaro"],
133+
must_succeed: false
134+
fig = "#{HOMEBREW_PREFIX}/bin/fig"
135+
File.delete(fig) if File.symlink?(fig) || File.exist?(fig)
136+
File.symlink("#{HOMEBREW_PREFIX}/bin/figaro", fig)
137+
uninstall: |
138+
fig = "#{HOMEBREW_PREFIX}/bin/fig"
139+
File.delete(fig) if File.symlink?(fig)
140+
caveats: |
141+
Also installed as `fig`. If a daemon from an older figaro is running,
142+
restart it: figaro stop

‎README.md‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,29 @@ A coding agent with few opinions, firmly held. Supports multi-agent, context for
1919

2020
## Install
2121

22+
One static binary, no runtime deps. Pick a line.
23+
24+
```bash
25+
curl -fsSL https://figar.org/install.sh | sh
26+
```
27+
28+
```powershell
29+
irm https://figar.org/install.ps1 | iex
30+
```
31+
2232
```bash
33+
brew install jack-work/tap/figaro # macOS
2334
nix profile install github:jack-work/figaro
24-
# or
2535
go install github.com/jack-work/figaro/cmd/figaro@latest
2636
```
2737

28-
Also reachable as `fig` (the Nix package installs the symlink; for `go install`, add one manually).
38+
The shell installer takes `--version` and `--dir`, which under a pipe go after
39+
`--`: `curl -fsSL https://figar.org/install.sh | sh -s -- --dir /usr/local/bin`.
40+
It installs to `~/.local/bin` by default, verifies the release sha256, and
41+
writes shell completions.
42+
43+
Also reachable as `fig`. Every path above installs the alias except
44+
`go install`, which needs one line: `ln -s figaro $(go env GOPATH)/bin/fig`.
2945

3046
Config lives at `~/.config/figaro/`.
3147

@@ -201,7 +217,17 @@ so that answer is read rather than guessed.
201217

202218
```bash
203219
grep -n '^replace' go.mod && echo "strip before tagging" || echo ok
204-
git tag vX.Y.Z && git push origin vX.Y.Z
220+
scripts/release.sh patch # flake.nix version -> tag -> GitHub release
221+
```
222+
223+
The version lives in `flake.nix` and nowhere else; the tag is minted to match.
224+
Pushing the tag wakes `.github/workflows/release.yml`, which runs GoReleaser:
225+
it builds the linux/darwin/windows archives, uploads them plus `checksums.txt`
226+
to the release the script already created (`mode: keep-existing`, so the
227+
hand-written notes survive), and pushes the Homebrew cask to
228+
`jack-work/homebrew-tap`.
229+
230+
```bash
205231
# smoke test:
206232
go install github.com/jack-work/figaro/cmd/figaro@vX.Y.Z
207233
# then bump flake.nix vendorHash if needed

0 commit comments

Comments
 (0)