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¶
- Fetch
<base>/latest.json. Refuse an unknownschema; refuse amanifestthat is not under the base. - Fetch the manifest it names; validate it the same way.
- Pick the download whose
osandarchmatch the running binary. None means this release does not ship for the platform. - Fetch
checksums.txtand, 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'ssha256lets a reader refuse a mismatched archive before it ever reaches the verifier. - To list versions, follow
previousfrom manifest to manifest. To pin a version, fetch<base>/<tag>/release.jsondirectly.
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:
- Upload every archive.
- Upload
checksums.txt(andchecksums.txt.sigwhen signing). On the estate's store this is the completion sentinel: a tag withchecksums.txtis a finished publish. - Upload
<tag>/release.json. - Only then move
<base>/latest.jsonto name the new tag, with a conditional write (If-Matchon 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:
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.
Related¶
- Configure self-updating: choosing the channel for a generated tool.
- Secure releases: checksums, signatures and the trust set the reader verifies against.
- Spec 0203: the decisions behind the layout.