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-09-15 12:06 EDT

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 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/CLAUDE.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.
  2. Write the ## [X.Y.Z] CHANGELOG section (the one deliberately manual step; every gate enforces its presence).
  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 the gate on the squashed tree and, with maintainer authorization, push and wait for CI on that commit. The content stamp survives a squash that leaves the checked bytes unchanged. Push rarely; preserve already-published commits instead of rewriting history at release time.
  4. 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 drift detectionWeekly maintenance workflow

Current release stance

  • 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. 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)

Required by the macOS jobs of release-desktop.yml (and by cargo-dist macOS codesigning if macos-sign is enabled, which uses the separate CODESIGN_* names documented in the strategy doc):

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

Rotation: replacing the certificate or notary key means updating these secrets and nothing else; no workflow edits are needed. A maintainer must re-create all of them on any new repository (secrets do not transfer).

The development loop

Set on 2026-08-27, after a single parser fix 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, once. It mirrors per-push CI exactly, so CI is a confirmation and never a discovery. 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: format, just release-lint, gate, squash every commit since the last push into one release commit carrying the changelog section, gate once more, push, wait for CI, then just release-tag. Push rarely and preserve already-pushed history.

Nothing on this path needs data that is not in the repository.


This page last changed: 2026-09-15 (commit bb4bef82). The whole book last changed: 2026-09-15 (commit bb4bef82).