name: pip-audit

on:
  workflow_call:
    inputs:
      working-directory:
        description: 'Directory containing the Python project to audit'
        required: true
        type: string
      soft-fail:
        description: 'When true, audit failures do not fail the build'
        required: false
        type: boolean
        default: false
      changed-files-only:
        description: 'Only run audit if dependency files changed in this directory'
        required: false
        type: boolean
        default: false

permissions:
  contents: read

jobs:
  pip-audit:
    name: Audit Python Dependencies
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        shell: pwsh

    steps:
      - name: Checkout repository
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          persist-credentials: false
          fetch-depth: 0

      - name: Derive artifact name
        id: meta
        run: |
          $name = '${{ inputs.working-directory }}' -replace '/', '-' -replace '^\.', '' -replace '^-', ''
          "artifact-name=pip-audit-$name" >> $env:GITHUB_OUTPUT

      - name: Check for dependency file changes
        if: inputs.changed-files-only
        run: |
          $baseRef = if ($env:GITHUB_BASE_REF) { $env:GITHUB_BASE_REF } else { 'main' }
          $workingDir = '${{ inputs.working-directory }}'
          $changed = git diff --name-only --diff-filter=ACMR "origin/$baseRef...HEAD" -- $workingDir |
            Where-Object { $_ -match '(pyproject\.toml|uv\.lock|requirements.*\.txt)$' }
          if ($changed) {
            "HAS_CHANGES=true" >> $env:GITHUB_ENV
            $fileCount = @($changed).Count
            Write-Output "Detected $fileCount changed dependency file(s)"
          } else {
            Write-Output "No dependency file changes detected under $workingDir"
          }

      - name: Install uv
        if: "!inputs.changed-files-only || env.HAS_CHANGES == 'true'"
        uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
        with:
          version: "0.10.9"

      - name: Setup Python
        if: "!inputs.changed-files-only || env.HAS_CHANGES == 'true'"
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.12"

      - name: Export locked dependencies
        if: "!inputs.changed-files-only || env.HAS_CHANGES == 'true'"
        run: |
          uv export --format requirements-txt --no-hashes > "$env:RUNNER_TEMP/requirements.txt"
          $lineCount = (Get-Content "$env:RUNNER_TEMP/requirements.txt" | Measure-Object).Count
          Write-Output "Exported $lineCount locked dependencies"
        working-directory: ${{ inputs.working-directory }}

      - name: Run pip-audit
        id: audit
        if: "!inputs.changed-files-only || env.HAS_CHANGES == 'true'"
        run: |
          $name = '${{ steps.meta.outputs.artifact-name }}'
          New-Item -ItemType Directory -Path "$env:GITHUB_WORKSPACE/logs" -Force | Out-Null
          $ignoreArgs = @()
          $knownVulnFile = 'pip-audit-known-vulnerabilities.txt'
          if (Test-Path $knownVulnFile) {
            $vulns = Get-Content $knownVulnFile | ForEach-Object { $_.Trim() } |
              Where-Object { $_ -and -not $_.StartsWith('#') }
            foreach ($vuln in $vulns) { $ignoreArgs += '--ignore-vuln'; $ignoreArgs += $vuln }
            Write-Output "Ignoring $($vulns.Count) known vulnerability ID(s)"
          }
          uvx pip-audit@2.10.0 `
            -r "$env:RUNNER_TEMP/requirements.txt" `
            --no-deps `
            --format json `
            -o "$env:GITHUB_WORKSPACE/logs/$name.json" `
            --desc on `
            --aliases on `
            --progress-spinner off `
            --strict `
            @ignoreArgs
          if ($LASTEXITCODE -ne 0) { "PIP_AUDIT_FAILED=true" >> $env:GITHUB_ENV }
          $global:LASTEXITCODE = 0
        working-directory: ${{ inputs.working-directory }}
        continue-on-error: ${{ inputs.soft-fail }}

      - name: Upload audit results
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        if: always() && (!inputs.changed-files-only || env.HAS_CHANGES == 'true')
        with:
          name: ${{ steps.meta.outputs.artifact-name }}
          path: logs/${{ steps.meta.outputs.artifact-name }}.json
          retention-days: 30
          if-no-files-found: ignore

      - name: Check results
        if: "!inputs.soft-fail && (!inputs.changed-files-only || env.HAS_CHANGES == 'true')"
        run: |
          if ($env:PIP_AUDIT_FAILED -eq 'true') {
            Write-Output '::error::pip-audit found vulnerabilities in Python dependencies'
            exit 1
          }
          Write-Output 'pip-audit: no vulnerabilities found'
