Improve dependency checks workflow
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 6 Feb 2026 13:42:29 +0000 (14:42 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 6 Feb 2026 13:42:29 +0000 (14:42 +0100)
.github/workflows/dependency-approval-command.yml [new file with mode: 0644]
.github/workflows/dependency-security.yml
scripts/parse_manifest_deps.py

diff --git a/.github/workflows/dependency-approval-command.yml b/.github/workflows/dependency-approval-command.yml
new file mode 100644 (file)
index 0000000..cdb3779
--- /dev/null
@@ -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');
index 71bd1d347ffeefb2783a6b8ebdf86f26658eea73..6ae92202ab0e6439e22c43e0b9316c84e707a069 100644 (file)
@@ -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
index 9addfa63854a28ef7116ddf563bb71395bb0c061..84c0cb3ad34980d25d7917b0f22c841594c817f8 100755 (executable)
@@ -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("<summary>Unchanged dependencies</summary>")
         print()
         for req in sorted(unchanged):
-            print(f"- `{req}`")
+            print(format_with_link(req, ""))
         print()
         print("</details>")