Quirlv0.5.1
ArchitectureDecisions

Rust-native releases and runtime assets

Canonical Quirl project documentation synced from docs/decisions/2026-08-18_192326126_rust-native-releases-and-runtime-assets.md.

Accepted · 2026-08-18

  • Amended: 2026-08-22 — runtime assets use independently advancing, version-scoped website channels with immutable content-addressed payloads
  • Extends: ADR 0021, ADR 0024, and ADR 0025
  • Supersedes: ADR 0025's checked-in/raw-main model distribution decision; model identity, validation, and retrieval policy remain unchanged

Context

Quirl needs one release policy that can be exercised locally and orchestrated by CI without reimplementing version selection, changelog generation, packaging, checksums, manifests, or Homebrew rendering in workflow YAML. It also needs to distribute a completion database and command-retrieval model without increasing the executable or forcing first-prompt network access.

These operations cross several failure boundaries: a release may fail after some platform builds but before publication; a tag or published byte may already exist; a runtime download may be truncated, corrupt, incompatible, or interrupted; and updating the separate Homebrew tap requires authority that the Quirl repository token does not have. Mutable payload identities or partially installed files would turn those failures into ambiguous releases or broken local state. At the same time, completion knowledge changes much more often than the executable. Requiring a binary release for every curated completion improvement would delay useful data and create meaningless SemVer churn.

Decision

Rust owns release policy

The existing xtask crate is the single source of release behavior. Its typed release, assets, and homebrew commands own SemVer planning from Conventional Commits, release preparation, verification, packaging, aggregation, checksums, release and asset manifests, provenance, and formula rendering. GitHub Actions invokes those Rust commands and transfers their outputs; it does not duplicate their rules in YAML or shell.

Mutating commands are dry runs unless an explicit --write flag is present. Machine-readable contracts carry a schema version and reject unknown fields. Filesystem reads, retained bytes, collections, subprocesses, and waits have explicit ceilings. Writes use same-directory staging, content synchronization, atomic replacement, and cleanup on failure.

One immutable binary-release candidate identity

A release candidate is the tuple of version and clean Git commit. Every native binary reports both values, and packaging rejects a binary that does not report the planned version or candidate commit. The tag, source archive, native packages, checksums, provenance, and release notes all name that same tuple. The initial runtime-asset snapshot assembled by the release workflow is also built from that candidate, but later compatible website generations deliberately have their own source identities.

Binary publication fails closed when a tag or GitHub Release already exists. Automation must never move a tag, replace an uploaded release asset, or rebuild bytes at an existing content-addressed URL. A failed unpublished candidate can be fixed and replanned. A binary-release failure after its tag or GitHub Release asset becomes public requires a new version; recovery may delete an empty draft only when no immutable tag or release asset was published.

Packages are reproducible for identical inputs: entries have stable names and ordering, metadata is normalized, and timestamps derive from the candidate commit rather than wall time. Aggregation verifies every platform record, rejects duplicates and unexpected targets, and derives checksums and manifests from the exact package bytes it copies.

Every native archive carries Quirl's license, the process bridge's retained third-party notices, and a deterministic target-specific third-party license report. The report is generated from a checked-in locked dependency inventory; packaging fails if the normal/build target closure, declared license metadata, or bounded admitted license texts drift. This keeps legal redistribution input reviewable without trusting an unpinned release-time scanner.

Runtime assets advance independently through version-scoped channels

The completion SQLite database and retrieval model are not embedded in the binary and are not installed by Homebrew. A binary release publishes an immutable candidate-built snapshot to GitHub Releases, while the website owns the independently advancing live channel for each exact Quirl version. For 0.1.0, clients read /reference/v0.1.0/asset-manifest-v2.json. A later binary reads its own path, so advancing one channel cannot silently retarget an older binary.

The small version-scoped manifest is a mutable pointer. Payloads are immutable: their filenames contain the complete SHA-256 digest, and an existing payload name is accepted only when its bytes are identical. A completion update publishes the new database and notice before advancing the manifest. It retains the current command-model record rather than rebuilding an unchanged model. Old content-addressed payloads remain available. The website workflow caps each binary version at 32 retained completion generations so growth is explicit and bounded.

