Merge pull request #9 from bug-ops/refactor/dry-phase1 #40
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 | |
| permissions: | |
| contents: read | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_NET_RETRY: 10 | |
| RUST_BACKTRACE: short | |
| RUSTFLAGS: "-D warnings" | |
| RUSTUP_MAX_RETRIES: 10 | |
| # Cancel previous runs on new push | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Fast checks - run first, fail fast | |
| lint: | |
| name: Lint (fmt + clippy) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Install Rust nightly | |
| uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| components: rustfmt | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "lint" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Check formatting | |
| run: cargo +nightly fmt --all -- --check | |
| - name: Clippy (workspace except Python bindings) | |
| run: cargo +stable clippy --all-targets --all-features --workspace --exclude feedparser-rs-py -- -D warnings | |
| - name: Install SARIF tools | |
| run: cargo install clippy-sarif sarif-fmt | |
| - name: Clippy SARIF (for GitHub PR annotations) | |
| run: | | |
| cargo clippy --all-targets --all-features --workspace \ | |
| --exclude feedparser-rs-py --message-format=json -- -D warnings | \ | |
| clippy-sarif | tee clippy-results.sarif | sarif-fmt | |
| continue-on-error: true | |
| - name: Upload SARIF to GitHub | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: clippy-results.sarif | |
| wait-for-processing: true | |
| - name: Check documentation | |
| run: cargo doc --no-deps --all-features --workspace --exclude feedparser-rs-py | |
| env: | |
| RUSTDOCFLAGS: "-D warnings" | |
| # Security audit | |
| security: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "security" | |
| - name: Install cargo-deny | |
| uses: taiki-e/install-action@cargo-deny | |
| - name: Scan for vulnerabilities | |
| run: cargo deny check advisories | |
| - name: Check licenses | |
| run: cargo deny check licenses | |
| - name: Check bans | |
| run: cargo deny check bans | |
| - name: Check sources | |
| run: cargo deny check sources | |
| # Cross-platform Rust tests | |
| test-rust: | |
| name: Test Rust (${{ matrix.os }}) | |
| needs: [lint] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install nextest | |
| uses: taiki-e/install-action@nextest | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "test-rust-${{ matrix.os }}" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Run tests | |
| run: cargo nextest run --all-features --no-fail-fast --workspace --exclude feedparser-rs-py | |
| - name: Run doctests | |
| run: cargo test --doc --all-features --workspace --exclude feedparser-rs-py | |
| # Python bindings tests | |
| test-python: | |
| name: Test Python (${{ matrix.os }} - Py${{ matrix.python }}) | |
| needs: [lint] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| python: ['3.9', '3.10', '3.11', '3.12', '3.13'] | |
| exclude: | |
| # Reduce matrix size - test all versions on Linux only | |
| - os: macos-latest | |
| python: '3.9' | |
| - os: macos-latest | |
| python: '3.10' | |
| - os: macos-latest | |
| python: '3.11' | |
| - os: windows-latest | |
| python: '3.9' | |
| - os: windows-latest | |
| python: '3.10' | |
| - os: windows-latest | |
| python: '3.11' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "test-python-${{ matrix.os }}" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: crates/feedparser-rs-py | |
| - name: Setup Python ${{ matrix.python }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install maturin and pytest | |
| run: uv pip install --system maturin pytest | |
| - name: Build Python wheel | |
| working-directory: crates/feedparser-rs-py | |
| run: maturin build --release --out dist | |
| - name: Install wheel | |
| working-directory: crates/feedparser-rs-py | |
| shell: bash | |
| run: uv pip install --system dist/*.whl | |
| - name: Run Python tests | |
| working-directory: crates/feedparser-rs-py | |
| run: pytest tests/ -v | |
| if: hashFiles('crates/feedparser-rs-py/tests/*.py') != '' | |
| - name: Test Python import | |
| run: python -c "import feedparser_rs; print(f'feedparser-rs version loaded successfully')" | |
| # Node.js bindings tests | |
| test-node: | |
| name: Test Node.js (${{ matrix.os }} - Node ${{ matrix.node }}) | |
| needs: [lint] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| node: ['20'] | |
| include: | |
| # Test Node 22 only on Linux | |
| - os: ubuntu-latest | |
| node: '22' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "test-node-${{ matrix.os }}" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: crates/feedparser-rs-node | |
| - name: Setup Node.js ${{ matrix.node }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| cache: 'npm' | |
| cache-dependency-path: crates/feedparser-rs-node/package-lock.json | |
| - name: Install dependencies | |
| working-directory: crates/feedparser-rs-node | |
| run: npm ci | |
| - name: Build native module | |
| working-directory: crates/feedparser-rs-node | |
| run: npm run build | |
| - name: Test | |
| working-directory: crates/feedparser-rs-node | |
| run: npm test | |
| # Rust code coverage | |
| coverage-rust: | |
| name: Rust Code Coverage | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: llvm-tools | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "coverage-rust" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Install nextest | |
| uses: taiki-e/install-action@nextest | |
| - name: Install cargo-llvm-cov | |
| uses: taiki-e/install-action@cargo-llvm-cov | |
| - name: Generate coverage | |
| run: | | |
| cargo llvm-cov --all-features --workspace --exclude feedparser-rs-py \ | |
| --lcov --output-path lcov.info nextest | |
| - name: Upload to codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: lcov.info | |
| fail_ci_if_error: true | |
| flags: rust-core | |
| verbose: true | |
| # Python code coverage | |
| coverage-python: | |
| name: Python Code Coverage | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "coverage-python" | |
| workspaces: crates/feedparser-rs-py | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.12' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv pip install --system maturin pytest pytest-cov | |
| - name: Build Python wheel | |
| working-directory: crates/feedparser-rs-py | |
| run: maturin build --release --out dist | |
| - name: Install wheel | |
| working-directory: crates/feedparser-rs-py | |
| shell: bash | |
| run: uv pip install --system dist/*.whl | |
| - name: Run tests with coverage | |
| working-directory: crates/feedparser-rs-py | |
| run: | | |
| pytest tests/ --cov=feedparser_rs --cov-report=xml --cov-report=term | |
| continue-on-error: true | |
| if: hashFiles('crates/feedparser-rs-py/tests/*.py') != '' | |
| - name: Upload Python coverage to codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: crates/feedparser-rs-py/coverage.xml | |
| fail_ci_if_error: false | |
| flags: python-bindings | |
| if: hashFiles('crates/feedparser-rs-py/tests/*.py') != '' | |
| # Node.js code coverage | |
| coverage-node: | |
| name: Node.js Code Coverage | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "coverage-node" | |
| workspaces: crates/feedparser-rs-node | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: crates/feedparser-rs-node/package-lock.json | |
| - name: Install dependencies | |
| working-directory: crates/feedparser-rs-node | |
| run: npm ci | |
| - name: Build native module | |
| working-directory: crates/feedparser-rs-node | |
| run: npm run build | |
| - name: Run tests with coverage | |
| working-directory: crates/feedparser-rs-node | |
| run: npm test -- --coverage | |
| continue-on-error: true | |
| - name: Upload Node.js coverage to codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| directory: crates/feedparser-rs-node/coverage | |
| fail_ci_if_error: false | |
| flags: node-bindings | |
| # MSRV check | |
| msrv: | |
| name: Check MSRV (1.88.0) | |
| needs: [lint] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust 1.88.0 | |
| uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.88.0" | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "msrv" | |
| - name: Check with MSRV | |
| run: cargo +1.88.0 check --all-features --workspace --exclude feedparser-rs-py | |
| # All checks passed gate | |
| ci-success: | |
| name: CI Success | |
| needs: [lint, security, test-rust, test-python, test-node, coverage-rust, coverage-python, coverage-node, msrv] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check all jobs | |
| run: | | |
| if [[ "${{ needs.lint.result }}" != "success" ]] || \ | |
| [[ "${{ needs.security.result }}" != "success" ]] || \ | |
| [[ "${{ needs.test-rust.result }}" != "success" ]] || \ | |
| [[ "${{ needs.test-python.result }}" != "success" ]] || \ | |
| [[ "${{ needs.test-node.result }}" != "success" ]] || \ | |
| [[ "${{ needs.coverage-rust.result }}" != "success" ]] || \ | |
| [[ "${{ needs.coverage-python.result }}" != "success" ]] || \ | |
| [[ "${{ needs.coverage-node.result }}" != "success" ]] || \ | |
| [[ "${{ needs.msrv.result }}" != "success" ]]; then | |
| echo "One or more jobs failed" | |
| exit 1 | |
| fi | |
| echo "All CI jobs passed successfully!" |