Skip to content

Static release channel layout

A tool on the static release channel (spec 0203) discovers and retrieves its releases from a plain HTTPS location with no forge involved. This page is the contract for that location: what must be there, named how, containing what, published in which order. Anything that produces this layout is a valid target, whether it is the scaffolded GoReleaser configuration, a hand-rolled uploader or another build system.

The examples use https://pkg.example.com/acme/mytool as the base URL, which is the one value a tool is configured with.

Paths

Object Path Mutable
Pointer <base>/latest.json Yes, the only one. Moved forward on each publish.
Release manifest <base>/<tag>/release.json No. Written once with the release.
Archives <base>/<tag>/<name> No.
Checksums <base>/<tag>/checksums.txt No. Written after every archive.
Signature <base>/<tag>/checksums.txt.sig No. Present only for a signed release.

<tag> is the release's tag as the tool reports it, v1.2.0, with the v. Everything for one release sits under its tag; nothing is shared between tags. The location needs no directory listing: a reader never lists, it fetches the pointer and follows what the documents say.

The pointer: latest.json

{
  "schema": 1,
  "tool": "mytool",
  "tag": "v1.2.0",
  "manifest": "https://pkg.example.com/acme/mytool/v1.2.0/release.json",
  "published_at": "2026-09-19T12:20:00Z"
}
Field Type Required Meaning
schema integer yes Document schema. This page describes 1. A reader refuses a value it does not know.
tool string yes The tool's name, as project_name in GoReleaser.
tag string yes The current release's tag, semver with an optional v.
manifest string yes Absolute URL of that release's release.json. Must be under the base URL.
published_at string yes RFC 3339 timestamp of the publish that moved the pointer.

The release manifest: <tag>/release.json

{
  "schema": 1,
  "tool": "mytool",
  "tag": "v1.2.0",
  "released_at": "2026-09-19T12:17:26Z",
  "previous": "v1.1.0",
  "checksums": "https://pkg.example.com/acme/mytool/v1.2.0/checksums.txt",
  "signature": "https://pkg.example.com/acme/mytool/v1.2.0/checksums.txt.sig",
  "notes": "",
  "downloads": [
    {
      "os": "linux",
      "arch": "amd64",
      "name": "mytool_Linux_x86_64.tar.gz",
      "url": "https://pkg.example.com/acme/mytool/v1.2.0/mytool_Linux_x86_64.tar.gz",
      "size": 23456789,
      "sha256": "ec90dd6d7f33fcaa8179a6647e0ad6e2fe17e0551b8989760ff2ce33ed907d81"
    }
  ]
}
Field Type Required Meaning
schema integer yes Document schema, 1.
tool string yes The tool's name.
tag string yes This release's tag.
released_at string yes RFC 3339 timestamp of the build.
previous string yes, may be empty The tag of the release published before this one, or "" for the first. A tag, never a URL: the chain cannot leave the base.
checksums string yes Absolute URL of checksums.txt, under the base.
signature string no Absolute URL of the detached signature over checksums.txt, under the base. Omit the key for an unsigned release.
notes string no Release notes as text. Omit or leave empty when there are none.
downloads array yes, at least one One entry per archive.

Each download:

Field Type Required Meaning
os string yes Go's GOOS: linux, darwin, windows, …
arch string yes Go's GOARCH: amd64, arm64, …
name string yes The archive's file name, as it appears in checksums.txt.
url string yes Absolute URL of the archive, under the base.
size integer yes Size in bytes, greater than zero.
sha256 string yes Lowercase hex SHA-256 of the archive, 64 characters, the same digest checksums.txt carries.

Downloads are ordered by os then arch, so the document is stable whatever order the build ran in.

Discovery and retrieval, from the reader's side

  1. Fetch <base>/latest.json. Refuse an unknown schema; refuse a manifest that is not under the base.
  2. Fetch the manifest it names; validate it the same way.
  3. Pick the download whose os and arch match the running binary. None means this release does not ship for the platform.
  4. Fetch checksums.txt and, when named, checksums.txt.sig; verify the signature against the trust set and the archive against the checksum exactly as on the forge channel. The manifest's sha256 lets a reader refuse a mismatched archive before it ever reaches the verifier.
  5. To list versions, follow previous from manifest to manifest. To pin a version, fetch <base>/<tag>/release.json directly.

A reader trusts nothing in a document that would take it off the base: every URL must share the base URL's scheme and host and sit under its path. https cannot be walked down to http.

Publishing, in order

The order is what keeps a reader safe at every instant:

  1. Upload every archive.
  2. Upload checksums.txt (and checksums.txt.sig when signing). On the estate's store this is the completion sentinel: a tag with checksums.txt is a finished publish.
  3. Upload <tag>/release.json.
  4. Only then move <base>/latest.json to name the new tag, with a conditional write (If-Match on the ETag read at the start of the publish) so two publishes cannot silently clobber each other.

A pointer that is behind (a publish that died before step 4) offers nobody a release the location does not fully hold, which is safe, and is fixed by re-running step 4. A pointer that is ahead is impossible in this order.

Nothing under <tag>/ is ever modified or removed once published. The pointer is the one object that changes.

Producing the manifest with GoReleaser

The scaffolded .goreleaser.yaml produces the manifest with the framework's tool, from what GoReleaser already knows:

after:
  hooks:
    - go tool releasemanifest --dist dist --base-url https://pkg.example.com/acme/mytool

releasemanifest reads dist/artifacts.json (archives and their checksums) and dist/metadata.json (tag and date), reads the current pointer at the base URL to fill previous (empty when there is none yet), and writes dist/release.json, which the blobs: pipe then uploads with everything else. It refuses a dist whose archives carry no checksum. --previous <tag> overrides the pointer lookup; --notes <file> supplies the notes.

Moving the pointer is a separate publish step after blobs:, because GoReleaser's blobs: pipe cannot send a conditional write. The scaffolded configuration carries it. A publisher without GoReleaser writes both documents from this page and follows the order above.