name: CI on: push: branches: [main] pull_request: branches: [main] concurrency: group: ci-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true permissions: contents: read env: CARGO_TERM_COLOR: always jobs: changes: name: Detect Change Scope runs-on: blacksmith-2vcpu-ubuntu-2404 outputs: docs_only: ${{ steps.scope.outputs.docs_only }} docs_changed: ${{ steps.scope.outputs.docs_changed }} rust_changed: ${{ steps.scope.outputs.rust_changed }} docs_files: ${{ steps.scope.outputs.docs_files }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Detect docs-only changes id: scope shell: bash run: | set -euo pipefail write_empty_docs_files() { { echo "docs_files<> "$GITHUB_OUTPUT" } if [ "${{ github.event_name }}" = "pull_request" ]; then BASE="${{ github.event.pull_request.base.sha }}" else BASE="${{ github.event.before }}" fi if [ -z "$BASE" ] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then { echo "docs_only=false" echo "docs_changed=false" echo "rust_changed=true" } >> "$GITHUB_OUTPUT" write_empty_docs_files exit 0 fi CHANGED="$(git diff --name-only "$BASE" HEAD || true)" if [ -z "$CHANGED" ]; then { echo "docs_only=false" echo "docs_changed=false" echo "rust_changed=false" } >> "$GITHUB_OUTPUT" write_empty_docs_files exit 0 fi docs_only=true docs_changed=false rust_changed=false docs_files=() while IFS= read -r file; do [ -z "$file" ] && continue if [[ "$file" == docs/* ]] \ || [[ "$file" == *.md ]] \ || [[ "$file" == *.mdx ]] \ || [[ "$file" == "LICENSE" ]] \ || [[ "$file" == ".markdownlint-cli2.yaml" ]] \ || [[ "$file" == .github/ISSUE_TEMPLATE/* ]] \ || [[ "$file" == .github/pull_request_template.md ]]; then if [[ "$file" == *.md ]] \ || [[ "$file" == *.mdx ]] \ || [[ "$file" == "LICENSE" ]] \ || [[ "$file" == .github/pull_request_template.md ]]; then docs_changed=true docs_files+=("$file") fi continue fi docs_only=false if [[ "$file" == src/* ]] \ || [[ "$file" == tests/* ]] \ || [[ "$file" == "Cargo.toml" ]] \ || [[ "$file" == "Cargo.lock" ]] \ || [[ "$file" == "deny.toml" ]]; then rust_changed=true fi done <<< "$CHANGED" { echo "docs_only=$docs_only" echo "docs_changed=$docs_changed" echo "rust_changed=$rust_changed" echo "docs_files<> "$GITHUB_OUTPUT" lint: name: Format & Lint needs: [changes] if: needs.changes.outputs.rust_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 20 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: toolchain: 1.92 components: rustfmt, clippy - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - name: Run rustfmt run: cargo fmt --all -- --check - name: Run clippy run: cargo clippy --locked --all-targets -- -D clippy::correctness test: name: Test needs: [changes] if: needs.changes.outputs.rust_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-240 timeout-minutes: 30 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: toolchain: 1.92 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - name: Run tests run: cargo test --locked --verbose build: name: Build (Smoke) needs: [changes] if: needs.changes.outputs.rust_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-240 timeout-minutes: 20 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: toolchain: 1.92 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - name: Build release binary run: cargo build --release --locked --verbose docs-only: name: Docs-Only Fast Path needs: [changes] if: needs.changes.outputs.docs_only == 'true' runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Skip heavy jobs for docs-only change run: echo "Docs-only change detected. Rust lint/test/build skipped." non-rust: name: Non-Rust Fast Path needs: [changes] if: needs.changes.outputs.docs_only != 'true' && needs.changes.outputs.rust_changed != 'true' runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Skip Rust jobs for non-Rust change scope run: echo "No Rust-impacting files changed. Rust lint/test/build skipped." docs-quality: name: Docs Quality needs: [changes] if: needs.changes.outputs.docs_changed == 'true' runs-on: blacksmith-2vcpu-ubuntu-240 timeout-minutes: 15 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Markdown lint uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22 with: globs: ${{ needs.changes.outputs.docs_files }} - name: Link check (offline) uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2 with: fail: true args: >- --offline --no-progress --format detailed ${{ needs.changes.outputs.docs_files }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ci-required: name: CI Required Gate if: always() needs: [changes, lint, test, build, docs-only, non-rust, docs-quality] runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Enforce required status shell: bash run: | set -euo pipefail docs_changed="${{ needs.changes.outputs.docs_changed }}" rust_changed="${{ needs.changes.outputs.rust_changed }}" docs_result="${{ needs.docs-quality.result }}" if [ "${{ needs.changes.outputs.docs_only }}" = "true" ]; then echo "docs=${docs_result}" if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then echo "Docs-only change touched markdown docs, but docs-quality did not pass." exit 1 fi echo "Docs-only fast path passed." exit 0 fi if [ "$rust_changed" != "true" ]; then echo "rust_changed=false (non-rust fast path)" echo "docs=${docs_result}" if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then echo "Docs changed but docs-quality did not pass." exit 1 fi echo "Non-rust fast path passed." exit 0 fi lint_result="${{ needs.lint.result }}" test_result="${{ needs.test.result }}" build_result="${{ needs.build.result }}" echo "lint=${lint_result}" echo "test=${test_result}" echo "build=${build_result}" echo "docs=${docs_result}" if [ "$lint_result" != "success" ] || [ "$test_result" != "success" ] || [ "$build_result" != "success" ]; then echo "Required CI jobs did not pass." exit 1 fi if [ "$docs_changed" = "true" ] && [ "$docs_result" != "success" ]; then echo "Docs changed but docs-quality did not pass." exit 1 fi echo "All required CI jobs passed."