crate-seq-core

crate-seq-core is the orchestration crate — it wires together every other crate in the workspace to implement the init, check, and publish workflows. It owns token resolution, pre-publish validation, topological workspace ordering, and the full publish pipeline (dry-run and live).


Module map

Module Purpose
lib.rs Crate root — re-exports, module declarations
workspace.rs Workspace member discovery from Cargo.toml
auth.rs Three-tier crates.io token resolution
init.rs init command orchestration
check.rs Ledger-vs-registry diff reporting
validate.rs Pre-publish path-dep and cargo check guards
topo.rs Topological sort of workspace members
pipeline/mod.rs Pipeline module root
pipeline/source.rs Source resolution (git tag or snapshot)
pipeline/dry_run.rs Dry-run publish pipeline
pipeline/execute.rs Live publish pipeline
pipeline/workspace_publish.rs Workspace-ordered multi-crate publish

workspace.rs — member discovery

Discovers workspace members from a root Cargo.toml. Supports both multi-crate workspaces and single-crate repos.

Key types

  • WorkspaceMember — a discovered crate with name: String, manifest_path: PathBuf, and crate_dir: PathBuf

Public API

  • discover_members(workspace_root: &Path) -> Result<Vec<WorkspaceMember>, Error>

    Reads the root Cargo.toml. If a [workspace].members array exists, expands each entry (including /* globs) and reads each member's Cargo.toml to extract [package].name. For single-crate (non-workspace) manifests, returns a single member representing the root crate.

Error cases

  • Error::Io — file not found or unreadable
  • Error::Toml — invalid TOML in a manifest
  • Error::MissingPackageName — member Cargo.toml has no [package].name

auth.rs — token resolution

Implements a three-tier strategy for resolving a crates.io API token, mirroring how CI and local development environments typically manage secrets.

Resolution order

  1. Tier 1 — CLI argument (--token <TOKEN>): highest priority, for scripting and CI
  2. Tier 2a — environment variable (token_env in .crate-seq.toml): reads the named env var
  3. Tier 2b — shell command (token_cmd in .crate-seq.toml): runs the command string in sh -c, captures trimmed stdout. Only used if token_env is not set
  4. Tier 3 — Cargo credentials (~/.cargo/credentials.toml): reads [registry].token from the standard cargo credentials file

Key types

  • TokenError — enum with variants:
    • EnvVarEmpty { var } — env var is set but empty
    • CmdFailed { reason } — shell command failed or produced empty output
    • CargoCredentials(io::Error) — credentials file exists but is unreadable/invalid
    • NotFound { tried } — all tiers exhausted

Public API

  • resolve_token(cli_token: Option<&str>, ledger_auth: &LedgerAuth) -> Result<Option<String>, TokenError>

    Walks the tier chain. Returns Ok(Some(token)) on success, Ok(None) if no token found (cargo may still have its own credentials).

  • require_token(cli_token: Option<&str>, ledger_auth: &LedgerAuth) -> Result<Option<String>, Error>

    Wraps resolve_token with actionable error messages — converts each TokenError variant into a user-friendly Error::TokenResolution string explaining what to fix.

Design notes

  • Tier 2a and 2b are mutually exclusive within a single resolution — env takes precedence if both are configured

init.rs — init orchestration

Implements the init command: discovers git tags and crates.io state, diffs them, and writes per-crate .crate-seq.toml ledger files.

Workflow

For each workspace member:

  1. Resolve tag pattern — from existing ledger (if present) or auto-detected
  2. Discover git tags — using the resolved tag pattern
  3. Fetch registry state — queries crates.io for existing versions
  4. Build entries — registry-published versions → Published/Yanked; git-only tags → Pending
  5. Write ledger — saves .crate-seq.toml alongside the crate's Cargo.toml

Error cases

  • Error::CrateNotFound--crate filter names a crate not in the workspace
  • Error::Git — tag discovery failure
  • Error::Registrycrates.io HTTP failure
  • Error::Ledger — ledger write failure

check.rs — ledger vs registry diff

Implements a read-only operation that diffs the local ledger against live crates.io state and reports the current status of all versions.

Key types

  • CheckReport — full report:
    • crate_name: String
    • registry_latest: Option<Version> — highest non-yanked version on crates.io
    • pending: Vec<LedgerEntry> — versions awaiting publish
    • skipped: Vec<LedgerEntry>
    • yanked: Vec<LedgerEntry>
    • orphaned: Vec<OrphanedEntry> — pending entries whose backing ref is missing
  • OrphanedEntryversion + recorded_ref for entries whose git tag or snapshot tarball no longer exists

Public API

  • check_crate(ledger_path, repo_path, crate_seq_version, snapshot_store) -> Result<CheckReport, Error>

    Loads the ledger, queries crates.io and git, classifies all entries, detects orphans. Does not modify any state.

Orphan detection

  • GitTag entries: checks if the tag name exists in the repo
  • Snapshot entries: scans the snapshot store for a .tar.gz file whose SHA-256 matches the ledger's ref_ field

Error cases

  • Error::Ledger — ledger load failure
  • Error::Registrycrates.io HTTP failure
  • Error::Git — git tag discovery failure

validate.rs — pre-publish guards

Two validation checks run before every publish attempt (both dry-run and live).

Public API

  • validate_no_path_deps(cargo_toml_path: &Path) -> Result<(), Error>

    Scans for path dependencies. If any are found, returns Error::PathDependencies { manifest, deps } listing all offenders. Handles both path = "..." string values and inline table { path = "..." } forms.

  • run_cargo_check(cargo_toml_path: &Path) -> Result<(), Error>

    Spawns cargo check --manifest-path <path>. Returns Error::CargoCheck { stderr } on non-zero exit, Error::Subprocess if cargo can't be spawned.

Pipeline integration

In the dry-run pipeline, validation failures are soft outcomes — they produce DryRunOutcome::PathDepsFound or DryRunOutcome::CargoCheckFailed without halting the entire run. In the live pipeline, they propagate as hard errors.


topo.rs — topological sort

Sorts workspace members into dependency tiers for correct publish ordering. Uses Kahn's algorithm (BFS) over the intra-workspace dependency graph.

Public API

  • topo_sort(all_members, selected_names) -> Result<Vec<Vec<&WorkspaceMember>>, Error>

    Returns tiered groups: tier 0 has no workspace deps, tier 1 depends only on tier 0, etc. Members not in selected_names participate in graph construction but don't appear in output.

Dependency parsing

  • Reads [dependencies] and [build-dependencies] tables from each member's Cargo.toml
  • Skips [dev-dependencies] (they don't affect publish order)
  • Handles package = "..." aliased dependencies
  • Only counts deps that are also workspace members

Cycle detection

If the BFS completes but not all selected members were processed, a cycle exists. Returns Error::DependencyCycle { crate_names } listing the involved crates.

Example

For a workspace where crate-seq-core depends on crate-seq-ledger, crate-seq-git, crate-seq-manifest, crate-seq-registry, and crate-seq-snapshot:

  • Tier 0: crate-seq-ledger, crate-seq-git, crate-seq-manifest, crate-seq-snapshot
  • Tier 1: crate-seq-registry
  • Tier N: crate-seq-core (depends on all of the above)

pipeline/source.rs — source resolution

Maps a LedgerEntry to a temporary directory ready for packaging.

Public API

  • resolve_source(entry, repo_path, snapshot_store) -> Result<TempDir, Error>
    • GitTag entries → checks out the tree at the tagged commit into a temp directory
    • Snapshot entries → scans the snapshot store for a .tar.gz matching the SHA-256 hash in entry.ref_, extracts it into a temp directory

Error cases

  • Error::Git — checkout failure
  • Error::SnapshotNotFound — no tarball matches the hash
  • Error::Snapshot — extraction failure

pipeline/dry_run.rs — dry-run pipeline

Validates and packages every pending version without any network writes. Used as the default publish behavior (live publish requires --execute).

Key types

  • DryRunOutcome — per-version result enum:
    • Passcargo package --allow-dirty succeeded
    • PackageFailed(String) — stderr from failed package
    • PathDepsFound(Vec<String>) — path dep names found
    • CargoCheckFailed(String) — stderr from failed check
  • DryRunReportcrate_name + Vec<DryRunVersionResult> (each with version, tag_ref, outcome)

Public API

  • publish_dry_run(ledger_path, repo_path, snapshot_store) -> Result<DryRunReport, Error>

Per-version flow

  1. Source resolution → temp directory
  2. Path dependency check → soft failure if path deps found
  3. cargo check → soft failure if check fails
  4. Version rewrite in Cargo.toml to set exact version
  5. cargo package --allow-dirty → final validation

Design notes

  • Validation failures are soft — recorded as outcomes, not propagated as errors. The report shows which versions would fail without halting the entire run.
  • Does not modify the ledger

pipeline/execute.rs — live publish pipeline

The full publish-to-crates.io pipeline with idempotency checking and crash-safe ledger persistence.

Key types

  • VersionPublishOutcome — enum:
    • Published — successfully published in this run
    • AlreadyPublished — already on crates.io, skipped
    • Skipped — ledger status was already Skipped
    • Failed(String) — error message
  • PublishReportcrate_name + Vec<PublishVersionResult>

Public API

  • publish_execute(ledger_path, repo_path, token, backoff_config, crate_seq_version, snapshot_store) -> Result<PublishReport, Error>

Per-version flow

  1. Pre-flight token check — runs once before any version is processed. Fails fast with actionable instructions if a configured token source breaks.
  2. Idempotency check — queries crates.io. If the version is already published, marks it in the ledger and skips.
  3. Source resolution — resolves the temp directory from git tag or snapshot
  4. Validation — path dependency check + cargo check (hard failures here)
  5. Version rewrite — sets exact version in Cargo.toml
  6. Packagecargo package --allow-dirty
  7. Publish — with exponential backoff (base 1s, cap 60s, 5 retries)
  8. Ledger updatemark_published() + save immediately after each version

Crash safety

The ledger is persisted after every individual version — if the process crashes mid-run, the ledger reflects exact progress. On restart, already-published versions are detected via the idempotency check and skipped.

Error handling

  • On first failure: saves current ledger state, includes partial results in the report, and stops processing further versions
  • Error::TokenResolution — pre-flight token failure
  • Error::Registrycrates.io HTTP failure
  • Error::Subprocesscargo spawn failure

pipeline/workspace_publish.rs — workspace-ordered publish

Orchestrates multi-crate publishing in topological dependency order with index propagation waits between tiers.

Public API

  • publish_workspace_ordered(ledger_paths, repo_path, token, backoff, crate_seq_version, snapshot_store) -> Result<Vec<PublishReport>, Error>

Flow

  1. Single-crate fast path — if only one ledger path is given, delegates directly without sorting or index-wait
  2. Discover members — finds all workspace members
  3. Topo-sort — produces tiered groups in dependency order
  4. Publish tier by tier:
    • For each tier, publish all member crates
    • After each tier (except the last), wait for every newly published version to appear in the crates.io index
  5. Collect reports — all per-crate PublishReports aggregated into a single Vec

Why index waits matter

When crate B depends on crate A, cargo publish for B will fail if A's new version hasn't propagated to the crates.io index yet. The tier-based approach with index waits between tiers eliminates this race condition.

Error cases

  • Error::DependencyCycle — circular workspace dependencies
  • All errors from the publish pipeline propagated

Error enum

crate-seq-core defines a comprehensive error type covering all failure modes:

Variant Source Description
Git(crate_seq_git::Error) git operations Tag discovery, checkout failure
Registry(crate_seq_registry::Error) crates.io HTTP failure, rate limiting
Ledger(crate_seq_ledger::Error) ledger I/O Read/write/parse failure
Manifest(crate_seq_manifest::Error) manifest ops Version rewrite, path dep detection
Snapshot(String) snapshot ops Extraction failure
SnapshotNotFound(String) source resolution No tarball matches SHA-256
Io { path, source } filesystem General I/O with path context
Toml { path, source } TOML parsing Invalid manifest with path context
Subprocess(String) process spawn cargo cannot be spawned
CargoCheck { stderr } validation cargo check failed
PathDependencies { manifest, deps } validation Path deps found in manifest
TokenResolution(String) auth Actionable token setup message
DependencyCycle { crate_names } topo sort Circular workspace deps
CrateNotFound(String) init --crate filter names unknown crate
MissingPackageName(PathBuf) workspace Member manifest has no [package].name

Dependency graph

crate-seq-core depends on all five library crates. It is the only crate in the workspace with this property — all other crates are leaf dependencies with no intra-workspace deps (except crate-seq-registry which depends on crate-seq-ledger for BackoffConfig).