Asset manifest schema v2 records compatibility at the manifest level and provenance per asset. Every entry carries its own source revision and source epoch plus logical name, format version, exact byte length, digest, content-addressed URL, platform contract, and exact compatible Quirl version. The source revision identifies the data generation; it is not required to equal the consuming binary's commit. This distinction permits data-only updates without weakening binary or byte identity.

The completion-database entry carries the complete bounded Carapace MIT notice, its digest, and a content-addressed notice URL. The notice sidecar is published beside the database. The model bundle retains its license inside its tar archive.

The CLI owns a provider-neutral asset manager. The version-scoped website channel is tried first, with GitHub Releases as the immutable initial-snapshot fallback. A future S3 bucket or mirror may serve the same admitted bytes while retaining the same validation and install transaction.

The website channel advances only after the corresponding immutable release tag and GitHub Release exist. Its publication workflow validates retained payloads and notices again, assembles the new complete manifest, and performs a full local install and status check with the exact tagged binary. Development code may produce newer completion data, but it cannot substitute its own runtime admission for the executable users actually installed.

Interactive startup never waits for an asset or network. Missing assets leave Quirl in a documented degraded mode and can schedule bounded background work after startup or on first use. A single explicit lock/state machine coalesces concurrent requests. Downloads use a private temporary file, no shell, explicit redirect/size/connect/body/deadline limits, and observable cancellation. Quirl validates length, digest, manifest compatibility, file format, and private file shape before an atomic install. The previous valid asset remains in place until that validation succeeds, and partial files are removed.

Transient failures use capped exponential backoff with bounded jitter and a bounded persisted retry record. Later launches may retry after the recorded deadline. Integrity, compatibility, and unexpected-file failures are permanent for that manifest identity and require an explicit update or retry; they do not loop forever. Status, retry, and update commands expose actionable diagnostics. Tests inject transport behavior and never require public network access.

Publication and cross-repository authority

The publication environment receives only contents: write; pull-request jobs receive no release or asset credentials. Asset-producing jobs that need private inputs use separate protected environments and narrowly scoped secrets, never fork pull requests. Logs contain identities and digests, not credentials or signed download URLs.

Updating niklas-heer/homebrew-tap always uses a branch and pull request. The preferred credential is a GitHub App installation token scoped to contents and pull requests for that repository. A fine-grained personal access token with the same single-repository permissions is the documented fallback. Neither credential can publish Quirl releases. Owners rotate the App private key or PAT independently, update the protected environment secret, verify a dry run, and revoke the previous credential.

Release preparation uses the repository-scoped built-in workflow token with only contents: write and pull-requests: write, after generation and the full quality gate succeed. The publication workflow independently checks the merged candidate again and requires it to equal the remote default branch. The cross-repository Homebrew App remains separate because the Quirl workflow token has no authority over the tap.

Failure model and invariants

  • A partial build cannot be aggregated because the complete target set and candidate tuple are checked before any release manifest is committed.
  • A partial binary publication cannot change an existing public identity; all existence conflicts stop the workflow. A channel refresh can only add content-addressed bytes before advancing its version-scoped manifest.
  • A partial runtime download cannot replace a valid installed asset; staging is private and replacement follows full verification.
  • A stalled or malicious source cannot grow memory, disk, retries, redirects, or wall time without reaching a configured limit and cancellation point.
  • Loss of GitHub, the asset host, credentials, or the Homebrew tap leaves Quirl usable from existing bytes and source, and leaves already published releases unchanged.
  • Provider changes cannot weaken identity checks: byte length, SHA-256, compatibility, and format admission are provider-independent.
  • A completion update cannot silently replace the model or another logical asset: the previous manifest is fully validated and new descriptors replace records only by exact logical name.
  • Asset provenance is not binary provenance. Release aggregation requires the initial snapshot's per-asset revisions to match the candidate; runtime admission requires binary compatibility and byte identity, not commit equality.

Consequences

  • Local and hosted release behavior is reviewable and testable as Rust rather than distributed across scripts and YAML.
  • Binary-release and content-addressed payload bytes are immutable. Completion knowledge can improve without consuming a SemVer version because the channel advances to a new immutable payload instead of changing old bytes.
  • Fresh installations can start immediately without the larger data assets, while offline installations retain explicit status and manual recovery.
  • Initial binary publication depends on GitHub Releases; normal compatible data refreshes use the website channel, and storage migration does not require changes to asset admission or compatibility policy.
  • Repository owners must configure protected release and tap environments plus the tap-scoped App installation before the first publication.

On this page