Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CI and Release

Status: Current Last updated: 2026-10-27 (commit c0e233d0)

Pre-Merge Verification

Run the shared local gate from Developer Verification Checks:

just gate

This runs the checks used by per-push CI, including doctests, both Rust workspaces, generated-artifact currency, and the book. Wait for GitHub Actions on the exact pushed commit before announcing it as ready. Release-only checks run separately through just release-lint. That recipe first checks app versions and the changelog section/link, before formatting checks and compiler checks, so a missing release entry fails before compilation.

Generated artifact drift

After changing grammar, spec, or a registry, run just regen, then just test. The regeneration recipe builds derived artifacts in dependency order; currency tests detect stale output. Never hand-edit generated artifacts.

See Spec Workflow and spec/AGENTS.md for the current source-of-truth guidance.

Release Process

TalkBank/chatter is the public release source of truth. release.yml (cargo-dist) builds the CLI artifacts and publishes the GitHub Release; release-desktop.yml, called from it, creates the unpublished draft and adds the desktop installers first. Signing differs by platform as described below. The full publication order is in docs/strategy/coordinated-release.md.

Cutting a release: the two-command procedure

The version literal lives in many places (the workspace version, every internal path-dep pin, the desktop package.json, the CHANGELOG section), and the tag is the release trigger, so both steps are mechanized and fail-closed. Hand-editing version fields or tagging with raw git tag is how releases break (v0.1.1 shipped a desktop version mismatch; v0.5.0 tagged a bump commit before its CI reported and the desktop build died on drift CI would have caught). The procedure:

  1. just release-bump X.Y.Z rewrites the canonical [workspace.package] version, every path = "crates/…" pin, and package.json and both root-version fields in package-lock.json, then refreshes both Rust lockfiles (root + spec/). The app-version check exercises this command against temporary manifests and lockfiles before checking the checkout, including independent drift in each lockfile version and preservation of dependency versions. The command accepts normal X.Y.Z versions: ASCII numeric components without leading zeros, as defined by SemVer. It does not trim input or treat an empty bump argument as a check. Stored canonical versions use the same admission. An optional --release-tag must agree before any version file is written; identity rejection preserves all files. The Python 3.11+ command parses the Cargo manifest before preparing an edit. It updates the explicit workspace-package version and inline internal dependency version fields, retaining quotes, comments and line endings. Missing, malformed or ambiguous fields refuse before writing; unsupported field layouts are reported rather than guessed. The prepared TOML is checked against exactly the owned version-key changes before it can be written. Check mode detects internal dependency-pin drift, and fix mode repairs it. Cargo, both npm documents and changelog input are prepared before any file changes. Missing fields, non-string or invalid version values, malformed JSON and duplicate keys refuse the whole preparation without writes. Every replacement is then written and closed in a private temporary directory beside its destination before publication starts. Staging failures leave all originals unchanged. Destinations must be regular files, not symlinks; replacement preserves permission bits, but not inode identity or other metadata. Other hardlinks keep their original content. Publication replaces files sequentially: a later rename failure can leave earlier replacements applied. Inspect the version files before retrying such a failure. This is not a multi-file transaction or a crash-durability guarantee.
  2. Write the ## [X.Y.Z] CHANGELOG section and its comparison link. The app-version check requires them; it does not write release notes for you.
  3. Format, run just release-lint and just gate, then squash the commits since the last push into one release commit whose message is the CHANGELOG section. Verify that the squashed tree retains the checked content and, with maintainer authorization for that candidate, push and wait for CI on that commit. Reuse the receipt when its checked inputs are unchanged; a squash alone is not a reason to rerun the gate. Push rarely; preserve already-published commits instead of rewriting history at release time.
  4. With release authorization, just release-tag X.Y.Z tags and pushes vX.Y.Z, refusing on a dirty tree, an unpushed HEAD, any version-copy drift, a missing CHANGELOG section, or CI/Cross-platform not yet green on the exact tagged commit.

