rust-diff-analyzer 1.6.0

Semantic analyzer for Rust PR diffs that distinguishes production code from test code
Documentation
name: 'Rust PR Diff Analyzer'
description: 'Analyze and limit PR size by distinguishing production code from test code using Rust AST analysis'
author: 'RAprogramm'

branding:
  icon: 'code'
  color: 'orange'

inputs:
  max_prod_units:
    description: 'Maximum number of production units allowed'
    required: false
    default: '30'
  max_weighted_score:
    description: 'Maximum weighted score allowed'
    required: false
    default: '100'
  max_prod_lines:
    description: 'Maximum production lines added'
    required: false
    default: ''
  fail_on_exceed:
    description: 'Fail the action if limits are exceeded'
    required: false
    default: 'true'
  output_format:
    description: 'Output format (github, json, human)'
    required: false
    default: 'github'
  config_file:
    description: 'Path to configuration file'
    required: false
    default: ''
  post_comment:
    description: 'Post analysis as PR comment'
    required: false
    default: 'false'
  update_comment:
    description: 'Update existing comment instead of creating new'
    required: false
    default: 'true'

outputs:
  prod_functions_changed:
    description: 'Number of production functions changed'
    value: ${{ steps.analyze.outputs.prod_functions_changed }}
  prod_structs_changed:
    description: 'Number of production structs changed'
    value: ${{ steps.analyze.outputs.prod_structs_changed }}
  prod_other_changed:
    description: 'Number of other production units changed'
    value: ${{ steps.analyze.outputs.prod_other_changed }}
  test_units_changed:
    description: 'Number of test units changed'
    value: ${{ steps.analyze.outputs.test_units_changed }}
  prod_lines_added:
    description: 'Lines added in production code'
    value: ${{ steps.analyze.outputs.prod_lines_added }}
  prod_lines_removed:
    description: 'Lines removed from production code'
    value: ${{ steps.analyze.outputs.prod_lines_removed }}
  test_lines_added:
    description: 'Lines added in test code'
    value: ${{ steps.analyze.outputs.test_lines_added }}
  test_lines_removed:
    description: 'Lines removed from test code'
    value: ${{ steps.analyze.outputs.test_lines_removed }}
  weighted_score:
    description: 'Weighted score of changes'
    value: ${{ steps.analyze.outputs.weighted_score }}
  exceeds_limit:
    description: 'Whether limits were exceeded'
    value: ${{ steps.analyze.outputs.exceeds_limit }}

