--- /dev/null
+# Dependency Approval via Comment Command
+# Allows maintainers to approve dependency changes by commenting /approve-dependencies
+
+name: Dependency Approval Command
+
+on:
+ issue_comment:
+ types: [created]
+
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ approve-via-command:
+ runs-on: ubuntu-latest
+ # Only run on PRs, not issues
+ if: github.event.issue.pull_request
+ steps:
+ - name: Check for approval command
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const comment = context.payload.comment;
+ const commentBody = comment.body.trim();
+
+ // Check if comment contains the approval command
+ if (!commentBody.match(/^\/approve-dependencies$/m)) {
+ core.info('Not an approval command, skipping');
+ return;
+ }
+
+ // Check if the comment creator is a maintainer/admin
+ const userPermission = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: comment.user.login
+ });
+
+ const hasPermission = ['admin', 'write'].includes(userPermission.data.permission);
+
+ if (!hasPermission) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `❌ @${comment.user.login} does not have permission to approve dependencies. Only maintainers with write access can approve.`
+ });
+ return;
+ }
+
+ // Check if already approved
+ const labels = context.payload.issue.labels.map(l => l.name);
+ const alreadyApproved = labels.includes('dependencies-reviewed');
+
+ if (alreadyApproved) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `ℹ️ Dependencies already approved - \`dependencies-reviewed\` label is present.`
+ });
+ return;
+ }
+
+ // Add the dependencies-reviewed label
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ labels: ['dependencies-reviewed']
+ });
+
+ // Add a confirmation comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: `✅ **Dependencies approved** by @${comment.user.login}\n\nThe \`dependencies-reviewed\` label has been added. Security checks will now pass and this PR can be merged.`
+ });
+
+ // Add a reaction to the command comment
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: comment.id,
+ content: '+1'
+ });
+
+ core.info('✅ Dependencies approved and label added');
on:
pull_request_target:
+ types: [opened, synchronize, reopened, labeled]
paths:
- "requirements_all.txt"
- "**/manifest.json"
+ - "pyproject.toml"
branches:
- stable
- dev
run: |
pip install pip-audit
- # Step 1: Run pip-audit for known vulnerabilities
+ # Step 1: Verify requirements_all.txt is in sync
+ - name: Check requirements_all.txt sync
+ id: req_sync
+ run: |
+ # Save current requirements_all.txt
+ cp requirements_all.txt requirements_all.txt.original
+
+ # Regenerate requirements_all.txt
+ python3 scripts/gen_requirements_all.py
+
+ # Check if it changed
+ if ! diff -q requirements_all.txt.original requirements_all.txt > /dev/null; then
+ echo "status=out_of_sync" >> $GITHUB_OUTPUT
+ echo "⚠️ **requirements_all.txt is out of sync**" > sync_report.md
+ echo "" >> sync_report.md
+ echo "The \`requirements_all.txt\` file should be auto-generated from \`pyproject.toml\` and provider manifests." >> sync_report.md
+ echo "" >> sync_report.md
+ echo "**Action required:** Run \`python scripts/gen_requirements_all.py\` and commit the changes." >> sync_report.md
+ # Restore original
+ mv requirements_all.txt.original requirements_all.txt
+ else
+ echo "status=synced" >> $GITHUB_OUTPUT
+ echo "✅ requirements_all.txt is properly synchronized" > sync_report.md
+ rm requirements_all.txt.original
+ fi
+
+ # Step 2: Run pip-audit for known vulnerabilities
- name: Run pip-audit on all requirements
id: pip_audit
continue-on-error: true
run: |
echo "# 🔒 Dependency Security Report" > security_report.md
echo "" >> security_report.md
- echo "Automated security check for dependency changes in this PR." >> security_report.md
- echo "" >> security_report.md
- echo "---" >> security_report.md
- echo "" >> security_report.md
- # Add all report sections
- cat audit_report.md >> security_report.md
- echo "" >> security_report.md
- echo "---" >> security_report.md
- echo "" >> security_report.md
+ if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ] || [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
+ # 1. Show sync status if out of sync
+ if [ "${{ steps.req_sync.outputs.status }}" == "out_of_sync" ]; then
+ cat sync_report.md >> security_report.md
+ echo "" >> security_report.md
+ echo "---" >> security_report.md
+ echo "" >> security_report.md
+ fi
- cat deps_report.md >> security_report.md
- echo "" >> security_report.md
+ # 2. Modified Dependencies Section (consolidated)
+ echo "## 📦 Modified Dependencies" >> security_report.md
+ echo "" >> security_report.md
+
+ # Combine requirements_all.txt and manifest changes
+ HAS_DEPS=false
+
+ if [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
+ cat manifest_report.md | grep -v "^## " >> security_report.md
+ HAS_DEPS=true
+ fi
+
+ if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ]; then
+ if [ "$HAS_DEPS" = "true" ]; then
+ echo "" >> security_report.md
+ fi
+ cat deps_report.md | grep -v "^## " >> security_report.md
+ fi
- if [ -f manifest_report.md ]; then
- echo "---" >> security_report.md
echo "" >> security_report.md
- cat manifest_report.md >> security_report.md
+ echo "---" >> security_report.md
echo "" >> security_report.md
- fi
- if [ -f safety_report.md ]; then
- echo "---" >> security_report.md
+ # 3. Vulnerability Scan Results
+ cat audit_report.md >> security_report.md
echo "" >> security_report.md
- cat safety_report.md >> security_report.md
+ echo "---" >> security_report.md
echo "" >> security_report.md
- fi
- echo "---" >> security_report.md
- echo "" >> security_report.md
- echo "## 📋 Security Checks" >> security_report.md
- echo "" >> security_report.md
-
- if [ "${{ steps.deps_check.outputs.has_changes }}" == "true" ] || [ "${{ steps.manifest_check.outputs.has_changes }}" == "true" ]; then
+ # 4. Automated Security Checks
echo "### Automated Security Checks" >> security_report.md
echo "" >> security_report.md
if [ "${{ steps.pr_type.outputs.is_automated }}" == "true" ]; then
echo "_Automated PRs with all checks passing will be auto-approved._" >> security_report.md
else
- echo "_After review, add the **\`dependencies-reviewed\`** label to approve this PR._" >> security_report.md
+ echo "**To approve:** Comment \`/approve-dependencies\` or manually add the \`dependencies-reviewed\` label." >> security_report.md
fi
else
echo "✅ No dependency changes detected in this PR." >> security_report.md
# ruff: noqa: T201
import json
+import re
import sys
print("No dependency changes")
return 0
- # Output in markdown format
+ # Helper to extract package name and create PyPI link
+ def format_with_link(req: str, emoji: str) -> str:
+ """Format requirement with PyPI link."""
+ match = re.match(r"^([a-zA-Z0-9_-]+)", req)
+ if match:
+ package = match.group(1)
+ version = req[len(package) :].strip()
+ pypi_url = f"https://pypi.org/project/{package}/"
+ return f"- {emoji} [{package}]({pypi_url}) {version}"
+ return f"- {emoji} {req}"
+
+ # Output in markdown format with PyPI links
if added:
print("**Added:**")
for req in sorted(added):
- print(f"- ✅ `{req}`")
+ print(format_with_link(req, "✅"))
print()
if removed:
print("**Removed:**")
for req in sorted(removed):
- print(f"- ❌ `{req}`")
+ print(format_with_link(req, "❌"))
print()
if unchanged and (added or removed):
print("<summary>Unchanged dependencies</summary>")
print()
for req in sorted(unchanged):
- print(f"- `{req}`")
+ print(format_with_link(req, ""))
print()
print("</details>")