Skip to content

Merge pull request #9 from bug-ops/refactor/dry-phase1 #40

Merge pull request #9 from bug-ops/refactor/dry-phase1

Merge pull request #9 from bug-ops/refactor/dry-phase1 #40

Workflow file for this run

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!"