From 4c433cc81abc6297e08a5be4d141635ff54c7185 Mon Sep 17 00:00:00 2001 From: Rex P Date: Wed, 22 Apr 2026 13:28:30 +1000 Subject: [PATCH 1/8] Add dependency review plan --- .agents/skills/dependency_review_plan.md | 37 ++++++++ tools/review_dependency_prs.py | 102 +++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 .agents/skills/dependency_review_plan.md create mode 100755 tools/review_dependency_prs.py diff --git a/.agents/skills/dependency_review_plan.md b/.agents/skills/dependency_review_plan.md new file mode 100644 index 00000000000..b72356d5b8e --- /dev/null +++ b/.agents/skills/dependency_review_plan.md @@ -0,0 +1,37 @@ +# Dependency Update Review Plan - google/osv.dev + +This document outlines the automated workflow for reviewing and managing dependency update Pull Requests in the `google/osv.dev` repository. + +## 1. Discovery & Triage +The goal is to identify all open dependency updates. +- **Action**: Use `gh pr list` to fetch PRs with the `dependencies` label. +- **Criteria**: Filter for `state:open`. + +## 2. Analysis & Review +The analysis process has been automated into a deterministic Python script: `tools/review_dependency_prs.py`. It performs the following checks: +- **CI/CD Status**: Analyzes structured JSON output from `gh pr view --json statusCheckRollup` to reliably identify pending or failing checks (ignoring `SUCCESS`, `SKIPPED`, and `NEUTRAL`). +- **Change Scope**: Uses `gh pr diff --name-only` to ensure modifications are restricted to expected files. + - Dependency manifests (`go.mod`, `poetry.lock`, `package.json`, etc.) + - Submodule updates + - Dockerfile and Terraform version updates + - GitHub Actions workflow updates (`.github/workflows/`) +- **Version Analysis**: Inspects the PR's branch name (e.g., looking for `renovate/major-...`) and PR title to identify major semantic version jumps. + +## 3. Reporting +Run the `tools/review_dependency_prs.py` script to generate a final summary report categorized into: +- ✅ **Ready for Submission**: Patch or Minor updates with passing CI and standard file changes. +- ⚠️ **Manual Review Required**: + - Major version upgrades (high risk of breaking changes). + - PRs with failing or pending CI checks. + - PRs modifying files outside the standard dependency manifests. + +## 4. Execution (Submission) +Approved PRs will be processed using the `approve_dependency_prs.sh` script. +The Python script generates an easy-to-paste list of PR numbers. +- **Approval**: Submit a review with an "LGTM" comment. +- **Submission**: Use `gh pr merge --auto --squash` to queue the PR for merging once all requirements are met. + +## 5. Notable Observations & Learnings +- **API Snapshot Tests**: The `PR-api-snapshot-tests` workflow is highly sensitive to transitive dependency changes and minor service updates. Failures here frequently necessitate manual review to ensure output formats haven't unintentionally regressed. +- **Go API Clients**: `google.golang.org/api` updates frequently across multiple services (vulnfeeds, indexer, tools). +- **Renovate Branch Patterns**: Renovate clearly signals major updates in the branch name (e.g., `renovate/major-docs`), which provides a reliable programmatic heuristic for version jumps. diff --git a/tools/review_dependency_prs.py b/tools/review_dependency_prs.py new file mode 100755 index 00000000000..ff27981f45c --- /dev/null +++ b/tools/review_dependency_prs.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Dependency Update PR Reviewer +This script automates the discovery, analysis, and reporting of dependency +update Pull Requests for the google/osv.dev repository. +""" + +import subprocess +import json +import sys + +def run_gh_command(args): + """Executes a GitHub CLI command and returns the standard output.""" + try: + result = subprocess.run(["gh"] + args, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running gh command: {' '.join(args)}\n{e.stderr}", file=sys.stderr) + sys.exit(1) + +def main(): + print("Fetching open dependency PRs...", file=sys.stderr) + prs_output = run_gh_command(["pr", "list", "--label", "dependencies", "--state", "open", "--json", "number,title,headRefName,url"]) + prs = json.loads(prs_output) + + ready_prs = [] + manual_prs = [] + + for pr in prs: + pr_num = pr["number"] + title = pr["title"] + branch = pr["headRefName"] + + print(f"Analyzing PR {pr_num}...", file=sys.stderr) + + # 1. Check CI status using structured JSON rollup + status_output = run_gh_command(["pr", "view", str(pr_num), "--json", "statusCheckRollup"]) + status_data = json.loads(status_output) + checks = status_data.get("statusCheckRollup", []) + + failed_or_pending_checks = [] + for check in checks: + status = check.get("status") + conclusion = check.get("conclusion") + if status != "COMPLETED": + failed_or_pending_checks.append(f"{check.get('name')} (Pending)") + elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): + failed_or_pending_checks.append(f"{check.get('name')} (Failed: {conclusion})") + + # 2. Analyze Version Jump (Heuristic: branch name contains 'major') + is_major = "major" in branch.lower() + + # 3. Analyze files changed + diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) + files_changed = [f for f in diff_output.strip().split('\n') if f] + files_summary = files_changed[0] + ("..." if len(files_changed) > 1 else "") + + # 4. Determine categorization + reasons_for_manual = [] + if failed_or_pending_checks: + reasons_for_manual.append(f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)") + if is_major: + reasons_for_manual.append("Major version jump") + + if reasons_for_manual: + manual_prs.append({ + "number": pr_num, + "title": title, + "reasons": ", ".join(reasons_for_manual) + }) + else: + ready_prs.append({ + "number": pr_num, + "title": title, + "files": files_summary + }) + + # Generate Markdown Report + print("\n### Dependency Update Review Report\n") + + print("#### ✅ Ready for Submission") + print("These PRs are patch or minor updates with passing CI and standard file changes.") + print("\n| PR Number | Title | Files Modified |") + print("| :--- | :--- | :--- |") + for pr in ready_prs: + print(f"| {pr['number']} | {pr['title']} | `{pr['files']}` |") + + print("\n**Submission List (Easy to paste):**") + print("```text") + for pr in ready_prs: + print(pr['number']) + print("```\n") + + print("#### ⚠️ Manual Review Required") + print("These PRs require manual intervention due to major version jumps, unusual modifications, or failing CI checks.") + print("\n| PR Number | Title | Reason for Manual Review |") + print("| :--- | :--- | :--- |") + for pr in manual_prs: + print(f"| {pr['number']} | {pr['title']} | {pr['reasons']} |") + +if __name__ == "__main__": + main() \ No newline at end of file From 090c3da49136306b0ac2dc9a74e055b4966c2a66 Mon Sep 17 00:00:00 2001 From: Rex P Date: Wed, 22 Apr 2026 13:47:31 +1000 Subject: [PATCH 2/8] Fix linting issues --- tools/review_dependency_prs.py | 188 ++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 86 deletions(-) diff --git a/tools/review_dependency_prs.py b/tools/review_dependency_prs.py index ff27981f45c..b0bee1749a5 100755 --- a/tools/review_dependency_prs.py +++ b/tools/review_dependency_prs.py @@ -9,94 +9,110 @@ import json import sys + def run_gh_command(args): - """Executes a GitHub CLI command and returns the standard output.""" - try: - result = subprocess.run(["gh"] + args, capture_output=True, text=True, check=True) - return result.stdout - except subprocess.CalledProcessError as e: - print(f"Error running gh command: {' '.join(args)}\n{e.stderr}", file=sys.stderr) - sys.exit(1) + """Executes a GitHub CLI command and returns the standard output.""" + try: + result = subprocess.run( + ["gh"] + args, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + print( + f"Error running gh command: {' '.join(args)}\n{e.stderr}", + file=sys.stderr) + sys.exit(1) + def main(): - print("Fetching open dependency PRs...", file=sys.stderr) - prs_output = run_gh_command(["pr", "list", "--label", "dependencies", "--state", "open", "--json", "number,title,headRefName,url"]) - prs = json.loads(prs_output) - - ready_prs = [] - manual_prs = [] - - for pr in prs: - pr_num = pr["number"] - title = pr["title"] - branch = pr["headRefName"] - - print(f"Analyzing PR {pr_num}...", file=sys.stderr) - - # 1. Check CI status using structured JSON rollup - status_output = run_gh_command(["pr", "view", str(pr_num), "--json", "statusCheckRollup"]) - status_data = json.loads(status_output) - checks = status_data.get("statusCheckRollup", []) - - failed_or_pending_checks = [] - for check in checks: - status = check.get("status") - conclusion = check.get("conclusion") - if status != "COMPLETED": - failed_or_pending_checks.append(f"{check.get('name')} (Pending)") - elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): - failed_or_pending_checks.append(f"{check.get('name')} (Failed: {conclusion})") - - # 2. Analyze Version Jump (Heuristic: branch name contains 'major') - is_major = "major" in branch.lower() - - # 3. Analyze files changed - diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) - files_changed = [f for f in diff_output.strip().split('\n') if f] - files_summary = files_changed[0] + ("..." if len(files_changed) > 1 else "") - - # 4. Determine categorization - reasons_for_manual = [] - if failed_or_pending_checks: - reasons_for_manual.append(f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)") - if is_major: - reasons_for_manual.append("Major version jump") - - if reasons_for_manual: - manual_prs.append({ - "number": pr_num, - "title": title, - "reasons": ", ".join(reasons_for_manual) - }) - else: - ready_prs.append({ - "number": pr_num, - "title": title, - "files": files_summary - }) - - # Generate Markdown Report - print("\n### Dependency Update Review Report\n") - - print("#### ✅ Ready for Submission") - print("These PRs are patch or minor updates with passing CI and standard file changes.") - print("\n| PR Number | Title | Files Modified |") - print("| :--- | :--- | :--- |") - for pr in ready_prs: - print(f"| {pr['number']} | {pr['title']} | `{pr['files']}` |") - - print("\n**Submission List (Easy to paste):**") - print("```text") - for pr in ready_prs: - print(pr['number']) - print("```\n") - - print("#### ⚠️ Manual Review Required") - print("These PRs require manual intervention due to major version jumps, unusual modifications, or failing CI checks.") - print("\n| PR Number | Title | Reason for Manual Review |") - print("| :--- | :--- | :--- |") - for pr in manual_prs: - print(f"| {pr['number']} | {pr['title']} | {pr['reasons']} |") + """Main entry point.""" + print("Fetching open dependency PRs...", file=sys.stderr) + prs_output = run_gh_command([ + "pr", "list", "--label", "dependencies", "--state", "open", "--json", + "number,title,headRefName,url" + ]) + prs = json.loads(prs_output) + + ready_prs = [] + manual_prs = [] + + for pr in prs: + pr_num = pr["number"] + title = pr["title"] + branch = pr["headRefName"] + + print(f"Analyzing PR {pr_num}...", file=sys.stderr) + + # 1. Check CI status using structured JSON rollup + status_output = run_gh_command( + ["pr", "view", str(pr_num), "--json", "statusCheckRollup"]) + status_data = json.loads(status_output) + checks = status_data.get("statusCheckRollup", []) + + failed_or_pending_checks = [] + for check in checks: + status = check.get("status") + conclusion = check.get("conclusion") + if status != "COMPLETED": + failed_or_pending_checks.append(f"{check.get('name')} (Pending)") + elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): + failed_or_pending_checks.append( + f"{check.get('name')} (Failed: {conclusion})") + + # 2. Analyze Version Jump (Heuristic: branch name contains 'major') + is_major = "major" in branch.lower() + + # 3. Analyze files changed + diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) + files_changed = [f for f in diff_output.strip().split('\n') if f] + files_summary = files_changed[0] + ( + "..." if len(files_changed) > 1 else "") + + # 4. Determine categorization + reasons_for_manual = [] + if failed_or_pending_checks: + reasons_for_manual.append( + f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)") + if is_major: + reasons_for_manual.append("Major version jump") + + if reasons_for_manual: + manual_prs.append({ + "number": pr_num, + "title": title, + "reasons": ", ".join(reasons_for_manual) + }) + else: + ready_prs.append({ + "number": pr_num, + "title": title, + "files": files_summary + }) + + # Generate Markdown Report + print("\n### Dependency Update Review Report\n") + + print("#### ✅ Ready for Submission") + print("These PRs are patch or minor updates with passing CI and " + "standard file changes.") + print("\n| PR Number | Title | Files Modified |") + print("| :--- | :--- | :--- |") + for pr in ready_prs: + print(f"| {pr['number']} | {pr['title']} | `{pr['files']}` |") + + print("\n**Submission List (Easy to paste):**") + print("```text") + for pr in ready_prs: + print(pr['number']) + print("```\n") + + print("#### ⚠️ Manual Review Required") + print("These PRs require manual intervention due to major version jumps, " + "unusual modifications, or failing CI checks.") + print("\n| PR Number | Title | Reason for Manual Review |") + print("| :--- | :--- | :--- |") + for pr in manual_prs: + print(f"| {pr['number']} | {pr['title']} | {pr['reasons']} |") + if __name__ == "__main__": - main() \ No newline at end of file + main() From 8651ad8c2c990a0178864e85924d28ef01cd8b0f Mon Sep 17 00:00:00 2001 From: Rex P Date: Wed, 22 Apr 2026 13:53:08 +1000 Subject: [PATCH 3/8] Fix another lint --- tools/review_dependency_prs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/review_dependency_prs.py b/tools/review_dependency_prs.py index b0bee1749a5..207baa3d5ad 100755 --- a/tools/review_dependency_prs.py +++ b/tools/review_dependency_prs.py @@ -64,8 +64,7 @@ def main(): # 3. Analyze files changed diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) files_changed = [f for f in diff_output.strip().split('\n') if f] - files_summary = files_changed[0] + ( - "..." if len(files_changed) > 1 else "") + files_summary = files_changed[0] + ("..." if len(files_changed) > 1 else "") # 4. Determine categorization reasons_for_manual = [] From 7352e8dc95345280ddb4d37dea003a32cf0bbf67 Mon Sep 17 00:00:00 2001 From: Rex P Date: Wed, 22 Apr 2026 14:29:05 +1000 Subject: [PATCH 4/8] Move to actual SKILLS.md folder --- .../SKILLS.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .agents/skills/{dependency_review_plan.md => dependency_review_plan/SKILLS.md} (100%) diff --git a/.agents/skills/dependency_review_plan.md b/.agents/skills/dependency_review_plan/SKILLS.md similarity index 100% rename from .agents/skills/dependency_review_plan.md rename to .agents/skills/dependency_review_plan/SKILLS.md From c5f84952c811379f5ebc91de336452ea836b1c18 Mon Sep 17 00:00:00 2001 From: Rex P Date: Wed, 22 Apr 2026 15:45:25 +1000 Subject: [PATCH 5/8] Update wording to specify that it doesn't do any writes or merges itself --- .../skills/dependency_review_plan/SKILLS.md | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.agents/skills/dependency_review_plan/SKILLS.md b/.agents/skills/dependency_review_plan/SKILLS.md index b72356d5b8e..ebc0efb107f 100644 --- a/.agents/skills/dependency_review_plan/SKILLS.md +++ b/.agents/skills/dependency_review_plan/SKILLS.md @@ -1,21 +1,21 @@ # Dependency Update Review Plan - google/osv.dev -This document outlines the automated workflow for reviewing and managing dependency update Pull Requests in the `google/osv.dev` repository. +This document outlines the workflow for reviewing and managing dependency update Pull Requests in the `google/osv.dev` repository. ## 1. Discovery & Triage -The goal is to identify all open dependency updates. +Identify all open dependency updates. - **Action**: Use `gh pr list` to fetch PRs with the `dependencies` label. - **Criteria**: Filter for `state:open`. ## 2. Analysis & Review -The analysis process has been automated into a deterministic Python script: `tools/review_dependency_prs.py`. It performs the following checks: -- **CI/CD Status**: Analyzes structured JSON output from `gh pr view --json statusCheckRollup` to reliably identify pending or failing checks (ignoring `SUCCESS`, `SKIPPED`, and `NEUTRAL`). -- **Change Scope**: Uses `gh pr diff --name-only` to ensure modifications are restricted to expected files. +Execute the analysis process using the deterministic Python script: `tools/review_dependency_prs.py`. Perform the following checks: +- **CI/CD Status**: Analyze structured JSON output from `gh pr view --json statusCheckRollup` to reliably identify pending or failing checks (ignoring `SUCCESS`, `SKIPPED`, and `NEUTRAL`). +- **Change Scope**: Use `gh pr diff --name-only` to ensure modifications are restricted to expected files: - Dependency manifests (`go.mod`, `poetry.lock`, `package.json`, etc.) - Submodule updates - Dockerfile and Terraform version updates - GitHub Actions workflow updates (`.github/workflows/`) -- **Version Analysis**: Inspects the PR's branch name (e.g., looking for `renovate/major-...`) and PR title to identify major semantic version jumps. +- **Version Analysis**: Inspect the PR's branch name (e.g., looking for `renovate/major-...`) and PR title to identify major semantic version jumps. ## 3. Reporting Run the `tools/review_dependency_prs.py` script to generate a final summary report categorized into: @@ -25,13 +25,11 @@ Run the `tools/review_dependency_prs.py` script to generate a final summary repo - PRs with failing or pending CI checks. - PRs modifying files outside the standard dependency manifests. -## 4. Execution (Submission) -Approved PRs will be processed using the `approve_dependency_prs.sh` script. -The Python script generates an easy-to-paste list of PR numbers. -- **Approval**: Submit a review with an "LGTM" comment. -- **Submission**: Use `gh pr merge --auto --squash` to queue the PR for merging once all requirements are met. +## 4. Final Review +Present the final summary report to the user. Do not execute any approval or merge commands (e.g., `approve_dependency_prs.sh`, `gh pr review`, or `gh pr merge`). The user will use the provided report to manually trigger any necessary scripts or actions. ## 5. Notable Observations & Learnings -- **API Snapshot Tests**: The `PR-api-snapshot-tests` workflow is highly sensitive to transitive dependency changes and minor service updates. Failures here frequently necessitate manual review to ensure output formats haven't unintentionally regressed. -- **Go API Clients**: `google.golang.org/api` updates frequently across multiple services (vulnfeeds, indexer, tools). -- **Renovate Branch Patterns**: Renovate clearly signals major updates in the branch name (e.g., `renovate/major-docs`), which provides a reliable programmatic heuristic for version jumps. +Consider the following during the review process: +- **API Snapshot Tests**: Monitor the `PR-api-snapshot-tests` workflow, as it is highly sensitive to transitive dependency changes. Manual review is required if it fails to ensure output formats haven't regressed. +- **Go API Clients**: Expect frequent updates to `google.golang.org/api` across multiple services (vulnfeeds, indexer, tools). +- **Renovate Branch Patterns**: Use Renovate branch names (e.g., `renovate/major-docs`) as a reliable heuristic for identifying major version jumps. \ No newline at end of file From 97d1a5f0ffe3120a2a6b18bb1297c3c3c418ed41 Mon Sep 17 00:00:00 2001 From: Rex P Date: Thu, 23 Apr 2026 10:05:21 +1000 Subject: [PATCH 6/8] Refactor the script to make it clearer what it's doing. --- tools/review_dependency_prs.py | 263 +++++++++++++++++++++------------ 1 file changed, 172 insertions(+), 91 deletions(-) diff --git a/tools/review_dependency_prs.py b/tools/review_dependency_prs.py index 207baa3d5ad..7d6054c2acc 100755 --- a/tools/review_dependency_prs.py +++ b/tools/review_dependency_prs.py @@ -5,113 +5,194 @@ update Pull Requests for the google/osv.dev repository. """ -import subprocess +import argparse import json +import subprocess import sys - - -def run_gh_command(args): - """Executes a GitHub CLI command and returns the standard output.""" - try: - result = subprocess.run( - ["gh"] + args, capture_output=True, text=True, check=True) - return result.stdout - except subprocess.CalledProcessError as e: - print( - f"Error running gh command: {' '.join(args)}\n{e.stderr}", - file=sys.stderr) - sys.exit(1) - - -def main(): - """Main entry point.""" - print("Fetching open dependency PRs...", file=sys.stderr) - prs_output = run_gh_command([ - "pr", "list", "--label", "dependencies", "--state", "open", "--json", - "number,title,headRefName,url" - ]) - prs = json.loads(prs_output) - - ready_prs = [] - manual_prs = [] - - for pr in prs: +from typing import Any, Dict, List, Optional + +# List of file patterns or directories that are expected to be modified by +# dependency update tools (e.g., Renovate, Dependabot). +EXPECTED_DEP_FILES = [ + "go.mod", + "go.sum", + "package.json", + "package-lock.json", + "poetry.lock", + "pyproject.toml", + "requirements.txt", + "Dockerfile", + "terraform/", + ".github/workflows/", +] + + +def run_gh_command(args: List[str]) -> str: + """Executes a GitHub CLI command and returns the standard output.""" + try: + result = subprocess.run( + ["gh"] + args, capture_output=True, text=True, check=True + ) + return result.stdout + except subprocess.CalledProcessError as e: + print( + f"Error running gh command: {' '.join(args)}\n{e.stderr}", + file=sys.stderr, + ) + sys.exit(1) + + +def is_expected_file(filename: str) -> bool: + """Checks if a filename matches expected dependency update patterns.""" + for pattern in EXPECTED_DEP_FILES: + if filename.startswith(pattern) or filename.endswith(pattern): + return True + return False + + +def analyze_pr(pr: Dict[str, Any]) -> Dict[str, Any]: + """Analyzes a single PR and returns categorization and reasoning.""" pr_num = pr["number"] - title = pr["title"] branch = pr["headRefName"] + reasons_for_manual = [] - print(f"Analyzing PR {pr_num}...", file=sys.stderr) - - # 1. Check CI status using structured JSON rollup + # 1. Check CI status status_output = run_gh_command( - ["pr", "view", str(pr_num), "--json", "statusCheckRollup"]) + ["pr", "view", str(pr_num), "--json", "statusCheckRollup"] + ) status_data = json.loads(status_output) checks = status_data.get("statusCheckRollup", []) failed_or_pending_checks = [] for check in checks: - status = check.get("status") - conclusion = check.get("conclusion") - if status != "COMPLETED": - failed_or_pending_checks.append(f"{check.get('name')} (Pending)") - elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): - failed_or_pending_checks.append( - f"{check.get('name')} (Failed: {conclusion})") + status = check.get("status") + conclusion = check.get("conclusion") + if status != "COMPLETED": + failed_or_pending_checks.append(f"{check.get('name')} (Pending)") + elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): + failed_or_pending_checks.append( + f"{check.get('name')} (Failed: {conclusion})" + ) - # 2. Analyze Version Jump (Heuristic: branch name contains 'major') - is_major = "major" in branch.lower() + if failed_or_pending_checks: + reasons_for_manual.append( + f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)" + ) + + # 2. Analyze Version Jump + if "major" in branch.lower(): + reasons_for_manual.append("Major version jump") # 3. Analyze files changed diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) - files_changed = [f for f in diff_output.strip().split('\n') if f] - files_summary = files_changed[0] + ("..." if len(files_changed) > 1 else "") + files_changed = [f for f in diff_output.strip().split("\n") if f] + unexpected_files = [f for f in files_changed if not is_expected_file(f)] - # 4. Determine categorization - reasons_for_manual = [] - if failed_or_pending_checks: - reasons_for_manual.append( - f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)") - if is_major: - reasons_for_manual.append("Major version jump") - - if reasons_for_manual: - manual_prs.append({ - "number": pr_num, - "title": title, - "reasons": ", ".join(reasons_for_manual) - }) - else: - ready_prs.append({ - "number": pr_num, - "title": title, - "files": files_summary - }) - - # Generate Markdown Report - print("\n### Dependency Update Review Report\n") - - print("#### ✅ Ready for Submission") - print("These PRs are patch or minor updates with passing CI and " - "standard file changes.") - print("\n| PR Number | Title | Files Modified |") - print("| :--- | :--- | :--- |") - for pr in ready_prs: - print(f"| {pr['number']} | {pr['title']} | `{pr['files']}` |") - - print("\n**Submission List (Easy to paste):**") - print("```text") - for pr in ready_prs: - print(pr['number']) - print("```\n") - - print("#### ⚠️ Manual Review Required") - print("These PRs require manual intervention due to major version jumps, " - "unusual modifications, or failing CI checks.") - print("\n| PR Number | Title | Reason for Manual Review |") - print("| :--- | :--- | :--- |") - for pr in manual_prs: - print(f"| {pr['number']} | {pr['title']} | {pr['reasons']} |") + if unexpected_files: + reasons_for_manual.append(f"Unexpected files: {', '.join(unexpected_files)}") + + files_summary = ( + files_changed[0] + ("..." if len(files_changed) > 1 else "") + if files_changed + else "No files" + ) + + return { + "number": pr_num, + "title": pr["title"], + "url": pr["url"], + "files_summary": files_summary, + "reasons_for_manual": reasons_for_manual, + "is_ready": len(reasons_for_manual) == 0, + } + + +def perform_pr_actions(pr_number: int, approve: bool, merge: bool): + """Performs actions on a PR like approval and enabling auto-merge.""" + if approve: + print(f"Approving PR {pr_number}...", file=sys.stderr) + run_gh_command(["pr", "review", str(pr_number), "--approve", "-b", "LGTM"]) + + if merge: + print(f"Enabling auto-merge for PR {pr_number}...", file=sys.stderr) + run_gh_command(["pr", "merge", str(pr_number), "--auto", "--squash"]) + + +def generate_report(ready_prs: List[Dict], manual_prs: List[Dict]): + """Generates and prints the Markdown report.""" + print("\n### Dependency Update Review Report\n") + + print("#### ✅ Ready for Submission") + print( + "These PRs are patch or minor updates with passing CI and " + "standard file changes." + ) + print("\n| PR Number | Title | Files Modified |") + print("| :--- | :--- | :--- |") + for pr in ready_prs: + print(f"| {pr['number']} | {pr['title']} | `{pr['files_summary']}` |") + + if ready_prs: + print("\n**Submission List (Easy to paste):**") + print("```text") + print(" ".join(str(pr["number"]) for pr in ready_prs)) + print("```\n") + + print("#### ⚠️ Manual Review Required") + print( + "These PRs require manual intervention due to major version jumps, " + "unusual modifications, or failing CI checks." + ) + print("\n| PR Number | Title | Reason for Manual Review |") + print("| :--- | :--- | :--- |") + for pr in manual_prs: + reasons = ", ".join(pr["reasons_for_manual"]) + print(f"| {pr['number']} | {pr['title']} | {reasons} |") + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser(description="Review dependency update PRs.") + parser.add_argument( + "--approve", action="store_true", help="Approve ready PRs (use with caution)" + ) + parser.add_argument( + "--merge", + action="store_true", + help="Enable auto-merge for ready PRs (use with caution)", + ) + args = parser.parse_args() + + print("Fetching open dependency PRs...", file=sys.stderr) + prs_output = run_gh_command( + [ + "pr", + "list", + "--label", + "dependencies", + "--state", + "open", + "--json", + "number,title,headRefName,url", + ] + ) + prs = json.loads(prs_output) + + ready_prs = [] + manual_prs = [] + + for pr_data in prs: + print(f"Analyzing PR {pr_data['number']}...", file=sys.stderr) + analysis = analyze_pr(pr_data) + if analysis["is_ready"]: + ready_prs.append(analysis) + if args.approve or args.merge: + perform_pr_actions(analysis["number"], args.approve, args.merge) + else: + manual_prs.append(analysis) + + generate_report(ready_prs, manual_prs) if __name__ == "__main__": - main() + main() From 06f08cbd98faf923feba3afe996246b2e143bfde Mon Sep 17 00:00:00 2001 From: Rex P Date: Fri, 24 Apr 2026 14:00:55 +1000 Subject: [PATCH 7/8] Fix formatting --- tools/review_dependency_prs.py | 290 ++++++++++++++++----------------- 1 file changed, 140 insertions(+), 150 deletions(-) diff --git a/tools/review_dependency_prs.py b/tools/review_dependency_prs.py index 7d6054c2acc..dcf1896f725 100755 --- a/tools/review_dependency_prs.py +++ b/tools/review_dependency_prs.py @@ -9,7 +9,7 @@ import json import subprocess import sys -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List # List of file patterns or directories that are expected to be modified by # dependency update tools (e.g., Renovate, Dependabot). @@ -28,171 +28,161 @@ def run_gh_command(args: List[str]) -> str: - """Executes a GitHub CLI command and returns the standard output.""" - try: - result = subprocess.run( - ["gh"] + args, capture_output=True, text=True, check=True - ) - return result.stdout - except subprocess.CalledProcessError as e: - print( - f"Error running gh command: {' '.join(args)}\n{e.stderr}", - file=sys.stderr, - ) - sys.exit(1) + """Executes a GitHub CLI command and returns the standard output.""" + try: + result = subprocess.run( + ["gh"] + args, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + print( + f"Error running gh command: {' '.join(args)}\n{e.stderr}", + file=sys.stderr, + ) + sys.exit(1) def is_expected_file(filename: str) -> bool: - """Checks if a filename matches expected dependency update patterns.""" - for pattern in EXPECTED_DEP_FILES: - if filename.startswith(pattern) or filename.endswith(pattern): - return True - return False + """Checks if a filename matches expected dependency update patterns.""" + for pattern in EXPECTED_DEP_FILES: + if filename.startswith(pattern) or filename.endswith(pattern): + return True + return False def analyze_pr(pr: Dict[str, Any]) -> Dict[str, Any]: - """Analyzes a single PR and returns categorization and reasoning.""" - pr_num = pr["number"] - branch = pr["headRefName"] - reasons_for_manual = [] - - # 1. Check CI status - status_output = run_gh_command( - ["pr", "view", str(pr_num), "--json", "statusCheckRollup"] - ) - status_data = json.loads(status_output) - checks = status_data.get("statusCheckRollup", []) - - failed_or_pending_checks = [] - for check in checks: - status = check.get("status") - conclusion = check.get("conclusion") - if status != "COMPLETED": - failed_or_pending_checks.append(f"{check.get('name')} (Pending)") - elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): - failed_or_pending_checks.append( - f"{check.get('name')} (Failed: {conclusion})" - ) - - if failed_or_pending_checks: - reasons_for_manual.append( - f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)" - ) - - # 2. Analyze Version Jump - if "major" in branch.lower(): - reasons_for_manual.append("Major version jump") - - # 3. Analyze files changed - diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) - files_changed = [f for f in diff_output.strip().split("\n") if f] - unexpected_files = [f for f in files_changed if not is_expected_file(f)] - - if unexpected_files: - reasons_for_manual.append(f"Unexpected files: {', '.join(unexpected_files)}") - - files_summary = ( - files_changed[0] + ("..." if len(files_changed) > 1 else "") - if files_changed - else "No files" - ) - - return { - "number": pr_num, - "title": pr["title"], - "url": pr["url"], - "files_summary": files_summary, - "reasons_for_manual": reasons_for_manual, - "is_ready": len(reasons_for_manual) == 0, - } + """Analyzes a single PR and returns categorization and reasoning.""" + pr_num = pr["number"] + branch = pr["headRefName"] + reasons_for_manual = [] + + # 1. Check CI status + status_output = run_gh_command( + ["pr", "view", str(pr_num), "--json", "statusCheckRollup"]) + status_data = json.loads(status_output) + checks = status_data.get("statusCheckRollup", []) + + failed_or_pending_checks = [] + for check in checks: + status = check.get("status") + conclusion = check.get("conclusion") + if status != "COMPLETED": + failed_or_pending_checks.append(f"{check.get('name')} (Pending)") + elif conclusion not in ("SUCCESS", "SKIPPED", "NEUTRAL"): + failed_or_pending_checks.append( + f"{check.get('name')} (Failed: {conclusion})") + + if failed_or_pending_checks: + reasons_for_manual.append( + f"Failing/Pending CI ({len(failed_or_pending_checks)} checks)") + + # 2. Analyze Version Jump + if "major" in branch.lower(): + reasons_for_manual.append("Major version jump") + + # 3. Analyze files changed + diff_output = run_gh_command(["pr", "diff", str(pr_num), "--name-only"]) + files_changed = [f for f in diff_output.strip().split("\n") if f] + unexpected_files = [f for f in files_changed if not is_expected_file(f)] + + if unexpected_files: + reasons_for_manual.append( + f"Unexpected files: {', '.join(unexpected_files)}") + + files_summary = ( + files_changed[0] + ("..." if len(files_changed) > 1 else "") + if files_changed else "No files") + + return { + "number": pr_num, + "title": pr["title"], + "url": pr["url"], + "files_summary": files_summary, + "reasons_for_manual": reasons_for_manual, + "is_ready": len(reasons_for_manual) == 0, + } def perform_pr_actions(pr_number: int, approve: bool, merge: bool): - """Performs actions on a PR like approval and enabling auto-merge.""" - if approve: - print(f"Approving PR {pr_number}...", file=sys.stderr) - run_gh_command(["pr", "review", str(pr_number), "--approve", "-b", "LGTM"]) + """Performs actions on a PR like approval and enabling auto-merge.""" + if approve: + print(f"Approving PR {pr_number}...", file=sys.stderr) + run_gh_command(["pr", "review", str(pr_number), "--approve", "-b", "LGTM"]) - if merge: - print(f"Enabling auto-merge for PR {pr_number}...", file=sys.stderr) - run_gh_command(["pr", "merge", str(pr_number), "--auto", "--squash"]) + if merge: + print(f"Enabling auto-merge for PR {pr_number}...", file=sys.stderr) + run_gh_command(["pr", "merge", str(pr_number), "--auto", "--squash"]) def generate_report(ready_prs: List[Dict], manual_prs: List[Dict]): - """Generates and prints the Markdown report.""" - print("\n### Dependency Update Review Report\n") - - print("#### ✅ Ready for Submission") - print( - "These PRs are patch or minor updates with passing CI and " - "standard file changes." - ) - print("\n| PR Number | Title | Files Modified |") - print("| :--- | :--- | :--- |") - for pr in ready_prs: - print(f"| {pr['number']} | {pr['title']} | `{pr['files_summary']}` |") - - if ready_prs: - print("\n**Submission List (Easy to paste):**") - print("```text") - print(" ".join(str(pr["number"]) for pr in ready_prs)) - print("```\n") - - print("#### ⚠️ Manual Review Required") - print( - "These PRs require manual intervention due to major version jumps, " - "unusual modifications, or failing CI checks." - ) - print("\n| PR Number | Title | Reason for Manual Review |") - print("| :--- | :--- | :--- |") - for pr in manual_prs: - reasons = ", ".join(pr["reasons_for_manual"]) - print(f"| {pr['number']} | {pr['title']} | {reasons} |") + """Generates and prints the Markdown report.""" + print("\n### Dependency Update Review Report\n") + + print("#### ✅ Ready for Submission") + print("These PRs are patch or minor updates with passing CI and " + "standard file changes.") + print("\n| PR Number | Title | Files Modified |") + print("| :--- | :--- | :--- |") + for pr in ready_prs: + print(f"| {pr['number']} | {pr['title']} | `{pr['files_summary']}` |") + + if ready_prs: + print("\n**Submission List (Easy to paste):**") + print("```text") + print(" ".join(str(pr["number"]) for pr in ready_prs)) + print("```\n") + + print("#### ⚠️ Manual Review Required") + print("These PRs require manual intervention due to major version jumps, " + "unusual modifications, or failing CI checks.") + print("\n| PR Number | Title | Reason for Manual Review |") + print("| :--- | :--- | :--- |") + for pr in manual_prs: + reasons = ", ".join(pr["reasons_for_manual"]) + print(f"| {pr['number']} | {pr['title']} | {reasons} |") def main(): - """Main entry point.""" - parser = argparse.ArgumentParser(description="Review dependency update PRs.") - parser.add_argument( - "--approve", action="store_true", help="Approve ready PRs (use with caution)" - ) - parser.add_argument( - "--merge", - action="store_true", - help="Enable auto-merge for ready PRs (use with caution)", - ) - args = parser.parse_args() - - print("Fetching open dependency PRs...", file=sys.stderr) - prs_output = run_gh_command( - [ - "pr", - "list", - "--label", - "dependencies", - "--state", - "open", - "--json", - "number,title,headRefName,url", - ] - ) - prs = json.loads(prs_output) - - ready_prs = [] - manual_prs = [] - - for pr_data in prs: - print(f"Analyzing PR {pr_data['number']}...", file=sys.stderr) - analysis = analyze_pr(pr_data) - if analysis["is_ready"]: - ready_prs.append(analysis) - if args.approve or args.merge: - perform_pr_actions(analysis["number"], args.approve, args.merge) - else: - manual_prs.append(analysis) - - generate_report(ready_prs, manual_prs) + """Main entry point.""" + parser = argparse.ArgumentParser(description="Review dependency update PRs.") + parser.add_argument( + "--approve", + action="store_true", + help="Approve ready PRs (use with caution)") + parser.add_argument( + "--merge", + action="store_true", + help="Enable auto-merge for ready PRs (use with caution)", + ) + args = parser.parse_args() + + print("Fetching open dependency PRs...", file=sys.stderr) + prs_output = run_gh_command([ + "pr", + "list", + "--label", + "dependencies", + "--state", + "open", + "--json", + "number,title,headRefName,url", + ]) + prs = json.loads(prs_output) + + ready_prs = [] + manual_prs = [] + + for pr_data in prs: + print(f"Analyzing PR {pr_data['number']}...", file=sys.stderr) + analysis = analyze_pr(pr_data) + if analysis["is_ready"]: + ready_prs.append(analysis) + if args.approve or args.merge: + perform_pr_actions(analysis["number"], args.approve, args.merge) + else: + manual_prs.append(analysis) + + generate_report(ready_prs, manual_prs) if __name__ == "__main__": - main() + main() From b70df7d76a977246133f227522762971df1d8ae1 Mon Sep 17 00:00:00 2001 From: Rex P Date: Fri, 24 Apr 2026 14:04:55 +1000 Subject: [PATCH 8/8] Move to the correct name again --- .agents/skills/dependency_review_plan/{SKILLS.md => SKILL.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .agents/skills/dependency_review_plan/{SKILLS.md => SKILL.md} (100%) diff --git a/.agents/skills/dependency_review_plan/SKILLS.md b/.agents/skills/dependency_review_plan/SKILL.md similarity index 100% rename from .agents/skills/dependency_review_plan/SKILLS.md rename to .agents/skills/dependency_review_plan/SKILL.md