Puzzled about how people publish multi-crate workspaces

It seems my needs should be quite common: I have 2 libraries/crates in a single repository/workspace. CrateA depends on CrateB. In CrateA's Cargo.toml, I have

version = "1.0.0"
...
[dependencies]
crate_b = { path = "../crate_b", version="1.0", registry="my_registry" }

Code changes are landing in the repository (after going through integration tests, code reviews, etc...), but without publishing anything.

I would like to regularly run an automated "publish" job that would do the following:

  • if only things in crate_a changed (since last published versions of the crates), bump crate_a version, and publish crate_a
  • if things changed both in crate_a and crate_b, bump both crates versions, and also the version in crate_a's dependency to crate_b to match, and publish the 2 crates (it doesn't really have to be atomic)
  • if things changed only in crate_b, and are minor, bump and publish crate_b
  • if things changed only in crate_b, but are major, bump and publish crate_b, but then also propagate to change/publish crate_a

Basically, just "publish new versions if there is new stuff, but don't publish a new version if nothing changed". And I am treating a major dependency update as a major change.

I'm struggling to find a reasonable way to achieve this. So far, my best solution would be:

  • run cargo semver-checks once, and parse the output to determine what first wave of version bumps have to happen
  • run cargo release version <level> -p <crate> to perform these required bumps
  • implement myself something that propagates major changes (my last case from above)
  • run cargo release again to perform the publication, tagging, pushing, etc
1 Like

This is the exact use case cargo workspaces publish was designed for. I already use it in a couple repos at work, as well as for some personal projects.

For the version bumping and changelog side of things I follow a Release Please workflow.

Basically, I took the release workflow from @fasterthanlime's amazing "My Ideal Rust Workflow" article and adapted it for my use case.

If it helps, here is the GitHub Actions workflow I use for releases:

.github/workflows/release-please.yml
name: Release Please

on:
  push:
    branches:
      - main
    tags: "*"
  repository_dispatch:

env:
  RUST_BACKTRACE: 1

jobs:
  release:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Install release-please
        run: npm install --global release-please@15.11
      - name: Update the Release PR
        run: |
          release-please release-pr \
            --debug \
            --token=${{ secrets.RELEASE_PLEASE_GH_TOKEN }} \
            --repo-url=${{ github.repositoryUrl }} \
            --config-file=.github/release-please/config.json \
            --manifest-file=.github/release-please/manifest.json
      - name: Publish the GitHub Release
        run: |
          release-please github-release \
            --debug \
            --token=${{ secrets.RELEASE_PLEASE_GH_TOKEN }} \
            --repo-url=${{ github.repositoryUrl }} \
            --config-file=.github/release-please/config.json \
            --manifest-file=.github/release-please/manifest.json

  publish-to-crates-io:
    name: Publish to crates.io (if necessary)
    runs-on: ubuntu-latest
    needs:
      - release
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v2
      - name: Setup Rust
        uses: dsherret/rust-toolchain-file@v1
      - name: Rust Cache
        uses: Swatinem/rust-cache@v2
      - name: Install cargo-workspaces
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-workspaces
      - name: Publish
        run: cargo workspaces publish --from-git --token "${{ secrets.CRATES_IO_TOKEN }}" --yes

  upload-artifacts:
    name: Upload Release Artifacts (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    if: startsWith(github.ref, 'refs/tags/')
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - windows-latest
          - macos-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Rust
        uses: dsherret/rust-toolchain-file@v1
      - name: Rust Cache
        uses: Swatinem/rust-cache@v2
      - name: Generate Release Artifacts
        run: cargo xtask dist
      - name: Upload Release Artifacts
        run:
          gh release upload ${TAG_NAME#refs/tags/} ./target/pirita.*.zip
        # Note: The ${VAR#REPLACE} syntax is bash-specific
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_GH_TOKEN }}
          TAG_NAME: ${{ github.ref }}
7 Likes

Thanks for the suggestion.

I don't wish to introduce a new drastic syntax and restrictions on commit messages, so it looks like "release please" wouldn't work.
At this point I'd probably be fine with something that just goes: "anything changed in the crate, let's bump the major version".

But also on the "cargo workspaces" side, I'm a bit confused by the "independant" option: I just want to avoid bumping versions "just because the global version changed", but I still want a bump when dependencies got bumped. Even in the simplest crate_a->crate_b case, I'm not sure which should be "independant".

It seems that using these would also mean a lot of glue code and combining 2 different plugins.