Files
qqq-cli/docs/release-runbook.md
James MaesandClaude Opus 5.5 bf9c093bdd
node / node (push) Successful in 1m5s
build / node (push) Successful in 1m5s
ci(release): open tap and bucket pull requests instead of pushing to main
For a final version the publish job now pushes branch release/qqq-<version>
to QRun-IO/homebrew-tap and QRun-IO/scoop-bucket with PACKAGES_TOKEN and
opens a pull request "qqq <version>" in each; a human merges. A prerelease,
or a refused push or pull request, attaches qqq.rb / qqq.json to the release
as before, and never fails the release. The logic moves to
scripts/publish-packaging.sh, with a DRY_RUN=1 path that clones and commits
locally but writes nothing remote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:23:00 -06:00

8.1 KiB

Release runbook

From "main is green" to "qqq X.Y.Z is published and installable everywhere". A release is one tag, v<CLI version>, on the merge commit; Gitea CI (.gitea/workflows/release.yaml) does the rest. Nothing publishes from a branch.

Since 2026-11-08 releases run on Gitea. The CircleCI/qqq-orb path (npm packages on a v<client version> tag, binaries on a GitHub release cli-v<version>, hand-made tap and bucket pull requests) is retired. Older releases (1.0.0 and before) stay on GitHub.

What ships:

Artifact Where Made by
qqq binaries with the terminal UI, 5 targets (linux x64/arm64, darwin x64/arm64, windows x64), plus SHA256SUMS Gitea release v<version> of QRun-IO/qqq-cli release.yaml: Linux targets and darwin-x64 (cross) in the container, darwin-arm64 on the macOS runner, windows-x64 on the Windows runner
@qrun-io/qqq-ui-api-client, qqq-core, qqq-ui-api-conformance, qqq-cli QRun-IO Gitea npm registry, https://git.kof22.com/api/packages/QRun-IO/npm/ release.yaml publish job
Homebrew formula qqq QRun-IO/homebrew-tap, Formula/qqq.rb pull request from release.yaml (final versions), merged by a human
Scoop manifest qqq QRun-IO/scoop-bucket, bucket/qqq.json pull request from release.yaml (final versions), merged by a human

1. Versions

On a branch from main:

File Change
packages/cli/package.json, packages/cli/src/version.ts (CLI_VERSION) the CLI version, e.g. 1.1.0-rc.1 or 1.1.0
packages/core/package.json, packages/conformance/package.json bump any package whose content changed
packages/ui-api-client/package.json follows the UI API version of openapi/qqq-v1.json
workspace dependencies in packages/cli and packages/core the same versions, exact
CHANGELOG.md a ## [X.Y.Z] - <date> entry (it becomes the release notes; docs/release-notes/X.Y.Z.md wins if present)

Then npm install (refreshes package-lock.json), npm run lint (fails if any version disagrees) and node scripts/release.mjs (the same gate CI runs; add --skip-binaries to save time). A final version must pass the readiness gate (npm run release -- --check: parity, findings filed, CHANGELOG entry, documented commands, all four packages final); a prerelease is not gated. Merge through a pull request.

Each package version is published once. The publish job skips a package whose exact version is already in the registry (unchanged packages ride along), so bump every package whose content changed.

2. Tag

git checkout main && git pull --ff-only
git tag -s v1.1.0 -m "qqq 1.1.0" && git push origin v1.1.0

Or create the tag on the merge commit through the Gitea API (POST /repos/QRun-IO/qqq-cli/tags). The tag must equal packages/cli/package.json or the release fails at once. A version with a - (-rc.N) is a prerelease: npm dist-tag next for prerelease packages, a Gitea prerelease, and no tap or bucket update.

3. What CI does

