Skip to content

Commands

Dan Riddell edited this page Sep 13, 2026 · 2 revisions

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

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

When the plan fails, nothing was built

$ 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.

release

$ 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

Re-running after a failure

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.

verify

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.

diff

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.

yank

Retracts a published release. It does four things, and the order matters less than the fact that none of them is "delete it":

  1. 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.
  2. Adds a retract directive to go.mod, carrying --reason as the text go list -m -retracted shows to anyone about to depend on it.
  3. Rolls the Homebrew formula back to the previous release, regenerating it from that release's manifest. --keep-tap leaves it pointing at the retracted one.
  4. 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.

update

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 update

The 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.

Clone this wiki locally