-
Notifications
You must be signed in to change notification settings - Fork 0
Publishing
Every release publishes an install.sh generated by the build that produced it,
with each archive's SHA-256 written into the script:
$ curl -fsSL https://github.com/danielriddell21/foo/releases/latest/download/install.sh | sh
install: downloading foo_1.3.0_linux_amd64.tar.gz
install: ./bin/foo
install: foo 1.3.0 installed to ./bin
The usual curl | sh installer downloads a checksum file from the same page as
the archive, which proves the page is internally consistent and nothing else.
This one carries the digests, so substituting an archive fails:
install: foo_1.3.0_linux_amd64.tar.gz does not match its recorded digest
expected 9f2ab1c4...
got 3e7d0a91...
It is POSIX sh, needs only tar plus either curl or wget, and pins the
exact version it was generated for — a script fetched from /latest/ installs
that release and stays pinned to it.
Add one line to letsgo.mod:
brew danielriddell21/tap
Each release then writes a formula to that tap, one per command, pointing at the archives it just published with the digests it just recorded:
uploaded 8, skipped 0
created Formula/letsgo.rb in danielriddell21/homebrew-tap
desc and license come from the repository's own description, not from more
config. The generated test do block asserts the version the binary reports —
letsgo has already run it and checked, so the formula can assert the same thing
instead of merely proving the binary starts.
Re-running a release renders the same bytes and writes nothing. A tap is someone else's repository, and filling its history with commits that say nothing changed is a cost somebody else pays.
Inside GitHub Actions the default token cannot write to another repository at all, so a tap needs a PAT or an App token.
letsgo plan --publishsays so before anything is built.
One line turns them on:
image
That publishes ghcr.io/<owner>/<project>, multi-arch, tagged with the version,
built from the binaries the release already produced:
pushed ghcr.io/you/tool:1.3.0 (8567febc6508) for [linux/amd64 linux/arm64],
4 blobs uploaded and 0 already present
No Dockerfile, no daemon, no buildx. An image around a static Go binary is a tar
layer and a JSON config, and letsgo already has a deterministic tar writer — so
the image digest is reproducible for the same reason the archives are, and it
goes into letsgo.json where verify can check the tag still resolves to it.
The base is scratch by default: one layer, nothing to patch. That is wrong for
anything that makes HTTPS calls, since there are no CA certificates, so name a
base and its layers are stacked below yours:
image ghcr.io/you/tool
image base gcr.io/distroless/static@sha256:1c2c046bc0…
A base on the same registry is mounted rather than copied, which moves no bytes at all. A base named by tag works and warns — the digest it resolved to is recorded in the manifest either way, so the release always says what it actually built on.
Every release publishes a CycloneDX 1.6 document, sbom.cdx.json, describing the module graph the release was built from. It is written by hand rather than by a generator: the schema is small, the module graph is already known exactly, and the alternative is taking a dependency for a tool whose whole job is to say which dependencies a release has.
It is reproducible, which generated SBOMs usually are not. The conventional ones stamp a wall-clock timestamp and a random serial number into every run, so two SBOMs for the same commit differ and neither can be checked against the other. Here the timestamp is the commit time and the serial number is derived from the module, version and commit — so the file is byte-identical for the same reason the binaries are, and letsgo verify covers it through SHA256SUMS.
The SBOM and the release manifest take their artifact digests from the same place. There is exactly one record of what a release contains, and both files are views of it.
name: release
on:
push:
tags: ["v*"]
permissions:
contents: write
id-token: write # provenance attestation
attestations: write
packages: write # only if you publish a container image
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # no fetch-depth: 0 needed
- uses: actions/setup-go@v5
with: { go-version: stable }
- run: letsgo release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}A shallow clone is fine. If the changelog needs history we don't have, we fall back to the GitHub compare API instead of failing.
There is also a packaged action that installs letsgo and runs it, so the two
run steps above become one uses — see GitHub Action.
The same GITHUB_TOKEN authenticates the image push to ghcr.io, so there is no
second credential to configure. A Homebrew tap is the exception: a workflow token
cannot write to another repository at all, and needs a PAT or an App token
instead.