diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 99ccfbd..7117cee 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -129,6 +129,157 @@ jobs: echo "✓ Library build verified for ${{ matrix.description }}" + test-lint: + name: Test rust/lint + runs-on: ubuntu-latest + strategy: + matrix: + include: + - test-case: 'well-formatted' + should-pass: true + description: 'Well formatted code should pass' + - test-case: 'badly-formatted' + should-pass: false + description: 'Badly formatted code should fail' + - test-case: 'clippy-warnings' + should-pass: false + description: 'Code with clippy warnings should fail' + steps: + - uses: actions/checkout@v4 + + - name: Create test project + run: | + cargo init --lib test-lint + cd test-lint + + # Create Cargo.toml with features + cat > Cargo.toml <<'EOF' + [package] + name = "test-lint" + version = "0.1.0" + edition = "2021" + + [features] + default = [] + serde = ["dep:serde"] + + [dependencies] + serde = { version = "1.0", features = ["derive"], optional = true } + EOF + + - name: Create well-formatted code + if: matrix.test-case == 'well-formatted' + run: | + cd test-lint + cat > src/lib.rs <<'EOF' + #[cfg(feature = "serde")] + use serde::{Deserialize, Serialize}; + + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + pub struct TestStruct { + pub value: i32, + } + + pub fn add(a: i32, b: i32) -> i32 { + a + b + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_add() { + assert_eq!(add(2, 2), 4); + } + } + EOF + + - name: Create badly-formatted code + if: matrix.test-case == 'badly-formatted' + run: | + cd test-lint + cat > src/lib.rs <<'EOF' + pub fn add(a:i32,b:i32)->i32{a+b} + pub fn multiply( a : i32 , b : i32 ) -> i32 { a * b } + EOF + + - name: Create code with clippy warnings + if: matrix.test-case == 'clippy-warnings' + run: | + cd test-lint + cat > src/lib.rs <<'EOF' + pub fn add(a: i32, b: i32) -> i32 { + a + b + } + + // Clippy will warn about this + pub fn unused_variable() { + let x = 42; // unused variable + } + + // Clippy will warn about this + pub fn needless_return(x: i32) -> i32 { + return x + 1; // needless return + } + EOF + + - name: Run lint (${{ matrix.description }}) + id: lint + continue-on-error: true + run: | + cd test-lint + + # Simulate the lint action behavior + echo "Running cargo fmt --check..." + FMT_RESULT=0 + cargo fmt --all --check || FMT_RESULT=$? + + echo "Running cargo clippy..." + CLIPPY_RESULT=0 + cargo clippy --all-targets -- -D warnings || CLIPPY_RESULT=$? + + if [ $FMT_RESULT -ne 0 ] || [ $CLIPPY_RESULT -ne 0 ]; then + echo "Lint failed as expected for ${{ matrix.test-case }}" + exit 1 + else + echo "Lint passed" + exit 0 + fi + + - name: Verify expected result + run: | + LINT_EXIT_CODE=${{ steps.lint.outcome }} + + if [ "${{ matrix.should-pass }}" = "true" ]; then + if [ "$LINT_EXIT_CODE" = "success" ]; then + echo "✓ Test passed: ${{ matrix.description }}" + else + echo "✗ Test failed: Expected success but got failure" + exit 1 + fi + else + if [ "$LINT_EXIT_CODE" = "failure" ]; then + echo "✓ Test passed: ${{ matrix.description }}" + else + echo "✗ Test failed: Expected failure but got success" + exit 1 + fi + fi + + - name: Test lint action directly (well-formatted only) + if: matrix.test-case == 'well-formatted' + working-directory: test-lint + run: | + # Test the individual commands that the action would run + echo "Testing cargo fmt --check..." + cargo fmt --all --check + + echo "Testing cargo clippy..." + cargo clippy --all-targets -- -D warnings + + echo "✓ Lint action commands work correctly" + test-extract-version: name: Test versioning/extract-version runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1749e..af53971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **rust/build-library** - Build Rust libraries with flexible profile and feature control +- **rust/lint** - Run cargo fmt and cargo clippy for code quality checks + - Configurable fmt and clippy checks (can enable/disable individually) + - Custom clippy arguments and lint levels + - Feature support (all-features, specific features, no-default-features) + - Workspace support (entire workspace or specific packages) + - Fail-on-warnings option + - Detailed step summaries and outputs - Comprehensive test coverage for build-library action (dev/release profiles, feature flags) +- Comprehensive test coverage for lint action (well-formatted, badly-formatted, clippy warnings) - Example library CI workflow demonstrating feature matrix testing - Release workflow automation (triggers on release published event) - RELEASE_PROCESS.md documentation for release workflow diff --git a/README.md b/README.md index 5223215..e5e3dc6 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ jobs: | [`rust/setup-rust-build`](rust/setup-rust-build/README.md) | Set up Rust toolchain with cross-compilation support | [📖 Docs](rust/setup-rust-build/README.md) | | [`rust/build-binary`](rust/build-binary/README.md) | Build Rust binaries for x86_64 and ARM64 | [📖 Docs](rust/build-binary/README.md) | | [`rust/build-library`](rust/build-library/README.md) | Build Rust libraries with flexible profile and feature control | [📖 Docs](rust/build-library/README.md) | +| [`rust/lint`](rust/lint/README.md) | Run cargo fmt and cargo clippy for code quality | [📖 Docs](rust/lint/README.md) | | [`rust/security-scan`](rust/security-scan/README.md) | Scan Rust dependencies for vulnerabilities (cargo-audit) | [📖 Docs](rust/security-scan/README.md) | | [`rust/generate-sbom`](rust/generate-sbom/README.md) | Generate Software Bill of Materials (CycloneDX) | [📖 Docs](rust/generate-sbom/README.md) | @@ -298,7 +299,7 @@ These actions are built with the following principles: | Language | Actions Available | Count | |----------|-------------------|-------| -| **Rust** | cache-cargo, setup-rust-build, build-binary, build-library, security-scan, generate-sbom | 6 | +| **Rust** | cache-cargo, setup-rust-build, build-binary, build-library, lint, security-scan, generate-sbom | 7 | | **Go** | trivy-scan, cosign-sign, verify-signed-commits, license-check, setup-docker, extract-version | 6 | | **Python** | trivy-scan, cosign-sign, verify-signed-commits, license-check, setup-docker, extract-version | 6 | | **Node.js** | trivy-scan, cosign-sign, verify-signed-commits, license-check, setup-docker, extract-version | 6 | diff --git a/rust/lint/README.md b/rust/lint/README.md new file mode 100644 index 0000000..05e4c02 --- /dev/null +++ b/rust/lint/README.md @@ -0,0 +1,509 @@ +# Lint Rust Code + +A composite GitHub Action that runs `cargo fmt` and `cargo clippy` to check code formatting and quality for Rust projects. + +## Features + +- **Automated Formatting Checks** - Verify code follows Rust style guidelines with `cargo fmt` +- **Code Quality Analysis** - Catch common mistakes and improve code with `cargo clippy` +- **Flexible Configuration** - Enable/disable individual checks, customize clippy warnings +- **Feature Support** - Lint with specific features, all features, or no default features +- **Workspace Support** - Lint entire workspaces or specific packages +- **Fail-Fast Option** - Choose whether to fail on warnings or just report them +- **Detailed Summaries** - GitHub Actions step summaries with clear pass/fail indicators + +## Usage + +### Basic Example + +```yaml +- name: Lint Rust code + uses: firestoned/github-actions/rust/lint@v1 +``` + +This runs both `cargo fmt --check` and `cargo clippy -- -D warnings` (fails on any warnings). + +### Format Check Only + +```yaml +- name: Check formatting + uses: firestoned/github-actions/rust/lint@v1 + with: + check-clippy: false +``` + +### Clippy Only + +```yaml +- name: Run Clippy + uses: firestoned/github-actions/rust/lint@v1 + with: + check-fmt: false +``` + +### Custom Clippy Arguments + +```yaml +- name: Lint with custom clippy settings + uses: firestoned/github-actions/rust/lint@v1 + with: + clippy-args: '-- -D warnings -W clippy::pedantic' +``` + +### Lint with All Features + +```yaml +- name: Lint with all features + uses: firestoned/github-actions/rust/lint@v1 + with: + all-features: true +``` + +### Lint with Specific Features + +```yaml +- name: Lint with specific features + uses: firestoned/github-actions/rust/lint@v1 + with: + features: 'serde,async' +``` + +### Lint Entire Workspace + +```yaml +- name: Lint workspace + uses: firestoned/github-actions/rust/lint@v1 + with: + workspace: true + all-features: true +``` + +### Lint Specific Package + +```yaml +- name: Lint specific package + uses: firestoned/github-actions/rust/lint@v1 + with: + package: my-core-library +``` + +### Allow Warnings + +```yaml +- name: Lint without failing on warnings + uses: firestoned/github-actions/rust/lint@v1 + with: + fail-on-warnings: false +``` + +## Inputs + +| Name | Description | Required | Default | +|------|-------------|----------|---------| +| `check-fmt` | Run `cargo fmt --check` | No | `true` | +| `check-clippy` | Run `cargo clippy` | No | `true` | +| `clippy-args` | Additional arguments for cargo clippy | No | `-- -D warnings` | +| `all-features` | Check with all features enabled | No | `false` | +| `features` | Space or comma-separated list of features | No | `''` | +| `no-default-features` | Do not activate default features | No | `false` | +| `workspace` | Lint all workspace members | No | `false` | +| `package` | Package to lint (for workspaces) | No | `''` | +| `fail-on-warnings` | Fail if clippy emits warnings | No | `true` | + +## Outputs + +| Name | Description | +|------|-------------| +| `fmt-status` | Formatting check status: `success` or `failure` | +| `clippy-status` | Clippy check status: `success` or `failure` | + +## How It Works + +### Formatting Check (`cargo fmt`) + +1. **Command**: `cargo fmt --all --check` (or with `--package` for specific packages) +2. **Behavior**: Checks if code is properly formatted without modifying files +3. **Failure**: Exits with error if any file needs formatting +4. **Fix**: Run `cargo fmt` locally to auto-format code + +### Clippy Check (`cargo clippy`) + +1. **Command**: `cargo clippy --all-targets [features] [clippy-args]` +2. **Behavior**: Analyzes code for common mistakes and style issues +3. **Default**: Fails on warnings (`-D warnings`) +4. **Customizable**: Pass custom lint levels via `clippy-args` + +### Lint Levels + +Clippy supports several lint levels: + +| Flag | Description | +|------|-------------| +| `-D warnings` | Deny warnings (fail on any warning) | +| `-W warnings` | Warn on warnings (don't fail) | +| `-A warnings` | Allow warnings (suppress all) | +| `-D clippy::pedantic` | Deny pedantic lints | +| `-W clippy::nursery` | Warn on nursery lints (experimental) | + +## Examples + +### Complete CI Workflow + +```yaml +name: Rust CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache dependencies + uses: firestoned/github-actions/rust/cache-cargo@v1 + + - name: Run linters + uses: firestoned/github-actions/rust/lint@v1 + with: + all-features: true + + build: + name: Build + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build + uses: firestoned/github-actions/rust/build-library@v1 +``` + +### Feature Matrix Linting + +```yaml +jobs: + lint: + strategy: + matrix: + features: + - name: 'default' + flags: '' + - name: 'serde' + flags: 'serde' + - name: 'all' + all: true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Lint with ${{ matrix.features.name }} features + uses: firestoned/github-actions/rust/lint@v1 + with: + features: ${{ matrix.features.flags }} + all-features: ${{ matrix.features.all || false }} +``` + +### Workspace Linting + +```yaml +jobs: + lint-workspace: + name: Lint Entire Workspace + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Lint workspace + uses: firestoned/github-actions/rust/lint@v1 + with: + workspace: true + all-features: true + + lint-packages: + name: Lint Individual Packages + strategy: + matrix: + package: + - my-core + - my-cli + - my-utils + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Lint ${{ matrix.package }} + uses: firestoned/github-actions/rust/lint@v1 + with: + package: ${{ matrix.package }} +``` + +### Pedantic Clippy + +```yaml +- name: Strict linting + uses: firestoned/github-actions/rust/lint@v1 + with: + clippy-args: '-- -D warnings -D clippy::pedantic -D clippy::cargo' +``` + +### Format-Only PR Check + +```yaml +name: PR Format Check + +on: + pull_request: + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check formatting only + uses: firestoned/github-actions/rust/lint@v1 + with: + check-clippy: false +``` + +## Best Practices + +### 1. Run Linting Early + +Put linting before builds to fail fast: + +```yaml +jobs: + lint: + # Runs first + build: + needs: lint # Only runs if lint passes +``` + +### 2. Separate Format and Clippy + +For better CI feedback, run as separate steps: + +```yaml +- name: Check formatting + uses: firestoned/github-actions/rust/lint@v1 + with: + check-clippy: false + +- name: Run Clippy + uses: firestoned/github-actions/rust/lint@v1 + with: + check-fmt: false +``` + +### 3. Use the Same Features + +Lint with the same features you build with: + +```yaml +- name: Lint + uses: firestoned/github-actions/rust/lint@v1 + with: + all-features: true + +- name: Build + uses: firestoned/github-actions/rust/build-library@v1 + with: + all-features: true +``` + +### 4. Commit Formatting Locally + +Set up a pre-commit hook to avoid CI failures: + +```bash +# .git/hooks/pre-commit +#!/bin/sh +cargo fmt -- --check || exit 1 +``` + +### 5. Use rustfmt.toml + +Customize formatting rules in `rustfmt.toml`: + +```toml +edition = "2021" +max_width = 100 +use_small_heuristics = "Max" +``` + +### 6. Use clippy.toml + +Configure clippy lints in `clippy.toml`: + +```toml +# Warn on pedantic lints +pedantic-lints = "warn" + +# Deny specific lints +avoid-breaking-exported-api = true +``` + +## Troubleshooting + +### Formatting Check Fails + +**Problem**: `cargo fmt --check` fails in CI but passes locally + +**Solution**: Ensure same Rust version and rustfmt.toml: +```yaml +- uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy +``` + +### Clippy Warnings Not Failing + +**Problem**: Warnings shown but action passes + +**Solution**: Ensure `-D warnings` is in clippy-args: +```yaml +with: + clippy-args: '-- -D warnings' +``` + +### Too Many Clippy Warnings + +**Problem**: Legacy code has many clippy warnings + +**Solution**: Start with warnings, gradually move to errors: +```yaml +# Phase 1: Just warn +with: + clippy-args: '-- -W clippy::all' + fail-on-warnings: false + +# Phase 2: Deny after cleanup +with: + clippy-args: '-- -D warnings' +``` + +### Workspace Package Not Found + +**Problem**: `package` not found in workspace + +**Solution**: Ensure package name matches exactly: +```yaml +with: + package: my-package # Must match [package] name in Cargo.toml +``` + +### Clippy Errors on Feature Combinations + +**Problem**: Clippy fails with certain feature combinations + +**Solution**: Test feature combinations explicitly: +```yaml +strategy: + matrix: + features: ['', 'serde', 'async', 'serde,async'] +``` + +## Advanced Usage + +### Auto-Fix Formatting + +Use in a separate workflow to auto-fix formatting: + +```yaml +name: Auto Format + +on: + pull_request: + +jobs: + format: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Run cargo fmt + run: cargo fmt + + - name: Commit changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git diff --quiet && git diff --staged --quiet || \ + git commit -m "chore: auto-format code [skip ci]" + git push +``` + +### Custom Clippy Configuration per Package + +For workspaces with different lint requirements: + +```yaml +- name: Lint core (strict) + uses: firestoned/github-actions/rust/lint@v1 + with: + package: my-core + clippy-args: '-- -D warnings -D clippy::pedantic' + +- name: Lint CLI (relaxed) + uses: firestoned/github-actions/rust/lint@v1 + with: + package: my-cli + clippy-args: '-- -D warnings' +``` + +### Clippy Fix Mode (Local Development) + +Document for developers: + +```bash +# Check for issues +cargo clippy -- -D warnings + +# Auto-fix where possible +cargo clippy --fix + +# Fix and allow dirty/staged files +cargo clippy --fix --allow-dirty --allow-staged +``` + +### GitHub Actions Annotations + +Clippy automatically creates annotations in PR files. View them in: +- Files changed tab +- Annotations section of workflow run + +## Related Actions + +- [**rust/cache-cargo**](../cache-cargo/README.md) - Cache dependencies before linting +- [**rust/build-library**](../build-library/README.md) - Build after successful linting +- [**rust/security-scan**](../security-scan/README.md) - Security scanning +- [**security/license-check**](../../security/license-check/README.md) - License compliance + +## Compatibility + +- **GitHub-hosted runners**: ✅ ubuntu-latest, macos-latest, windows-latest +- **Self-hosted runners**: ✅ Supported (requires Rust toolchain with rustfmt and clippy) +- **Rust versions**: Any stable, beta, or nightly with rustfmt and clippy components + +## License + +This action is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details. + +--- + +**Author**: Erick Bourgeois +**Organization**: firestoned +**Repository**: [firestoned/github-actions](https://github.com/firestoned/github-actions) diff --git a/rust/lint/action.yml b/rust/lint/action.yml new file mode 100644 index 0000000..f2b9818 --- /dev/null +++ b/rust/lint/action.yml @@ -0,0 +1,177 @@ +# Copyright (c) 2025 Erick Bourgeois, firestoned +# SPDX-License-Identifier: MIT + +name: 'Lint Rust Code' +description: 'Run cargo fmt and cargo clippy to check code formatting and quality' +author: 'Erick Bourgeois ' + +branding: + icon: 'check-circle' + color: 'green' + +inputs: + check-fmt: + description: 'Run cargo fmt --check' + required: false + default: 'true' + check-clippy: + description: 'Run cargo clippy' + required: false + default: 'true' + clippy-args: + description: 'Additional arguments for cargo clippy (e.g., "-- -D warnings")' + required: false + default: '-- -D warnings' + all-features: + description: 'Check with all features enabled' + required: false + default: 'false' + features: + description: 'Space or comma-separated list of features to activate' + required: false + default: '' + no-default-features: + description: 'Do not activate the default features' + required: false + default: 'false' + workspace: + description: 'Lint all workspace members' + required: false + default: 'false' + package: + description: 'Package to lint (for workspaces with multiple packages)' + required: false + default: '' + fail-on-warnings: + description: 'Fail if clippy emits warnings' + required: false + default: 'true' + +outputs: + fmt-status: + description: 'Formatting check status (success or failure)' + value: ${{ steps.fmt.outputs.status }} + clippy-status: + description: 'Clippy check status (success or failure)' + value: ${{ steps.clippy.outputs.status }} + +runs: + using: 'composite' + steps: + - name: Check formatting + id: fmt + if: inputs.check-fmt == 'true' + shell: bash + run: | + echo "Running cargo fmt --check..." + + # Build command + FMT_CMD="cargo fmt --all --check" + + # Add package if specified + if [ -n "${{ inputs.package }}" ]; then + FMT_CMD="cargo fmt --package ${{ inputs.package }} --check" + fi + + echo "Command: $FMT_CMD" + + if $FMT_CMD; then + echo "status=success" >> $GITHUB_OUTPUT + echo "✅ Formatting check passed" + else + echo "status=failure" >> $GITHUB_OUTPUT + echo "❌ Formatting check failed" + echo "" + echo "Run 'cargo fmt' to fix formatting issues" + exit 1 + fi + + - name: Run Clippy + id: clippy + if: inputs.check-clippy == 'true' + shell: bash + run: | + echo "Running cargo clippy..." + + # Build base command + CLIPPY_CMD="cargo clippy" + + # Add workspace or package flags + if [ "${{ inputs.workspace }}" = "true" ]; then + CLIPPY_CMD="$CLIPPY_CMD --workspace" + echo "Linting entire workspace" + elif [ -n "${{ inputs.package }}" ]; then + CLIPPY_CMD="$CLIPPY_CMD --package ${{ inputs.package }}" + echo "Linting package: ${{ inputs.package }}" + else + CLIPPY_CMD="$CLIPPY_CMD --all-targets" + echo "Linting all targets" + fi + + # Add features + if [ "${{ inputs.all-features }}" = "true" ]; then + CLIPPY_CMD="$CLIPPY_CMD --all-features" + echo "Features: all" + elif [ -n "${{ inputs.features }}" ]; then + CLIPPY_CMD="$CLIPPY_CMD --features ${{ inputs.features }}" + echo "Features: ${{ inputs.features }}" + fi + + # Add no-default-features + if [ "${{ inputs.no-default-features }}" = "true" ]; then + CLIPPY_CMD="$CLIPPY_CMD --no-default-features" + echo "Default features: disabled" + fi + + # Add clippy arguments + if [ -n "${{ inputs.clippy-args }}" ]; then + CLIPPY_CMD="$CLIPPY_CMD ${{ inputs.clippy-args }}" + fi + + echo "Command: $CLIPPY_CMD" + echo "" + + if $CLIPPY_CMD; then + echo "status=success" >> $GITHUB_OUTPUT + echo "✅ Clippy check passed" + else + echo "status=failure" >> $GITHUB_OUTPUT + echo "❌ Clippy check failed" + if [ "${{ inputs.fail-on-warnings }}" = "true" ]; then + exit 1 + fi + fi + + - name: Summary + shell: bash + run: | + echo "## Lint Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ inputs.check-fmt }}" = "true" ]; then + if [ "${{ steps.fmt.outputs.status }}" = "success" ]; then + echo "**Formatting**: ✅ Passed" >> $GITHUB_STEP_SUMMARY + else + echo "**Formatting**: ❌ Failed" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [ "${{ inputs.check-clippy }}" = "true" ]; then + if [ "${{ steps.clippy.outputs.status }}" = "success" ]; then + echo "**Clippy**: ✅ Passed" >> $GITHUB_STEP_SUMMARY + else + echo "**Clippy**: ❌ Failed" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [ "${{ inputs.all-features }}" = "true" ]; then + echo "**Features**: All features enabled" >> $GITHUB_STEP_SUMMARY + elif [ -n "${{ inputs.features }}" ]; then + echo "**Features**: \`${{ inputs.features }}\`" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ inputs.workspace }}" = "true" ]; then + echo "**Scope**: Entire workspace" >> $GITHUB_STEP_SUMMARY + elif [ -n "${{ inputs.package }}" ]; then + echo "**Scope**: Package \`${{ inputs.package }}\`" >> $GITHUB_STEP_SUMMARY + fi