Skip to content

Prereleases

Dan Riddell edited this page Sep 30, 2026 · 1 revision

Prereleases

letsgo tag --pre
letsgo promote <rc-tag>
letsgo update --channel <name>

A prerelease is a real release that most consumers should not get. letsgo keeps that distinction in every channel it publishes to — the tap, the registry, the updater — rather than only in a checkbox on the release page.

Cutting one

letsgo tag --pre proposes a prerelease instead of a stable version, as -rc.N, and otherwise works exactly as letsgo tag does: reading conventional commits, and diffing the exported API where the module has one.

The channel is the first dot-separated prerelease identifier, so rc.1 is the rc channel and beta.3 is beta.

What a prerelease publishes

  • Homebrew. A prerelease never writes the stable formula foo. Every release — stable or not — writes foo@next, pointing at whichever is newer of the newest prerelease and the newest stable. So brew install you/tap/foo is always the stable line and foo@next is always the bleeding edge, with no moment where one silently becomes the other.
  • Docker. A prerelease pushes <version> and <channel>. A stable release pushes <version> and moves <major>.<minor>, <major>, latest and each channel tag only if it is newer than what that tag already points at. A v1.2.9 backported after v1.3.0 moves 1.2 and nothing else. Build metadata maps + to _, which Docker tags allow and semver does not.
  • Self-update. selfupdate.Options.Channel selects: empty is stable, a name is stable plus that channel, and "next" is stable plus any prerelease. Drafts and yanked releases are never selected.
letsgo update --channel rc     # stable or rc, whichever is higher
letsgo update --channel next   # any prerelease
letsgo update                  # stable only

The previous release

Notes, the API gate and letsgo tag all ask the same question — what came before this? — and they all get the same answer, which depends on the kind of release being made:

  • the previous release of a prerelease is the highest release of any kind below it, so v1.3.0-rc.2 compares against rc.1;
  • the previous release of a stable release is the highest stable release below it, so v1.3.0 compares against v1.2.8 and not against its own RCs.

That is what makes a stable release's notes cover everything since the last stable, rather than only the handful of commits since the final RC.

Promoting

letsgo promote v1.3.0-rc.1 turns an RC into the stable release it was rehearsing:

  1. The RC release is set back to prerelease and not-latest, unconditionally and first. Whatever else happens, the repository does not end the run with an RC sitting on the latest badge.
  2. v1.3.0 is tagged on the RC's own commit.
  3. The release is rebuilt and compared against the RC's manifest. The only differences allowed are the version string, the archive names, and the digests those imply. Anything else — a changed toolchain, a moved dependency — fails the promotion, listing the fields.
  4. A new stable release is created and marked latest.

The RC is not modified. Its tag, assets and notes stay exactly as published, so letsgo verify v1.3.0-rc.1 still passes afterwards. The stable manifest records promoted_from: {tag, manifest_sha256}, and the stable notes cover everything since the previous stable with a collapsed "Prerelease history" section.

Promotion is rebuilt rather than copied because a promotion that renamed the RC's archives would be shipping bytes whose recorded version does not match the version in their own name.

Flag Description
--yes Promote without asking
--append-notes Add the notes after an existing description instead of replacing it
--work Scratch directory for the rebuild
--token Forge token (default: $GITHUB_TOKEN or $GH_TOKEN)
--tap-token Token for the Homebrew tap, when it lives elsewhere
--release-token Token for the release, when it differs from the tap's

It refuses a yanked RC, a draft, a tag that is not a prerelease, and a target tag that already exists. It is re-runnable after a failure, resuming from the step that failed.

Promoting from the GitHub UI

Flipping an RC from pre-release to release in the UI fires release: released, which a workflow can turn into a promotion:

on:
  release:
    types: [released]

jobs:
  promote:
    # released also fires for ordinary stable releases; only a prerelease tag promotes.
    if: contains(github.event.release.tag_name, '-')
    runs-on: ubuntu-latest
    permissions: { contents: write, id-token: write, attestations: write }
    steps:
      - uses: actions/checkout@v7
        with: { ref: '${{ github.event.release.tag_name }}', fetch-tags: true }
      - uses: actions/setup-go@v7
        with: { go-version-file: go.mod }
      - uses: danielriddell21/letsgo-action@v1
        with:
          command: promote
          args: ${{ github.event.release.tag_name }}

Editing a release with the default GITHUB_TOKEN does not start workflows, so use an App token if the flip should trigger this automatically. Either way a re-trigger is harmless: promote refuses once v1.3.0 already exists.

Publishing a prerelease deliberately

release prerelease=true in letsgo.mod publishes every release from that config as a prerelease, rather than having a workflow edit the release afterwards. Promotion stays a manual act, and marking the release as the full release is what fires released.

Clone this wiki locally