Skip to content

fix(cli): distinguish config-load errors and reject symlinked entries… #822

fix(cli): distinguish config-load errors and reject symlinked entries…

fix(cli): distinguish config-load errors and reject symlinked entries… #822

Workflow file for this run

name: CI
on:
push:
branches: [master, main, develop]
pull_request:
branches: [master, main, develop]
schedule:
# Weekly security audit on Sundays at midnight
- cron: "0 0 * * 0"
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
# Don't use RUSTFLAGS=-D warnings, use workspace lints configuration instead
RUSTUP_MAX_RETRIES: 10
# Cancel previous runs on new push
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Detect whether the diff touches anything beyond documentation so the full
# check chain can be skipped on docs-only changes. Workflow file changes
# always force the full chain to run, since they could be the thing that's
# broken. Scheduled runs (weekly security audit) always run everything too,
# since there's no meaningful diff to compare against.
changes:
name: Detect Changes
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
run-checks: ${{ github.event_name == 'schedule' || steps.filter.outputs.code == 'true' || steps.filter.outputs.workflows == 'true' }}
steps:
- uses: actions/checkout@v7
- name: Check changed paths
if: github.event_name != 'schedule'
uses: dorny/paths-filter@v4
id: filter
with:
filters: |
code:
- '**'
- '!**/*.md'
- '!specs/**'
- '!docs/**'
- '!README*'
- '!CHANGELOG*'
- '!CONTRIBUTING*'
- '!LICENSE*'
- '!SECURITY*'
workflows:
- '.github/workflows/**'
# Quick checks first - fail fast strategy
check:
name: Check
needs: [changes]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
if: always() && needs.changes.outputs.run-checks == 'true'
steps:
- uses: actions/checkout@v7
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Rust nightly (for rustfmt)
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "check"
save-if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
- name: Check formatting (nightly)
run: cargo +nightly fmt --all -- --check
- name: Install cargo-sort
uses: taiki-e/install-action@v2.85.10
with:
tool: cargo-sort
- name: Check Cargo.toml dependency order
run: cargo sort --check --workspace --order package,lints,workspace,lib,bin,features,dependencies,build-dependencies,dev-dependencies
- name: Clippy (all targets, all features)
run: cargo +stable clippy --all-targets --all-features --workspace
- name: Check documentation
run: cargo +stable doc --no-deps --all-features --workspace
env:
RUSTDOCFLAGS: "-D warnings"
# Security audit using cargo-deny
security:
name: Security Audit
needs: [changes]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
if: always() && needs.changes.outputs.run-checks == 'true'
steps:
- uses: actions/checkout@v7
- name: Run cargo-deny (advisories, licenses, bans, sources)
uses: EmbarkStudios/cargo-deny-action@v2
with:
log-level: warn
command: check
arguments: --all-features
command-arguments: advisories licenses bans sources
# Cross-platform build matrix, run in parallel with `test` (not sequentially
# before it): nextest builds whatever it needs on its own, so this job only
# verifies that all targets/features compile across the full OS/toolchain
# matrix without blocking the test job.
build:
name: Build (${{ matrix.os }}, Rust ${{ matrix.rust }})
needs: [changes, check]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
permissions:
contents: read
if: needs.changes.outputs.run-checks == 'true' && needs.check.result == 'success'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
include:
# Test on beta channel on Linux only
- os: ubuntu-latest
rust: beta
steps:
- uses: actions/checkout@v7
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "build-${{ matrix.os }}-${{ matrix.rust }}"
save-if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
- name: Build (all targets, all features)
run: cargo build --all-targets --all-features --workspace
# Cross-platform tests with matrix
test:
name: Test (${{ matrix.os }}, Rust ${{ matrix.rust }})
needs: [changes, check]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
permissions:
contents: read
if: needs.changes.outputs.run-checks == 'true' && needs.check.result == 'success'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
include:
# Test on beta channel on Linux only
- os: ubuntu-latest
rust: beta
steps:
- uses: actions/checkout@v7
- name: Install Rust ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "test-${{ matrix.os }}-${{ matrix.rust }}"
save-if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@v2.85.10
with:
tool: nextest
# mcp-execution-codegen's test suite compiles and executes generated TypeScript under
# `tsc`/`node` (e.g. the #201 runtime-bridge regression tests) to catch TypeScript-level
# regressions that a Rust-only check can't see. Those tests hard-fail (rather than
# silently skip) when the `CI` env var is set, so this toolchain must be present here.
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: "22"
- name: Install TypeScript
run: npm install -g typescript
- name: Run tests (nextest)
run: cargo nextest run --all-features --workspace --no-fail-fast
- name: Run doctests
run: cargo test --doc --all-features --workspace
# Code coverage (Linux only for speed)
coverage:
name: Code Coverage
needs: [changes, check]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: write # For uploading artifacts
if: needs.changes.outputs.run-checks == 'true' && needs.check.result == 'success'
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install llvm-cov
uses: taiki-e/install-action@v2.85.10
with:
tool: cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@v2.85.10
with:
tool: nextest
# See the `test` job for why this toolchain is required: mcp-execution-codegen's
# TypeScript-executing regression tests hard-fail under `CI` rather than skip.
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: "22"
- name: Install TypeScript
run: npm install -g typescript
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
- name: Generate coverage (nextest)
run: |
cargo llvm-cov --all-features --workspace --lcov \
--output-path lcov.info nextest
- name: Upload to codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false
verbose: true
flags: mcp-core,mcp-introspector,mcp-codegen,mcp-files,mcp-skill,mcp-server,mcp-cli
name: codecov-umbrella
- name: Archive coverage report
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: lcov.info
retention-days: 30
# MSRV check (Minimum Supported Rust Version)
msrv:
name: Check MSRV (1.91)
needs: [changes, check]
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
if: needs.changes.outputs.run-checks == 'true' && needs.check.result == 'success'
steps:
- uses: actions/checkout@v7
- name: Install Rust 1.91
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91"
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "msrv"
- name: Check with MSRV
run: cargo check --all-features --workspace
# Benchmarks (build only, don't run in CI)
benchmark:
name: Benchmark Build
needs: [changes, check]
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
if: needs.changes.outputs.run-checks == 'true' && needs.check.result == 'success'
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "bench"
- name: Build benchmarks (no run)
run: cargo bench --no-run --profile bench-fast --workspace
# Release build check (master only — skip on PRs and other branches)
release:
name: Release Build
needs: [changes, test]
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
if: github.ref == 'refs/heads/master' && needs.changes.outputs.run-checks == 'true' && needs.test.result == 'success'
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: "release"
- name: Build release
run: cargo build --release --all-features --workspace
- name: Check binary sizes
run: |
echo "Binary sizes:"
ls -lh target/release/ | grep -E "(mcp-execution-cli|mcp-examples)" || true
echo ""
echo "Target directory size:"
du -sh target/release/
# All checks passed
ci-success:
name: CI Success
needs: [changes, check, security, build, test, coverage, msrv, benchmark]
runs-on: ubuntu-latest
if: always()
permissions:
contents: read
steps:
- name: Check all jobs
# `skipped` is an acceptable outcome here: on docs-only diffs, the
# gated jobs (check/security/build/test/coverage/msrv/benchmark) skip
# themselves via the `changes` job's `run-checks` output rather than
# failing, and this status check must still report success/neutral
# rather than staying red or permanently pending.
run: |
for result in \
"${{ needs.changes.result }}" \
"${{ needs.check.result }}" \
"${{ needs.security.result }}" \
"${{ needs.build.result }}" \
"${{ needs.test.result }}" \
"${{ needs.coverage.result }}" \
"${{ needs.msrv.result }}" \
"${{ needs.benchmark.result }}"; do
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
echo "One or more required jobs failed or were cancelled"
exit 1
fi
done
echo "All required jobs passed successfully (or were skipped as docs-only)!"