From c875ca50040812c7a51a9d291c0dcc19bcfe0eed Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 6 Feb 2026 14:42:29 +0100 Subject: [PATCH] Improve dependency checks workflow --- .../workflows/dependency-approval-command.yml | 90 +++++++++++++++++++ .github/workflows/dependency-security.yml | 88 ++++++++++++------ scripts/parse_manifest_deps.py | 20 ++++- 3 files changed, 167 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/dependency-approval-command.yml diff --git a/.github/workflows/dependency-approval-command.yml b/.github/workflows/dependency-approval-command.yml new file mode 100644 index 00000000..cdb37797 --- /dev/null +++ b/.github/workflows/dependency-approval-command.yml @@ -0,0 +1,90 @@ +# 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'); diff --git a/.github/workflows/dependency-security.yml b/.github/workflows/dependency-security.yml index 71bd1d34..6ae92202 100644 --- a/.github/workflows/dependency-security.yml +++ b/.github/workflows/dependency-security.yml @@ -5,9 +5,11 @@ name: Dependency Security Check on: pull_request_target: + types: [opened, synchronize, reopened, labeled] paths: - "requirements_all.txt" - "**/manifest.json" + - "pyproject.toml" branches: - stable - dev @@ -52,7 +54,33 @@ jobs: 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 @@ -232,40 +260,46 @@ jobs: 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 @@ -324,7 +358,7 @@ jobs: 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 diff --git a/scripts/parse_manifest_deps.py b/scripts/parse_manifest_deps.py index 9addfa63..84c0cb3a 100755 --- a/scripts/parse_manifest_deps.py +++ b/scripts/parse_manifest_deps.py @@ -7,6 +7,7 @@ to identify changes in the requirements field. # ruff: noqa: T201 import json +import re import sys @@ -56,17 +57,28 @@ def main() -> int: 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): @@ -74,7 +86,7 @@ def main() -> int: print("Unchanged dependencies") print() for req in sorted(unchanged): - print(f"- `{req}`") + print(format_with_link(req, "")) print() print("") -- 2.34.1