2025/07/15
はじめに
Rustのcrateを公開する際に、利便性と柔軟性のバランスを取ったちょうどいいワークフローの設定をしてみたので、その内容と観点を記事に記す。
前提として、個人用OSSのリポジトリを想定し、気軽にpublishできるようにすることを目指す。
構造
プロジェクト全体
プロジェクトの全体像はこのような構造
$ tree . -L 1
.
├── Cargo.lock
├── Cargo.toml
├── example
├── LICENSE
├── README.md
├── src
└── target
.github
ディレクトリ
$ tree .github/
.github/
└── workflows
├── rust-cicd.yml # CI/CDのメインワークフロー
├── rust-ci-pr.yml # PR時のCI
└── rust-fmt-lint-build-test.yml # 共通処理で、Rustのフォーマット、Lint、ビルド、テスト
rust-cicd.yml
ciが完了したとき、Cargo.toml
のversion
情報に基づいて、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.toml
のversion
情報に基づいて、その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が存在するため正常処理になる。
rust-ci-pr.yml
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-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.toml
のversion
フィールドを更新するだけで、自動的にGitHubリリースとcrates.ioへのpublishが実行される点である。
個人開発におけるOSS公開では、手動でのリリース作業が煩雑になりがちな一方、常に自動デプロイされるよりも、明示的にリリースを行いたい場合が多いので、このようにマニフェスト更新による自動トリガーを実現した。
[package]
name = "localtrace"
version = "0.1.0" # ここを更新するだけ
edition = "2021"
重複publish実行の防止
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
CI/CDの分離
PRではrust-ci-pr.yml
でテストまでみ実行し、mainブランチにマージされた際にrust-cicd.yml
でデプロイまで行う設計にしてある。これにより、開発フローが明確になり、権限的にもクリーンになる。
再利用可能なワークフロー
rust-fmt-lint-build-test.yml
はworkflow_call
イベントを使用して、他のワークフローから呼び出せるように設計されている。
これにより、コードの重複を避け、CIのステップを変更するときのメンテナンス性を向上させている。
必要な設定
このワークフローを使用するために、以下の設定が必要となる。
GitHub Secrets
CARGO_TOKEN
: crates.ioのAPIトークン- crates.io/settingstokensでアカウント設定からAPIトークンを生成
- GitHubリポジトリの Settings > Secrets and variables > Actions で設定
パッケージ名の変更
現在のワークフローでは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')
使用方法
- プロジェクトをセットアップ
- 上記のワークフローファイルを
.github/workflows/
に配置 - GitHub SecretsにCARGO_TOKENを設定
- パッケージ名を自分のプロジェクトに合わせて変更
Cargo.toml
のバージョンを更新してmainブランチにpush
これだけで、自動的にテスト、タグ作成、リリース、crates.ioへのpublishが実行される。
おわりに
このワークフローにより、Rustのcrateの開発からリリースまでの一連の流れを自動化できます。個人プロジェクトやOSSの開発において、手動での作業を最小限に抑え、継続的なデプロイを実現した。