name: CI
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
deps: ${{ steps.filter.outputs.deps }}
reuse: ${{ steps.filter.outputs.reuse }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
rust:
- 'src/**'
- 'tests/**'
- 'benches/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.rustfmt.toml'
deps:
- 'Cargo.toml'
- 'Cargo.lock'
reuse:
- 'LICENSES/**'
- '.reuse/**'
- '**/*.rs'
- '**/*.toml'
- '**/*.yml'
- '**/*.md'
pr-analysis:
name: PR Size Analysis
runs-on: ubuntu-latest
needs: changes
if: github.event_name == 'pull_request' && needs.changes.outputs.rust == 'true'
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: ./
with:
max_prod_lines: '200'
fail_on_exceed: 'true'
post_comment: 'true'
update_comment: 'true'
fmt:
name: Format
runs-on: ubuntu-latest
needs: [changes, pr-analysis]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo +nightly fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
needs: [changes, pr-analysis]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test
runs-on: ubuntu-latest
needs: [changes, fmt, clippy, pr-analysis]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.fmt.result == 'success' || needs.fmt.result == 'skipped') &&
(needs.clippy.result == 'success' || needs.clippy.result == 'skipped') &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- name: Run tests with coverage
run: cargo llvm-cov nextest --all-features --lcov --output-path lcov.info
- name: Generate JUnit report
run: cargo nextest run --all-features --profile ci
continue-on-error: true
- name: Upload coverage and test results to Codecov
if: github.event_name != 'pull_request'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false
report_type: test_results
- name: Upload test results artifact
uses: actions/upload-artifact@v7
with:
name: test-results
path: target/nextest/ci/results.junit.xml
doc:
name: Documentation
runs-on: ubuntu-latest
needs: [changes, fmt, clippy, pr-analysis]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.fmt.result == 'success' || needs.fmt.result == 'skipped') &&
(needs.clippy.result == 'success' || needs.clippy.result == 'skipped') &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo doc --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings
bench:
name: Benchmark
runs-on: ubuntu-latest
needs: [changes, test]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
needs.test.result == 'success'
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo bench --no-run
audit:
name: Security Audit
runs-on: ubuntu-latest
needs: [changes, pr-analysis]
if: |
always() &&
(needs.changes.outputs.deps == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
reuse:
name: REUSE Compliance
runs-on: ubuntu-latest
needs: [changes, pr-analysis]
if: |
always() &&
(needs.changes.outputs.reuse == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.pr-analysis.result == 'success' || needs.pr-analysis.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: fsfe/reuse-action@v6
build:
name: Build Release
runs-on: ubuntu-latest
needs: [changes, test, doc, bench, audit, reuse, pr-analysis]
if: |
always() &&
needs.pr-analysis.result != 'failure' &&
(needs.changes.outputs.rust == 'true' || startsWith(github.ref, 'refs/tags/v')) &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.doc.result == 'success' || needs.doc.result == 'skipped') &&
(needs.bench.result == 'success' || needs.bench.result == 'skipped') &&
(needs.audit.result == 'success' || needs.audit.result == 'skipped') &&
(needs.reuse.result == 'success' || needs.reuse.result == 'skipped')
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --all-features
- uses: actions/upload-artifact@v7
with:
name: rust-diff-analyzer
path: target/release/rust-diff-analyzer
changelog:
name: Update Changelog
runs-on: ubuntu-latest
needs: build
if: |
always() &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
needs.build.result == 'success'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Generate changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --verbose
env:
OUTPUT: CHANGELOG.md
- name: Commit changelog
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "docs: update changelog [skip ci]"
git pull --rebase origin main
git push
fi
auto-tag:
name: Auto Tag Release
runs-on: ubuntu-latest
needs: changelog
if: |
always() &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
(needs.changelog.result == 'success' || needs.changelog.result == 'skipped')
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Get version from Cargo.toml
id: version
run: |
VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Check if tag exists
id: check_tag
run: |
TAG="v${{ steps.version.outputs.version }}"
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Tag $TAG already exists on remote"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Tag $TAG does not exist on remote"
fi
- name: Create and push tag
if: steps.check_tag.outputs.exists == 'false'
run: |
TAG="v${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git pull --rebase origin main
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "Created and pushed tag $TAG"
release-build:
name: Release Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
needs: [test, doc, audit, reuse]
if: |
always() &&
startsWith(github.ref, 'refs/tags/v') &&
needs.test.result == 'success' &&
needs.doc.result == 'success' &&
(needs.audit.result == 'success' || needs.audit.result == 'skipped') &&
(needs.reuse.result == 'success' || needs.reuse.result == 'skipped')
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
cross: false
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
archive: tar.gz
cross: true
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
cross: true
- target: x86_64-apple-darwin
os: macos-latest
archive: tar.gz
cross: false
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
cross: false
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
cross: false
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Install cross
if: matrix.cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build with cross
if: matrix.cross
run: cross build --release --target ${{ matrix.target }}
- name: Build with cargo
if: ${{ !matrix.cross }}
run: cargo build --release --target ${{ matrix.target }}
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../rust-diff-analyzer-${{ matrix.target }}.tar.gz rust-diff-analyzer
- name: Package (Windows)
if: matrix.os == 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
Compress-Archive -Path rust-diff-analyzer.exe -DestinationPath ../../../rust-diff-analyzer-${{ matrix.target }}.zip
- uses: actions/upload-artifact@v7
with:
name: rust-diff-analyzer-${{ matrix.target }}
path: rust-diff-analyzer-${{ matrix.target }}.${{ matrix.archive }}
release:
name: Create Release
needs: [release-build]
runs-on: ubuntu-latest
if: |
always() &&
startsWith(github.ref, 'refs/tags/v') &&
needs.release-build.result == 'success'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Generate release notes
id: notes
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --verbose --latest --strip header
env:
OUTPUT: RELEASE_NOTES.md
- uses: actions/download-artifact@v8
with:
path: artifacts
- name: Create Release
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTES.md
files: artifacts/**/*
- name: Update major version tag
run: |
MAJOR=$(echo "${{ github.ref_name }}" | sed 's/v\([0-9]*\).*/v\1/')
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa "$MAJOR" -m "Points to ${{ github.ref_name }}"
git push origin "$MAJOR" --force
publish:
name: Publish to crates.io
needs: [release-build]
runs-on: ubuntu-latest
if: |
always() &&
startsWith(github.ref, 'refs/tags/v') &&
contains(github.ref, '.') &&
needs.release-build.result == 'success'
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Publish
run: |
output=$(cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} --allow-dirty 2>&1) || {
if echo "$output" | grep -q "already exists"; then
echo "::notice::Version already published to crates.io"
exit 0
else
echo "$output"
exit 1
fi
}