ci: add cargo-public-api check for breaking API changes #2045
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main, release ] | |
| tags: | |
| - 'release-*' | |
| pull_request: | |
| branches: [ main, release ] | |
| env: | |
| CARGO_TERM_COLOR: always | |
| ARTIFACT_DIR: release-artifacts | |
| jobs: | |
| commit-lint: | |
| name: Lint Commit Messages | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install commitlint | |
| run: | | |
| npm install --save-dev @commitlint/cli @commitlint/config-conventional | |
| echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js | |
| - name: Lint commit messages | |
| run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose | |
| fmt: | |
| name: Code Formatting | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust fmt | |
| run: rustup toolchain install nightly --component rustfmt | |
| - name: Check formatting | |
| run: cargo +nightly fmt --all -- --check | |
| clippy: | |
| name: Lint with Clippy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Run clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| semver: | |
| name: SemVer Check | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cargo-semver-checks | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-semver-checks | |
| - name: Determine semver release type | |
| run: | | |
| if git log --format=%B \ | |
| ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} \ | |
| | grep -Eq '(^[A-Za-z0-9_-]+(\([^)]*\))?!:|^BREAKING[ -]CHANGE:)'; then | |
| echo "SEMVER_RELEASE_TYPE=major" >> "$GITHUB_ENV" | |
| else | |
| echo "SEMVER_RELEASE_TYPE=minor" >> "$GITHUB_ENV" | |
| fi | |
| - name: Check rmcp (default features) | |
| run: | | |
| cargo semver-checks \ | |
| --package rmcp \ | |
| --baseline-rev ${{ github.event.pull_request.base.sha }} \ | |
| --release-type "$SEMVER_RELEASE_TYPE" \ | |
| --only-explicit-features \ | |
| --features default | |
| - name: Check rmcp (all features except local) | |
| run: | | |
| FEATURES=$(cargo metadata --no-deps --format-version 1 \ | |
| | jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[] | |
| | select(startswith("__") | not) | |
| | select(. != "local")] | join(",")') | |
| cargo semver-checks \ | |
| --package rmcp \ | |
| --baseline-rev ${{ github.event.pull_request.base.sha }} \ | |
| --release-type "$SEMVER_RELEASE_TYPE" \ | |
| --only-explicit-features \ | |
| --features "$FEATURES" | |
| public-api: | |
| name: Public API Check | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| # cargo-public-api builds rustdoc JSON, which requires a nightly toolchain | |
| # to be installed (it does not need to be the default; the tool invokes it | |
| # via `cargo +nightly`). | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cargo-public-api | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-public-api | |
| # Mirror the SemVer Check job's release-type detection: a breaking-change | |
| # commit marker (`!:` or `BREAKING CHANGE:`) means a major release (any API | |
| # change is allowed); otherwise a minor release (additions allowed, but | |
| # changed/removed public items are denied). This catches breaking changes | |
| # that cargo-semver-checks cannot yet detect, such as a change to a | |
| # function's return type or a field's type. | |
| # See https://github.com/obi1kenobi/cargo-semver-checks/issues/5 | |
| - name: Determine release type and deny flags | |
| run: | | |
| if git log --format=%B \ | |
| ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} \ | |
| | grep -Eq '(^[A-Za-z0-9_-]+(\([^)]*\))?!:|^BREAKING[ -]CHANGE:)'; then | |
| SEMVER_RELEASE_TYPE=major | |
| else | |
| SEMVER_RELEASE_TYPE=minor | |
| fi | |
| case "$SEMVER_RELEASE_TYPE" in | |
| major) DENY="" ;; | |
| patch) DENY="--deny added --deny changed --deny removed" ;; | |
| *) DENY="--deny changed --deny removed" ;; | |
| esac | |
| echo "SEMVER_RELEASE_TYPE=$SEMVER_RELEASE_TYPE" >> "$GITHUB_ENV" | |
| echo "DENY=$DENY" >> "$GITHUB_ENV" | |
| - name: Check rmcp (default features) | |
| run: | | |
| cargo public-api \ | |
| --package rmcp \ | |
| -ss \ | |
| diff \ | |
| $DENY \ | |
| --force \ | |
| ${{ github.event.pull_request.base.sha }}..${{ github.sha }} | |
| - name: Check rmcp (all features except local) | |
| run: | | |
| FEATURES=$(cargo metadata --no-deps --format-version 1 \ | |
| | jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[] | |
| | select(startswith("__") | not) | |
| | select(. != "local")] | join(",")') | |
| cargo public-api \ | |
| --package rmcp \ | |
| --features "$FEATURES" \ | |
| -ss \ | |
| diff \ | |
| $DENY \ | |
| --force \ | |
| ${{ github.event.pull_request.base.sha }}..${{ github.sha }} | |
| spelling: | |
| name: spell check with typos | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Spell Check Repo | |
| uses: crate-ci/typos@master | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # install nodejs | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Set up Python | |
| run: uv python install | |
| - name: Create venv for python | |
| run: uv venv | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Run tests | |
| run: cargo test --all-features | |
| test-no-local: | |
| name: Run Tests (no local feature) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # install nodejs | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Set up Python | |
| run: uv python install | |
| - name: Create venv for python | |
| run: uv venv | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Run tests without local feature | |
| run: | | |
| FEATURES=$(cargo metadata --no-deps --format-version 1 \ | |
| | jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[] | |
| | select(startswith("__") | not) | |
| | select(. != "local")] | join(",")') | |
| cargo test -p rmcp --features "$FEATURES" | |
| coverage: | |
| name: Code Coverage | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # install nodejs | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Set up Python | |
| run: uv python install | |
| - name: Create venv for python | |
| run: uv venv | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cargo-llvm-cov | |
| run: cargo install cargo-llvm-cov | |
| - name: Install llvm-tools-preview | |
| run: rustup component add llvm-tools-preview | |
| - name: Run tests with coverage | |
| run: cargo llvm-cov --all-features | |
| example-test: | |
| name: Example test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # install nodejs | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Set up Python | |
| run: uv python install | |
| - name: Create venv for python | |
| run: uv venv | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Add target WASI preview 2 | |
| run: | | |
| rustup target add wasm32-wasip2 | |
| - name: Build examples | |
| run: | | |
| for dir in examples/*/ ; do | |
| if [ -f "$dir/Cargo.toml" ]; then | |
| echo "Building $dir" | |
| if [[ "$dir" == *"wasi"* ]]; then | |
| cargo build --manifest-path "$dir/Cargo.toml" --target wasm32-wasip2 | |
| else | |
| cargo build --manifest-path "$dir/Cargo.toml" --all-features --tests | |
| fi | |
| fi | |
| done | |
| - name: Run tests in examples | |
| run: | | |
| # Tests are run for each subdirectory in the example directory. | |
| for dir in examples/*/ ; do | |
| if [ -f "$dir/Cargo.toml" ]; then | |
| if [[ "$dir" != *"wasi"* ]]; then | |
| echo "Testing $dir" | |
| cargo test --manifest-path "$dir/Cargo.toml" --all-features --all-targets | |
| fi | |
| fi | |
| done | |
| security_audit: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install cargo-audit | |
| run: cargo install cargo-audit | |
| - name: Run cargo-audit | |
| run: cargo audit | |
| doc: | |
| name: Generate Documentation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@nightly | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Generate documentation | |
| run: | | |
| cargo +nightly doc --no-deps -p rmcp -p rmcp-macros --all-features | |
| env: | |
| RUSTDOCFLAGS: --cfg docsrs -Dwarnings | |
| release: | |
| name: Release crates | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/release') | |
| needs: [fmt, clippy, test] | |
| steps: | |
| # Since this job has access to the `CRATES_TOKEN`, it's probably a good | |
| # idea to be extra careful about what Actions are being called. The reason | |
| # is that if an attacker gains access to other actions such as | |
| # `Swatinem/rust-cache`, they could use that to steal the `CRATES_TOKEN`. | |
| # This happened recently in the attack on `tj-actions/changed-files`, but | |
| # has happened many times before as well. | |
| - uses: actions/checkout@v7 | |
| - name: Update Rust | |
| run: | | |
| rustup update stable | |
| rustup default stable | |
| - name: Cargo login | |
| run: cargo login ${{ secrets.CRATES_TOKEN }} | |
| - name: Publish macros dry run | |
| run: cargo publish -p rmcp-macros --dry-run | |
| continue-on-error: true | |
| - name: Publish rmcp dry run | |
| run: cargo publish -p rmcp --dry-run | |
| continue-on-error: true | |
| - name: Publish macro | |
| if: ${{ startsWith(github.ref, 'refs/tags/release') }} | |
| continue-on-error: true | |
| run: cargo publish -p rmcp-macros | |
| - name: Publish rmcp | |
| if: ${{ startsWith(github.ref, 'refs/tags/release') }} | |
| continue-on-error: true | |
| run: cargo publish -p rmcp |