-
Notifications
You must be signed in to change notification settings - Fork 0
Commands
letsgo plan [--explain] [--publish] resolve and check a release without performing one
letsgo build [--snapshot] [-o dir] build every artifact into dist/ without publishing
letsgo release [--draft] [-o dir] build and publish, resumably
letsgo release --snapshot rehearse a release without publishing
letsgo verify [tag] rebuild a published release and compare it
letsgo diff <from> [to] compare two releases: size, dependencies, API
letsgo yank <tag> [--reason "..."] retract a release, including the go.mod directive
letsgo tag [--major|--minor|--patch] work out the next version and tag it
letsgo update [--check] update letsgo itself, verified against its manifest
letsgo fmt [file] format letsgo.mod
letsgo version print the version (also --version)
Run a command with -h for its options.
plan resolves everything it can and checks it, with no side effects, in about
two seconds. Nothing is built.
$ cd ~/code/foo
$ git tag v1.3.0
$ letsgo plan
letsgo 0.1.0 · foo v1.3.0 · commit 9f2ab1c
gates
✓ worktree clean
✓ tag v1.3.0 is semver, not yet released
✓ module path github.com/danielriddell21/foo agrees with tag major version
✓ ldflags symbols exist — main.version, main.commit, main.date
✓ smoke test — ./foo --version printed "foo 1.3.0"
✓ govulncheck — no reachable vulnerabilities
✓ apidiff — API is backward compatible with v1.2.3
✓ GITHUB_TOKEN valid (contents:write)
artifacts (6)
foo_1.3.0_linux_amd64.tar.gz
foo_1.3.0_linux_arm64.tar.gz
foo_1.3.0_darwin_amd64.tar.gz
foo_1.3.0_darwin_arm64.tar.gz
foo_1.3.0_windows_amd64.zip
foo_1.3.0_source.tar.gz
+ SHA256SUMS, letsgo.json
plan ok in 1.9s · run `letsgo release` to publish
$ letsgo plan
✗ apidiff — v1.3.0 is a minor bump, but the API is not backward compatible
removed func (*Client) Do(context.Context, *Request) (*Response, error)
a breaking change requires v2.0.0 and a /v2 module path suffix
override: letsgo release --allow-breaking
plan failed in 2.1s · nothing was built
That last line is the point. The common failure mode of release tooling is discovering a problem after a six-minute cross-compile.
--explain shows which defaults were inferred and from what evidence.
--publish additionally checks the credentials publishing would need — including
the one a workflow token cannot satisfy, writing to a Homebrew tap.
$ letsgo release
✓ plan 1.9s
✓ built 5 targets 11.2s
✓ archived and checksummed 0.8s
✓ created release v1.3.0
✓ uploaded 8 assets 9.4s
✓ attested provenance
✓ warmed proxy.golang.org
released in 24s
https://github.com/danielriddell21/foo/releases/tag/v1.3.0
Every step is idempotent and keyed by content digest, so a re-run does only what's left:
$ letsgo release # the previous attempt died mid-upload
✓ plan 1.8s
✓ 5 targets from cache 0.3s
✓ 6 of 8 assets already uploaded and matching
✓ uploaded 2 assets 2.1s
released in 4s
There is no state in which a release is half-published and unrecoverable.
Because builds are byte-for-byte reproducible, anyone can rebuild a published release and check it against what was actually shipped:
$ letsgo verify v1.3.0
✓ fetched manifest from release
✓ rebuilt 5/5 targets at commit 9f2ab1c
✓ all digests match published assets
✓ dependency graph matches recorded go.sum
✓ provenance attestation valid (github-actions, danielriddell21/foo)
v1.3.0 verified
You can run this against your own releases. So can anyone else, on hardware you don't control. See Reproducibility.
Commit messages are a human-authored account of a change. This reads the artifacts:
$ letsgo diff v1.2.3 v1.3.0
v1.2.3 -> v1.3.0
binary size
linux/amd64 12.4 MB -> 16.8 MB +35%
darwin/arm64 12.1 MB -> 16.5 MB +36%
dependencies
+ github.com/some/large-dep v1.4.0
~ golang.org/x/net v0.21.0 -> v0.23.0
api
! example.com/foo: Client.Do: changed from func([]byte) error to func(context.Context, []byte) error
toolchain
go1.23.4 -> go1.24.7
Every line comes from two JSON manifests — no rebuild, no downloads, no
checkout. It works on releases of repositories you have never cloned, and either
side can be a local dist/letsgo.json instead of a tag, so "what did the build I
just ran do to the binary" is the same command.
The toolchain line is printed last and is often the answer: a compiler change moves every target at once, which no dependency accounts for.
Retracts a published release. It does four things, and the order matters less than the fact that none of them is "delete it":
- Marks the GitHub release as a prerelease, and says so at the top of its description. Not deleted — deleting would break every checksum anybody recorded, and the module proxy has the module regardless.
- Adds a
retractdirective togo.mod, carrying--reasonas the textgo list -m -retractedshows to anyone about to depend on it. - Rolls the Homebrew formula back to the previous release, regenerating it
from that release's manifest.
--keep-tapleaves it pointing at the retracted one. - Tells you the version the retraction has to be published in.
That last step is the part people get wrong. A retract directive only takes
effect once it is published in a later tag — the directive lives in
go.mod, and go.mod is only visible to the toolchain through a version that
carries it. Retracting v1.2.3 and stopping there leaves the retraction in a
release nobody will fetch, so yank works out the patch bump you need and
names it.
| Flag | Description |
|---|---|
--reason "..." |
Why the release should not be used; shown by go list -m -retracted
|
--yes |
Retract without asking |
--keep-tap |
Leave the Homebrew formula pointing at the retracted release |
--token |
Forge token (default: $GITHUB_TOKEN or $GH_TOKEN) |
letsgo yank v1.2.3 --reason "publishes the wrong binary on darwin/arm64"Retracting the first release of a module leaves the formula alone — there is nothing to roll back to.
Updates letsgo itself, checking what it downloads against the release manifest: the archive against its recorded digest, and then the extracted binary against its own. Both digests were fixed by the build that published them.
| Flag | Description |
|---|---|
--check |
Report whether a newer release exists, and change nothing |
--yes |
Install without asking |
--token |
Forge token (default: $GITHUB_TOKEN or $GH_TOKEN) |
letsgo update --check
letsgo updateThe repository it updates from is hard-coded. An updater that takes its source from a flag is an updater that can be pointed at somebody else's binary.
The same machinery is available to your own programs — see Self-update.