ys memos
Blog

Rustのcrateを公開するリポジトリ向けのGitHub workflow


rust

2025/07/15


Rustのcrateを公開する際に、利便性と柔軟性のバランスを取ったちょうどいいワークフローの設定をしてみたので、その内容と観点を記事に記す。

前提として、個人用OSSのリポジトリを想定し、気軽にpublishできるようにすることを目指す。



プロジェクトの全体像はこのような構造

$ tree . -L 1
.
├── Cargo.lock
├── Cargo.toml
├── example
├── LICENSE
├── README.md
├── src
└── target

$ tree .github/
.github/
└── workflows
    ├── rust-cicd.yml                   # CI/CDのメインワークフロー
    ├── rust-ci-pr.yml                  # PR時のCI
    └── rust-fmt-lint-build-test.yml    # 共通処理で、Rustのフォーマット、Lint、ビルド、テスト

ciが完了したとき、Cargo.tomlversion情報に基づいて、GitHubのリリースを作成し、crateをpublishする。

ここで、サンプルとして localtraceと入れているが、ここは適宜パッケージ名に沿わせて更新する。

name: Rust CI

on:
  push:
    branches: ['main']

env:
  CARGO_TERM_COLOR: always

jobs:
  ci:
    uses: ./.github/workflows/rust-fmt-lint-build-test.yml

  cd:
    runs-on: ubuntu-latest
    needs: ci
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v3

      - name: Extract crate metadata
        id: metadata
        run: |
          PACKAGE="localtrace"
          VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r --arg name "localtrace" '.packages[] | select(.name == $name) | .version')
          echo "version=$VERSION" >> $GITHUB_OUTPUT
      - name: Check tag
        id: tag
        run: |
          TAG="v${{ steps.metadata.outputs.version }}"
          echo "name=${TAG}" >> $GITHUB_OUTPUT
          if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$" >/dev/null 2>&1; then
            echo "Tag ${TAG} already exists, skipping push."
            echo "exists=true" >> $GITHUB_OUTPUT
          else
            echo "Tag ${TAG} does not exist, will create and push."
            echo "exists=false" >> $GITHUB_OUTPUT
          fi
      - name: Push tag
        if: steps.tag.outputs.exists == 'false'
        run: |
          TAG="${{ steps.tag.outputs.name }}"
          echo "Pushing tag ${TAG}"
          git tag "${TAG}"
          git push origin "${TAG}"
      - name: Publish
        if: steps.tag.outputs.exists == 'false'
        run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}

CIが完了したとき、Cargo.tomlversion情報に基づいて、そのtagが存在するかをチェックし、存在する場合はそのバージョンがすでに公開されているものとしてpublish処理をスキップする。存在しない場合は、tagを作成してpublishを行う。誤ってpublishしようとしても

$ cargo publish
    Updating crates.io index
error: crate localtrace@0.1.8 already exists on crates.io index

と、エラーを返してくれるので、誤ってtagを削除してしまった場合でも、tag作成後のpublishは期待通りに失敗してくれるし、以降はtagが存在するため正常処理になる。

main向けのPRで、汎用的な部分(fmt,lint,build,test)のチェックを行う。

name: Rust CI

on:
  pull_request:
    branches: ['main']

env:
  CARGO_TERM_COLOR: always

jobs:
  ci:
    uses: ./.github/workflows/rust-fmt-lint-build-test.yml

Rust向けの汎用的なCI。fmt, lint, build, testを行う。 他ワークフローから呼び出せるようにしておく。

name: Rust fmt, lint, build and test

on:
  workflow_call:

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Cache cargo dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}

      - name: Check Formatting
        run: cargo fmt -- --check

      - name: Check Linting
        run: cargo clippy --all-targets --all-features -- -D warnings

      - name: Build
        run: cargo build --verbose

      - name: Test
        run: cargo test --verbose


このワークフローの最大のメリットは、Cargo.tomlversionフィールドを更新するだけで、自動的にGitHubリリースとcrates.ioへのpublishが実行される点である。

個人開発におけるOSS公開では、手動でのリリース作業が煩雑になりがちな一方、常に自動デプロイされるよりも、明示的にリリースを行いたい場合が多いので、このようにマニフェスト更新による自動トリガーを実現した。

[package]
name = "localtrace"
version = "0.1.0"  # ここを更新するだけ
edition = "2021"

Check tagステップでは、既に同じバージョンのタグが存在するかチェックし、重複したリリースを効率的にskipする。

このチェック方式は、もうちょっと凝ってもいいかもしれない。

if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$" >/dev/null 2>&1; then
echo "Tag ${TAG} already exists, skipping push."
echo "exists=true" >> $GITHUB_OUTPUT

PRではrust-ci-pr.ymlでテストまでみ実行し、mainブランチにマージされた際にrust-cicd.ymlでデプロイまで行う設計にしてある。これにより、開発フローが明確になり、権限的にもクリーンになる。


rust-fmt-lint-build-test.ymlworkflow_callイベントを使用して、他のワークフローから呼び出せるように設計されている。

これにより、コードの重複を避け、CIのステップを変更するときのメンテナンス性を向上させている。


このワークフローを使用するために、以下の設定が必要となる。


  1. CARGO_TOKEN: crates.ioのAPIトークン

現在のワークフローではPACKAGE="localtrace"となっているため、自分のパッケージ名に変更する必要がある。

PACKAGE="your-package-name"  # ここを変更
VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r --arg name "your-package-name" '.packages[] | select(.name == $name) | .version')

  1. プロジェクトをセットアップ
  2. 上記のワークフローファイルを.github/workflows/に配置
  3. GitHub SecretsにCARGO_TOKENを設定
  4. パッケージ名を自分のプロジェクトに合わせて変更
  5. Cargo.tomlのバージョンを更新してmainブランチにpush

これだけで、自動的にテスト、タグ作成、リリース、crates.ioへのpublishが実行される。


このワークフローにより、Rustのcrateの開発からリリースまでの一連の流れを自動化できます。個人プロジェクトやOSSの開発において、手動での作業を最小限に抑え、継続的なデプロイを実現した。


関連タグを探す