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.