README: the platform release recipe, on main at last
The recipe written for v0.19.0 sat on worktree-release-v0.19.0 and never merged, so main's "Releasing the platform" held only per-release notes. This puts it back as nine steps, updated from cutting v0.20.0: build on a real disk, not the /tmp tmpfs; the deb description's hyphen wrap; the stdin-only signing key and the pubkey check; copy review for notes; the tap PR; the apt rebuild, sign and ship, with its old traps. The copy was approved in review unchanged.
Human
Prompt
Can we do #174 and get the recipe on main?
From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+28
−0
README.md+28 −0
CI passed — run details
diff --git a/README.md b/README.md| index 0386f2718..faa6765c0 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -226,6 +226,34 @@ step, and an unhurried one now that nothing resolves through them. |
| ## Releasing the platform |
| +A release is a `vX.Y.Z` tag on this repo plus signed binaries on [carlosframework/releases](https://github.com/carlosframework/releases). Everything downstream reads that one set of files: `carlos update`, the Homebrew tap, apt, and every deployment's `carlos system update`. Cut it from a green `main` and verify it from the public side, never from your build directory. |
| + |
| +1. **Tag.** `git tag -a vX.Y.Z <sha> -m "carlos vX.Y.Z"` on the `main` commit you are releasing, then `git push origin vX.Y.Z` to the forge. |
| +2. **Build.** Six targets, darwin, linux and windows on amd64 and arm64, each with `make build GOOS=<os> GOARCH=<arch> SHA=vX.Y.Z` from a clean checkout of the tag. Build with `TMPDIR` and `GOCACHE` on a real disk: `/tmp` is a shared tmpfs, and a full one fails the link with "no space left on device". Zip each Windows binary as a lone `carlos.exe`. |
| +3. **Debs.** One per architecture, built with `dpkg-deb --root-owner-group`, holding `usr/bin/carlos`. Copy the last release's `control` and change the version and the description's last sentence. Wrap the description without breaking on hyphens, or apt shows "step- by-step". |
| +4. **Sums and signature.** Write `SHA256SUMS.txt` over the eight files. Sign it with the ed25519 seed in SSM `/carlos/cli-update-key`, passed on stdin and never in argv, and write the signature base64-encoded to `SHA256SUMS.txt.sig`. Before signing, check that the key's public half equals `cliUpdatePubKey` in `update.go`. A mismatch publishes a release nobody can update to. |
| +5. **Release notes.** Draft them, and put them through copy review before anything is published. Lead with what changed for people using the CLI. Add a short section for operators, taken from the release notes below this recipe, about anything to check before rolling. |
| +6. **Publish.** `gh release create vX.Y.Z -R carlosframework/releases --latest --title vX.Y.Z --notes-file NOTES.md` with all ten files. Confirm it is the latest with `gh api repos/carlosframework/releases/releases/latest -q .tag_name`. |
| +7. **Homebrew.** In `carlosframework/homebrew-tap`, bump `Formula/carlos.rb`: the version, the four url and sha256 pairs from the published `SHA256SUMS.txt`, and the `carlos vX.Y.Z` test string. Open a PR, check each hash against the sums file, and squash-merge it. |
| +8. **apt.** Download the live tree, `apps/bab/apt-carlosframework/releases/<last>/site.tar.gz` from the flagship bucket, and unpack it. Add the two debs under `pool/main/c/carlos/`, and regenerate each architecture's `Packages` with `dpkg-scanpackages --multiversion`. Delete `Release`, `InRelease` and `Release.gpg`, then rebuild `Release` with `apt-ftparchive release`, passing Origin and Label `CARLOS`, Suite and Codename `stable`, the architectures and the component. Write it outside the tree first, or it lists itself. Sign `InRelease` and `Release.gpg` with the SSM key `/carlos/apt-signing-key` in a throwaway `GNUPGHOME`, set `latest.txt` to the tag, and check both signatures with `gpgv` against the tree's own keyring. |
| +9. **Ship apt.** Ship the directory and let `carlos ship` build the tarball. A hand-made tar with a leading `./` entry took apt down once. With `AWS_PROFILE=keymail` and `CARLOS_DEPLOYMENT_BUCKET` set to the flagship bucket, run `carlos ship -app apt-carlosframework -account-id bab -kind static -version vX.Y.Z-1 site/`, then `carlos promote -app apt-carlosframework -account-id bab vX.Y.Z-1 canary/apt` with `CARLOS_RELEASE_KEY` from SSM `/carlos/release-key` set for that one command. The channel is the literal `canary/apt`. |
| + |
| +Then verify from the public side: |
| + |
| +```sh |
| +v=vX.Y.Z; rel=https://github.com/carlosframework/releases/releases/download/$v |
| +curl -sSLO $rel/carlos-linux-arm64; curl -sSLO $rel/SHA256SUMS.txt; curl -sSLO $rel/SHA256SUMS.txt.sig |
| +printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00' > pub.der |
| +echo 'R+EO3qQ47VsuKl9Qq/pkDXBucPYvzp5lU2ut4zpnJPc=' | base64 -d >> pub.der |
| +base64 -d SHA256SUMS.txt.sig > sig.bin |
| +openssl pkeyutl -verify -pubin -inkey pub.der -keyform DER -rawin -in SHA256SUMS.txt -sigfile sig.bin |
| +sha256sum -c --ignore-missing SHA256SUMS.txt |
| +curl -sS https://apt.carlosframework.com/latest.txt # prints $v |
| +carlos system update --console <console> --version $v # then: carlos system status |
| +``` |
| + |
| +Last, roll it: `carlos system update --console <console> --version vX.Y.Z`, then `carlos system status`. Every box should read `current` within the spread. A box that reads `rolled-back` or `refused` says why on the same line; fix that box, and do not re-cut the release. |
| + |
| A release-specific note goes here when a release changes something an |
| operator needs to check or act on before rolling it. Read the note for |
| the release you are about to roll. |