Skip to content

Commit 0026f5d

Browse files
Patel230claude
andauthored
fix(release): make v0.3.0 releasable and every install path truthful (#341)
* chore(release): set VERSION to 0.3.0 The Go module proxy already serves v0.1.0, v0.1.1 and v0.2.0 for github.com/GrayCodeAI/rho (hawk-era tags), so a VERSION of 0.0.1 made source builds report a version below the published ones and could never become @latest. The next rho release is v0.3.0. - VERSION: 0.0.1 -> 0.3.0 - compatibility matrix "stable": rho 0.3.0 with flux 0.0.1 (the pinned go.mod dependency) instead of the never-released rho 0.0.1 - testaudit: VERSION must be SemVer without a leading "v" and >= 0.3.0 Refs: F018, F024 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(release): build and CI-check windows/arm64 README promises binaries for linux/darwin/windows on amd64/arm64, `make build-all`, install.sh and the npm scaffolding (rho-win32-arm64) all handle windows/arm64, but .goreleaser.yml ignored that target and the CI build matrix excluded it, so no windows_arm64 archive was ever produced. `GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build ./cmd/rho` succeeds, so build the target instead of narrowing the promise: - .goreleaser.yml: drop the windows/arm64 ignore (six archives) - ci.yml: drop the matrix exclude so the target is compile-checked - npm assemble script: drop the comments about the missing target Refs: F282 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(release): drop the Homebrew publisher The brews stanza pushed a formula to GrayCodeAI/homebrew-tap, a repository that does not exist, with HOMEBREW_TAP_TOKEN, a secret that is not set. On the next tag GoReleaser would have created the GitHub release and then failed in the brew publisher, leaving the run red and the release unsigned. `brews` is also deprecated in the pinned GoReleaser v2.17.0, so `goreleaser check` exited 1 on this config. Per the owner decision the tap is not being created now: remove the stanza and the unused HOMEBREW_TAP_TOKEN/TAP_GITHUB_TOKEN env wiring. `goreleaser check` now validates the config. Re-adding Homebrew (homebrew_casks) is documented as a follow-up in docs/RELEASING.md. Refs: F021 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(release): make a v* tag produce a signed, SBOM'd rho release The tag-triggered workflow had never completed (10 of 10 runs failed): - GoReleaser's `sboms:` stanza shells out to syft, which the runner does not have ("exec: syft: executable file not found"). Install it with anchore/sbom-action/download-syft pinned by SHA. - The cosign step ran `gh release download ... -D dist/` into the dist/ directory GoReleaser had just written, so it failed on the existing dist/checksums.txt before signing anything. Signing now happens inside GoReleaser (`signs:`, cosign keyless over checksums.txt, emitting the Sigstore bundle checksums.txt.sigstore.json) so the signature is uploaded with the release. cosign-installer is bumped to v4.1.2 (cosign v3), whose sign-blob requires --bundle. A final step re-downloads the published files and verifies them with the exact certificate identity install.sh uses. - `packages: write` was unused (no images or packages are published). - The checkout-flux step cloned sibling repos the build never used (go.mod pins published flux v0.0.1); the job now sets GOWORK=off and builds only from go.mod/go.sum, with the local-replace guard. - New scripts/check-release-tag.sh refuses a tag that is not "v" + VERSION or whose CHANGELOG.md has no non-empty section for it, and extracts that section as the release notes (--release-notes), so published notes match the curated changelog instead of an auto-generated list of every commit since the root. - Builds use -trimpath; /dist/ is gitignored. The shared templates are regenerated from the fixed files. `goreleaser check` passes and `goreleaser release --snapshot --clean --skip=publish,sign` builds six rho_* archives, six SPDX SBOMs and checksums.txt locally (signing needs the Actions OIDC token and was not exercised). testaudit now pins these invariants. Refs: F022, F023, F024, F031, F259 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(install): verify releases fail-closed and parse flags strictly install.sh could not install anything and was weaker than it claimed: - It fetched rho_<ver>_* from releases/latest (v0.2.0), whose assets are hawk_*: every install 404ed. Releases before v0.3.0 predate the rename, so they are now refused with a clear message and no download (no hawk_* fallback: those archives contain a `hawk` binary). - Signature: it looked for checksums.txt.sig/.cert, which no release has, and matched the certificate with an unanchored regexp built from the unescaped version ("anchor and escape" said the comment). It now verifies the Sigstore bundle checksums.txt.sigstore.json that the release workflow uploads, with the exact --certificate-identity https://github.com/<repo>/.github/workflows/release.yml@refs/tags/v<ver>. When cosign is present, a missing or invalid signature aborts. When it is absent the script prints a prominent warning and reports "signature: NOT verified" in the summary; RHO_REQUIRE_COSIGN=1 makes cosign mandatory. - The SHA-256 check always runs (sha256sum, shasum or openssl) and requires exactly one exact-name entry in checksums.txt. - Flags are parsed in a loop: --version/--prefix in any order, the --flag=value forms, --help, and exit 2 on unknown or empty flags (previously `--version X --prefix Y` silently ignored the prefix). - The version is validated before use, unsupported OS/arch fail fast, temp files are cleaned on exit/signals, and RHO_INSTALL_REPO (validated owner/repo) lets a fork install its own signed releases. internal/testaudit/install_script_test.go runs the real script end to end against a fake release served by stub curl/uname/cosign, with PATH restricted so the host's tools cannot change the outcome. Refs: F019, F029, F260 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(action): install rho from verified release archives The composite action downloaded the GitHub "latest" release's source tarball with `curl -sL | tar` (no -f, no checksum, no signature) and ran `go build ./cmd/rho` on it. Today that tarball is the hawk-era v0.2.0 tree, which has no cmd/rho, so the action failed for every default invocation; for pinned versions it prepended a second "v" to inputs documented as "v1.2.3"; and a `@main` pin tried to download tag "vmain". The install step now: - installs cosign (sigstore/cosign-installer, pinned SHA) and runs the repository's install.sh at the action's own ref with RHO_REQUIRE_COSIGN=1, so the rho_<ver>_<os>_<arch> archive is checked against the signed checksums.txt (exact release-workflow identity) and its SHA-256 before anything runs; Go is no longer required; - resolves the version as: rho-version input (0.3.0, v0.3.0 or latest), else the release tag the action is pinned to, else the latest release via `gh release view` with the github-token input; - validates rho-repo (owner/repo) and the version before use. Releases before v0.3.0 are refused with a clear message by install.sh. Also drops the stale feat/ecosystem-wiring-pr branch mapping in ci.yml. Checked locally by extracting the step: a @main pin resolves latest (v0.2.0) and is refused as pre-rename, rho-version 0.3.0 fails with a clear 404 until that release exists, and malformed rho-version/rho-repo exit 2. Refs: F031, F254 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(readme): make the install section truthful Every documented install path failed for a new user: - `curl .../main/install.sh | sh` requested rho_0.2.0_* assets from the latest release, which only has hawk_* assets (404); - `brew install graycodeai/tap/rho`: GrayCodeAI/homebrew-tap does not exist; - `npm install -g @graycodeai/rho`: nothing is published (E404); - `go install .../cmd/rho@latest`: @latest is the hawk-era v0.2.0, which has no cmd/rho ("does not contain package"). The Install section now says prebuilt installs are available from v0.3.0 (the first rho-named release) and points to the source build until then; shows the script from the immutable v0.3.0 tag (tags are protected) with what it verifies and RHO_REQUIRE_COSIGN=1; lists the release assets; and states plainly that Homebrew and npm packages do not exist. The contributor `go install` line uses @main, verified today with a clean GOBIN against proxy.golang.org, and explains why @latest fails until v0.3.0 is tagged. Refs: F002, F019, F025, F262 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(npm): gate the unpublished npm packaging and fix its assembly `npm install -g @graycodeai/rho` is E404: nothing is published, there is no publish job, and the owner decided not to create the npm org now. The scaffolding also had defects that would have broken a publish: - every package.json said "license": "Apache-2.0" while the project is MIT (LICENSE, goreleaser); - the assemble script stamped only the sub-packages, leaving the meta package and its optionalDependencies at 0.0.0-development, so every install would report "platform package not installed"; - it looked for dist/rho_<os>_arm64/ but GoReleaser v2 writes dist/rho_<os>_arm64_v8.0/, so the arm64 targets always failed. Changes: license MIT everywhere; "private": true on all seven packages so `npm publish` refuses until a real channel exists; the assemble script takes the version from VERSION (or RHO_NPM_VERSION), validates it, stamps the meta package and its optionalDependencies, and uses the real GoReleaser v2 directory names. npm/README.md states the channel is not available and lists what a publish needs. The README no longer advertises npm. Checked on a scratch copy against the local snapshot dist/: all six targets assembled at 0.3.0 and the meta package pins 0.3.0. Refs: F020 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(nix): remove the flake that cannot evaluate flake.nix declared inputs for five GitHub repositories that no longer exist (merlin, kestrel, shrike, swift, harrier), so `nix build` and `nix develop` failed while fetching inputs. Even with those removed it would have built with vendorHash = null and GONOSUMCHECK=1 (no dependency integrity), hardcoded version 0.0.1, and no flake.lock. Nothing in CI evaluated it and the README does not offer Nix. Nix is not installed where this change was prepared and CI never ran it, so a fixed flake (real vendorHash, flake.lock, `nix flake check` in CI) could not be verified. Rather than ship another unverified install path, remove flake.nix and the `use flake` .envrc that only loaded it; /.envrc is now gitignored so a local direnv file (which `make test-live` may read keys from) is never committed. Re-adding a verified flake is recorded as a follow-up in docs/RELEASING.md. Refs: F027, F211, F263 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: fail the build when the release binary exceeds its size budget The CI "size check" only emitted a ::warning above 98 MB and never failed; `make size-check` warned at a different threshold (80 MB, "matching CI"), and both comments claimed the binary was ~95 MB. The stripped linux/amd64 build of ./cmd/rho is 62,492,834 bytes (59.6 MiB) today, so any regression short of +38 MB passed silently. The budget now lives in one place, SIZE_LIMIT_MB := 70 in the Makefile (~10 MiB headroom), `make size-check` exits 1 above it (and no longer needs bc or platform-specific stat flags), and the CI build job runs `make size-check` on linux/amd64 instead of its own copy of the logic. Verified: `make size-check` passes at 60 MiB and fails with SIZE_LIMIT_MB=50. Refs: F030 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(packaging): make the systemd unit's state reachable under its hardening packaging/systemd/rho-daemon.service could not work as written: - ProtectHome=true makes /home, /root and /run/user inaccessible, yet the unit granted ReadWritePaths=%h/.rho/state and ReadOnlyPaths=%h/.rho. In a system unit %h is always /root (systemd.unit(5)), and rho does not keep state in ~/.rho anyway (it uses the user config dir). - The docs said the unit runs as user `rho`, but it had no User=, so it ran as root. - `--api-key ${RHO_DAEMON_API_KEY}` put the key on the command line (visible in ps) although the daemon already reads the variable. - Documentation= pointed at https://docs.rho.ai, which does not resolve. The unit now runs as User=rho with StateDirectory=rho/CacheDirectory=rho and points HOME, RHO_CONFIG_DIR, RHO_STATE_DIR and RHO_CACHE_DIR there, keeps ProtectSystem=strict/ProtectHome=true/PrivateTmp, loads the API key from an optional root-owned EnvironmentFile, and links the rho troubleshooting guide, whose systemd section now documents the user, paths, log file and how to grant project directories. internal/testaudit/packaging_test.go parses the unit and fails on paths hidden by ProtectHome, a missing User=, state outside StateDirectory, an API key on ExecStart, or non-repository documentation links. The unit was not started under systemd here (no systemd/systemd-analyze on this machine); the ProtectHome and %h semantics are from systemd.exec(5) and systemd.unit(5). Refs: F032 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: remove the unenforced ast-grep configuration sgconfig.yaml pointed testDirs at a tests/ directory that does not exist, and the setup never enforced anything: - ast-grep (0.39.5, checked locally) only discovers sgconfig.yml, so `sg scan`/`sg test` in the repo root fail with "No ast-grep project configuration is found"; with `-c sgconfig.yaml`, `sg test` runs 0 tests; - the lefthook hook ran `sg scan` only when sg happened to be installed, piped through `head`, so it could never block a commit; - CI never ran it; - its single rule (no fmt.Println outside tests) contradicts the CLI, which prints user output with fmt.Println in ~178 places; the real guard for internal/ is testaudit's TestNoRawFmtPrintInInternal. Delete sgconfig.yaml, rules/ and the lefthook hook rather than imply an enforcement that does not exist. Refs: F033 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(release): document the manual, tag-protected release process CONTRIBUTING.md, README.md, docs/versioning.md, docs/compatibility.md, the compatibility matrix, cmd/rho/main.go and the shared templates all said release-please bumps VERSION, writes CHANGELOG.md and tags releases, and told contributors never to edit either file. No release-please config, manifest or workflow exists; releases are tags pushed by hand and CHANGELOG entries are hand-written. - New docs/RELEASING.md: the release PR checklist (VERSION, CHANGELOG section, compatibility matrix, README install URL), tagging, what release.yml does, how to verify a release, the status of every distribution channel (Homebrew, npm and Nix explicitly not available, with what each would need), and v0.3.0 specifics (hawk-era tags, retraction, "Latest" release). - Documents the existing tag ruleset protect-release-tags (id 24053191): creation/update/deletion/non-fast-forward of refs/tags/v* restricted, repository admin role as the only bypass. - Every release-please claim is replaced with the manual process; the PR checklist now asks for an [Unreleased] CHANGELOG entry instead of forbidding CHANGELOG edits. - `make release` no longer runs `goreleaser release --clean` from a workstation (an unsigned publish with a personal token, which is how the hawk-era v0.2.0 assets were produced); it explains the tag-driven process and exits 1. New `make release-check` (the release.yml guard) and `make release-snapshot` (goreleaser check + snapshot build, no publish or signing) replace it for local validation. Refs: F026, F028, F269 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): repair the version history and record this release work CHANGELOG.md listed [0.2.0] (2026-07-13) above [0.4.0] and [0.3.0] (May 2026), so versions were not monotonic, and it already had a "## [0.3.0]" heading for hawk-era work although v0.3.0 is the next rho release (the release guard would have accepted that stale section as the v0.3.0 notes). [Unreleased] had two "### Changed" lists. - A short history note explains that tags v0.1.0, v0.1.1 and v0.2.0 were built from the github.com/GrayCodeAI/hawk module and that v0.3.0 is the first rho release. - Historical sections keep their text but are relabelled "[hawk 0.2.0]" (the v0.2.0 tag) and "[hawk X.Y.Z, untagged]" (earlier numbering that was never tagged), so no heading can be mistaken for a rho version. - "Flux v0.2.1" is annotated as the eyrie version of the time, and the internal/jsonc, internal/providers and internal/permissions/verdict.go bullets are marked "(since removed)". - The two [Unreleased] "### Changed" lists are merged, and this PR's release, install, packaging and removal changes are recorded. testaudit's TestChangelogStructure requires a single leading [Unreleased], release headings of the form "## [X.Y.Z] — YYYY-MM-DD" in descending order and not above VERSION, "[hawk …]" for historical entries, and unique subsection headings per section. The "## [0.3.0] — <date>" section itself is written in the release PR right before tagging (docs/RELEASING.md); until then `make release-check` and release.yml refuse the v0.3.0 tag. Refs: F270 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(version): report the tagged module version for go install builds `go install github.com/GrayCodeAI/rho/cmd/rho@vX.Y.Z` builds without ldflags, so main.Version stays "dev". DisplayVersion then fell back to any VERSION file in the working directory or its parents (so running rho inside another project could print that project's version) and otherwise "dev"; `rho update` compared the raw "dev" string, which never parses, and always reported an update. DisplayVersion now prefers, after the ldflags version, the main module version from debug.ReadBuildInfo when it is a clean release (e.g. v0.3.0 -> "0.3.0"); "(devel)", pseudo-versions (@main, untagged commits) and +dirty builds still fall back to the VERSION file, then "dev". `rho update` uses DisplayVersion, so go-install and source builds compare their real version. Tests stub the build info for a tagged module, each development form, and the ldflags precedence. Refs: F018, F025 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(go.mod): retract the hawk-era v0.1.0–v0.2.0 tags proxy.golang.org serves v0.1.0, v0.1.1 and v0.2.0 for github.com/GrayCodeAI/rho, but those tags were cut before the rename: their go.mod declares module github.com/GrayCodeAI/hawk and they contain no cmd/rho, so `go install .../cmd/rho@latest` fails with "does not contain package" and tooling (pkg.go.dev, dependabot) lists them as rho versions. The proxy keeps them forever, so retract the range with a rationale; retractions take effect from the first release that carries this go.mod (v0.3.0), after which @latest is v0.3.0 and the old versions are marked retracted. `go mod tidy -diff`, `go mod verify` and the replace-directive guard pass; a testaudit test keeps the range retracted and ensures VERSION is never inside it. Refs: F025 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 13e05ce commit 0026f5d

48 files changed

Lines changed: 2184 additions & 637 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.envrc‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎.github/actions/rho/action.yml‎

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ inputs:
9393
default: ""
9494
github-token:
9595
description: >-
96-
Token used to post a PR comment when post-to is pr-comment.
97-
Defaults to ${{ github.token }}.
96+
Token used to look up the latest Rho release and, when post-to is
97+
pr-comment, to post the PR comment. Defaults to ${{ github.token }}.
9898
required: false
9999
default: ${{ github.token }}
100100
working-directory:
@@ -103,12 +103,16 @@ inputs:
103103
default: ${{ github.workspace }}
104104
rho-version:
105105
description: >-
106-
Rho release version/tag to install (e.g. v1.2.3 or latest).
107-
Defaults to the ref this action was resolved at.
106+
Rho release to install (e.g. 0.3.0 or v0.3.0), or latest. Defaults to
107+
the release tag this action is pinned to (uses: ...@v0.3.0); a branch or
108+
commit-SHA pin installs the latest release. Requires Rho v0.3.0 or later:
109+
earlier tags predate the rename and have no rho_* archives.
108110
required: false
109111
default: ""
110112
rho-repo:
111-
description: Repository to install Rho from (owner/repo).
113+
description: >-
114+
Repository to install Rho from (owner/repo). The release archive's
115+
cosign signature must come from that repository's release workflow.
112116
required: false
113117
default: GrayCodeAI/rho
114118

@@ -126,6 +130,11 @@ outputs:
126130
runs:
127131
using: composite
128132
steps:
133+
# cosign verifies the release signature; install.sh runs with
134+
# RHO_REQUIRE_COSIGN=1 so an unsigned or tampered release fails the step.
135+
- name: Install cosign
136+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
137+
129138
- name: Install Rho
130139
id: install
131140
shell: bash
@@ -134,47 +143,43 @@ runs:
134143
RHO_VERSION_INPUT: ${{ inputs.rho-version }}
135144
ACTION_REF: ${{ github.action_ref }}
136145
ACTION_PATH: ${{ github.action_path }}
146+
GH_TOKEN: ${{ inputs.github-token }}
137147
RUNNER_TEMP: ${{ runner.temp }}
138148
run: |
139149
set -euo pipefail
140150
if [ "${RUNNER_OS}" = "Windows" ]; then
141151
echo "::error::The Rho action currently supports Linux and macOS runners only." >&2
142152
exit 1
143153
fi
144-
# Resolve the version to install: explicit input wins; otherwise the ref
145-
# this action was checked out at (a tag); otherwise the latest release.
154+
if ! [[ "${RHO_REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
155+
echo "::error::rho-repo must look like owner/repo." >&2
156+
exit 2
157+
fi
158+
release_re='^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'
159+
# Resolve the version: explicit input wins; otherwise the release tag
160+
# this action is pinned to; otherwise (branch or SHA pin) the latest
161+
# release of rho-repo.
146162
version="${RHO_VERSION_INPUT}"
147-
if [ -z "${version}" ]; then
163+
if [ -z "${version}" ] && [[ "${ACTION_REF}" =~ ${release_re} ]]; then
148164
version="${ACTION_REF}"
149165
fi
150-
# A commit-SHA pin (github.action_ref is the 40-hex SHA) is not a release
151-
# tag, so fall back to latest rather than fetching releases/download/v<sha>.
152-
case "${version}" in
153-
""|refs/*|*/*) version="latest" ;;
154-
esac
155-
if printf '%s' "${version}" | grep -qiE '^[0-9a-f]{40}$'; then
156-
version="latest"
166+
if [ -z "${version}" ] || [ "${version}" = "latest" ]; then
167+
version="$(gh release view --repo "${RHO_REPO}" --json tagName --jq .tagName)"
157168
fi
158-
install_dir="${RUNNER_TEMP}/rho-bin"
159-
src_dir="${RUNNER_TEMP}/rho-src"
160-
mkdir -p "${install_dir}" "${src_dir}"
161-
# Download the Rho source archive and extract it
162-
if [ "${version}" = "latest" ]; then
163-
tarball_url=$(curl -sL "https://api.github.com/repos/${RHO_REPO}/releases/latest" \
164-
| grep '"tarball_url":' | sed 's/.*"\([^"]*\)".*/\1/')
165-
else
166-
tarball_url="https://github.com/${RHO_REPO}/archive/refs/tags/v${version}.tar.gz"
167-
fi
168-
curl -sL "${tarball_url}" | tar -xz -C "${src_dir}" --strip-components=1
169-
# Build the Rho binary from source (Go must be available on the runner)
170-
if ! command -v go &>/dev/null; then
171-
echo "::error::Go is required but not found on the runner PATH. Add actions/setup-go before this action, or install Go manually." >&2
172-
exit 1
169+
if ! [[ "${version}" =~ ${release_re} ]]; then
170+
echo "::error::rho-version must be a release version such as 0.3.0, or 'latest'." >&2
171+
exit 2
173172
fi
174-
go build -o "${install_dir}/rho" "${src_dir}/cmd/rho"
175-
chmod +x "${install_dir}/rho"
176-
echo "${install_dir}" >> "${GITHUB_PATH}"
177-
echo "Installed Rho from ${RHO_REPO}@${version}"
173+
version="${version#v}"
174+
# install.sh ships with this action at the same ref. It downloads the
175+
# release archive and checksums.txt, verifies the cosign signature
176+
# (required here) and the SHA-256, and installs under the prefix.
177+
installer="$(cd "${ACTION_PATH}/../../.." && pwd)/install.sh"
178+
prefix="${RUNNER_TEMP}/rho"
179+
RHO_INSTALL_REPO="${RHO_REPO}" RHO_REQUIRE_COSIGN=1 \
180+
sh "${installer}" --version "${version}" --prefix "${prefix}"
181+
echo "${prefix}/bin" >> "${GITHUB_PATH}"
182+
echo "Installed Rho ${version} from ${RHO_REPO}"
178183
179184
- name: Run Rho
180185
id: run

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ jobs:
9696
fetch-depth: 0
9797
- uses: ./.github/actions/checkout-flux
9898
with:
99-
ref: ${{ github.head_ref == 'feat/ecosystem-wiring-pr' && 'feat/ecosystem-wiring' || github.head_ref || github.ref_name }}
99+
ref: ${{ github.head_ref || github.ref_name }}
100100
- name: Generate ecosystem workspace
101101
run: bash ./scripts/generate-workspace.sh
102102
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
@@ -487,9 +487,6 @@ jobs:
487487
matrix:
488488
goos: [linux, darwin, windows]
489489
goarch: [amd64, arm64]
490-
exclude:
491-
- goos: windows
492-
goarch: arm64
493490
steps:
494491
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
495492
with:
@@ -507,26 +504,11 @@ jobs:
507504
GOARCH: ${{ matrix.goarch }}
508505
CGO_ENABLED: "0"
509506
run: go build -trimpath -v ./...
510-
- name: Binary size check (linux/amd64 only)
507+
- name: Binary size budget (linux/amd64 only)
511508
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
512-
run: |
513-
# Use -ldflags="-s -w" to match release build size (Makefile LDFLAGS).
514-
# This catches binary bloat regressions against the actual shipped artifact size.
515-
size=$(go build -trimpath -ldflags="-s -w" -o /tmp/rho-bin ./cmd/rho && wc -c < /tmp/rho-bin)
516-
size_mb=$((size / 1024 / 1024))
517-
echo "Binary size: ${size_mb}MB"
518-
# Threshold history: 110MB → 80MB (binary was ~76MB) → 98MB.
519-
# The 80MB line became stale: the audit-sweep merges (swift
520-
# rebrand, harrier at-rest encryption, engine features) grew the
521-
# release binary to ~95MB (linux/amd64, -trimpath -ldflags="-s -w").
522-
# Re-baselined to 98MB (3MB headroom) so this check detects new
523-
# bloat regressions instead of warning on every run. A real size
524-
# reduction (e.g. swapping modernc.org/sqlite for cgo sqlite,
525-
# lazy-loading WASM/CEL) is tracked separately.
526-
if [ "$size_mb" -gt 98 ]; then
527-
echo "::warning::Binary size ${size_mb}MB exceeds 98MB threshold"
528-
fi
529-
rm -f /tmp/rho-bin
509+
# Builds bin/rho with the release ldflags (-s -w) and fails when it
510+
# exceeds SIZE_LIMIT_MB in the Makefile — the single source of the budget.
511+
run: make size-check
530512

531513
# -------------------------------------------------------------------------
532514
# Fuzz — short corpus runs to catch panics in fuzz targets.

‎.github/workflows/release.yml‎

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Canonical release workflow for GrayCodeAI Go binary repos.
2-
# Triggered by release-please when it pushes a v* tag.
1+
# Release workflow for rho. A maintainer pushes a v<VERSION> tag after the
2+
# release PR (VERSION + CHANGELOG) has merged; creating v* tags is restricted by
3+
# the repository ruleset "protect-release-tags". See docs/RELEASING.md.
34
# Source of truth: .shared-templates/workflows/go-release.yml.tmpl
45

56
name: release
@@ -9,66 +10,76 @@ on:
910
tags: ["v*"]
1011

1112
permissions:
12-
contents: write
13-
packages: write
14-
id-token: write # for cosign keyless signing if enabled later
13+
contents: write # create the GitHub release and upload its assets
14+
id-token: write # cosign keyless signing (GitHub OIDC -> Sigstore Fulcio)
15+
16+
concurrency:
17+
group: release-${{ github.ref }}
18+
cancel-in-progress: false
1519

1620
jobs:
1721
goreleaser:
1822
runs-on: ubuntu-latest
1923
env:
2024
GOPROXY: "https://proxy.golang.org,direct"
25+
# Release builds resolve every dependency (including flux) from
26+
# go.mod/go.sum through the public proxy — never from sibling checkouts.
27+
GOWORK: "off"
2128
steps:
2229
- name: Checkout
2330
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2431
with:
2532
fetch-depth: 0 # goreleaser needs full history for changelog
33+
persist-credentials: false
2634

27-
# Releases build against the sibling repos resolved via the workspace go.work.
28-
- uses: ./.github/actions/checkout-flux
29-
with:
30-
ref: ${{ github.ref_name }}
35+
# Refuses a tag that disagrees with VERSION or has no CHANGELOG section,
36+
# and extracts that section as the release notes.
37+
- name: Check tag against VERSION and CHANGELOG
38+
run: bash ./scripts/check-release-tag.sh --notes "${RUNNER_TEMP}/release-notes.md" "${GITHUB_REF_NAME}"
3139

3240
- name: Set up Go
3341
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
3442
with:
3543
go-version: "1.26.6"
3644
cache: true
3745

46+
- name: Refuse local replace directives
47+
run: bash ./scripts/check-no-replace-directives.sh
48+
49+
# GoReleaser shells out to syft for the `sboms:` stanza; the runner image
50+
# does not ship it.
51+
- name: Install syft
52+
uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
53+
54+
# GoReleaser shells out to cosign for the `signs:` stanza (keyless
55+
# signature over checksums.txt, uploaded as checksums.txt.sigstore.json).
56+
- name: Install cosign
57+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
58+
3859
- name: Run GoReleaser
3960
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
4061
with:
4162
distribution: goreleaser
4263
# Must match a real goreleaser release — verify at
4364
# https://github.com/goreleaser/goreleaser/releases before bumping.
4465
version: "v2.17.0"
45-
args: release --clean
66+
args: release --clean --release-notes=${{ runner.temp }}/release-notes.md
4667
env:
4768
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48-
# Optional secrets used by some repos' goreleaser configs:
49-
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
50-
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
5169

52-
# Sign release artifacts with cosign keyless signing. The
53-
# id-token: write permission above enables OIDC federation with
54-
# Fulcio's root CA — no long-lived signing keys to manage.
55-
- name: Install cosign
56-
uses: sigstore/cosign-installer@d8a3f51ef971a853e51de6231a89f4483c5a7a0e # v3.4.0
57-
58-
- name: Sign release checksums with cosign
70+
- name: Verify the published signature
71+
env:
72+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5973
run: |
6074
set -euo pipefail
61-
# Download the checksums artifact produced by goreleaser.
62-
gh release download "${GITHUB_REF_NAME}" --pattern "checksums.txt" -D dist/
63-
# Sign with keyless OIDC — output is dist/checksums.txt.sig (bundle).
64-
cosign sign-blob \
65-
--yes \
66-
--output-signature dist/checksums.txt.sig \
67-
--output-certificate dist/checksums.txt.cert \
68-
dist/checksums.txt
69-
# Upload the signature and certificate back to the release.
70-
gh release upload "${GITHUB_REF_NAME}" \
71-
dist/checksums.txt.sig \
72-
dist/checksums.txt.cert
73-
env:
74-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
# Re-download what users will download and verify it the way
76+
# install.sh does, so a broken signature fails this run loudly.
77+
verify_dir="$(mktemp -d)"
78+
gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \
79+
--pattern checksums.txt --pattern checksums.txt.sigstore.json \
80+
--dir "${verify_dir}"
81+
cosign verify-blob \
82+
--bundle "${verify_dir}/checksums.txt.sigstore.json" \
83+
--certificate-identity "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@${GITHUB_REF}" \
84+
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
85+
"${verify_dir}/checksums.txt"

‎.gitignore‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Ignore build output in bin/, but track committed source scripts like bin/buf.
44
bin/*
55
!bin/buf
6+
# GoReleaser output (goreleaser release --snapshot / make release).
7+
/dist/
68
rho_bin
79
*.exe
810
*.dll
@@ -38,10 +40,12 @@ coverage.html
3840
CODE_REVIEW_REPORT.md
3941
review_report.md
4042

41-
# Workspace checksum file — regenerated by `make setup` / `go work sync`;
42-
# deleted and regenerated by the Dockerfile and flake.nix builds.
43+
# Workspace checksum file — regenerated by `make setup` / `go work sync`.
4344
go.work.sum
4445

46+
# Local direnv file (may hold personal keys for `make test-live`).
47+
/.envrc
48+
4549
# Lefthook-generated git hook wrappers
4650
.githooks/
4751
__pycache__/

0 commit comments

Comments
 (0)