.gitea/workflows/release.yaml, in order; any failure stops it:

  1. linux (container-build, node:22.23.4-bookworm, Bun 1.4.4 checksum-pinned): scripts/release.mjs --skip-binaries (npm ci, generate drift, build, readiness for finals, tests, lint, TUI tests and type check, pack dry runs), then the linux-x64, linux-arm64 and darwin-x64 binaries (scripts/build-binaries.mjs), the scripts/verify-binary.sh smoke on linux-x64, and a draft Gitea release with those binaries. An existing release for the tag fails the run.
  2. macos (qqq-macos-arm64) and windows (qqq-windows-vm6218), in parallel: pinned Node.js (SHASUMS256.txt) and Bun (pinned sha256), npm ci, build, test, lint, the native binary (--host), the verify-binary.sh / verify-binary.ps1 smoke, and the upload to the draft.
  3. publish (container-build): SHA256SUMS from the three jobs' checksums, uploaded; the release is published and every download is checked against SHA256SUMS; the four packages go to the Gitea npm registry with PACKAGES_TOKEN; scripts/update-packaging.mjs fills the formula and the manifest. For a final version scripts/publish-packaging.sh pushes branch release/qqq-<version> to QRun-IO/homebrew-tap and QRun-IO/scoop-bucket with PACKAGES_TOKEN and opens a pull request qqq <version> in each; a human checks the sums and merges. When the push or the pull request is refused (or for a prerelease) qqq.rb and qqq.json are attached to the release instead (step 4); the release never fails over this. Dry run, no writes: DRY_RUN=1 VERSION=<version> SUMS_FILE=<SHA256SUMS> scripts/publish-packaging.sh.

Watch it under Actions in QRun-IO/qqq-cli. The release page is https://git.kof22.com/QRun-IO/qqq-cli/releases/tag/v<version>.

No musl binaries. build-binaries.mjs --musl is best effort: on the glibc build container Bun does not install OpenTUI's musl native packages, so the release carries no qqq-linux-*-musl. On Alpine and other musl hosts use the npm package (@qrun-io/qqq-cli, CLI only) until a musl build lane exists.

4. Homebrew tap and Scoop bucket (when CI could not push)

PACKAGES_TOKEN is the package-scoped publisher token (user qqq-package-publisher); unless it is granted write on the tap and the bucket, CI attaches the filled files to the release instead. Their main branches should be protected so CI can only propose. For a final version, download them and open a pull request on each:

curl -fsSLO https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.1.0/qqq.rb
curl -fsSLO https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.1.0/qqq.json
# QRun-IO/homebrew-tap: Formula/qqq.rb   QRun-IO/scoop-bucket: bucket/qqq.json

Compare each sum with the release's SHA256SUMS. A wrong sum makes brew install and scoop install fail their checksum; the curl installer and direct downloads are unaffected.

5. Signing (not automated)

The binaries are not signed by CI: there is no Apple Developer ID / notary credential or Authenticode certificate on the runners. Homebrew and the curl installer do not quarantine, so they work unsigned; a browser-downloaded macOS binary needs xattr -d com.apple.quarantine ./qqq-darwin-arm64, and Windows SmartScreen warns on first run. scripts/sign-macos.sh and scripts/sign-windows.sh remain for a manual signed release (they recompute dist/SHA256SUMS); wiring them into the native jobs needs the credentials as runner-side secrets (owner decision).

Gitea has no equivalent of GitHub's artifact attestations; SHA256SUMS on the release and the signed tag are the provenance.

6. Smoke after publishing

Against a real server (--server), from clean machines or containers:

# curl installer (macOS, Linux)
curl -fsSL https://git.kof22.com/QRun-IO/qqq-cli/raw/branch/main/scripts/install.sh | sh -s -- --version 1.1.0
scripts/verify-binary.sh ~/.local/bin/qqq --server https://<server>

# Homebrew
brew install qrun-io/tap/qqq && brew test qrun-io/tap/qqq

# npm, Node 20+
echo "@qrun-io:registry=https://git.kof22.com/api/packages/QRun-IO/npm/" >> ~/.npmrc
npm install -g @qrun-io/qqq-cli@1.1.0 && scripts/verify-binary.sh qqq --cli-only --server https://<server>
scoop bucket add qrun https://git.kof22.com/QRun-IO/scoop-bucket; scoop install qqq
pwsh scripts\verify-binary.ps1 -Binary (Get-Command qqq).Source -Server https://<server>

Users who added the tap or the bucket from GitHub re-add them from Gitea (brew tap qrun-io/tap https://git.kof22.com/QRun-IO/homebrew-tap).

When something goes wrong

  • The run fails before the publish job: nothing is public. Delete the draft release (and the tag, if the version must change), fix, and tag again.
  • A package is broken: npm deprecate @qrun-io/<pkg>@<version> "<reason>" against the Gitea registry and publish a new version; a version is never republished.
  • A binary is broken: delete the release, bump the version and release again; the formula and the manifest carry the sums, so never replace an asset in place.
  • The tap's or bucket's check fails on the new file: do not merge; users keep the previous version.