Release and downloadable-asset operations
Canonical Quirl project documentation synced from docs/releasing.md.
Release policy lives in Quirl's Rust xtask. Workflow files are orchestration
only. ADR 0026
defines the identity, immutability, recovery, and storage contracts.
The 0.1 distribution is the native GitHub Release plus Homebrew tap. Product
crates are explicitly publish = false; crates.io and cargo install are not
release targets until the path dependency graph, package metadata, and support
contract are designed for them.
Local commands
Inspect the next version and deterministic notes without changing files:
cargo xtask release plan
cargo xtask release prepareApply the idempotent version and changelog preparation only after reviewing the plan, then verify the clean candidate:
cargo xtask release prepare --write
npm ci --prefix website
npm --prefix website run sync:docs
npm --prefix website run sync:reference
cargo xtask check
cargo xtask website-checkPreparation writes the reviewable canonical notes to RELEASE_NOTES.md and
updates the local workspace package versions in Cargo.lock. The compiled
website references embed the workspace version, so refresh both reference pages
as well as the source mirrors. Review and commit the preparation with
chore(release): prepare next version, then run cargo xtask release verify
against the clean candidate. The preparation workflow performs these same
generation and validation steps before opening its pull request.
Website documentation follows the current source tree. Keep installation and
download labels on the latest published version until the new GitHub Release
exists, and identify source-only features in first-session instructions. A
prepared changelog entry is not evidence of publication. After the release
PR is merged, preview the exact tag operation with cargo xtask release tag --expected-tag vVERSION; publication adds --write to create and non-force
push that exact lightweight tag.
Platform jobs package one native target. Aggregation accepts only the complete target set for one version and commit:
cargo xtask release package --target aarch64-apple-darwin --output target/release-package
cargo xtask release aggregate --input target/release-input --output target/release-aggregateEach native workflow job also builds the benchmark harness from the same clean
candidate and runs the enforcing performance gate against the packaged binary's
independently calculated SHA-256. macOS jobs first turn off Spotlight indexing:
on the 3-vCPU ARM64 runner, mds and mdworker indexed the checkout and the
fresh build at over 200% CPU for minutes, and the published v0.3.0 source then
missed the unchanged 21 ms first-prompt budget just like the 0.4.0 candidate.
Before measuring, every job also waits up to four minutes for the one-minute
load average to fall below half its CPUs. Budgets are not relaxed. A size or latency miss stops that package from
reaching aggregation. The performance-<target> workflow artifact preserves the
JSON report, including a failed budget, for seven days. These runner measurements
supplement the named-terminal review in the release checklist.
If packaging succeeded, the separate package-<target> artifact is retained for
one day even when measurement fails. It is diagnostic evidence, not an approved
release: the failed job still blocks aggregation and publication. Compare the
executable inside that archive with the report's binary digest when investigating
a failure.
Measured PTY children use a private home, project database, and XDG directories. Project discovery remains enabled against that empty home; it cannot reuse a developer's project cache or scan personal repositories. The harness retains the real Cargo and Rustup locations for the normal toolchain prompt probes.
Each native archive has exactly four top-level contracts: executable
bin/quirl, Quirl's LICENSE, process-bridge THIRD_PARTY_NOTICES.md, and a
deterministic target-specific THIRD_PARTY_LICENSES.txt. Packaging checks the
locked release dependency graph against distribution/quirl-cli-third-party.tsv
and fails if a dependency, declared license, source, platform closure, or
bounded license text drifts.
Build separate downloadable assets and their strict manifest:
cargo xtask assets build --kind completion-database --output target/assets
cargo xtask assets build --kind command-model --output target/assets
cargo xtask assets manifest --input target/assets --output target/assets/asset-manifest-v2.jsonTo refresh completions for the currently shipped binary without rebuilding the
model or cutting a release, build only completion-database, then merge it with
the current version-scoped website manifest:
cargo xtask assets build --kind completion-database --identity current --output target/assets/completion-database
cargo xtask assets manifest --identity current --input target/assets --previous-manifest website/public/reference/vVERSION/asset-manifest-v2.json --output target/assets/asset-manifest-v2.json
cargo xtask assets rebase-manifest --input target/assets/asset-manifest-v2.json --base-url https://quirl.vercel.app/reference/vVERSION --output target/assets/asset-manifest-v2-website.jsonThe checked-in completion workflow performs the bounded publication transaction: it copies new digest-named payload and notice files without overwriting existing names, verifies every retained sibling again, admits the complete result with the exact tagged binary, validates the complete website, and updates the small manifest last. It refuses to publish before that version's immutable tag and GitHub Release exist.
Render and validate the tap formula from the aggregate release manifest:
cargo xtask homebrew render --release-manifest target/release-aggregate/release-manifest-v1.json --expected-tag vVERSION --tap-root ../homebrew-tap --write
cargo xtask homebrew check --tap-root ../homebrew-tap --release-manifest target/release-aggregate/release-manifest-v1.json --package-root target/release-aggregateRendering is a dry run by default. The example writes only because it assumes a
dedicated tap update branch. The checkout tag, HEAD, manifest commit, version,
files, URLs, and hashes must all agree. The package check uses an already
downloaded host archive and runs quirl --version without fetching runtime
assets. Never edit generated hashes or platform URLs by hand.
GitHub configuration
Use protected environments so untrusted pull requests never receive secrets:
| Environment | Authority | Contents |
|---|---|---|
release | Quirl repository only | GitHub-provided token with contents: write; required reviewer for publication |
release-assets | Asset source only | Narrow read credentials needed to acquire the model/database inputs; none when inputs are public |
homebrew-tap | Tap repository only | GitHub App client ID and private key; fine-grained PAT fallback |
Repository variables may contain the public repository names and GitHub App
client ID. Secrets contain only the App private key, source credentials,
or fallback PAT. Do not store checksums, versions, URLs, or policy as variables;
xtask derives them from candidate inputs.
For the checked-in workflows, define HOMEBREW_APP_CLIENT_ID as a repository
variable and store HOMEBREW_APP_PRIVATE_KEY on the protected homebrew-tap
environment. The release workflow calls the same-repository Homebrew workflow
with secrets: inherit; without that forwarding, the protected environment's
private key was unavailable in the reusable call for v0.2.0 even though the
standalone workflow worked. The tap job retains its protected environment and
checks only whether the required variable and secret are present before creating
its narrowly scoped token; it never prints either credential. Give the release
environment a required reviewer, enable
immutable releases, and protect release tags from mutation. The preparation
workflow uses the repository's built-in token with job-local contents: write
and pull-requests: write; the publication job is the only other job that
receives contents: write.
The GitHub App is installed only on niklas-heer/homebrew-tap and has
repository Contents: read and write and Pull requests: read and write.
Metadata read is implicit. It has no Quirl release authority. The fallback PAT
is fine-grained, restricted to the same repository and permissions, and has a
short expiry. Rotate by creating a replacement, updating the protected
environment, exercising a formula-render dry run and branch PR, then revoking
the old credential. Rotate immediately after suspected disclosure.
When the App is unavailable, use the PAT only from a trusted local checkout:
render with cargo xtask homebrew render --expected-tag vVERSION --write, run
the manifest/package form of cargo xtask homebrew check shown above, create a
non-default tap branch, push that branch with the PAT, and open the PR. Do not
place the PAT in a command argument, repository remote, workflow file, or log;
provide it through the credential manager or GH_TOKEN for the single PR
operation, then unset it.
Workflows pin third-party and GitHub actions to full commit SHAs and declare
permissions per job. The supported native matrix uses explicit current standard
runner labels: ubuntu-24.04 (Linux x86_64), ubuntu-24.04-arm (Linux ARM64),
macos-15-intel (macOS x86_64), and macos-15 (macOS ARM64). Recheck GitHub's
hosted-runner reference before changing those labels.
Publication and recovery
- Merge the preparation PR and select its clean commit as candidate A.
- Run the local or hosted verification gate against A.
- Package all four native targets and build the separate completion/model assets. Each binary must report the planned version and A's commit.
- Aggregate once. Review the release manifest, checksums, notes, and
provenance before approving the protected
releaseenvironment. - Run
cargo xtask release tag --expected-tag vVERSION --writeto create the immutable exact-candidate tag, then create the GitHub Release and upload only the aggregate's exact bytes. Refuse a conflicting tag, release, or asset. - Wait for the release-dispatched completion workflow to seed or advance the exact version's website channel and smoke-admit it with the released binary.
- Generate
Formula/quirl.rbfrom that published release manifest on a new tap branch and open a PR. Never push the tap default branch.
Before a tag or GitHub Release asset exists, fix a binary-release failure and rerun from a new clean candidate. After either is public, do not move, delete-and-recreate, or overwrite it as routine recovery. Preserve logs and manifests, correct the problem in source, and publish a new SemVer version. An empty unpublished draft may be removed only after verifying that no tag or release asset escaped. Website channel manifests may advance independently, but only after their new content-addressed bytes exist; an existing payload URL is never overwritten.
If the tap PR job fails after publication, the Quirl release remains valid. Preserve the failed job's logs and retry the standalone Homebrew workflow against the existing immutable tag:
gh workflow run homebrew.yml --repo niklas-heer/quirl --ref main -f tag=vVERSIONThe workflow regenerates and checks the formula from the published manifest,
then requires the normal protected homebrew-tap environment review before
opening or updating its tap PR. Review that PR and the exact archive digests
before merging; do not push directly to the tap default branch. This recovery
does not rebuild or replace release artifacts or move the tag. The standalone
path succeeded for v0.2.0 in
run 33990534633.
If asset hosting is temporarily unavailable, existing installed assets and Quirl's degraded mode remain valid; do not repoint a published manifest to mutable replacement bytes.
Runtime assets and offline behavior
Version 0.1 publishes an initial completion SQLite database, command model
bundle, their retained license notices, and asset-manifest-v2.json as GitHub
Release assets. Homebrew installs quirl plus the redistribution documents; it
must not fetch runtime assets in install or test. The formula installs LICENSE,
THIRD_PARTY_NOTICES.md, and THIRD_PARTY_LICENSES.txt below
share/quirl/licenses so the redistribution notices accompany the binary.
The live website manifest is version-scoped, for example
/reference/v0.1.0/asset-manifest-v2.json, and may advance without a binary
release. Each entry has its own source revision and epoch, exact binary
compatibility, format, byte size, digest, and content-addressed URL. A completion
refresh replaces only that logical record and retains the current model. The
workflow publishes new bytes before the manifest, never deletes or overwrites a
payload, and admits at most 32 completion generations per binary version.
Quirl stores downloaded assets below the platform data/cache roots:
- macOS:
$HOME/Library/Application Support/quirl/assetswith retry/cache metadata below$HOME/Library/Caches/quirl/assets; - Linux:
$XDG_DATA_HOME/quirl/assetsor$HOME/.local/share/quirl/assets, with retry/cache metadata in$XDG_CACHE_HOME/quirl/assetsor$HOME/.cache/quirl/assets.
Missing assets never delay the first prompt. Completion uses builtins and local discovery, and natural-command retrieval uses its lexical/degraded path while a bounded background update is eligible. An offline launch reports status without discarding a previously valid asset. Transient failures retry on later launches with capped backoff; integrity and compatibility failures require explicit operator action.
Use the CLI asset commands to inspect state and request bounded work:
quirl assets status
quirl assets retry
quirl assets updateCancellation removes the partial staging file. A valid installed generation is not removed until its replacement passes exact size, SHA-256, compatibility, format, and private-file checks.
Adding S3 or a mirror
Add a provider that resolves a versioned logical asset to an immutable URL and bounded byte stream. Do not change manifest admission, compatibility checks, download limits, retry classification, or the install transaction. Publish the same digest under all mirrors; provider failover may change where bytes are read, never which bytes are accepted.