name: Lint & Security on: pull_request: workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: precommit-and-security: name: lint-and-security runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.13" - name: Install Poetry run: pip install "poetry>=2,<3" - name: Install dev dependencies (lockfile-pinned pre-commit) run: poetry install --with dev --no-root - name: Run pre-commit hooks run: poetry run pre-commit run --all-files --color always - name: Install pip-audit run: pip install "pip-audit==2.10.1" - name: Audit dependencies (Poetry lockfile) run: | set -euo pipefail poetry self add poetry-plugin-export poetry export -f requirements.txt --without-hashes -o requirements-audit.txt # pip-audit exits non-zero for ANY vulnerability; capture the report, then gate below. pip-audit --no-deps -r requirements-audit.txt -f json -o pip-audit.json || true echo "Fixable vulnerabilities:" jq -r ' .dependencies[]? | select(has("vulns")) | . as $d | .vulns[] | select((.fix_versions | length) > 0) | "\($d.name)@\($d.version) -> \(.id) [fix: \(.fix_versions | join(", "))]" ' pip-audit.json echo echo "All reported vulnerabilities:" jq -r ' .dependencies[]? | select(has("vulns")) | . as $d | .vulns[] | "\($d.name)@\($d.version) -> \(.id) [fix: \(if (.fix_versions | length) > 0 then (.fix_versions | join(", ")) else "none" end)]" ' pip-audit.json # Fail only on fixable findings (mirrors the Grype policy in security-scan.yml). # The type guard ensures a crashed/empty report fails rather than silently passing. jq -e ' (.dependencies | type) == "array" and ( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 ) ' pip-audit.json