Repository navigation
Release Process
How rhythm and its tools are released and how a new tool is added, for maintainers. A release is a pushed vX.Y.Z tag; a shared workflow builds, checksums, signs and publishes it. Tuner's checks also publish on their own, as a check pack, on every push to master. An Echo release also publishes its npm and Composer connectors.
rhythm and each tool version independently with semantic versioning. To release one:
-
Make sure
masteris green in CI on Linux, macOS and Windows and has everything you want to ship. -
For
rhythm, refresh the Sigstore trusted root the kit embeds and check the release config (see Before a rhythm release). -
Tag the commit and push the tag:
git switch master && git pull git tag v0.3.2 git push origin v0.3.2 -
The repository's
releaseworkflow runs, builds every platform and publishes the GitHub release with notes generated from the commits.
Any tag starting with v triggers a release, so only push tags you mean to ship. A tag with a suffix, like v0.4.0-rc1, is published as a prerelease (release.prerelease: auto in .goreleaser.yaml): GitHub never reports a prerelease as "latest", so rhythm update and the installers don't offer it. You can test it with rhythm install tuner@0.4.0-rc1 or by downloading it directly.
Each repository's .github/workflows/release.yml runs on push of tags matching v* and calls the shared, reusable workflow release-go.yml in RhythmShahriar/.github. The caller must grant all three permissions the workflow asks for, or GitHub refuses to start the release:
jobs:
release:
uses: RhythmShahriar/.github/.github/workflows/release-go.yml@master
permissions:
contents: write # publish the release
id-token: write # keyless signing and attestations
attestations: write # GitHub artifact attestations
secrets:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} # optional, for package managersrelease-go.yml:
- Checks out the full history without keeping the token (
persist-credentials: false), so GoReleaser can find the previous tag. - Sets up Go from the repository's
go.mod. - Installs cosign v3.0.6 and syft v1.54.1.
- Runs GoReleaser v2.18.3 (
goreleaser release --clean) withGOWORK=off, so the build uses exactly the kit versiongo.modpins. - Attests the build provenance of
checksums.txt, every archive, SBOM and Linux package (actions/attest-build-provenance).
Every action in it is pinned to a full commit SHA with its version in a comment. The tool versions are pinned too, so a moved tag upstream can't change what builds or signs a release. Dependabot keeps them current.
GoReleaser reads the repository's .goreleaser.yaml:
| Step | Setting |
|---|---|
| Build |
linux, darwin, windows × amd64, arm64; CGO_ENABLED=0, -trimpath; version stamped with -X main.version
|
| Archive |
<binary>_<os>_<arch>.tar.gz, .zip on Windows, with README and LICENSE |
| SBOM |
<archive>.sbom.json, SPDX 2.3 JSON written by syft for every archive |
| Packages |
rhythm only: .deb, .rpm and .apk for amd64 and arm64, with the binary in /usr/bin and the license and README in /usr/share/doc/rhythm
|
| Checksums |
checksums.txt, SHA-256 of every archive, SBOM and package |
| Sign |
cosign sign-blob over checksums.txt, keyless, written to checksums.txt.sigstore.json (the Sigstore bundle format cosign v3 writes) |
| Package managers |
rhythm only: a Homebrew cask, a Scoop manifest and a winget pull request, pushed with TAP_GITHUB_TOKEN (see Package managers) |
| Notes | From GitHub, grouped into Features (feat), Fixes (fix), Performance (perf) and Other |
Because SBOMs and packages are listed in checksums.txt, the one signature covers them too.
Important
The archive names, checksums.txt and checksums.txt.sigstore.json are a contract. kit/update (used by every rhythm command that installs), install.sh and install.ps1 look for exactly <binary>_<os>_<arch>.tar.gz (.zip on Windows), checksums.txt and its signature at the tag v<version>. They refuse to install anything without a matching checksum line or a valid signature from release-go.yml for that repository and tag. Renaming the binary, the project, the archive template or the workflow breaks installs and updates for every user.
Keep these in sync when adding a tool: the GoReleaser project_name must equal the binary name (rhythm-<tool>), so archives come out as rhythm-<tool>_<os>_<arch>.
Warning
The signing identity names release-go.yml@refs/heads/master, which is why every caller still uses the shared workflows @master. Moving them to a tag or SHA changes the identity users verify. It has to land in one change with the README, .goreleaser.yaml, both installers, Verifying releases and the refs kit/sigverify accepts.
The shared ci-go.yml workflow runs on every push to master and every pull request. On each runner in its os matrix: go vet, go test -race, a build and the optional benchmark. On Linux only: gofmt, golangci-lint v2.15.0, shellcheck, actionlint, the optional pinned-actions and writing-style checks, the kit pin check and goreleaser check. A separate job runs govulncheck and fails on a vulnerability in code the module calls.
| Input | Default | Effect |
|---|---|---|
os |
'["ubuntu-latest"]' |
JSON list of runners; rhythm uses Ubuntu, Windows and macOS. Check names become go (<os>), so branch protection must name those. |
pin-check |
false |
fail when a third-party action isn't pinned to a 40-hex commit SHA (local actions and the shared workflows are exempt) |
style |
false |
run check-style.py (no em dashes, no comma before "and" or "but") |
kit-pin-check |
true |
fail when go.mod pins the kit more than one minor release behind the latest rhythm tag; skipped when there is no kit requirement |
bench, bench-packages
|
empty, ./...
|
run one benchmark regexp once; a regexp that matches nothing gives a warning |
tools-ref |
master |
the ref of RhythmShahriar/.github the shared scripts load from |
The old rhythm-kit input is gone: the kit is a normal go.mod requirement. A caller that still passes it gets an "invalid input" error from GitHub. rhythm's own CI also runs the installer tests on all three systems (see Contributing) and a packages job that builds a snapshot release and checks it. A clean CI run on the tagged commit is the best sign the release will build.
| Target | Does |
|---|---|
make trusted-root |
refreshes kit/sigverify/trusted_root.json from Sigstore through TUF; commit the result |
make release-check |
goreleaser check at v2.18.3 |
make dist-check |
a snapshot release, then checks that every archive has an SBOM, every file is in checksums.txt, each package holds /usr/bin/rhythm and the winget manifests validate (needs syft on PATH) |
make pins, make actionlint
|
every third-party action pinned to a SHA; the workflows lint clean |
make vuln |
govulncheck |
To try the whole release workflow without publishing, run the release-dry-run workflow from the Actions tab on a branch. It calls release-go.yml with snapshot: true: it builds, signs and attests a snapshot and publishes nothing. It refuses to run on a tag, so a snapshot is never signed with a release tag as its source. Its attestations name the branch, which gh attestation verify --source-ref refs/tags/v<version> rejects.
The rhythm config also generates a Homebrew cask (RhythmShahriar/homebrew-tap), a Scoop manifest (RhythmShahriar/scoop-bucket) and a winget manifest (RhythmShahriar.rhythm, a pull request to microsoft/winget-pkgs from a fork). They are pushed only when the repository has the TAP_GITHUB_TOKEN secret, a fine-grained token with contents and pull-request write access on those repositories; without it they are generated into dist/ and not uploaded. Prereleases are never pushed. Until those repositories and the secret exist, the .deb, .rpm and .apk assets and the installers are the way to install. The cask clears the quarantine flag on macOS, since the binaries aren't notarized.
A change to release-go.yml or ci-go.yml can break every caller at once, since they all use @master. Merge in this order and say so in the release notes:
-
The callers first. When the workflow will ask for a new permission (as
attestations: writedid), add it torelease.ymlin tuner, soundcheck, echo and rhythm. GitHub refuses to start a release that asks for more than its caller grants. -
Tools behind on the kit.
kit-pin-checkis on by default, so bump each tool's kit requirement (or setkit-pin-check: falseas a stopgap) before the check lands, or its CI goes red. - Then
RhythmShahriar/.github. -
Then the callers that use the new inputs.
rhythm'sci.ymlpassesos,pin-check,benchandbench-packages, which aci-go.ymlwithout them rejects.
Tools import the shared kit (github.com/RhythmShahriar/rhythm/kit/...) as a normal Go module, pinned in their go.mod. A tool release builds against that pinned version (GOWORK=off), never against rhythm's master. So when a tool needs a kit change, release in this order:
-
Tag the kit. Merge the kit change and tag a new
rhythmversion, e.g.v0.7.0. The kit is part of therhythmmodule, so this is arhythmrelease. -
Bump the tool. In the tool repository:
go get github.com/RhythmShahriar/rhythm@v0.7.0 go mod tidy git commit -am "build(deps): require rhythm v0.7.0" -
Release the tool, once that commit is green.
Tagging the kit first also means users who run rhythm update get the new kit/update logic in the router before any tool depends on it. Keep every tool within one minor release of the latest kit: the kit pin check fails a tool that falls further behind, so kit security fixes can't silently stop reaching its users.
$ RHYTHM_KIT_LATEST=v0.6.0 sh .github/scripts/check-kit-pin.sh tuner/go.mod
check-kit-pin: tuner/go.mod pins github.com/RhythmShahriar/rhythm v0.1.0 but the latest is v0.6.0; move to v0.5.0 or laterThe check reads go.mod only, never go.work, takes the latest tag from git ls-remote (retried once after RHYTHM_KIT_RETRY_DELAY seconds, default 5) unless RHYTHM_KIT_LATEST is set and exits 0 when current, 1 when stale and 2 when it can't tell.
Soundcheck is the worked example: a new repository, RhythmShahriar/soundcheck, with the binary rhythm-soundcheck, reached as rhythm soundcheck and rhythm sc.
-
Module and binary. The module is
github.com/RhythmShahriar/<tool>and the command lives incmd/rhythm-<tool>. The binary must berhythm-<tool>: the router runsrhythm-<tool>from thePATH. -
The kit. Require a tagged
rhythmfor the kit (Soundcheck requiresv0.2.1) and use it for the banner, theme, user folders, update notice and shared flags, so the tool looks and behaves like the others. -
The standard commands.
version(printingrhythm-<tool> <version>),help, the exit codes0,1(--fail-on),2(usage),3(couldn't run),130(interrupted) andupdate, whichrhythm updatecalls after updating the binary to refresh the tool's data (for Soundcheck, its browser). Anupdatewith nothing to do should still exit0.kit/cli.Maingives a new tool all of these, plusversion --json,update --jsondata packs and shell completion throughrhythm; see Extensions for what each one buys. -
Responsible use. A one-time notice through
kit/config.FirstRun(Soundcheck's marker issoundcheck-authorised-use) and the background release check throughkit/update.Noticewith the tool's repository and binary. -
Release config. Copy
.goreleaser.yaml,.github/workflows/release.yml(with all three permissions) andci.ymlfrom an existing tool. Setproject_nameto the binary name so archives are namedrhythm-<tool>_<os>_<arch>; see the naming contract. Copy rhythm'ssboms:block too, so the tool's archives get SBOMs. -
Community files.
make hooksand the commit message hook, a README that links into this wiki, the license and private vulnerability reporting turned on in the repository's security settings.
Tag and push v1.0.0 (or whatever the first version is) as in Release a version. Then verify the release. The release must exist before the router points at it: rhythm install and rhythm update read /releases/latest.
The router knows the tools listed in cmd/rhythm/tools.json. Add one entry:
{
"name": "soundcheck",
"repo": "RhythmShahriar/soundcheck",
"aliases": ["sc"],
"description": "did my change break anything? whole-site before/after checks",
"default_install": true,
"min_rhythm": ""
}rhythm help, did-you-mean, completion and rhythm install read it; default_install: true puts the tool in rhythm install --defaults, which the installers ask for their default list. Set min_rhythm when the tool needs a newer router than some users have. go test ./cmd/rhythm checks the file. The installers still carry a built-in list (tuner soundcheck echo in scripts/install.sh and scripts/install.ps1) for older routers and a "Get started" line per tool: add the tool to both. Then add the tool to the wiki: its pages, the sidebar, Home, Install, Updating, Uninstall and this page.
Release rhythm with the registration. Users get the tool with rhythm install <tool>. New installs get it by default.
rhythm (kit the tool needs) → the tool's first release → rhythm (registers the tool)
-
rhythmwith the kit, if the tool needs kit packages that aren't released yet. For Soundcheck that wasv0.2.0, which addedkit/crawl,kit/ratelimit,kit/cacheandkit/cli. Then camev0.2.1for the crawl fixes it now requires. For Echo it'sv0.4.0, which addedkit/secrets. -
The tool, requiring that
rhythmversion. For Echo, the onev1.0.0tag releases the binary and the npm package.split-laravel.sh v1.0.0then publishes the Laravel package (see Echo's packages). A tool that others build on releases before them: Soundcheckv1.1.0, which imports Echo'sclientandcompare, comes after Echov1.0.0. -
rhythmagain, registering the tool and installing it by default. For Echo,v0.5.0registersechowith the aliaseand makes the installers default totuner soundcheck echo.
Registering a tool as a default before it has a release breaks every fresh install: the installer installs the other tools, then ends with Installed: tuner, soundcheck. Failed: echo (see above). and exits 1 for everyone using the default RHYTHM_TOOLS.
An Echo release ships three things from one tag of RhythmShahriar/echo: the rhythm-echo binary (through the shared release workflow, like every tool), the Next.js and Node connector on npm and the Laravel connector on Packagist. The WordPress connector is embedded in the binary and needs nothing extra.
The Echo repository's npm workflow runs on the same v* tag push. In connectors/node, it installs with npm ci, sets the package version from the tag (npm version "${GITHUB_REF_NAME#v}"), runs npm test and publishes with provenance:
npm publish --provenance --access public --tag latest # or --tag next for a v1.1.0-rc1 style tagIt needs the NPM_TOKEN repository secret: an npm granular access token with write access to @rhythmsh/echo, used until npm Trusted Publishing is set up for the package. The workflow has id-token: write for the provenance statement. A tag with a suffix is published under next, so npm install @rhythmsh/echo never picks up a release candidate. Check the result on the package's npm page (the Provenance section) or with npm audit signatures; see Verifying releases.
Packagist needs a repository whose root is the package, so connectors/laravel is split into its own repository, RhythmShahriar/echo-laravel, with scripts/split-laravel.sh in the Echo repository:
scripts/split-laravel.sh v1.0.0It runs git subtree split --prefix connectors/laravel, pushes the result to echo-laravel's master and creates the same tag there. Then it prints Pushed connectors/laravel at v1.0.0 to <remote>. ECHO_LARAVEL_REMOTE overrides the remote. Packagist reads rhythmsh/echo-laravel from that repository and picks up the new tag through its GitHub hook (or Update on the package's Packagist page). Run it from an up-to-date master after tagging Echo, since the subtree history is pushed as it is.
No workflow runs the split: the Echo repository's workflows are ci.yml, release.yml (the binaries) and npm.yml (the Node package), so after each Echo tag, run scripts/split-laravel.sh yourself.
-
make checkin the Echo repository, plus the connectors' own tests:make -C connectors/laravel check(Docker) andcd connectors/node && npm test. - The
versioninconnectors/node/package.jsondoesn't matter: the workflow sets it from the tag. - The mu-plugin's
Version:header andVERSIONconstant inconnectors/wordpress/echo.phpshould match the release.
Tuner's checks also ship without a release. The checks-pack workflow runs on every push to master that touches checks/** (or the workflow itself). It can also be run by hand:
- Runs every check's fixtures:
go test ./engine ./checkpack. - Builds the pack with
make pack(checks.tar.gz,checks-manifest.json,checks-sha256.txt), byte for byte as you'd get locally from the same commit and Go version. - Uploads the three assets to the rolling
checks-latestrelease, replacing the previous pack.
Runs never overlap: a newer run waits for the current upload to finish. The first run creates the checks-latest tag on a parentless commit that holds only a README, so it's never reachable from master and GoReleaser can't mistake it for a version tag.
Warning
checks-latest is a prerelease and must never be marked as the latest release. rhythm update, rhythm install and both installers read /releases/latest, which only returns the most recent non-prerelease, non-draft release. If checks-latest became "latest", installs and updates of Tuner would find no binaries. The workflow re-applies --prerelease --latest=false on every run; don't change it by hand in the GitHub UI.
If a checks change only behaves correctly on a newer rhythm-tuner, bump min_tuner in checks/pack.json in the same change. See Check Packs for how clients choose and verify packs.
After the workflow finishes:
-
Assets. The release has six archives, an SBOM for each,
checksums.txtandchecksums.txt.sigstore.json; arhythmrelease also has two each of.deb,.rpmand.apk:gh release view v0.3.2 -R RhythmShahriar/tuner --json assets --jq '.assets[].name' -
Latest. The new version is marked Latest while
checks-latestis still a Pre-release:gh release list -R RhythmShahriar/tuner --limit 5
v0.3.2 Latest v0.3.2 … Check pack Pre-release checks-latest … -
Signature. On a machine with the router,
rhythm install tuner@0.3.2 --dir "$(mktemp -d)"should printSignature ok RhythmShahriar/tuner @ refs/tags/v0.3.2 (release-go.yml). By hand, downloadchecksums.txtand its bundle and check them with cosign and gh as described in Verifying releases:gh release download v0.3.2 -R RhythmShahriar/tuner -p 'checksums.txt*' cosign verify-blob --bundle checksums.txt.sigstore.json \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity https://github.com/RhythmShahriar/.github/.github/workflows/release-go.yml@refs/heads/master \ --certificate-github-workflow-repository RhythmShahriar/tuner \ --certificate-github-workflow-ref refs/tags/v0.3.2 \ checksums.txt gh attestation verify checksums.txt --repo RhythmShahriar/tuner \ --signer-workflow RhythmShahriar/.github/.github/workflows/release-go.yml \ --source-ref refs/tags/v0.3.2 --deny-self-hosted-runners -
Update path. On a machine with the previous version,
rhythm update --checkshould exit 10 and offer the new one.rhythm updateshould install it:rhythm-tuner 0.3.1 → 0.3.2 available -
Fresh install. Run the installer in a clean environment, such as a throwaway container or VM. Then check
rhythm list(every toolyesunder SIGNED) andrhythm verify. -
Notes. Read the generated release notes and edit them if a change needs more explanation, such as a breaking change or a shared workflow change callers must follow.
← Contributing · Home · Architecture →
rhythm · Useful ideas. Working systems. · Check only sites you own or have permission to test. Keep staging sealed (responsible use) · This wiki is published from wiki/ in the rhythm repository; suggest changes there with a pull request.
Get started
rhythm in depth
Tuner
- Overview
- Your first scan
- Reading results
- Choosing checks
- Requests and politeness
- Output and reports
- Batch scans
- Local scans
Tuner in depth
- Known vulnerabilities
- PHP advisories
- CI and code scanning
- Writing checks
- Check packs
- Finding reference
- CLI reference
- FAQ and troubleshooting
Soundcheck
- Overview
- Your first check
- Reading results
- Discovery
- Page health
- Visual changes
- Interactions
- Forms and flows
- Cache
- Speed
Soundcheck in depth
Echo
- Overview
- Your first run
- Connectors
- Sealing staging
- Messages and checks
- Actions and compare
- With Soundcheck
- Replay
- Deliverability
Echo in depth
Project