runs:
  using: 'composite'
  steps:
    - name: Get PR diff
      id: get_diff
      shell: bash
      run: |
        if [ "${{ github.event_name }}" = "pull_request" ]; then
          git fetch origin ${{ github.base_ref }} --depth=1
          
          # Filter commits by author if config has ignored_authors
          if [ -n "${{ inputs.config_file }}" ] && [ -f "${{ github.workspace }}/${{ inputs.config_file }}" ]; then
            # Read ignored_authors from config
            CONFIG_PATH="${{ github.workspace }}/${{ inputs.config_file }}"
            IGNORED_AUTHORS=$(grep -A 20 '\[classification\]' "$CONFIG_PATH" | grep 'ignored_authors' | sed 's/.*= //' | tr -d '[]"' | tr ',' '\n' | tr -d ' ')
            
            if [ -n "$IGNORED_AUTHORS" ]; then
              # Get the first commit that is NOT from ignored authors
              FIRST_VALID=""
              for commit in $(git log --format=%H origin/${{ github.base_ref }}..HEAD); do
                author=$(git log -1 --format=%ae "$commit")
                ignored=false
                for ignored_author in $IGNORED_AUTHORS; do
                  if echo "$author" | grep -q "$ignored_author"; then
                    ignored=true
                  fi
                done
                if [ "$ignored" = "false" ]; then
                  FIRST_VALID="$commit"
                  break
                fi
              done
              
              if [ -z "$FIRST_VALID" ]; then
                echo "All commits are from ignored authors - skipping analysis"
                echo "SKIP_ANALYSIS=true" >> $GITHUB_ENV
                exit 0
              fi
              
              echo "Analyzing from commit $FIRST_VALID onwards"
              git diff origin/${{ github.base_ref }}...$FIRST_VALID > /tmp/pr_diff.txt
            else
              git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr_diff.txt
            fi
          else
            git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr_diff.txt
          fi
        else
          git diff HEAD~1 > /tmp/pr_diff.txt
        fi

    - name: Setup Rust
      uses: actions-rust-lang/setup-rust-toolchain@v1
      with:
        toolchain: stable

    - name: Build analyzer
      shell: bash
      run: |
        cd ${{ github.action_path }}
        cargo build --release

    - name: Run analysis
      id: analyze
      shell: bash
      run: |
        # Skip analysis if all commits are from ignored authors
        if [ "${{ env.SKIP_ANALYSIS }}" = "true" ]; then
          echo "Skipping analysis - all commits from ignored authors"
          echo "prod_functions_changed=0" >> $GITHUB_OUTPUT
          echo "prod_structs_changed=0" >> $GITHUB_OUTPUT
          echo "prod_other_changed=0" >> $GITHUB_OUTPUT
          echo "test_units_changed=0" >> $GITHUB_OUTPUT
          echo "prod_lines_added=0" >> $GITHUB_OUTPUT
          echo "prod_lines_removed=0" >> $GITHUB_OUTPUT
          echo "test_lines_added=0" >> $GITHUB_OUTPUT
          echo "test_lines_removed=0" >> $GITHUB_OUTPUT
          echo "weighted_score=0" >> $GITHUB_OUTPUT
          echo "exceeds_limit=false" >> $GITHUB_OUTPUT
          echo "EXCEEDS_LIMIT=false" >> $GITHUB_ENV
          exit 0
        fi
        
        ARGS="--diff-file /tmp/pr_diff.txt"
        ARGS="$ARGS --max-units ${{ inputs.max_prod_units }}"
        ARGS="$ARGS --max-score ${{ inputs.max_weighted_score }}"
        ARGS="$ARGS --format ${{ inputs.output_format }}"
        ARGS="$ARGS --no-fail"

        if [ -n "${{ inputs.max_prod_lines }}" ]; then
          ARGS="$ARGS --max-lines ${{ inputs.max_prod_lines }}"
        fi

        if [ -n "${{ inputs.config_file }}" ]; then
          ARGS="$ARGS --config ${{ inputs.config_file }}"
        fi

        OUTPUT=$(${{ github.action_path }}/target/release/rust-diff-analyzer $ARGS)

        echo "$OUTPUT"

        if [ "${{ inputs.output_format }}" = "github" ]; then
          echo "$OUTPUT" >> $GITHUB_OUTPUT
        fi

        if echo "$OUTPUT" | grep -q "exceeds_limit=true"; then
          echo "EXCEEDS_LIMIT=true" >> $GITHUB_ENV
        else
          echo "EXCEEDS_LIMIT=false" >> $GITHUB_ENV
        fi

    - name: Post PR comment
      if: inputs.post_comment == 'true' && github.event_name == 'pull_request'
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
      run: |
        ARGS="--diff-file /tmp/pr_diff.txt"
        ARGS="$ARGS --max-units ${{ inputs.max_prod_units }}"
        ARGS="$ARGS --max-score ${{ inputs.max_weighted_score }}"
        ARGS="$ARGS --format comment"
        ARGS="$ARGS --no-fail"

        if [ -n "${{ inputs.max_prod_lines }}" ]; then
          ARGS="$ARGS --max-lines ${{ inputs.max_prod_lines }}"
        fi

        if [ -n "${{ inputs.config_file }}" ]; then
          ARGS="$ARGS --config ${{ inputs.config_file }}"
        fi

        COMMENT=$(${{ github.action_path }}/target/release/rust-diff-analyzer $ARGS)
        MARKER="<!-- rust-diff-analyzer-comment -->"
        PR_NUMBER=${{ github.event.pull_request.number }}

        if [ "${{ inputs.update_comment }}" = "true" ]; then
          EXISTING=$(gh api repos/${{ github.repository }}/issues/${PR_NUMBER}/comments \
            --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1)

          if [ -n "$EXISTING" ]; then
            gh api repos/${{ github.repository }}/issues/comments/${EXISTING} \
              -X PATCH -f body="$COMMENT"
            echo "Updated existing comment"
          else
            gh pr comment $PR_NUMBER --body "$COMMENT"
            echo "Created new comment"
          fi
        else
          gh pr comment $PR_NUMBER --body "$COMMENT"
          echo "Created new comment"
        fi

    - name: Fail if limits exceeded
      if: env.EXCEEDS_LIMIT == 'true' && inputs.fail_on_exceed == 'true'
      shell: bash
      run: |
        echo "::error::Production code changes exceed configured limits"
        exit 1