Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/goreleaser-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ name: GoReleaser Snapshot

# Smoke-tests the goreleaser pipeline on every PR that touches the
# .goreleaser.yaml or release-related Go files. Catches manifest
# typos, missing dependencies, and dockers_v2 schema drift before
# a real tag pushes them to production.
# typos, missing dependencies, and schema drift before a real tag
# pushes them to production.
#
# Skips publishing — only generates artifacts to dist/ and validates
# they're produced. Asserts every advertised artifact format
# (binary, deb, rpm, brew, docker) materialised.
# (binary, deb, rpm, apk) materialised.

on:
pull_request:
Expand Down Expand Up @@ -41,12 +41,10 @@ jobs:
with:
distribution: goreleaser
version: latest
# Skip docker too — building images in a snapshot smoke is
# noisy (depends on the binary layout produced by goreleaser
# itself, which can drift between versions) and doesn't add
# signal beyond the artifact-exists checks below. Docker
# builds run in the real release pipeline.
args: release --snapshot --clean --skip=publish,sign,docker
# Signing is skipped: cosign keyless needs an OIDC token this
# read-only PR job deliberately does not have. It runs for real
# in release.yml.
args: release --snapshot --clean --skip=publish,sign

- name: Inspect dist/
run: |
Expand All @@ -57,7 +55,10 @@ jobs:
# goreleaser names archives as `<binary>_<version>_<os>_<arch>.<ext>`
# with underscores (its default name_template), not hyphens.
# Each format below is in goreleaser.yaml; if removed, drop
# from the matrix here at the same time.
# from the matrix here at the same time. GitHub Releases is the
# only publication channel, so every glob here is a file attached
# to the release and hashed into checksums.txt — which is exactly
# what the cosign signature covers.
run: |
set -e
fail=0
Expand All @@ -70,7 +71,6 @@ jobs:
'dist/*.rpm'
'dist/*.apk'
'dist/checksums.txt'
'dist/homebrew/*.rb'
)
for g in "${want_globs[@]}"; do
files=( $g )
Expand Down
52 changes: 20 additions & 32 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
name: Release

on:
# Tag-triggered releases are temporarily paused (TX-167). The pipeline
# itself is kept intact; run it manually from the Actions tab or via
# `gh workflow run release.yml` when releases resume.
# Releases are cut by hand, deliberately — there is no `push: tags:`
# trigger and this is not a pause waiting to be lifted. Pushing a tag is
# cheap and easy to do by accident; publishing a signed release under this
# project's name should take an explicit act by someone who meant it. Run
# from the Actions tab, or `gh workflow run release.yml --ref <tag>`.
workflow_dispatch:

permissions:
contents: write
id-token: write

# This job holds `contents: write` and `id-token: write` — it can publish a
# release AND mint the OIDC token cosign signs with. Everything it runs is
# therefore inside the trusted computing base of every signature this project
# produces, so third-party actions are pinned to a commit SHA rather than a
# tag: a tag is a mutable pointer its owner can repoint at any time, which
# would change the signer with no diff here. The trailing comment records
# which release each SHA was, so bumps stay readable.
jobs:
release:
runs-on: ubuntu-latest
# Resolve secrets into job-level env so step-level `if:` conditions
# can probe them. The `secrets` context isn't accessible from `if:`
# directly, and step-level `env:` is evaluated after `if:` so the
# only working pattern is hoisting the resolution to the job.
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0

- uses: actions/setup-go@v5
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"

Expand All @@ -36,30 +39,15 @@ jobs:
# the workflow level grants the token; the signs: block in
# .goreleaser.yaml invokes cosign during the release.
- name: Install cosign
uses: sigstore/cosign-installer@v3

# Multi-arch docker images go through buildx + qemu so the linux
# arm64 image can be built on this amd64 runner. Without these,
# the goreleaser docker step fails at the first `--platform` arg.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# Skip the login on forks that haven't set DOCKERHUB_USERNAME — the
# secret resolves to "" via the job-level env: block above, which
# is something `if:` is allowed to probe.
- name: Log in to Docker Hub
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1

# goreleaser itself is pinned for the same reason as the actions above:
# `version: latest` let an upstream release change what builds and signs
# this project's artifacts without any change on this branch.
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:
version: latest
version: "~> v2.17"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88 changes: 32 additions & 56 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ project_name: trond

before:
hooks:
- go mod tidy
# `go mod download`, NOT `go mod tidy`: tidy WRITES to go.mod/go.sum, so
# the release could be built from a lockfile that differs from the
# reviewed, committed one. Observed in practice — a local
# `goreleaser release --snapshot` left go.sum modified in the worktree.
# download only fetches what go.sum already pins; `go mod verify` then
# re-checks those modules against their recorded hashes.
- go mod download
- go mod verify

builds:
- id: trond
Expand Down Expand Up @@ -40,7 +47,7 @@ builds:

archives:
- id: default
builds:
ids:
- trond
formats:
- tar.gz
Expand All @@ -52,7 +59,7 @@ archives:
- schemas/*

- id: replay
builds:
ids:
- replay
formats:
- tar.gz
Expand Down Expand Up @@ -136,60 +143,29 @@ nfpms:
file_info:
mode: 0644

# Homebrew tap so `brew install tronprotocol/tap/trond` works on macOS
# and Linuxbrew. Requires the GitHub user to maintain a `homebrew-tap`
# repo under the same org; goreleaser bumps the formula automatically
# on every release.
# Distribution policy: GitHub Releases is the ONLY publication channel.
#
# TODO: goreleaser is phasing out `brews` in favor of `homebrew_casks`.
# Casks are traditionally for GUI/.app installs but Homebrew has been
# pushing CLI tools through them too. Migration changes the user-facing
# install command from `brew install` to `brew install --cask` and
# requires retesting against Linuxbrew. Hold until goreleaser actually
# removes brews — it's been "phasing out" for >1 year and the formula
# path is still the cleaner UX for CLI tools.
brews:
- name: trond
# Scope to the trond archive only — the homebrew tap installs the
# main CLI, not the sibling replay tool. Without ids, goreleaser
# tries to package both archives under the same OS/Arch slot and
# errors with "one tap can handle only one archive".
ids:
- default
repository:
owner: tronprotocol
name: homebrew-tap
branch: main
homepage: https://github.com/tronprotocol/tron-deployment
description: CLI for declarative TRON node deployment
license: MIT
test: |
system "#{bin}/trond version"
install: |
bin.install "trond"
generate_completions_from_executable(bin/"trond", "completion")

# Docker image — useful for CI runners and one-off "exec into a trond"
# scenarios. The dockers_v2 block replaced the older `dockers` +
# `docker_manifests` pair: it builds a single multi-arch manifest in
# one pass instead of two per-arch builds plus a manual manifest fan-in.
# `platforms` enumerates the targets; goreleaser fans out to buildx.
dockers_v2:
- id: trond
images:
- "tronprotocol/trond"
tags:
- "{{ .Version }}"
- "latest"
platforms:
- linux/amd64
- linux/arm64
dockerfile: Dockerfile.release
labels:
org.opencontainers.image.source: "https://github.com/tronprotocol/tron-deployment"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"

# A Homebrew tap and a Docker Hub image were both configured here and were
# deliberately removed. Each needed a long-lived credential stored in this
# repository's Actions secrets — a cross-repo PAT for `tronprotocol/homebrew-tap`
# (the default GITHUB_TOKEN cannot write to another repository) and a
# DOCKERHUB_TOKEN — and anything holding those can publish a "trond" that users
# trust, under the official name, indefinitely, until someone notices and
# rotates. The cosign signing above is keyless: its credential is an OIDC token
# minted for one workflow run and worthless a minute later. Adding a channel
# that reintroduces a standing secret undoes most of what that buys.
#
# It is also what the signature can actually reach. `signs:` covers
# checksums.txt, and checksums.txt lists the file artifacts goreleaser built —
# the archives and the deb/rpm/apk packages. A tap formula is pushed to another
# repository and a container image is pushed by digest to a registry; neither
# appears in checksums.txt, so neither was covered by the signature the README
# told users to rely on.
#
# Reintroducing either is a real option, not a prohibition — but it needs a
# named credential-rotation owner and a verification story of its own (a
# fine-grained PAT scoped to contents:write on the tap alone; `cosign sign` on
# the image digest, or GHCR + build provenance instead of Docker Hub).
release:
github:
owner: tronprotocol
Expand Down
11 changes: 8 additions & 3 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,14 @@ The implementation language for trond V1 is **Go**. This decision is binding bec
bump and a documented migration path.
- Pipelines MUST pin trond to an explicit version. Use of `latest` in CI is forbidden in
documentation and examples.
- Distribution channels for V1: GitHub Releases binaries (with SHA256), official Docker
image (`tronprotocol/trond:<version>`), and the `setup-trond` GitHub Action. V2 may add
Homebrew tap, install.sh, apt/yum packages.
- Distribution channel for V1: GitHub Releases ONLY — cross-compiled binaries and
.deb/.rpm/.apk packages, hashed into `checksums.txt`, which is signed with cosign
keyless OIDC. `scripts/install.sh` consumes that release and verifies the hash.
Channels needing a standing publish credential (a Homebrew tap, a container
registry) are deliberately excluded: that credential could publish under this
project's name until someone rotated it, and its artifacts cannot appear in the
`checksums.txt` the signature covers. Adding one is a V2 decision that MUST come
with a named rotation owner and a verification story of its own.
- Offline / air-gapped environments MUST be supported via the `TROND_DOWNLOAD_MIRROR`
environment variable, which redirects all of trond's external downloads (jars, snapshots,
bootstrap dependencies) to a user-supplied internal mirror.
Expand Down
31 changes: 25 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,10 @@ The agent-ergonomics arc lands across four sequenced PRs:
`.github/dependabot.yml`
- `cmd/gendoc` — emits man(1) pages and per-command markdown
- `scripts/install.sh` — single-shot installer with SHA256 verification
- `Dockerfile.release` — slim alpine image with `tronprotocol/trond`

### Changed
- `goreleaser` now produces a homebrew tap, .deb / .rpm / .apk, and
multi-arch docker image alongside tar.gz archives; release notes
group commits by feat/fix
- `goreleaser` now produces .deb / .rpm / .apk packages alongside the
tar.gz archives; release notes group commits by feat/fix
- CI matrix expanded: `lint`, `test+coverage`, `govulncheck`, and
cross-compile jobs run on every PR; e2e is its own workflow with
the heavier schedule
Expand Down Expand Up @@ -263,6 +261,29 @@ The agent-ergonomics arc lands across four sequenced PRs:
the SSH key after apply)

### Security
- **GitHub Releases is the only publication channel.** The Homebrew tap and
the `tronprotocol/trond` Docker image were configured and are removed
before the first tag. Both needed a long-lived credential in this
repository's Actions secrets — a cross-repo PAT for the tap (the default
`GITHUB_TOKEN` cannot write to another repository) and a `DOCKERHUB_TOKEN`
— and either would let its holder publish under this project's name until
someone noticed and rotated it. That is the standing-secret risk cosign
keyless signing exists to remove. Neither channel's artifact could be
listed in `checksums.txt` either, so the README's claim that the signature
covered "docker images, the homebrew formula" was false; a snapshot build
confirms `checksums.txt` holds only the archives and the deb/rpm/apk
packages. README, the goreleaser manifest, and the project constitution
now agree on this
- Third-party actions in `release.yml` are pinned to a commit SHA rather
than a moving tag, and `goreleaser-action`'s `version: latest` is pinned
to a range. That job holds `contents: write` and `id-token: write`, so
everything it runs is inside the trusted computing base of every
signature the project issues; a tag repointed upstream would have been an
unreviewed change to the signer
- The goreleaser `before:` hook ran `go mod tidy`, which WRITES to
`go.mod`/`go.sum` — a release could be built from a lockfile other than
the reviewed, committed one (a local snapshot run did modify `go.sum`).
Replaced with `go mod download` + `go mod verify`
- Reject control characters (`\n`, `\r`, …) in every free-form intent
field; struct-tag `safe_string` + manual map/slice walk close compose
YAML and systemd unit injection vectors
Expand Down Expand Up @@ -317,8 +338,6 @@ The agent-ergonomics arc lands across four sequenced PRs:

## [0.1.0-alpha] — 2026-XX-XX

## [0.1.0-alpha] — 2026-XX-XX

Initial public alpha. The project transitions from a curated set of HOCON
configuration templates into a CLI for declarative TRON node deployment.

Expand Down
22 changes: 0 additions & 22 deletions Dockerfile.release

This file was deleted.

18 changes: 3 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ SHA256, and drops the binary into `/usr/local/bin` (or `~/.local/bin` if it
can't write the system path). Set `TROND_VERSION=v0.x.y` to pin a specific
release, or `TROND_DEST=/path` to install elsewhere.

### Homebrew (macOS / Linuxbrew)

```bash
brew install tronprotocol/tap/trond
```

### Linux packages

`.deb`, `.rpm`, and `.apk` packages are attached to every release, e.g.
Expand All @@ -60,14 +54,6 @@ sudo dpkg -i trond_*.deb
sudo rpm -i https://github.com/tronprotocol/tron-deployment/releases/latest/download/trond_VERSION_linux_amd64.rpm
```

### Docker

```bash
docker run --rm -v ~/.trond:/home/trond/.trond \
-v /var/run/docker.sock:/var/run/docker.sock \
tronprotocol/trond:latest --help
```

### From release tarball

Download from [Releases](https://github.com/tronprotocol/tron-deployment/releases):
Expand Down Expand Up @@ -131,7 +117,9 @@ autoload -U compinit && compinit

## Verifying a release

Every published release is signed with [Sigstore](https://www.sigstore.dev/) `cosign` keyless OIDC — no long-lived signing key, identity is the GitHub Actions workflow that built the release. The signature covers `checksums.txt`, which transitively covers every artifact (tarballs, .deb / .rpm / .apk packages, docker images, the homebrew formula).
Every published release is signed with [Sigstore](https://www.sigstore.dev/) `cosign` keyless OIDC — no long-lived signing key, identity is the GitHub Actions workflow that built the release. The signature covers `checksums.txt`, and `checksums.txt` lists every artifact attached to the release: the tarballs and the .deb / .rpm / .apk packages. Verifying the signature and then checking your download against that file therefore covers everything this project publishes.

GitHub Releases is the only channel — there is no Homebrew tap and no container image. Publishing to either would mean keeping a long-lived credential that could release under this project's name, which is the thing keyless signing exists to avoid, and neither a tap formula nor a registry image can be listed in `checksums.txt` in the first place. If you want a package-manager install, use the `.deb` / `.rpm` / `.apk` above or build [from source](#from-source).

One-time setup:

Expand Down
Loading
Loading