After the tag: release-tag-dispatch.yml dispatches release.yml for that tag, which builds, runs the desktop publication job, publishes and then adds the app banner; verify the release page carries the CLI archives, the LSP standalone artifacts, the desktop installers and the CHANGELOG notes before announcing.

Workflows that actually exist in this repo

WorkflowPurposeNotes
.github/workflows/ci.ymlMain build/test/book CIPrimary shared signal on pushes and PRs
.github/workflows/cross-platform.ymlCross-platform build coverageSupplements the main CI workflow
.github/workflows/crates-io-foundation.ymlFirst-wave crates.io readinessChecks foundation-crate metadata, package surfaces, hold-backs, and publish order
.github/workflows/release-tag-dispatch.ymlTag triggerOn a version tag push, dispatches release.yml for that tag
.github/workflows/release.ymlcargo-dist release automationGenerated by dist (dist generate; never hand-edit). Builds CLI artifacts, calls the desktop publish job, then publishes the draft in its announce step
.github/workflows/release-desktop.ymlDesktop installer release automationdist publish job: creates the draft with dist’s announcement title and CHANGELOG body, builds and uploads the installers and updater bundles, and verifies the candidate; workflow_dispatch runs build-only
.github/workflows/release-app-banner.ymlRelease page bannerdist post-announce job: prepends the desktop app banner to the published release notes
.github/workflows/release-lint.ymlRelease-time lintjust release-lint: clippy over both workspaces plus the feature-off build. Runs on a version tag and on workflow_dispatch, never per push
.github/workflows/clippy-rolling.ymlNew-stable clippy lintsWeekly scheduled clippy, so a new stable’s lints surface within days

Current release stance

Cross-platform workspace verification uses Cargo’s normal Windows dynamic C runtime through a job-local TAURI_CONFIG override. Tauri 2.7’s static-runtime build shim otherwise shadows msvcrt.lib for unrelated workspace doctests. Doctests remain enabled. This override does not apply to desktop release packaging, which retains Tauri’s static-runtime default; passing workspace tests does not replace verifying the packaged Windows artifact.

  • release.yml is about workspace artifact packaging via cargo-dist, not about crates.io publication.
  • The first-wave crates.io path is documented separately in Crates.io Publication and is checked by just crates-io-foundation-check plus .github/workflows/crates-io-foundation.yml.

Desktop release workflow: how the release jobs compose

dist-workspace.toml sets create-release = false, github-release = "announce", publish-jobs = ["./release-desktop"] and post-announce-jobs = ["./release-app-banner"]. After the CLI artifacts build, release.yml calls release-desktop.yml, which creates the draft release with dist’s announcement title and body, builds the Tauri installers and updater bundles, uploads them and verifies the candidate. Only when that job succeeds does the announce step upload the CLI archives, checksums and installer scripts and publish the draft; the banner job then edits the published notes. Nothing polls, and a failed desktop build leaves an unpublished draft. Installer compatibility consumes the completed global Actions artifact, not originals that are uploaded only during announcement. Only derived legacy aliases are uploaded beforehand and verified by downloading their bytes; release-bound admission keeps pending inputs distinct from uploaded assets. The final gate and CI execute the actual preparation, verification and announcement commands with a stateful transport regression. See Coordinated release publication for the full phase and partial-draft recovery contract. Complete recovery requires both CLI and LSP product inventories; a plan omitting either product cannot certify the draft as complete. Each product must reference executable archives covering all five CLI/LSP targets: macOS Apple Silicon and Intel, Linux ARM64 and x86-64 musl, and Windows x86-64. Each archive must declare its targets and the product’s expected executable; pointing the LSP record at a CLI archive or removing a target from both the plan and uploaded assets cannot establish completeness. This remains declared target inventory admission, not archive-content, native platform, provenance or installation acceptance.

