ci: add weekly dependency audit workflow #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Dependency Audit | |
| on: | |
| schedule: | |
| - cron: '0 9 * * 1' | |
| workflow_dispatch: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'pyproject.toml' | |
| - 'uv.lock' | |
| # Self-callout: re-run when this workflow changes so YAML edits are validated in PRs. | |
| - '.github/workflows/dependency-audit.yml' | |
| permissions: | |
| contents: read | |
| env: | |
| PIP_AUDIT_VERSION: '2.9.0' | |
| jobs: | |
| runtime-audit: | |
| name: Runtime Dependency Audit | |
| runs-on: ubuntu-latest | |
| if: github.repository == 'a2aproject/a2a-python' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version-file: .python-version | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| - name: Add uv to PATH | |
| run: | | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| - name: Export Runtime Dependencies | |
| run: | | |
| # pip-audit cannot consume editable requirements with hashes from uv export. | |
| uv export --frozen --format requirements-txt --all-extras --no-dev --no-hashes --no-emit-project -o /tmp/runtime-dependencies.txt > /dev/null | |
| - name: Audit Runtime Dependencies | |
| id: audit | |
| continue-on-error: true | |
| run: | | |
| uvx --from pip-audit==${PIP_AUDIT_VERSION} pip-audit -r /tmp/runtime-dependencies.txt --format json -o /tmp/runtime-audit.json | |
| - name: Summarize Runtime Audit | |
| if: always() | |
| id: summarize | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| audit_path = Path('/tmp/runtime-audit.json') | |
| entries = [] | |
| report_exists = int(audit_path.exists()) | |
| if audit_path.exists(): | |
| payload = json.loads(audit_path.read_text()) | |
| for dependency in payload.get('dependencies', []): | |
| for vuln in dependency.get('vulns', []): | |
| entries.append( | |
| { | |
| 'name': dependency['name'], | |
| 'version': dependency['version'], | |
| 'id': vuln['id'], | |
| 'fixes': ', '.join(vuln.get('fix_versions', [])) or 'n/a', | |
| } | |
| ) | |
| with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output: | |
| output.write(f"count={len(entries)}\n") | |
| output.write(f"report_exists={report_exists}\n") | |
| with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary: | |
| summary.write('## Runtime dependency audit\n\n') | |
| if not entries: | |
| summary.write('No known vulnerabilities found in runtime dependencies.\n') | |
| else: | |
| summary.write(f'Found {len(entries)} vulnerability entries in runtime dependencies.\n\n') | |
| for entry in entries[:20]: | |
| summary.write( | |
| f"- `{entry['name']} {entry['version']}` - `{entry['id']}` (fix: `{entry['fixes']}`)\n" | |
| ) | |
| if len(entries) > 20: | |
| summary.write(f'\n... and {len(entries) - 20} more entries.\n') | |
| PY | |
| - name: Fail on Runtime Vulnerabilities | |
| if: always() && steps.summarize.outputs.count != '0' | |
| run: | | |
| echo "::error title=Runtime dependency vulnerabilities::Found ${{ steps.summarize.outputs.count }} runtime vulnerability entries. See the job summary for details." | |
| exit 1 | |
| - name: Fail on Runtime Audit Errors | |
| if: always() && steps.audit.outcome == 'failure' && steps.summarize.outputs.report_exists != '1' | |
| run: | | |
| echo "::error title=Runtime dependency audit failed::pip-audit did not complete successfully. See the step logs for details." | |
| exit 1 | |
| dev-audit: | |
| name: Development Dependency Audit | |
| runs-on: ubuntu-latest | |
| if: github.repository == 'a2aproject/a2a-python' | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version-file: .python-version | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| - name: Add uv to PATH | |
| run: | | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| - name: Export Development Dependencies | |
| run: | | |
| # This models the dependency set used by local development and CI tooling. | |
| uv export --frozen --format requirements-txt --all-extras --group dev --no-hashes --no-emit-project -o /tmp/development-dependencies.txt > /dev/null | |
| - name: Audit Development Dependencies | |
| id: audit | |
| continue-on-error: true | |
| run: | | |
| uvx --from pip-audit==${PIP_AUDIT_VERSION} pip-audit -r /tmp/development-dependencies.txt --format json -o /tmp/development-audit.json | |
| - name: Summarize Development Audit | |
| if: always() | |
| id: summarize | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| audit_path = Path('/tmp/development-audit.json') | |
| entries = [] | |
| report_exists = int(audit_path.exists()) | |
| if audit_path.exists(): | |
| payload = json.loads(audit_path.read_text()) | |
| for dependency in payload.get('dependencies', []): | |
| for vuln in dependency.get('vulns', []): | |
| entries.append( | |
| { | |
| 'name': dependency['name'], | |
| 'version': dependency['version'], | |
| 'id': vuln['id'], | |
| 'fixes': ', '.join(vuln.get('fix_versions', [])) or 'n/a', | |
| } | |
| ) | |
| with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output: | |
| output.write(f"count={len(entries)}\n") | |
| output.write(f"report_exists={report_exists}\n") | |
| with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary: | |
| summary.write('## Development dependency audit\n\n') | |
| if not entries: | |
| summary.write('No known vulnerabilities found in development dependencies.\n') | |
| else: | |
| summary.write( | |
| f'Found {len(entries)} vulnerability entries in development dependencies. ' | |
| 'This job is informational and does not fail the workflow.\n\n' | |
| ) | |
| for entry in entries[:20]: | |
| summary.write( | |
| f"- `{entry['name']} {entry['version']}` - `{entry['id']}` (fix: `{entry['fixes']}`)\n" | |
| ) | |
| if len(entries) > 20: | |
| summary.write(f'\n... and {len(entries) - 20} more entries.\n') | |
| PY | |
| - name: Emit Development Warning | |
| if: always() && steps.summarize.outputs.count != '0' | |
| run: | | |
| echo "::warning title=Development dependency vulnerabilities::Found ${{ steps.summarize.outputs.count }} development vulnerability entries. See the job summary for details." | |
| - name: Fail on Development Audit Errors | |
| if: always() && steps.audit.outcome == 'failure' && steps.summarize.outputs.report_exists != '1' | |
| run: | | |
| echo "::error title=Development dependency audit failed::pip-audit did not complete successfully. See the step logs for details." | |
| exit 1 |