Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Semver Bump and Cargo Publish

CI

A GitHub Action that automatically bumps the version of a Rust crate according to semantic versioning rules and publishes it to crates.io.

Quick recap of semver rules:

  • Bump major if you make incompatible API changes.
  • Bump minor if you add functionality in a backwards-compatible manner.
  • Bump patch if you make backwards-compatible bug fixes.

Features

  • 🔄 Automatic semantic version bumping (patch, minor, major)
  • 📦 Publishes to crates.io
  • 🏷️ Creates git tags automatically
  • ✅ Waits for configurable status checks to pass
  • 🧪 Dry run mode for testing
  • 🔒 Registry-confirmed rollback on publish failure, as a force-push or a revert (rollback_mode)
  • 🌿 Branch-aware publishing (only publishes from main by default)
  • 🛠️ Built-in Rust toolchain management
  • 📋 Comprehensive validation and testing

The guarantee

The git tag and the crates.io artifact are created by one job from one commit, so they cannot diverge. A hand-run cargo publish followed by a later tag is how a release ends up pointing at code that never shipped — the published .crate records the commit it was packaged from (.cargo_vcs_info.json), and nothing ties a manually placed tag to it. Here, push runs before publish (a branch that moved fails the run before anything irreversible), and rollback consults the registry before touching history.

Limitations

  • You need to manually create a GitHub release from each tag.
  • No automatic release notes management.
  • Only supports crates.io as the registry.
  • Only supports packages with a single crate in the root directory.
  • Little to no built-in CI checks: this keeps costs down and avoids complexity, but you should have your own CI checks in place.
  • SECURITY: requires you to add a PAT to your repository secrets with write access to the repo.

Usage

Basic Usage

name: Publish

on:
  workflow_dispatch:
    inputs:
      bump_type:
        description: "Version bump type"
        required: true
        default: "patch"
        type: choice
        options:
          - patch
          - minor
          - major
      dry_run:
        description: "Dry run mode"
        required: false
        default: true
        type: boolean

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0

      - name: Publish crate
        uses: tsnl/semver-bump-and-cargo-publish@v1
        with:
          bump_type: ${{ github.event.inputs.bump_type }}
          dry_run: ${{ github.event.inputs.dry_run }}
          cargo_registry_token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          pat_token: ${{ secrets.PAT_TOKEN }}

Advanced Usage with Status Check Dependencies

name: Publish

on:
  workflow_dispatch:
    inputs:
      bump_type:
        description: "Version bump type"
        required: true
        default: "patch"
        type: choice
        options:
          - patch
          - minor
          - major
      dry_run:
        description: "Dry run mode"
        required: false
        default: true
        type: boolean

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0

      - name: Publish crate
        uses: tsnl/semver-bump-and-cargo-publish@v1
        with:
          bump_type: ${{ github.event.inputs.bump_type }}
          dry_run: ${{ github.event.inputs.dry_run }}
          cargo_registry_token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          pat_token: ${{ secrets.PAT_TOKEN }}
          wait_for_checks: "CI / Test, CI / Lint, CI / Check Formatting"
          check_wait_interval: "60"
          check_timeout_count: "20"
          rust_toolchain: "stable"
          git_user_email: "[email protected]"
          git_user_name: "Release Bot"

Inputs

Input Description Required Default
branch Branch to publish from Yes main
bump_type Version bump type (patch, minor, major) Yes patch
dry_run Skip automated commit, push, and publish No true
cargo_registry_token Cargo registry token for crates.io Yes -
pat_token Personal access token for repository access Yes -
rollback_mode How a confirmed-failed publish is undone: force-push (clean history; PAT must be allowed to force-push the branch) or revert (append-only; works on protected branches) No force-push
rust_toolchain Rust toolchain version to use No stable
git_user_email Git user email for commits No [email protected]
git_user_name Git user name for commits No GitHub Actions
wait_for_checks Comma-separated list of status check contexts to wait for No ""
check_wait_interval Time to wait between status check polling (seconds) No 60
check_timeout_count Number of polling attempts before timeout No 20

Outputs

Output Description
package_name Name of the published package
old_version Previous version before bump
new_version New version after bump
tag_name Git tag name created
published Whether the package was published to crates.io

Prerequisites

Required Secrets

You need to set up the following secrets in your repository:

  1. CARGO_REGISTRY_TOKEN: Your crates.io API token

    • Get this from crates.io/me
    • Go to Account Settings → API Tokens → New Token
  2. PAT_TOKEN: GitHub Personal Access Token

    • Go to GitHub Settings → Developer settings → Personal access tokens
    • Create a fine-grained token with:
      • Repository access: only the one repository this action runs in.
      • Repository permissions: Contents: Read and write (Metadata: Read is added automatically). Nothing else.
    • This is what pushes the bump commit and tag back to the repository. With the default rollback_mode: force-push it must also be allowed to force-push the publish branch — see Security and required permissions.

Required Permissions

The action itself needs no elevated GITHUB_TOKEN permissions — every privileged operation (pushing the bump commit and tag, reading check runs) goes through pat_token, so the default token permissions suffice.

Grant contents: write only if a later step in your workflow needs it — for example creating a GitHub Release with the default token, as examples/publish.yml does.

Status Check Integration

The action can wait for specific GitHub status checks to pass before proceeding with the publish. This is useful for ensuring CI tests pass before publishing.

