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

crates.io packaging and release-please

Operational guide for publishing Assura libraries to crates.io and shipping CLI installers via cargo-dist. Versioning and GitHub Releases are driven by release-please, not by manually pushing v* tags.

Do not merge a release-please PR without explicit maintainer approval. Merging it creates the tag, GitHub Release, cargo-dist artifacts, and crates.io publish for the version in that PR.

Current status (CLI co-publish, #838)

ChannelWhatNotes
crates.io librariesCore graph ending at assura-pipelineEmbed API for apps
crates.io frontendsassura-rust-analyzer, assura-llm, assura-lsp, assura-mcpShared by the CLI
crates.io CLIassura binary packagecargo install assura --locked
GitHub Releases / cargo-distSame assura binaryPrebuilt multi-platform installers ([package.metadata.dist] dist = true)

Versions share [workspace.package] version. After a release, co-publish uploads any package whose version is not yet on crates.io (idempotent skips for already-published members).

Published crates inherit rust-version = "1.88.0" (edition 2024). That is the MSRV shown on crates.io and the rustc cargo rejects below that. Edition 2024 is 1.85. Current dependencies such as rmcp, tonic, time, zip, and darling declare rust-version 1.88.

What ships on crates.io

Publish order is graph-derived by scripts/publish-crates.sh (all path deps including dev). Typical order:

assura-ast → assura-config → assura-diagnostics → assura-runtime → assura-rust-analyzer → assura-parser → assura-macros → assura-llm → assura-fmt → assura-stdlib → assura-resolve → assura-types → assura-codegen → assura-smt → assura-pipeline → assura-lsp → assura-mcp → assura (CLI).

Path dependencies pin version = "…" so packaging resolves from crates.io; scripts/sync-path-dep-versions.sh keeps pins aligned on release PRs.

What does not ship on crates.io

PackageReason
assura-test-supportInternal monorepo test helpers only (publish = false).
assura-server / assura-bench / editor toolingNot co-published; use monorepo or separate install docs.

How the release pipeline works

push to main
  └─ release-please.yml
       ├─ opens/updates release PR (label: autorelease: pending)
       │    └─ sync-path-dep-versions (align path dep pins on that branch)
       └─ when release PR is merged (release_created=true):
            tag + GitHub Release (App token)
            ├─ tag-push usually starts release.yml (preferred path)
            └─ dispatch-release: only if no healthy tag-push Release exists
                 plan → build-local/global (cargo-dist) → host (upload assets)
                 → publish-crates (scripts/publish-crates.sh) → announce

Important details:

  • Auto-merge uses the assura-auto-approve GitHub App (vars.AUTO_APPROVE_CLIENT_ID + secrets.AUTO_APPROVE_PRIVATE_KEY) so merge pushes are not suppressed. Historical note: Auto-merge with GITHUB_TOKEN does not start push workflows (including release-please.yml). Prefer a human merge for release-related PRs, use the hourly cron catch-up, or gh workflow run "Release Please". See issue #785.
  • Release tag + single pipeline (#1380): release-please creates tags with the App token, which does fire release.yml on.push.tags. Pure GITHUB_TOKEN tags would not. dispatch-release still exists as a fallback, but it skips workflow_dispatch when a healthy tag-push Release already exists (queued / in progress / success). Concurrency is keyed by the release tag (release-${{ inputs.tag || github.ref_name }}) so a race cannot run two full cargo-dist matrices in parallel. Manual re-run: gh workflow run Release -f tag=vX.Y.Z (still works alone).
  • Auto-approve skips PRs labeled autorelease: pending. Release PRs must be merged by a human.
  • Virtual workspace + release-type: simple: release-please only rewrites workspace.package.version (via extra-files). That does not update Cargo.lock the way release-type: rust does for a single root package (e.g. patchloom). The sync-release-pr-versions job runs sync-path-dep-versions.sh and sync-cargo-lock-workspace-versions.sh on the release PR branch so CI with --locked stays green. Do not omit the lock step when copying this layout to another monorepo.
  • Use chore: for CI/docs/refactor. Reserve fix: / feat: for user-visible changes so version bumps stay intentional.
  • Optional: commit RELEASE_NOTES.md on main (not on the release-please branch) before merging the release PR to override the GitHub Release body. Remove it after the release lands (see prior issue #813).

Config files:

PathRole
release-please-config.jsonrelease-type: simple (workspace root has no [package]), updates workspace.package.version, CHANGELOG.md, bump-minor-pre-major
.release-please-manifest.jsonLast released version per package (root ".")
scripts/publish-crates.shFail-closed graph filter + topo publish (pre-check, 429 handling, skip already-uploaded)
scripts/sync-path-dep-versions.shPath-dep version= pins = workspace version
scripts/sync-cargo-lock-workspace-versions.shAlign Cargo.lock workspace member versions after a version bump (required for CI --locked)
scripts/check-publish-plan.shAssert publish order matches the 13-crate library stack
scripts/check-cargo-package.shcargo package gate; full verify when version is on crates.io, --list on co-publish version-bump PRs (#814, co-publish skew)
.github/workflows/release-please.ymlOpens release PR on main push; syncs path-deps + lock on that PR; dispatches Release only if tag-push did not (#1380)
.github/workflows/release.ymlcargo-dist installers + publish-crates (tag / dispatch); concurrency by tag name; then calls Publish MCP Registry
.github/workflows/publish-mcp-registry.ymlAfter crates.io has mcp-name: for that version, publish server.json with GitHub OIDC
server.jsonOfficial MCP Registry metadata. assura mcp over stdio. Description max 100 characters
dist-workspace.tomlcargo-dist targets / installers

Preflight (before merging a release PR)

git status   # clean working tree preferred
bash scripts/check-publish-plan.sh
bash scripts/check-cargo-package.sh   # full package+verify if version on crates.io;
                                      # auto --list on pre-publish version-bump PRs
# Optional dry-run of the publish script (no token required):
bash scripts/publish-crates.sh --dry-run

On a release-please PR that bumps to a version not yet on crates.io, full cargo package cannot resolve path+version deps against the registry (only the old version exists). The script falls back to --list so CI still checks tarball membership; co-publish (publish-crates.sh) is the ordered full verify at release time.

check-cargo-package.sh is the gate that would have caught the v0.1.0 assura-smt failure (monorepo include_str! paths outside the package tarball). CI runs the same script on every rust-touching change (job Cargo package (publishable)).

Shared native crate versions (rowan lesson, #1448 / #1451): when packaging a downstream crate (for example assura-fmt), Cargo resolves path+version deps against crates.io for siblings already published. If the monorepo bumps a private dependency (for example rowan 0.16 → 0.17) only on assura-parser, but a sibling crate still depends on rowan directly, verify can dual-link two rowan majors and fail type-checking (NodeOrToken from 0.16 vs 0.17). Keep rowan private to assura-parser; downstream code should use assura_parser types and methods (as_token / as_node) only.

Fast listing only (no verify build):

bash scripts/check-cargo-package.sh --list-only

Fail-closed real publish normally runs only in CI after a release-please merge:

CARGO_REGISTRY_TOKEN=… bash scripts/publish-crates.sh

Cutting a new release (normal path)

  1. Land normal work on main with conventional commits (feat:, fix:, chore: as appropriate).
  2. release-please updates the open release PR (or opens a new one). An open PR such as “release 0.1.1” after 0.1.0 is expected.
  3. Review version bump, CHANGELOG.md, and path-dep pins (sync-path-dep-versions on the release workflow run).
  4. Optionally push curated notes on a one-file release-note-<semver> branch (no PR). Host or Apply release notes copies them onto the GitHub Release and deletes the branch.
  5. Merge the release PR with an explicit human decision (never auto-merge autorelease: pending).
  6. Watch the Release workflow; verify crates.io + GitHub Release assets.
gh run list --workflow=release.yml --limit 5
gh run watch <RUN_ID>
JobMeaning
release-please (in release-please workflow)Tag + GitHub Release created; dispatches Release with tag
plancargo-dist plan for that tag
build-local-artifacts / build-global-artifactsCLI installers
hostUpload assets (idempotent if the Release already exists); applies notes from release-note-<semver> if present
publish-cratesLibraries via scripts/publish-crates.sh (skips versions already on crates.io)
announceFinal confirmation
Apply release notesCheap re-apply of curated notes (no compile); deletes the notes branch

If publish-crates fails mid-graph, fix on main and re-dispatch the same tag (idempotent for already-uploaded crates). You cannot overwrite a version on crates.io.

# Re-dispatch Release for an existing tag after a script/CI fix
gh workflow run release.yml -f tag=v0.1.0
# or re-run failed jobs on an existing run:
gh run rerun <RUN_ID> --failed

Post-release verification

crates.io

for c in assura-ast assura-config assura-diagnostics assura-macros \
  assura-runtime assura-parser assura-fmt assura-stdlib assura-resolve \
  assura-types assura-codegen assura-smt assura-pipeline; do
  echo -n "$c: "
  curl -fsS "https://crates.io/api/v1/crates/$c" | python3 -c \
    "import sys,json; d=json.load(sys.stdin); print(d['crate']['max_version'])"
done

GitHub Release

gh release view vX.Y.Z
gh release view vX.Y.Z --json assets --jq '.assets[].name'
  • Co-published crates (libraries + frontends + assura CLI) show the released version on crates.io (bash scripts/publish-crates.sh --plan-only).
  • Release page has installers for configured targets.
  • Notes state experimental status and CLI install path (cargo install assura --locked and/or GitHub Release installers).

IR templates and packaging pitfalls

IR prompt markdown used by include_str! must live under crates/assura-smt/templates/ir/ (crate-local). Monorepo templates/ir/ is a pointer README only (#812). scripts/guards.sh section 13 fails if pattern bodies reappear at the monorepo root. scripts/check-cargo-package.sh fails if any publishable crate cannot package/verify.

Manual / break-glass

Only if CI cannot run and a maintainer approves a laptop publish:

git checkout vX.Y.Z
CARGO_REGISTRY_TOKEN=… bash scripts/publish-crates.sh

Prefer re-running GitHub Actions so logs and token handling stay in one place.

Historical: first release (v0.1.0)

The first cut used a temporary release-as: 0.1.0 pin (otherwise historical commits computed 1.0.0), multiple Release re-dispatches, and script fixes for graph order and “already exists” pre-checks. That pin is gone. Do not re-apply release-as unless intentionally forcing a version for a future cut. Details live in session notes / assura-contrib skill, not in day-to-day procedure above.

Canonical patterns live in ci-build-release (release-please same-workflow, cargo-dist host vs release-please body ownership, RELEASE_NOTES.md override, cargo package preflight for publishable members) and ci-branch-protection (never auto-merge autorelease: pending).