ワンクリックで
rust-multi-crate-publishing-to-crates-io
// Automated workflow patterns for publishing Rust crates to crates.io from GitHub Actions, including version injection, token auth, and multi-crate dependency ordering.
// Automated workflow patterns for publishing Rust crates to crates.io from GitHub Actions, including version injection, token auth, and multi-crate dependency ordering.
{what this skill teaches agents}
Structure public Rust crate docs so docs.rs reads as both a quick-start guide and an API reference
Refactor Rust crates toward httprunner-style bounded-context directories with thin mod.rs facades.
Validate internal Rust module splits without accidentally treating them like feature work.
Update documentation after a source-tree move without breaking repo-root command guidance
Audit repo-wide source-tree relocations while preserving root entrypoints and catching hidden path-bearing surfaces.
| name | Rust Multi-Crate Publishing to crates.io |
| description | Automated workflow patterns for publishing Rust crates to crates.io from GitHub Actions, including version injection, token auth, and multi-crate dependency ordering. |
| domain | release-automation, crates.io, cargo-publish |
| confidence | high |
| source | observed from httprunner and azdocli reference implementations |
| tools | [{"name":"github-mcp-server","description":"GitHub Actions workflow inspection and file fetching","when":"Analyzing release workflows in reference repos"}] |
Rust projects published to crates.io require automation to handle version updates, multi-platform builds, and credential management. This skill captures proven patterns from two reference implementations (httprunner and azdocli) that apply to multi-crate workspaces like httpgenerator.
Pattern: Use a single VERSION env var in the GitHub Actions workflow, injected to Cargo.toml files before publishing.
Implementation:
env:
VERSION: 0.9.${{ github.run_number }} # or hardcoded, or computed from git tags
jobs:
publish-crates:
steps:
- name: Update Version
shell: pwsh
run: |
$toml = (Get-Content -Path Cargo.toml -Raw) -replace 'version = "0.1.0"', 'version = "${{ env.VERSION }}"'
$toml | Set-Content -Path Cargo.toml
Why this works:
For multi-crate workspaces: Apply sed/replace to each crate's Cargo.toml individually:
$toml = (Get-Content -Path src/rust/httpgenerator-core/Cargo.toml -Raw) -replace 'version = "0.1.0"', 'version = "${{ env.VERSION }}"'
$toml | Set-Content -Path src/rust/httpgenerator-core/Cargo.toml
Workspace dependency variant: If internal crates are declared once in the root [workspace.dependencies] table with both path and version, replace every version = "0.1.0" occurrence in the root Cargo.toml so the workspace package version and internal dependency pins stay aligned.
Pattern: Store CRATES_TOKEN as a GitHub Organization Secret, pass to cargo publish via --token flag.
Implementation:
- name: Publish to crates.io
run: cargo publish --allow-dirty --token ${{ secrets.CRATES_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} # optional, redundant
Prerequisites:
CRATES_TOKEN in GitHub Organization Secrets (or Repository Secrets if org-level is unavailable)Note: --allow-dirty is required because the workflow modifies Cargo.toml before publish; this is expected and safe in CI/CD.
Pattern: Publish crates sequentially in dependency order (no parallel jobs for the same crate set).
Implementation:
publish-crates:
steps:
- name: Publish Core to crates.io
run: cargo publish --allow-dirty --token ${{ secrets.CRATES_TOKEN }} -p httpgenerator-core
- name: Publish OpenAPI to crates.io (depends on core)
run: cargo publish --allow-dirty --token ${{ secrets.CRATES_TOKEN }} -p httpgenerator-openapi
- name: Publish CLI to crates.io (depends on both)
run: cargo publish --allow-dirty --token ${{ secrets.CRATES_TOKEN }} -p httpgenerator
Why sequential:
cargo publish with explicit -p {crate} targets specific workspace membersPattern: Chain jobs to ensure builds complete before publishing; optionally gate publishing on GitHub Release creation.
Implementation (Sequential)):
jobs:
build:
# ... build binaries, create archives, upload artifacts
release:
needs: build
# ... download artifacts, create GitHub Release with assets
publish-crates:
needs: release # Wait for GitHub Release before publishing to crates.io
# ... run cargo publish jobs
Alternative (Parallel): All publish jobs depend on build, not release (faster, but less coupled to GitHub Release timing).
httpgenerator recommendation: Use sequential (publish-crates depends on release) to ensure GitHub Releases are finalized before crates.io publication.
Pattern: When validating crates.io readiness on a branch that already contains uncommitted release metadata or workflow edits, run publish-focused checks in two stages: first without --allow-dirty to capture the worktree gate, then with --allow-dirty to expose the real packaging result.
Why this works:
Httpgenerator example:
httpgenerator-core passes cargo package --allow-dirty and cargo publish --dry-run --allow-dirtyhttpgenerator-openapi and httpgenerator still fail until httpgenerator-core is available from crates.io for package verificationInterpretation rule:
--allow-dirty is an expected release-workflow condition, not a product regressionPattern: Put crates.io publication behind a workflow_call boolean input so stable release entrypoints can opt in while preview/dry-run callers reuse the same artifact jobs without publishing.
Implementation:
on:
workflow_call:
inputs:
publish-crates:
required: false
type: boolean
default: false
jobs:
publish-crates:
if: ${{ inputs.publish-crates }}
Why this works:
release.yml) opt in explicitly with publish-crates: truePattern: After publishing a dependency crate, poll the crates.io API until the new version is visible before dry-running/publishing downstream crates.
Implementation:
python .github/scripts/check-crates-io-version.py \
--crate httpgenerator-core \
--version "$RELEASE_VERSION" \
--state present \
--retries 30 \
--delay-seconds 10
Why this works:
cargo publish --dry-run only succeeds once the just-published dependency version is visiblePattern: Update per-crate README files with published version, so install instructions always show latest.
Implementation:
- name: Update README with version
run: |
$readme = (Get-Content -Path src/rust/httpgenerator-core/README.md -Raw) -replace 'httpgenerator-core = "0.1.0"', 'httpgenerator-core = "${{ env.VERSION }}"'
$readme | Set-Content -Path src/rust/httpgenerator-core/README.md
When to use: Only if per-crate README files exist and contain version-pinned install instructions. httprunner does this; azdocli does not.
Pattern: Document crates.io, GitHub Releases, and editor extensions as complementary delivery channels instead of treating one as a full replacement for the others.
Implementation guidance:
cargo install <cli-crate> as the Rust-native install path for published releases.PATH or an override setting.Why this works:
httprunner-core + httprunner (CLI)0.9.${{ github.run_number }} versioning (auto-increments)src/core/README.md with versionVERSION: 0.4.1 (manual updates)git describe --tags)publish-crates to false; release.yml opts in for stable publication while preview callers keep artifact-only behaviorcargo install httpgeneratorhttpgenerator-<version>-<platform> archiveshttpgenerator, httpgenerator-core, httpgenerator-openapihttpgenerator-compat❌ Parallel crate publishing: Publishing multiple interdependent crates in parallel jobs without waiting for crates.io index propagation.
❌ Hardcoding version in Cargo.toml: Forgetting to inject version from workflow env var, or skipping per-crate Cargo.toml updates in workspace.
❌ Path-only internal workspace dependencies for publishable crates: Local builds work, but crates.io packages cannot resolve unpublished sibling crates.
{ version = "0.1.0", path = "..." } for publishable sibling crates, then update those version anchors during release automation.❌ Omitting --allow-dirty: Publishing fails with "working directory is not clean" error.
--allow-dirty when publishing from CI/CD after version injection.❌ Missing token secret: Using literal token in workflow file or committing token to git.
CRATES_TOKEN in GitHub Secrets, never in workflows or source code.❌ Publishing before GitHub Release: Publishing to crates.io before creating GitHub Release with binaries.
release job that depends on build, then publish-crates depends on release.❌ Blind fixed sleeps between crates: Sleeping 60–180 seconds and hoping crates.io has indexed the dependency.
❌ Conflating crates.io with extension delivery: Telling users that VS Code or Visual Studio installs the CLI from crates.io.
PATH or explicit configuration.All 3 crates public? Publish httpgenerator (CLI crate name), httpgenerator-core, and httpgenerator-openapi. Keep httpgenerator-compat private with publish = false.
Edition/MSRV pairing? Rust 2024 is valid as of Rust 1.85. Keep Edition 2024 if the repo is already on it, and make the corresponding rust-version = "1.85" promise explicit instead of downgrading based on stale pre-release guidance.
Homepage/docs split? Use the GitHub Pages site (https://christianhelle.github.io/httpgenerator/) as the human-facing homepage and https://docs.rs/<crate> as the canonical per-crate API documentation URL.
Version source? httprunner uses run number (0.9.NNN), azdocli hardcodes. Consider git tags for semantic versioning (e.g., v1.0.0 → 1.0.0).
Per-crate README? httprunner updates src/core/README.md; azdocli does not. Defer unless per-crate install instructions are needed.
Job chaining? Recommend sequential (publish-crates depends on release) to couple crates.io publication with GitHub Release finalization.
edition = "2024" is invalid. That was true before stabilization, but Rust 2024 shipped in Rust 1.85.httpgenerator; keeping httpgenerator-cli only as a folder name is fine.Reference Decision: See .squad/decisions/inbox/hudson-crates-publishing-patterns.md for analysis of httprunner and azdocli implementations.