Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b62a1d7
ci: restructure CI pipeline with change detection and sccache
bug-ops Apr 29, 2026
596ffa6
ci: overhaul workflow with detect-changes, sccache, MSRV, and doc-tests
bug-ops Apr 29, 2026
b0eef6d
ci: add binary build job and separate coverage from tests
bug-ops Apr 29, 2026
ee6c2ef
ci: run build in parallel with linters and share binary via artifact
bug-ops Apr 29, 2026
f5bf0ad
ci: replace build artifact with nextest archive for test reuse
bug-ops Apr 29, 2026
4457612
ci: add debug binary build and split tests into unit, integration, e2…
bug-ops Apr 29, 2026
f0c21d0
ci: restrict sccache to build jobs, use cargo cache only elsewhere
bug-ops Apr 29, 2026
1496e0b
ci: fix unit test scope, e2e filter, and build job permissions
bug-ops Apr 29, 2026
e92536b
ci: fix nextest archive run flags and include test targets in archive
bug-ops Apr 29, 2026
f609117
ci: make test-e2e non-blocking, requires LSP servers unavailable in CI
bug-ops Apr 29, 2026
13cfb92
ci: install rust-analyzer for e2e tests and restore as blocking job
bug-ops Apr 29, 2026
491b52f
fix(core): allow server to start with no LSP servers configured
bug-ops Apr 29, 2026
b0c2cea
fix(core): allow server to start with no LSP servers configured
bug-ops Apr 29, 2026
4673f79
fix(core): invert if-not-else to satisfy clippy::if_not_else
bug-ops Apr 29, 2026
ba45589
ci: add OS matrix to build and test jobs
bug-ops Apr 29, 2026
825eac7
test(e2e): update tool count from 8 to 16 and enumerate all tool names
bug-ops Apr 29, 2026
350c610
style: apply rustfmt to e2e protocol tests
bug-ops Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 272 additions & 55 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,109 +4,308 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

permissions: {}

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RUSTFLAGS: "-D warnings"

permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Check
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 2
outputs:
run-full-ci: ${{ steps.classify.outputs.run-full-ci }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # dtolnay/rust-toolchain stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # Swatinem/rust-cache v2
- run: cargo check --all-targets --all-features
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Filter changed paths
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
code:
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
workflows:
- '.github/workflows/**'
- '.cargo/**'
- name: Classify changes
id: classify
run: |
CODE="${{ steps.filter.outputs.code }}"
WORKFLOWS="${{ steps.filter.outputs.workflows }}"

if [[ "$CODE" == "true" || "$WORKFLOWS" == "true" ]]; then
echo "run-full-ci=true" >> "$GITHUB_OUTPUT"
else
echo "run-full-ci=false" >> "$GITHUB_OUTPUT"
fi

fmt:
name: Format
name: Lint (fmt)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- run: rustup toolchain install nightly --component rustfmt --allow-downgrade
- run: cargo +nightly fmt --all -- --check
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly
with:
toolchain: nightly
components: rustfmt
- name: Check formatting
run: cargo +nightly fmt --all -- --check

clippy:
name: Clippy
name: Lint (clippy)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # dtolnay/rust-toolchain stable
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # Swatinem/rust-cache v2
- run: cargo clippy --all-targets --all-features -- -D warnings
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: "ci"
- name: Clippy
run: cargo clippy --all-targets --all-features --workspace -- -D warnings

build-tests:
name: Build Tests (${{ matrix.os }})
needs: [detect-changes]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
permissions:
contents: read
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-targets: "false"
shared-key: "ci"
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # nextest
- name: Build and archive tests
run: cargo nextest archive --workspace --all-features --lib --bins --tests --archive-file nextest-archive.tar.zst
- name: Upload test archive
uses: actions/upload-artifact@v4
with:
name: nextest-archive-${{ matrix.os }}
path: nextest-archive.tar.zst
retention-days: 1

build:
name: Build Binary (${{ matrix.os }})
needs: [detect-changes]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
permissions:
contents: read
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-targets: "false"
shared-key: "build"
- uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
- name: Build binary
run: cargo build --bin mcpls
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: mcpls-binary-${{ matrix.os }}
path: target/debug/mcpls${{ matrix.os == 'windows-latest' && '.exe' || '' }}
retention-days: 1

test:
name: Test
name: Test (unit) (${{ matrix.os }})
needs: [detect-changes, fmt, clippy, build-tests]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # nextest
- name: Download test archive
uses: actions/download-artifact@v4
with:
name: nextest-archive-${{ matrix.os }}
- name: Run unit tests
run: cargo nextest run --archive-file nextest-archive.tar.zst --workspace-remap . -E 'kind(lib) or kind(bin)'

test-integration:
name: Test (integration) (${{ matrix.os }})
needs: [detect-changes, fmt, clippy, build-tests]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
include:
- os: ubuntu-latest
rust: beta
- os: ubuntu-latest
rust: "1.88" # MSRV
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # dtolnay/rust-toolchain master
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # nextest
- name: Download test archive
uses: actions/download-artifact@v4
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # Swatinem/rust-cache v2
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # taiki-e/install-action nextest
- run: cargo nextest run --all-features
name: nextest-archive-${{ matrix.os }}
- name: Run integration tests
run: cargo nextest run --archive-file nextest-archive.tar.zst --workspace-remap . -E 'kind(test) and not test(e2e)'

test-e2e:
name: Test (e2e) (${{ matrix.os }})
needs: [detect-changes, fmt, clippy, build-tests, build]
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # nextest
- name: Download test archive
uses: actions/download-artifact@v4
with:
name: nextest-archive-${{ matrix.os }}
- name: Download binary
uses: actions/download-artifact@v4
with:
name: mcpls-binary-${{ matrix.os }}
path: target/debug/
- name: Make binary executable
if: matrix.os != 'windows-latest'
run: chmod +x target/debug/mcpls
- name: Run e2e tests
run: cargo nextest run --archive-file nextest-archive.tar.zst --workspace-remap . --run-ignored ignored-only -E 'kind(test) and test(e2e)'

msrv:
name: MSRV check (1.88)
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.88"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: "msrv"
- name: cargo check (MSRV)
run: cargo check --workspace --all-targets --all-features --locked

docs:
name: Documentation
name: Rustdoc
needs: detect-changes
if: needs.detect-changes.outputs.run-full-ci == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
env:
RUSTDOCFLAGS: "-D warnings"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # dtolnay/rust-toolchain stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # Swatinem/rust-cache v2
- run: cargo doc --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: "ci"
- name: Build docs
run: cargo doc --no-deps --all-features
- name: Doc-tests
run: cargo test --doc --workspace --all-features

security:
name: Security Audit
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # EmbarkStudios/cargo-deny-action v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2

coverage:
name: Coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.run-full-ci == 'true'
needs: [detect-changes, fmt, clippy]
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
# RUSTFLAGS reset: -C instrument-coverage conflicts with -D warnings from the
# global env; linting is enforced by the dedicated clippy job.
env:
RUSTFLAGS: ""
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # dtolnay/rust-toolchain stable
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # Swatinem/rust-cache v2
- uses: taiki-e/install-action@caf4aedf2bfe5bfb679703b29290921f4711b2f3 # taiki-e/install-action cargo-llvm-cov
- run: cargo llvm-cov --all-features --lcov --output-path lcov.info
- uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # codecov/codecov-action v6
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: "coverage"
- uses: taiki-e/install-action@caf4aedf2bfe5bfb679703b29290921f4711b2f3 # cargo-llvm-cov
- uses: taiki-e/install-action@dee540ee3f3ff5c6a0665fed9996875d0ba04ca2 # nextest
- name: Generate coverage
run: cargo llvm-cov nextest --workspace --all-features --lib --bins --lcov --output-path lcov.info
- name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
Expand All @@ -115,11 +314,29 @@ jobs:
ci-gate:
name: CI Gate
if: always()
needs: [check, fmt, clippy, test, docs, security, coverage]
needs: [fmt, clippy, build-tests, build, test, test-integration, test-e2e, msrv, docs, security, coverage]
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "::error::One or more CI jobs failed or were cancelled"
exit 1
fi
- name: Check all jobs
run: |
results=(
"${{ needs.fmt.result }}"
"${{ needs.clippy.result }}"
"${{ needs.build-tests.result }}"
"${{ needs.build.result }}"
"${{ needs.test.result }}"
"${{ needs.test-integration.result }}"
"${{ needs.test-e2e.result }}"
"${{ needs.msrv.result }}"
"${{ needs.docs.result }}"
"${{ needs.security.result }}"
"${{ needs.coverage.result }}"
)
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "::error::One or more CI jobs failed or were cancelled"
exit 1
fi
done
echo "All jobs passed"
Loading
Loading