Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.
The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.
Three decisions carry the design:
* GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
sees it, so destroying on GET would destroy most secrets in transit and the
recipient's "already used" would be indistinguishable from interception.
Only POST /reveal consumes. Bot user-agent detection is an arms race;
removing the side effect from GET is not. Pinned by
TestGettingTheRevealPageNeverConsumesTheSecret.
* Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
where two simultaneous readers both win, and for a one-time secret that
window is the product. The store contract demands atomicity and the same
concurrency test runs against both implementations.
* Missing, already-revealed, expired and evicted are ONE indistinguishable
410. Separating them would confirm to a prober that a given link was real.
The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.
Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.
Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.
cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
5.3 KiB
Releasing
This document is the runbook for cutting a go-redis release. It is intended for maintainers with write/tag access to the repository.
For the format and style of the release notes themselves, see .github/RELEASE_NOTES_TEMPLATE.md.
Versioning
go-redis follows Semantic Versioning:
- Patch (
vX.Y.Z+1) — bug fixes, no API changes. - Minor (
vX.Y+1.0) — backwards-compatible new features, deprecations. - Major (
vX+1.0.0) — breaking changes. Coordinate with the team first.
Pre-releases use vX.Y.Z-beta.N / vX.Y.Z-rc.N.
Pre-release checklist
- Target branch is
masterand CI is green on the latest commit. - All PRs intended for this release are merged.
- There are no open issues in the release milestone (if used).
CHANGELOG/ release notes have been considered; dependabot-only and doc-only changes are excluded per the template.- Confirm the next version number and decide if it's a patch / minor / major.
1. Draft the release notes
- Open the draft release auto-generated by release-drafter on GitHub.
- Prepend a new section to
RELEASE-NOTES.mdusing.github/RELEASE_NOTES_TEMPLATE.mdas the format. Keep the file in chronological order (newest first). - Pick 3–5 Highlights — the most user-facing, impactful changes.
- Remove dependabot bumps and doc-only typo fixes from the lists.
- Verify every PR has a contributor attribution and link.
- Open a PR with just the release-notes change if you want review before bumping versions, otherwise include it in the release PR below.
2. Bump versions and open the release PR
Create a release branch from master:
git checkout master && git pull --ff-only
git checkout -b release/vX.Y.Z
Run the release script on that branch:
TAG=vX.Y.Z ./scripts/release.sh
What the script does (and explicitly does not do):
- ✅ Validates
TAGmatches the semver regex and isn't already a git tag. - ✅ Rewrites every
redis/go-redis*line in every sub-modulego.modto point at the newTAG. Trailing// indirectmarkers are preserved. - ✅ Runs
go mod tidy -compat=1.24in each sub-module. - ✅ Updates the return value in
version.go. - ❌ Does not switch branches (runs in your current branch).
- ❌ Does not require a clean working tree (so you can mix it with release-notes edits in the same branch).
- ❌ Does not commit, tag, or push anything.
Review and commit the changes yourself:
git diff # sanity-check the bumps
git add -u
git commit -m "chore: release vX.Y.Z"
git push origin release/vX.Y.Z
Then on GitHub:
- Open a PR from
release/vX.Y.Zintomaster. - Wait for all required CI checks (build, golangci-lint, spellcheck, doctests, e2e where applicable) to pass.
- Get at least one maintainer approval.
- Merge the PR (use a merge commit — the tag will point at the merge SHA).
3. Tag the release
After the release PR is merged, pull the latest master and dry-run the
tagger:
git checkout master && git pull --ff-only
TAG=vX.Y.Z ./scripts/tag.sh vX.Y.Z
The script defaults to dry-run and prints the commands it would run.
Verify the output, then apply for real with -t:
./scripts/tag.sh vX.Y.Z -t
This creates and pushes:
- The top-level tag
vX.Y.Z. - A per-module tag
<module>/vX.Y.Zfor each public sub-module (skippingexample/*andinternal/*).
4. Publish the GitHub release
- On GitHub, open the draft release created by release-drafter.
- Set the tag to
vX.Y.Zand the target tomaster. - Replace the auto-generated body with the curated notes from
RELEASE-NOTES.mdfor this version. - For pre-releases, check "Set as a pre-release".
- Publish.
5. Post-release
- Verify the release appears on pkg.go.dev within a few minutes (trigger a fetch by visiting the version URL if needed).
- Announce on Discord (see the link in
CONTRIBUTING.md). - Close the release milestone if one was used.
- Open follow-up issues for anything deferred from this release.
Hotfix / patch release
For an urgent fix on top of the latest release:
- Branch from the latest release tag:
git checkout -b hotfix/vX.Y.Z+1 vX.Y.Z. - Cherry-pick (or re-apply) only the required fix commits.
- Follow the normal release flow above with
TAG=vX.Y.Z+1. - Make sure the fix is also present on
master(forward-port if necessary).
Troubleshooting
release.shfails with "tag already exists" — the tag has already been created. Pick the next version, or delete the local tag first if it was created by mistake.tag.shreports version mismatch in ago.mod— a sub-module was not updated byrelease.sh. Fix thego.modmanually (or re-runrelease.sh), amend the release PR, and re-run the tagger.version.godoes not contain the tag —release.shdid not run or the bump was reverted. Re-runrelease.shon the release branch.- pkg.go.dev does not show the new version — visit
https://pkg.go.dev/github.com/redis/go-redis/v9@vX.Y.Zonce to trigger a fetch from the module proxy.