Releasing hush
A release is a signed tag. Nothing is published from anyone’s laptop.
- Everything green on
main. CI runs the suite on Linux and macOS, Node 22 and 24, the packaged-install check, andnpm run build:check. - Changelog. Move the
Unreleasedsection under a new heading,## x.y.z — YYYY-MM-DD. Say plainly what changed for someone upgrading, and call out anything that breaks. - Version.
npm version x.y.z --no-git-tag-versionupdatespackage.jsonand the lockfile, and theversionscript rewritessrc/version.ts— the one place the version is written. The release workflow refuses a tag that disagrees with either. - Commit
Release x.y.z. - Tag, signed.
git tag -s vx.y.z -m "hush x.y.z". A signed tag says who cut the release; GitHub shows it as verified when the signing key is on the maintainer’s account. Set one up once withgit config --global user.signingkey …(GPG or SSH signing both work). - Push the tag.
git push origin vx.y.z..github/workflows/release.ymlthen:- checks the tag matches
package.jsonandsrc/version.ts, - type-checks, proves the build reproducible, runs the whole suite,
- publishes through npm trusted publishing (no token exists anywhere) with provenance, which ties the tarball to that workflow run and commit;
- builds the single-file binaries for every target on one Linux runner, twice, and fails if the two builds differ; runs the whole suite against the Linux binary;
- signs and notarizes the macOS binaries when the Apple secrets exist (below), and runs the macOS binary end to end either way;
- writes
SHA256SUMSover the final bytes, attests every binary and the sums with GitHub build provenance (Sigstore), and creates the GitHub release from the tag with the changelog section as notes, the binaries, the sums,hush.rb(Homebrew),hush.json(Scoop) and the winget manifests; - lists the version in the official MCP registry (
server.json), signed in with the workflow’s GitHub OIDC identity; - pushes the formula to the Homebrew tap when
HOMEBREW_TAP_TOKENexists. Without it the tap catches up by itself within six hours: its own workflow takeshush.rbfrom the latest release after checking every hash in it against that release’sSHA256SUMS.
- checks the tag matches
- winget. Submit the manifests from
winget-manifests.tar.gzto microsoft/winget-pkgs — by hand (wingetcreate submit <dir>) until the package is established there.
A beta
Section titled “A beta”A tag with a hyphen, vx.y.z-beta.n, is a prerelease: the same workflow
publishes it to npm’s beta tag and as a GitHub prerelease, and leaves the
stable channel alone (npm latest, Homebrew, install.sh, the MCP registry).
Cut betas from the beta branch; when one is ready, merge beta into main
and tag the stable version there. The docs site deploys from main only.
The binaries, once
Section titled “The binaries, once”Nothing here is needed for a release to work — without it the binaries ship ad-hoc signed, and Homebrew users are one manual formula copy behind.
-
Homebrew tap.
omarei-omoto/homebrew-tapexists and updates itself from each release. For an immediate update instead of within six hours, make a fine-grained token with Contents: write on that repository only and store it as theHOMEBREW_TAP_TOKENsecret. -
Apple Developer ID ($99/year). With it, the macOS binaries are signed with the hardened runtime and notarized, so a copy downloaded in a browser opens without a Gatekeeper warning. (curl and Homebrew do not quarantine, so they work without it; the Secure Enclave identity does not need it either — docs/BIOMETRY.md.) Secrets:
APPLE_CERTIFICATE_P12— the Developer ID Application certificate and key, exported as .p12, base64-encoded;APPLE_CERTIFICATE_PASSWORD— its password;APPLE_SIGNING_IDENTITY— e.g.Developer ID Application: Name (TEAMID);APPLE_NOTARY_KEY,APPLE_NOTARY_KEY_ID,APPLE_NOTARY_ISSUER— an App Store Connect API key (the .p8 text, its id, the issuer id) for notarytool.
The one entitlement is
com.apple.security.cs.allow-jit(scripts/macos-entitlements.plist, which says why the others Bun suggests are left out). -
Bun. The version that builds the binaries is
.bun-version. Bumping it is a normal change: CI builds twice and runs the whole suite against the result.
The docs site at tryhush.dev, once
Section titled “The docs site at tryhush.dev, once”The site is built by .github/workflows/docs.yml and served by GitHub Pages
under the custom domain tryhush.dev (the build writes the CNAME file).
- Verify the domain with GitHub first, so nobody else’s Pages site can
claim it: GitHub → your Settings → Pages → Add a domain →
tryhush.dev, then add theTXTrecord it shows in Cloudflare. - DNS in Cloudflare (DNS → Records), all DNS only (grey cloud) until
GitHub has issued the certificate:
A@→185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153AAAA@→2606:50c0:8000::153,2606:50c0:8001::153,2606:50c0:8002::153,2606:50c0:8003::153CNAMEwww→omarei-omoto.github.io
- The repository: Settings → Pages → Custom domain
tryhush.dev, then tick Enforce HTTPS once the certificate is issued (it can take a while). - The site deploys from
mainonly, so it always describes the stable release.
If you later switch the Cloudflare records to Proxied, set SSL/TLS to Full (strict) so Cloudflare checks GitHub’s certificate.
Checking a release
Section titled “Checking a release”-
A binary:
gh attestation verify hush-linux-x64 --repo omarei-omoto/hushshows the workflow and commit that built it;sha256sum -c SHA256SUMS --ignore-missingchecks the bytes. To rebuild one yourself: check out the tag, install the Bun in.bun-version,node scripts/build-binaries.mjs --all— the ad-hoc-signed binaries matchSHA256SUMSbyte for byte; the Developer ID-signed macOS ones differ only by their signature. -
npm view @omarei/hush@x.y.z --json | jq .dist— the tarball’s integrity hash. -
The package page on npmjs.com shows the provenance statement: the repository, the workflow, and the commit it was built from.
-
To rebuild it yourself: check out the tag,
npm ci,npm run build:checkprints a sha256 overdist/;npm packproduces the same tarball contents.
Security releases
Section titled “Security releases”Fix on a private branch or a draft GitHub security advisory (it provides a private fork), release with a changelog line that says what to do without explaining how to exploit it, and publish the advisory about a week later. See SECURITY.md.