Skip to content

Commit c6d6075

Browse files
ci: add weekly dependency audit workflow
1 parent 7af6050 commit c6d6075

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
name: Dependency Audit
3+
on:
4+
schedule:
5+
- cron: '0 9 * * 1'
6+
workflow_dispatch:
7+
pull_request:
8+
branches: [main]
9+
paths:
10+
- 'pyproject.toml'
11+
- 'uv.lock'
12+
# Self-callout: re-run when this workflow changes so YAML edits are validated in PRs.
13+
- '.github/workflows/dependency-audit.yml'
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
runtime-audit:
19+
name: Runtime Dependency Audit
20+
runs-on: ubuntu-latest
21+
if: github.repository == 'a2aproject/a2a-python'
22+
steps:
23+
- name: Checkout Code
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
25+
- name: Set up Python
26+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
27+
with:
28+
python-version-file: .python-version
29+
- name: Install uv
30+
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
31+
- name: Add uv to PATH
32+
run: |
33+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
34+
- name: Export Runtime Dependencies
35+
run: |
36+
# pip-audit cannot consume editable requirements with hashes from uv export.
37+
uv export --frozen --format requirements-txt --all-extras --no-dev --no-hashes --no-emit-project -o /tmp/runtime-dependencies.txt > /dev/null
38+
- name: Audit Runtime Dependencies
39+
id: audit
40+
continue-on-error: true
41+
run: |
42+
uvx pip-audit -r /tmp/runtime-dependencies.txt --format json -o /tmp/runtime-audit.json
43+
- name: Summarize Runtime Audit
44+
if: always()
45+
id: summarize
46+
run: |
47+
python - <<'PY'
48+
import json
49+
import os
50+
from pathlib import Path
51+
52+
audit_path = Path('/tmp/runtime-audit.json')
53+
entries = []
54+
report_exists = int(audit_path.exists())
55+
if audit_path.exists():
56+
payload = json.loads(audit_path.read_text())
57+
for dependency in payload.get('dependencies', []):
58+
for vuln in dependency.get('vulns', []):
59+
entries.append(
60+
{
61+
'name': dependency['name'],
62+
'version': dependency['version'],
63+
'id': vuln['id'],
64+
'fixes': ', '.join(vuln.get('fix_versions', [])) or 'n/a',
65+
}
66+
)
67+
68+
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output:
69+
output.write(f"count={len(entries)}\n")
70+
output.write(f"report_exists={report_exists}\n")
71+
72+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary:
73+
summary.write('## Runtime dependency audit\n\n')
74+
if not entries:
75+
summary.write('No known vulnerabilities found in runtime dependencies.\n')
76+
else:
77+
summary.write(f'Found {len(entries)} vulnerability entries in runtime dependencies.\n\n')
78+
for entry in entries[:20]:
79+
summary.write(
80+
f"- `{entry['name']} {entry['version']}` - `{entry['id']}` (fix: `{entry['fixes']}`)\n"
81+
)
82+
if len(entries) > 20:
83+
summary.write(f'\n... and {len(entries) - 20} more entries.\n')
84+
PY
85+
- name: Fail on Runtime Vulnerabilities
86+
if: always() && steps.summarize.outputs.count != '0'
87+
run: |
88+
echo "::error title=Runtime dependency vulnerabilities::Found ${{ steps.summarize.outputs.count }} runtime vulnerability entries. See the job summary for details."
89+
exit 1
90+
- name: Fail on Runtime Audit Errors
91+
if: always() && steps.audit.outcome == 'failure' && steps.summarize.outputs.report_exists != '1'
92+
run: |
93+
echo "::error title=Runtime dependency audit failed::pip-audit did not complete successfully. See the step logs for details."
94+
exit 1
95+
96+
dev-audit:
97+
name: Development Dependency Audit
98+
runs-on: ubuntu-latest
99+
if: github.repository == 'a2aproject/a2a-python'
100+
steps:
101+
- name: Checkout Code
102+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
103+
- name: Set up Python
104+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
105+
with:
106+
python-version-file: .python-version
107+
- name: Install uv
108+
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
109+
- name: Add uv to PATH
110+
run: |
111+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
112+
- name: Export Development Dependencies
113+
run: |
114+
# This models the dependency set used by local development and CI tooling.
115+
uv export --frozen --format requirements-txt --all-extras --group dev --no-hashes --no-emit-project -o /tmp/development-dependencies.txt > /dev/null
116+
- name: Audit Development Dependencies
117+
id: audit
118+
continue-on-error: true
119+
run: |
120+
uvx pip-audit -r /tmp/development-dependencies.txt --format json -o /tmp/development-audit.json
121+
- name: Summarize Development Audit
122+
if: always()
123+
id: summarize
124+
run: |
125+
python - <<'PY'
126+
import json
127+
import os
128+
from pathlib import Path
129+
130+
audit_path = Path('/tmp/development-audit.json')
131+
entries = []
132+
report_exists = int(audit_path.exists())
133+
if audit_path.exists():
134+
payload = json.loads(audit_path.read_text())
135+
for dependency in payload.get('dependencies', []):
136+
for vuln in dependency.get('vulns', []):
137+
entries.append(
138+
{
139+
'name': dependency['name'],
140+
'version': dependency['version'],
141+
'id': vuln['id'],
142+
'fixes': ', '.join(vuln.get('fix_versions', [])) or 'n/a',
143+
}
144+
)
145+
146+
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as output:
147+
output.write(f"count={len(entries)}\n")
148+
output.write(f"report_exists={report_exists}\n")
149+
150+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a', encoding='utf-8') as summary:
151+
summary.write('## Development dependency audit\n\n')
152+
if not entries:
153+
summary.write('No known vulnerabilities found in development dependencies.\n')
154+
else:
155+
summary.write(
156+
f'Found {len(entries)} vulnerability entries in development dependencies. '
157+
'This job is informational and does not fail the workflow.\n\n'
158+
)
159+
for entry in entries[:20]:
160+
summary.write(
161+
f"- `{entry['name']} {entry['version']}` - `{entry['id']}` (fix: `{entry['fixes']}`)\n"
162+
)
163+
if len(entries) > 20:
164+
summary.write(f'\n... and {len(entries) - 20} more entries.\n')
165+
PY
166+
- name: Emit Development Warning
167+
if: always() && steps.summarize.outputs.count != '0'
168+
run: |
169+
echo "::warning title=Development dependency vulnerabilities::Found ${{ steps.summarize.outputs.count }} development vulnerability entries. See the job summary for details."
170+
- name: Fail on Development Audit Errors
171+
if: always() && steps.audit.outcome == 'failure' && steps.summarize.outputs.report_exists != '1'
172+
run: |
173+
echo "::error title=Development dependency audit failed::pip-audit did not complete successfully. See the step logs for details."
174+
exit 1

0 commit comments

Comments
 (0)