Checks are awaited on the checked-out tip of branch — the commit that will actually be bumped and published — not on the commit that triggered the workflow. The two differ when the workflow is dispatched or push-triggered from another ref. If a named check never registers on that commit and no workflow is running that could still produce it, the action fails after three consecutive not-found polls instead of waiting out the full timeout.

Finding Status Check Names

To find the correct status check names for the wait_for_checks input:

  1. Go to a recent commit in your repository.
  2. Click on the status checks (✅ or ❌ icon).
  3. The names shown are what you should use in wait_for_checks

For GitHub Actions workflows, the format is typically:

  • {workflow_name} / {job_name}
  • Example: test, lint, Check Formatting, etc.

Example with Common CI Checks

wait_for_checks: "test, lint, Check Formatting"

Workflow Logic

  1. Validation: Validates inputs and required tokens
  2. Status Checks: Waits for specified status checks to pass (if configured)
  3. Setup: Installs Rust toolchain and required tools
  4. Version Bump: Bumps version in Cargo.toml and updates Cargo.lock
  5. Build: Builds the crate (cargo build --release). No tests, clippy, or formatting run here — gate the publish on your own CI via wait_for_checks
  6. Git Operations: Commits changes and creates git tag
  7. Dry Run Publish: Tests the publish process
  8. Push: Pushes changes to repository (if not dry run)
  9. Publish: Publishes to crates.io (if not dry run and on main branch)
  10. Rollback: On publish failure, first confirms with crates.io that the version really is absent, then rolls back. A publish that failed only on the response (version actually live) keeps its commit and tag; an unreachable registry keeps them too and fails with instructions, since a stale tag is recoverable and a rolled-back live version is not

Branch Behavior

  • Main branch: Full publish to crates.io
  • Other branches: Version bump and tag creation only (no crates.io publish)
  • Dry run: No commits, pushes, or publishes (testing only)

Failure modes

Every failure path is designed so the remote (git origin, crates.io) is the truth, and the unrecoverable direction is never chosen on a guess:

Failure What happens
Required check red or missing Aborts before any bump — nothing to undo
Tag already exists on origin Aborts before any bump (the remote is asked, not the local clone)
Origin unreachable for the tag check Aborts — refuses to release blind
cargo publish --dry-run fails Aborts with the bump committed locally only — nothing pushed
Branch moved between checkout and push Push fails non-fast-forward, run dies before publish
Publish fails, crates.io confirms version absent (404) Rollback per rollback_mode: tag deleted, bump undone
Publish fails, but the version is live on crates.io Commit and tag are kept; published output is true (cargo's exit code was wrong, the release happened)
Publish fails, crates.io unreachable Commit and tag are kept; run fails with instructions. A stale tag is recoverable (cargo publish again from it); rolling back a live version is not
Force-push rollback rejected Run fails telling you whether to grant force-push or switch to rollback_mode: revert

Security and required permissions

  • pat_token needs contents: write on the repository (pushing the bump commit and tag). Use a fine-grained PAT scoped to the one repository.
    • With rollback_mode: force-push (the default), the PAT must additionally be allowed to force-push the publish branch — i.e. the branch is unprotected for it, or it has a branch-protection bypass.
    • With rollback_mode: revert, no force-push permission is needed; rollback is an ordinary push.
  • cargo_registry_token should be a crates.io token scoped to publish-update for the one crate, not a global token.
  • Third-party actions used internally are pinned by commit SHA.

Examples

Simple Monthly Release

name: Monthly Release
on:
  schedule:
    - cron: '0 0 1 * *'  # First day of every month
  workflow_dispatch:

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0
      - uses: tsnl/semver-bump-and-cargo-publish@v1
        with:
          bump_type: "minor"
          dry_run: "false"
          cargo_registry_token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          pat_token: ${{ secrets.PAT_TOKEN }}

Release with Full CI Integration

name: Release
on:
  workflow_dispatch:
    inputs:
      version:
        type: choice
        options: [patch, minor, major]
        default: patch

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0
      - uses: tsnl/semver-bump-and-cargo-publish@v1
        with:
          bump_type: ${{ github.event.inputs.version }}
          dry_run: "false"
          cargo_registry_token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          pat_token: ${{ secrets.PAT_TOKEN }}
          wait_for_checks: "CI / test, CI / lint, CI / check-formatting"
          rust_toolchain: "stable"

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Troubleshooting

Status Check Issues

If you're having trouble with the wait_for_checks feature:

  1. Check the exact names: Go to a recent commit in your repository, click on the status checks, and copy the exact names shown.

  2. Format for GitHub Actions: Use the format workflow_name / job_name exactly as shown in the GitHub UI.

    • Example: CI / Test, CI / Lint, CI / Check Formatting
  3. Case sensitivity: Names are case-sensitive and must match exactly.

  4. External CI systems: For non-GitHub Actions (Travis CI, CircleCI, etc.), use the context name as shown in the status API.

  5. Debugging: If a check isn't found, the action will show helpful error messages and suggestions.

Common Issues

  • "Status check not found": Verify the exact name format and spelling
  • "Timeout waiting for status check": Check if the workflow is actually running or if it failed
  • "Invalid bump_type": Must be one of: patch, minor, major
  • Token issues: Ensure both CARGO_REGISTRY_TOKEN and PAT_TOKEN secrets are set

Getting Help

If you encounter issues:

  1. Check the action logs for detailed error messages
  2. Verify your repository secrets are configured correctly
  3. Test with dry_run: true first
  4. Open an issue with the full error log and your workflow configuration

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

GitHub action to automatically bump a Rust package's version and run `cargo publish`.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages