Tutorial

Version 0.3.1 Updated Sep 14, 2026

This tutorial assumes a Rust workspace with a root Cargo.toml and one or more member crates.

1. Create `lading.toml`

Create a minimal configuration file at the workspace root:

[bump.documentation]
globs = ["README.md", "docs/**/*.md"]

[publish]
strip_patches = "per-crate"

lading.toml can be omitted entirely. When absent, lading uses the defaults documented in the configuration reference below.

2. Bump versions

To update the workspace and member crate manifests to 1.2.3:

lading bump 1.2.3

To preview changes without writing any files:

lading bump 1.2.3 --dry-run

After updating manifest versions, lading bump automatically refreshes any git-tracked Cargo.lock files (excluding those under target/) that have an adjacent Cargo.toml, plus any additional manifests listed in bump.lockfile_manifests for lockfiles that git does not track or for nested lockfiles when the workspace is outside a Git repository. The result header counts each changed category, e.g. "N manifest(s)", "N documentation file(s)", "N readme file(s)", "N lockfile(s)", joined with Oxford-comma grammar. In the per-file body list, manifest paths carry no suffix; the other categories carry a parenthetical suffix: (documentation) for Markdown docs whose TOML code fences were updated, (readme) for crate READMEs adopted from the workspace README, and (lockfile) for regenerated Cargo.lock files. In dry-run mode, every file is listed, but none are modified (lockfile discovery still runs git ls-files, which is read-only).

Where a member crate sets readme.workspace = true, lading bump also adopts the workspace README.md into that crate's directory and rewrites relative Markdown links so they still resolve from the crate directory. Such adopted READMEs appear in the output with the (readme) suffix described above.

If bump.documentation.globs is configured, lading also searches those Markdown files for TOML code fences and updates version values that refer to workspace crates.

3. Publish in dry-run mode

By default, publish runs cargo publish --dry-run so the full pipeline can be validated without uploading crates.

lading publish

Before running cargo check and cargo test, lading publish validates that all git-tracked Cargo.lock files are fresh under --locked mode. It probes every tracked lockfile rather than stopping at the first stale one, so a single run reports the whole workspace. If any lockfile is stale — for example after manifest edits made without lading bump, or after a bump run with --no-rebuild-lockfiles — the command exits with code 1 and lists each stale lockfile alongside its own repair command:

Tracked Cargo.lock files are stale after manifest version changes.
This can happen after manifest changes made without `lading bump`, or after
running `lading bump --no-rebuild-lockfiles`; repair each stale lockfile
directly:
- <path>/Cargo.lock
  cargo generate-lockfile --manifest-path <path>/Cargo.toml
- <nested>/Cargo.lock
  cargo generate-lockfile --manifest-path <nested>/Cargo.toml

Run each repair command, commit the updated lockfiles, then re-run lading publish.

In a workspace that is not a Git repository, tracked-lockfile discovery cannot run, so lading publish logs a warning and skips this freshness check rather than failing. lading bump is not affected in the same way: it still regenerates the lockfiles adjacent to the manifests listed in the [bump] section's lockfile_manifests setting.

The cargo metadata --locked output that this probe reads is captured for those diagnostics but never mirrored to the console: it is the whole metadata document on one line, megabytes long for a large workspace, and echoing it broke CI log capture for everything that followed.

Skipping a pre-flight the caller has already run

The pre-flight rebuilds and retests the whole workspace in a throwaway target directory before anything is packaged. In a continuous-integration job that has already run the same suite, that repeat dominates the step: on a warm cache it accounted for 883 of the 936 seconds of a Linux publish step, while the packaging and dry-run publish the step exists to prove took under a minute.

In lading.toml, set skip = true in the [preflight] table. Alternatively pass --skip-preflight, or set LADING_SKIP_PREFLIGHT=1. Any of the three suppresses the auxiliary builds, cargo check, and cargo test:

[preflight]
skip = true
lading publish --skip-preflight

The flag overrides the configuration in both directions, so --no-skip-preflight reinstates the checks for one invocation.

The Cargo.lock freshness guard is unaffected: it costs one cargo metadata call rather than a rebuild, and a stale lockfile would still produce a wrong publication. The working-tree cleanliness guard is likewise unaffected, but it is opt-in, so it runs only when --forbid-dirty is also passed. Those two guards decide whether the publication would be correct rather than repeat the caller's build, which is why a skip never removes them. Every skipped run logs one line naming whichever input asked for it: the setting, the flag, the environment variable, or an in-process caller.

INFO: Skipping the publish pre-flight auxiliary builds, cargo check and cargo
test at the request of the --skip-preflight command-line flag. The Cargo.lock
freshness check still ran; the working-tree check remains opt-in through
--forbid-dirty.

Skip the pre-flight only when the same commit has already been checked. What remains is cargo package's own per-crate verification build, which compiles each packaged crate from its .crate archive; nothing else checks the workspace as a whole and no test runs at all.

To require a clean working tree before running the pre-flight checks, pass --forbid-dirty:

lading publish --forbid-dirty

To perform a real publish (no --dry-run), pass --live:

lading publish --live

Dry-run publishing packages every publishable crate first, then runs cargo publish --dry-run for every crate. Live publishing follows publish.order crate by crate: cargo package, then cargo publish, then the next crate. This lets a later crate depend on a newly published earlier crate in the same --live run. Live publishing is not transactional; if a later crate fails, crates already uploaded to crates.io are not rolled back. Reruns skip versions that are already present on crates.io and continue with the remaining crates.

Each cargo package and cargo publish invocation streams its own output (the Compiling lines from a verify build appear as cargo emits them) and reports its position in the publish order and its elapsed time when it succeeds, so an operator can tell which crate a run is on and a slow verify build can be attributed to a crate:

INFO: Running cargo package for crate alpha (1/3)
INFO: Successfully packaged crate alpha (1/3) in 84.2s
INFO: Running cargo publish --dry-run for crate alpha (1/3)
INFO: Dry-run publish succeeded for crate alpha (1/3) in 61.9s

The same durations are recorded per crate and subcommand in the metrics summary printed at exit (see Observability).

At the end of the run, lading publish prints a summary of the publish plan it computed, listing the crates to publish and any crates skipped because they are marked publish = false, excluded via publish.exclude, or named in publish.exclude but absent from the workspace:

Publish plan for <workspace>
Strip patch strategy: all
Crates to publish (2):
- alpha @ 0.1.0
- beta @ 0.1.0
Skipped (publish = false):
- gamma
Skipped via publish.exclude:
- delta
Configured exclusions not found in workspace:
- missing

When no crates are publishable, the summary reports Crates to publish: none instead of a list:

Publish plan for <workspace>
Strip patch strategy: per-crate
Crates to publish: none

bump adopts the workspace README.md for any member crate that sets readme.workspace = true. The adopted README is written into the crate directory and relative Markdown links are rewritten so they still resolve from that directory. publish then stages the already-prepared workspace into a temporary directory before packaging.

Subprocess output is decoded as UTF-8 and captured in full. Lading first mirrors each chunk through the configured text stream; if that stream rejects Unicode, the exact UTF-8 bytes are written through its binary buffer when one is available. For a text-only narrow stream, mirroring is disabled for that stream while capture continues.

Measuring compiler-cache use during the packaged builds

cargo package --verify and cargo publish --dry-run compile each crate from a packaged copy under target/package/, whose paths and manifest differ from the workspace build, so whether those builds hit an sccache store is a separate question from whether the workspace build did. Pass --sccache-stats to have lading publish query the sccache binary named by RUSTC_WRAPPER for a baseline before the first cargo build and again after every cargo package and cargo publish invocation, and log one line per invocation with the counters attributable to it:

RUSTC_WRAPPER=/path/to/sccache lading publish --sccache-stats
INFO: Compiler cache statistics enabled via /path/to/sccache
INFO: Compiler cache for cargo package alpha: 84.2s, requests=412 hits=398 misses=14 errors=0
INFO: Compiler cache for cargo publish alpha: 61.9s, requests=40 hits=40 misses=0 errors=0
INFO: Compiler cache over the publish pipeline: requests=452 hits=438 misses=14 errors=0
INFO: sccache statistics (cumulative for the server's lifetime):
Compile requests   11128
...

requests is sccache's compile_requests, hits and misses sum its per-language cache counts, and errors sums its cache error, read error, write error, and timeout counters. The per-crate and pipeline lines are differences between snapshots, so the counters sccache itself reports are never reset: a job-wide sccache --show-stats after the run still means what it did before. The final block mirrors that cumulative report so the Cache location line is on record.

Add --sccache-stats-json PATH (which implies --sccache-stats) to write a JSON report alongside, for an artefact upload or a later comparison. A relative PATH is resolved against the workspace root, like every other path lading accepts:

lading publish --sccache-stats-json target/sccache-publish.json

The report holds wrapper, the raw baseline and final payloads from sccache --show-stats --stats-format=json, a crates list with one record per cargo invocation (crate, subcommand, seconds, and the four counters), and the pipeline delta.

Both flags default off and can be set from the environment as LADING_SCCACHE_STATS=1 and LADING_SCCACHE_STATS_JSON=PATH, which lets a CI workflow turn the measurement on without editing a Makefile that hard-codes the lading publish invocation. The pre-flight cargo check and cargo test builds run before the baseline snapshot and are not counted.

The instrumentation never fails a run. When RUSTC_WRAPPER is unset or does not name an sccache binary, or an sccache query fails part-way through, lading publish logs a WARNING, stops querying, and carries on.

Dry-run limitations with unpublished workspace dependencies

cargo package validates dependency versions against the live crates.io index, even in dry-run mode. When two or more workspace crates are released together for the first time and one depends on another at a version that is not yet on crates.io, cargo package will fail with an error similar to:

error: failed to prepare local package for uploading

Caused by:
  failed to select a version for the requirement `inner_crate = "^0.8.0"`
  candidate versions found which didn't match: 0.7.0, 0.6.0, ...
  location searched: crates.io index
  required by package `outer_crate v0.8.0`

This affects dry-run release trains that introduce a new shared version across multiple workspace crates. lading publish --live avoids the limitation by publishing each crate immediately after it is packaged, so a later crate can resolve a dependency that an earlier crate in publish.order just uploaded. Plain dry-runs still use Cargo's live index. By default, lading downgrades these index-lookup failures to warnings when the missing dependency is also in the publish plan and appears earlier in publish order.

Manual staged publishing

When a release must be split manually, run lading publish --live for the foundational crate first, then run lading publish (dry-run) or lading publish --live for the remaining workspace once the new version is indexed:

# 1. Publish the foundational crate live so crates.io has the new version.
lading publish --live --workspace-root path/to/workspace

# 2. Once the new version is indexed, publish (or dry-run) dependent crates.
lading publish --workspace-root path/to/workspace

lading skips crates whose versions are already on crates.io, so the second invocation only acts on the remaining crates.

--allow-unpublished-workspace-deps (dry-run only)

For CI gating where a real publish is not desirable, dry-run mode defaults to the same behaviour as passing --allow-unpublished-workspace-deps: it downgrades the index-lookup failure to a warning when the missing dependency is itself part of the planned publish set and appears earlier in publish order:

lading publish --allow-unpublished-workspace-deps

The override applies to both the cargo package step and the subsequent cargo publish --dry-run step (which packages internally and hits the same crates.io index lookup), so the dry run completes end-to-end.

Use --no-allow-unpublished-workspace-deps to opt out during dry-runs and keep Cargo's index lookup strict:

lading publish --no-allow-unpublished-workspace-deps

--allow-unpublished-workspace-deps is rejected when combined with --live because the failure cannot be bypassed during a real publish. --no-allow-unpublished-workspace-deps remains valid with --live; it preserves the strict behaviour that live publishes already use. When the missing dependency is not in the publish plan, or when it appears after the current crate in publish.order, the failure is still treated as an error. Fix the explicit publish.order so foundational crates come before dependants, or remove publish.order and rely on dependency-derived topological sorting. Each such downgrade is counted and surfaced in the metrics summary emitted at exit; see Observability.