The updater producer and candidate verifier share one complete platform inventory: macOS Apple Silicon and Intel, Windows x86-64 and Linux x86-64. Missing, duplicate or unknown platforms, shared bundle filenames across platforms, blank signatures and unsafe asset basenames reject before a manifest can be emitted. Target triples must match the supported inventory exactly. This checks metadata consistency; it does not inspect executable architecture or verify signatures. Producer and verifier also share release-tag admission: a literal v followed by a SemVer core and optional prerelease, with no build metadata. Leading zeros in numeric identifiers, empty prerelease identifiers and trailing whitespace reject. The updater version must equal the admitted tag without v; conflicting metadata is refused before the generator opens its output file.

Two platform notes baked into the workflow:

  • macOS: Tauri signs, notarizes, and staples the .app, but NOT the .dmg it wraps around it. The workflow therefore submits the .dmg itself to the notary service and staples it, then verifies codesign, spctl, and stapler validate on both artifacts. The signing identity is supplied via environment, never hardcoded in tauri.conf.json.
  • Windows / Linux: artifacts are currently unsigned by decision; see docs/strategy/distribution-and-signing.md (“Decisions, 2026-06-12”) and the SmartScreen guidance in the install docs.

Release secrets (Actions secrets on this repository)

The macOS jobs of release-desktop.yml use:

SecretContent
APPLE_CERTIFICATEbase64-encoded Developer ID Application .p12
APPLE_CERTIFICATE_PASSWORDpassword for the .p12
APPLE_SIGNING_IDENTITYfull identity string, Developer ID Application: <Name> (<TEAMID>)
APPLE_API_KEYApp Store Connect API key ID (notarization)
APPLE_API_ISSUERApp Store Connect issuer ID
APPLE_API_KEY_CONTENTcontents of the AuthKey_*.p8 file

Every desktop platform also uses TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD to sign updater bundles. These are separate from Apple code signing and notarization. Their matching public key is committed in apps/chatter-desktop/src-tauri/tauri.conf.json under plugins.updater.pubkey; the private key must never enter the repository. An unsigned Windows or Linux installer does not mean its updater bundle can omit this signature.

The cargo-dist CLI signing path uses the separate CODESIGN_CERTIFICATE, CODESIGN_CERTIFICATE_PASSWORD and CODESIGN_IDENTITY secrets. Maintainers must verify access to each required credential before an authorized release; the presence of a secret name does not prove its value is usable. Do not print secret values in logs or paste them into issues.

Replacing an Apple certificate or notary key updates the corresponding secrets. Do not treat replacing the updater key as the same operation: existing installations trust their embedded public key, so a key change needs a reviewed update/reinstallation migration. Handover must preserve the updater key and verify the successor’s access without publishing it. Repository secrets do not automatically transfer to a new repository.

The development loop

The loop exists because a single parser fix can cost a day to the process around it rather than to the fix.

  1. Inner loop: just test. Write the failing test or the type change first, then make it green. clippy and fmt are run before a release, not per edit.
  2. After any change under grammar/, spec/ or a registry: just regen, then just test. Every derived artifact has a currency test, and they are far cheaper to satisfy together than one gate run at a time.
  3. Before committing: review the final diff once.
  4. Before pushing: just gate on the final candidate content. It runs the per-push checks locally; still inspect hosted CI on the exact pushed commit. The pre-push hook refuses a push without the stamp; the stamp hashes tree content, so a gate run on uncommitted changes stays valid once the same bytes are committed. Clippy and the feature-off build are just release-lint, run before a release.
  5. Releasing: use the candidate-bound procedure above. A content-preserving squash reuses its valid receipt; changed checked inputs require another gate. Push and tag only with their respective maintainer authorization, and preserve already-pushed history.

Functional tests use the repository’s finite specs and reference corpus; they do not require private transcript data. Building still requires the documented tools and dependencies. Publishing additionally needs GitHub access, the signing credentials above and candidate-bound runtime verification.


This page last changed: 2026-10-27 (commit c0e233d0). The whole book last changed: 2026-10-27 (commit 5bfee2f9).