chore(release): publish only to GitHub Releases, and harden the signer - #222
Merged
kuny0707 merged 2 commits intoAug 22, 2026
Merged
Conversation
The release pipeline advertised four channels — GitHub Releases, a Homebrew tap, .deb/.rpm/.apk packages, and a Docker Hub image. Two of them cost a long-lived credential and neither could be covered by the signature this project tells users to verify. Drop those two. `tronprotocol/homebrew-tap` does not exist, and the `brews:` block carried no `token:`, so it would have run on the default GITHUB_TOKEN — which cannot write to another repository. Making it work needed a cross-repo PAT. `tronprotocol/trond` on Docker Hub does not exist either and needed a DOCKERHUB_TOKEN. Both are secrets that sit in this repository until someone rotates them, and whoever holds one can publish a trond that users trust, under the official name. The cosign signing here is keyless precisely to avoid that: its credential is an OIDC token minted for one workflow run. They were also outside what the signature reaches. `signs:` covers checksums.txt; checksums.txt lists the file artifacts goreleaser built. A tap formula goes to another repository and an image goes to a registry by digest, so neither is in it. README nonetheless claimed the signature "transitively covers every artifact (tarballs, .deb / .rpm / .apk packages, docker images, the homebrew formula)". A local snapshot build shows checksums.txt holding 14 entries: archives and packages, no formula. That sentence is corrected, and the reason the channels are gone is recorded in .goreleaser.yaml and in the constitution so this is not re-added by accident. Two more findings from running the pipeline locally, both in the same blast radius: - The `before:` hook ran `go mod tidy`. tidy writes to go.mod/go.sum, so the release need not be built from the reviewed, committed lockfile — a local snapshot run left go.sum modified in the worktree. Now `go mod download` + `go mod verify`, which only reads. - release.yml referenced its actions by moving tag and pulled goreleaser at `version: latest`. The job holds contents:write and id-token:write, so anything it runs can publish and can sign. Actions are now pinned to commit SHAs at their current versions (no major bumps) and goreleaser to `~> v2.17`. Also fixed along the way: `archives.builds` -> `archives.ids` and the `brews` deprecation both cleared, so `goreleaser check` passes for the first time; the snapshot smoke no longer asserts a homebrew formula it would never find; CHANGELOG had `## [0.1.0-alpha]` twice; and install.sh's "could not query the latest release" blamed the network for what is really `prerelease: auto` — /releases/latest omits pre-releases, so an alpha-only tag 404s there. Verified: `goreleaser check` clean; `goreleaser release --snapshot` produces all 14 artifacts with go.mod/go.sum untouched; the snapshot workflow's assertion re-run under bash passes; make test green; no Go files touched. Not addressed here, and still open before a release can go out: the tag trigger stays disabled pending TX-167, the first version number is unpicked, and the other seven workflows still use unpinned action tags.
The trigger comment read "Tag-triggered releases are temporarily paused (TX-167)", which is wrong twice: it is not temporary, and TX-167 appears nowhere else in the repository, so nobody reading it can find out what it referred to. Manual dispatch is the intended policy — a pushed tag is easy to create by accident, and publishing a signed release should be a deliberate act.
barbatos2011
added a commit
to barbatos2011/tron-deployment
that referenced
this pull request
Aug 22, 2026
… section The changelog described a `0.1.0-alpha` release that never happened. There are no tags in this repository and no published releases, so that heading documented a version nobody could install, and its two link references at the bottom (`compare/v0.1.0-alpha...HEAD`, `releases/tag/v0.1.0-alpha`) were dead. It also appeared twice in a row until recently. Everything is therefore one release. `[Unreleased]` and `[0.1.0-alpha]` are merged into a single `## [0.1.0]`, with the Added list split into **Foundation** (what the alpha section described: the CLI, the intent schema, the render and SSH work) and **Since** (everything after). Changed / Fixed / Security / Repository changes carry over unchanged. Content is preserved line-for-line apart from the two intro paragraphs, which are rewritten into one; a diff of every non-heading line confirms 356 of 358 lines are identical and the 2 differences are those intros. `0.1.0` rather than `0.1.0-alpha`: goreleaser runs with `prerelease: auto`, so a tag carrying a pre-release suffix is filed as a pre-release, and `/releases/latest` — which `scripts/install.sh` queries and the README one-liner depends on — does not return pre-releases. The suffix would have cost a working install path to restate something `0.x` already says under semver. `0.1.0` rather than `1.0.0`: the public surface is not only the CLI flags. It is `schemas/intent.schema.json`, the ~30 schemas under `schemas/output/`, and the exit-code contract `AGENTS.md` instructs agents to branch on. The constitution requires strict semver, so `1.0.0` freezes all of that behind a major-bump promise before any of it has met a real user. `TODOS.md` still carries four deferred decisions, one of them an unreconciled overlap between `scripts/db_cp.sh` and `trond snapshot clone`. The date is left as `2026-XX-XX` because releases are cut by hand and the date is not known until someone dispatches the workflow. To stop that placeholder from shipping, release.yml now checks, before it builds anything, that the top released section names the tag being released and ends in a real date. Both failure modes were exercised locally: the file's current state is rejected for the placeholder, `v0.2.0` against a `[0.1.0]` heading is rejected for the version mismatch, and a filled-in date is accepted. Stacked on tronprotocol#222 — that PR's commits are included here and the diff shrinks to CHANGELOG.md plus the guard step once it merges.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The release pipeline advertised four channels: GitHub Releases, a Homebrew tap,
.deb/.rpm/.apkpackages, and a Docker Hub image. Two of them cost a long-lived credential, and neither of those two could be covered by the signature we tell users to verify. This drops them and keeps GitHub Releases as the only channel.Why the two channels go
tronprotocol/homebrew-tapdoes not exist, and thebrews:block carried notoken:— so it would have run on the defaultGITHUB_TOKEN, which cannot write to another repository. Making it work needed a cross-repo PAT.tronprotocol/trondon Docker Hub does not exist either (404) and needed aDOCKERHUB_TOKEN.Both are secrets that sit in this repository's Actions settings until someone rotates them, and whoever holds one can publish a
trondthat users trust, under the official name. The cosign signing here is keyless precisely to avoid that — its credential is an OIDC token minted for a single workflow run.They were also outside what the signature can reach.
signs:coverschecksums.txt, andchecksums.txtlists the file artifacts goreleaser built. A tap formula is pushed to another repository; an image is pushed to a registry by digest. Neither appears in it.README nonetheless said the signature "transitively covers every artifact (tarballs, .deb / .rpm / .apk packages, docker images, the homebrew formula)". A local snapshot build shows
checksums.txtholding 14 entries — archives and packages, no formula:That sentence is corrected, and the reason the channels are gone is recorded in
.goreleaser.yamland in the constitution so it is not re-added by accident.Two more findings, same blast radius
Both surfaced from actually running the pipeline rather than reading it.
The
before:hook rango mod tidy.tidywrites togo.mod/go.sum, so a release need not be built from the reviewed, committed lockfile. This is not theoretical — a local snapshot run leftgo.summodified in the worktree (it pruned a stalegolang.org/x/net v0.54.0). Nowgo mod download+go mod verify, which only read.release.ymlreferenced its actions by moving tag and pulled goreleaser atversion: latest. That job holdscontents: writeandid-token: write— it can publish and sign, so everything it runs is in the trusted computing base of every signature this project issues, and a tag repointed upstream would be an unreviewed change to the signer. Actions are now pinned to commit SHAs at their current versions (no major bumps), goreleaser to~> v2.17.Incidental fixes
archives.builds→archives.ids, and thebrewsdeprecation is gone with the block —goreleaser checkpasses for the first time (it previously exited non-zero, and is not run in CI).dist/homebrew/*.rb, which would now never appear. Removed, along with the dead--skip=docker.CHANGELOG.mdhad## [0.1.0-alpha] — 2026-XX-XXtwice in a row.install.shprinted "could not query the latest release" for what is reallyprerelease: auto:/releases/latestomits pre-releases, so an alpha-only tag 404s there and the message blamed the network. It now names the real cause. Installing a pre-release stays opt-in viaTROND_VERSION.Dockerfile.releasedeleted — its only consumer was the removeddockers_v2block.Verification
goreleaser checkgoreleaser release --snapshotgo.mod/go.sumafter that buildmake testStill open — not in this PR
prerelease: autofiles it as a pre-release and the README one-liner will not resolve it.release.yml, but the same class of issue.workflow_dispatchrun on a throwaway tag before the first real release.Reintroducing a tap or an image later is a real option, not a prohibition. It needs a named credential-rotation owner and a verification story of its own — a fine-grained PAT scoped to
contents:writeon the tap alone, andcosign signon the image digest (or GHCR plus build provenance, which needs no standing secret at all).