From bf72c5d2a47e7890bc9f4ddf5fc3856adc3dc72d Mon Sep 17 00:00:00 2001 From: d33bs Date: Fri, 9 Oct 2026 11:06:02 -0600 Subject: [PATCH 1/3] add dependabot pr triage skill --- .../skills/gh-triage-dependabot-prs/SKILL.md | 195 ++++++++++++++++++ .../scripts/snapshot.py | 177 ++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 .agents/skills/gh-triage-dependabot-prs/SKILL.md create mode 100755 .agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py diff --git a/.agents/skills/gh-triage-dependabot-prs/SKILL.md b/.agents/skills/gh-triage-dependabot-prs/SKILL.md new file mode 100644 index 0000000..820f201 --- /dev/null +++ b/.agents/skills/gh-triage-dependabot-prs/SKILL.md @@ -0,0 +1,195 @@ +--- +name: gh-triage-dependabot-prs +description: Safely review and merge Dependabot PRs requesting your review. +version: 0.1.0 +author: Dave Bunten (d33bs), Hermes Agent +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [dependabot, github, pull-requests, ci, triage] + related_skills: [] +--- + +# Dependabot PR Triage + +Safely triage open GitHub pull requests that request review from the current +user and are authored by the Dependabot GitHub App: inspect CI, classify each +PR into a risk tier, approve and merge green dependency updates, and apply +narrowly scoped mechanical `prek` fixes to eligible CI failures. Every GitHub +mutation stays inside the explicitly authorized Dependabot PR scope; a request +to inspect or report never authorizes reviews, pushes, or merges. + +## When to Use + +- Asked to process, recheck, approve, merge, or report on Dependabot PRs + requesting the current user's review, including PRs the user explicitly names + and directs you to process. +- Asked to apply pre-commit (`prek`) fixes to a Dependabot PR whose CI failed + on a mechanical formatting hook. +- Don't use for non-Dependabot PRs or generic dependency upgrades outside a PR + triage workflow. + +## Prerequisites + +- GitHub CLI (`gh`) authenticated as the intended user; `repo` token scope for + private repositories and PR mutations, `workflow` scope when a PR changes + `.github/workflows/`. Confirm with `terminal(command="gh auth status")`. +- Python 3 for the read-only snapshot helper. +- `prek` when local hook execution is needed. + +## Procedure + +1. Run `gh auth status`; require `repo`, and `workflow` before merging a PR + that changes `.github/workflows/`. Resolve the login with + `gh api user --jq .login`. +2. Run the read-only snapshot helper from this skill directory for a + candidate list: + `python3 scripts/snapshot.py` (add `--compact` for single-line JSON). + Inspect each entry's changed paths, check rollup, CI classification, and + risk hints. +3. Reconfirm each candidate is open, requests review from the authenticated + user, and uses the Dependabot GitHub App identity (`dependabot[bot]` in + search results or `app/dependabot` in PR metadata). Never select by title + alone. A PR the user explicitly names and directs you to process satisfies + the authorization requirement even without a review request; record that + the PR was user-directed. +4. Do not approve, commit, push, or merge unless the user explicitly + requested those actions. +5. Classify every candidate into a risk tier before approving or merging: + - Routine: patch/minor bumps, single-package lockfile updates, and grouped + updates whose changed paths are lockfiles or hook-rev-only + `.pre-commit-config.yaml` edits with no removed-option usage. Green CI + is sufficient evidence. + - Major: any major version bump, grouped update containing one, transitive + major change visible in the lockfile, or security-driven update with + documented behavior changes. Green CI is necessary but not sufficient — + perform a substantive review first: read the upstream release notes, + identify removed APIs or behavior changes, and search the repository for + usages that would break. Merge only when no affected usage exists. If a + risky path is plausibly exercised but not covered by CI, report instead + of merging. + - Report-only: changes to `.github/workflows/` beyond action-version + bumps, permission or security-policy changes, semantic source changes, + deployment changes, and ambiguous grouped updates. Do not merge these on + green CI alone; describe the diff and wait for explicit user direction. +6. A user instruction can promote a PR across tiers ("major updates are fine + if green", "workflow bumps are fine if green"); the promotion applies only + to that session and that risk class. Never treat a blanket approval as + covering report-only items the user did not mention. +7. Approve or merge only when all of these are true (the merge gate): + - The PR is not a draft and has at least one completed GitHub Actions CI + check; an external-only status (for example CodeRabbit) does not satisfy + this requirement. + - Every check run is successful, skipped, or neutral, and every external + status context is successful; no check is pending, action-required, + cancelled, timed out, or failing. GitHub's `statusCheckRollup` uses + `status`/`conclusion` for check runs but `state` for status contexts, so + do not treat a context's absent `status` as pending. + - The tested head SHA still equals the current head SHA. + - The authenticated user still has a pending review request on the PR, or + has already approved it — `gh pr review --approve` consumes the review + request, so a re-checked gate must accept a prior approval — and the PR + uses the Dependabot GitHub App identity. + - The changed files are consistent with Dependabot dependency maintenance, + including manifests, lockfiles, hook configuration, GitHub Actions + workflow/action version bumps, or Dependabot-managed grouped updates. + - The risk-tier classification allows merging: routine on green CI, major + only after the substantive review found no affected usage, and workflow + files only when the diff is exclusively action-version ref bumps or the + user promoted the class. + Leave missing-check, pending-check, failing-check, non-Dependabot, + non-review-requested (and not user-directed), draft, ambiguous semantic + source-change, and merge-conflict PRs untouched. Report the reason. +8. For each candidate that passes the merge gate: record the exact head SHA + and inspect changed files enough to confirm the PR is Dependabot + dependency maintenance; re-read the check rollup immediately before + mutation; approve with + `gh pr review --approve --body "CI checks pass."`; merge with a + merge commit — `gh pr merge --merge --match-head-commit ` — to + avoid synthesized co-author trailers. Never use squash merge. If + repository policy forbids merge commits, stop and report instead of + silently choosing another method. Verify `state`, `mergedBy`, `mergedAt`, + and `mergeCommit` with `gh pr view`. +9. Stuck mergeability: if `gh pr merge` fails with "Head branch is out of + date" but `gh api repos///compare/...` reports + `behind_by` 0 and the base branch is unprotected, the PR is stuck in + GitHub's `mergeable: null` recomputation, not genuinely stale. Do not + force; re-fetch to prompt recomputation and report if it persists. + Merging several PRs into the same base in quick succession commonly + triggers a transient `mergeable: UNKNOWN`/`BLOCKED` on the next sibling + while GitHub recomputes — re-fetch once and retry the same guarded gate; + never widen the gate. Leave conflicted (`DIRTY`/`CONFLICTING`) PRs + untouched and report them. `@dependabot rebase`/`recreate` are mutations; + post them only with explicit user authorization, and re-run the full merge + gate afterward. +10. Diagnose failed CI before any repair: read failed GitHub Actions logs + before cloning, and treat external checks as report-only unless + separately authorized. For failed `prek`, `pre-commit`, lint, or + formatting checks, check whether one additional mechanical `prek` commit + can plausibly make CI pass. The canonical eligible case is a + `prek`/`pre_commit_checks` failure whose log shows *files were modified + by this hook* together with *skip: triggered by a bot* (from + `pre-commit-ci/lite-action`): a formatter reformatted files and the fix + was never committed because the PR is bot-authored. Eligible fixes are + deterministic whitespace/EOF/format changes, formatter normalization, and + narrowly extending an existing lint exception when a tool update split an + already-ignored rule. Ineligible: changing function signatures, runtime + logic, tests, workflow behavior, permissions, dependency selections, or + broad lint policy merely to make CI green. Leave unrelated, flaky, + infrastructure, documentation-build, or conditional-skip failures + untouched and report them. +11. Apply an eligible `prek` fix only when the user authorized it: + confirm the head repository is writable and the remote branch has not + moved; clone the exact Dependabot branch into a dedicated temporary + directory (large repositories sequentially); from the failed CI log, + identify the hook(s) that modified files and run just that formatter + (`prek run --all-files`) using the repository's pinned/preferred + runner instead of invoking heavy hooks that need the built project; + inspect every generated diff and revert anything outside the eligible + mechanical scope; rerun the affected hook (or the complete + `prek run --all-files` when the environment can run every hook) until it + reports no further changes, then run `git diff --check`; confirm the + remote head still matches the cloned parent before committing; commit + under the user's configured Git identity with a concise message and no + `Co-authored-by` or other attribution trailers, verifying author, + committer, subject, and body with `git show -s`; push only to the exact + existing Dependabot branch; delete only the dedicated temporary checkout, + never performing broad cache or filesystem deletion without resolving + exact targets first. +12. Recheck and finish: monitor the newly created check rollup and wait for + newly-triggered downstream jobs — a fixed gating check can start jobs + that were previously skipped, so an empty or all-skipped rollup right + after the push is not yet a pass. If it becomes green and still passes + the merge gate, approve and merge against the exact pushed SHA. Otherwise + leave it open. + +## Pitfalls + +- `gh pr review --approve` consumes the review request; a re-run of the merge + gate must accept a prior approval rather than waiting for a new pending + request. +- External-only success (for example CodeRabbit) is not CI; require at least + one completed GitHub Actions check run. +- `statusCheckRollup` mixes check runs (`status`/`conclusion`) and status + contexts (`state`); a context's absent `status` field is not "pending". +- Green CI alone never justifies merging a major or report-only change. +- Do not treat a blanket user approval as covering report-only classes the + user did not mention; promotions are per session and per risk class. +- CI on the pushed branch is the authoritative full-suite check; a local + `prek` run of a single hook is only a reproduction, not the final gate. + +## Verification + +- For every merged PR, read back `state`, `mergedBy`, `mergedAt`, and + `mergeCommit` with `gh pr view` and confirm the merge method was a merge + commit with no co-author trailers. +- For every fixed-and-merged PR, confirm the final CI rollup is green on the + exact pushed SHA. +- Report per PR: URL, dependency scope, risk tier and (for majors) the + substantive-review evidence, initial CI state, action taken, fix commit SHA + if any, final CI state, merge commit if any, and the reason every untouched + PR was left alone. State explicitly that non-Dependabot PRs were untouched + and whether all commits lack co-author trailers. +- `scripts/snapshot.py` stays read-only: it may query GitHub but must not + submit reviews, edit branches, or merge PRs. \ No newline at end of file diff --git a/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py new file mode 100755 index 0000000..638aa3c --- /dev/null +++ b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Read-only snapshot of open Dependabot PRs requesting review from the current gh user.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import PurePosixPath +from typing import Any + + +ALLOWED_CHECK_CONCLUSIONS = {"SUCCESS", "SKIPPED", "NEUTRAL"} +DEPENDENCY_FILES = { + ".pre-commit-config.yaml", + "cargo.lock", + "cargo.toml", + "composer.json", + "composer.lock", + "gemfile", + "gemfile.lock", + "go.mod", + "go.sum", + "package-lock.json", + "package.json", + "pipfile", + "pipfile.lock", + "pnpm-lock.yaml", + "poetry.lock", + "pyproject.toml", + "requirements.txt", + "uv.lock", + "yarn.lock", +} + + +def gh_json(arguments: list[str]) -> Any: + completed = subprocess.run( + ["gh", *arguments], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return json.loads(completed.stdout) + + +def is_dependency_file(path: str) -> bool: + name = PurePosixPath(path).name.lower() + return name in DEPENDENCY_FILES or name.startswith("requirements") and name.endswith(".txt") + + +def classify_ci(checks: list[dict[str, Any]]) -> str: + if not checks: + return "no_checks" + runs = [check for check in checks if check.get("__typename") == "CheckRun"] + contexts = [check for check in checks if check.get("__typename") == "StatusContext"] + if len(runs) + len(contexts) != len(checks): + return "pending" + if any( + check.get("status") == "COMPLETED" + and check.get("conclusion") not in ALLOWED_CHECK_CONCLUSIONS + for check in runs + ) or any(check.get("state") in {"FAILURE", "ERROR"} for check in contexts): + return "failing" + if any(check.get("status") != "COMPLETED" for check in runs) or any( + check.get("state") != "SUCCESS" for check in contexts + ): + return "pending" + if not runs: + return "no_checks" + return "passing" + + +def risk_hints(title: str, paths: list[str], body: str = "") -> list[str]: + hints: list[str] = [] + lowered = [path.lower() for path in paths] + if any(path.startswith(".github/workflows/") for path in lowered): + hints.append("workflow_change") + if any(path.startswith(".github/dependabot") for path in lowered): + hints.append("dependabot_config_change") + if any(not is_dependency_file(path) for path in paths): + hints.append("non_dependency_file_change") + version_match = re.search(r"\bfrom\s+v?(\d+)(?:\.\d+)*\s+to\s+v?(\d+)(?:\.\d+)*\b", title, re.I) + if version_match and int(version_match.group(2)) > int(version_match.group(1)): + hints.append("possible_major_update") + if "group" in title.lower() or "updates" in title.lower(): + hints.append("grouped_update_requires_inspection") + body_match = re.search(r"\|\s*\S+\s+\|\s*`?v?(\d+)(?:\.\d+)*`?\s+\|\s*`?v?(\d+)(?:\.\d+)*`?\s*\|", body or "") + if body_match and int(body_match.group(2)) > int(body_match.group(1)): + hints.append("possible_major_update_in_group") + return hints + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--limit", type=int, default=100) + parser.add_argument("--compact", action="store_true") + args = parser.parse_args() + + user = gh_json(["api", "user"])["login"] + results = gh_json( + [ + "search", + "prs", + "--review-requested", + user, + "--state", + "open", + "--limit", + str(args.limit), + "--json", + "number,title,url,repository,author,isDraft,updatedAt", + ] + ) + + candidates = [] + for result in results: + if result.get("author", {}).get("login", "").lower() not in {"dependabot[bot]", "app/dependabot"}: + continue + details = gh_json( + [ + "pr", + "view", + result["url"], + "--json", + "url,number,title,state,isDraft,author,reviewRequests,headRefName,headRefOid,headRepository,baseRefName,mergeStateStatus,reviewDecision,files,statusCheckRollup,body", + ] + ) + review_requests = {item["login"].lower() for item in details.get("reviewRequests", [])} + if user.lower() not in review_requests: + continue + paths = [item["path"] for item in details.get("files", [])] + checks = details.get("statusCheckRollup") or [] + candidates.append( + { + "url": details["url"], + "repository": result["repository"]["nameWithOwner"], + "number": details["number"], + "title": details["title"], + "head_sha": details["headRefOid"], + "head_branch": details["headRefName"], + "base_branch": details["baseRefName"], + "draft": details["isDraft"], + "merge_state": details["mergeStateStatus"], + "review_decision": details.get("reviewDecision"), + "review_requests": sorted(review_requests), + "ci": classify_ci(checks), + "risk_hints": risk_hints(details["title"], paths, details.get("body") or ""), + "changed_paths": paths, + "checks": [ + { + "name": check.get("name") or check.get("context"), + "type": check.get("__typename"), + "status": check.get("status"), + "conclusion": check.get("conclusion"), + "state": check.get("state"), + "workflow": check.get("workflowName"), + } + for check in checks + ], + } + ) + + indent = None if args.compact else 2 + print(json.dumps({"user": user, "candidates": candidates}, indent=indent, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + print(error.stderr or str(error), file=sys.stderr) + raise SystemExit(error.returncode) from error From 4ee0b47c20cefcd1c0478eec8423572466f5189d Mon Sep 17 00:00:00 2001 From: d33bs Date: Fri, 9 Oct 2026 12:19:40 -0600 Subject: [PATCH 2/3] fixes --- .../skills/gh-triage-dependabot-prs/SKILL.md | 20 +++++-- .../scripts/snapshot.py | 58 ++++++++++++++++--- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/.agents/skills/gh-triage-dependabot-prs/SKILL.md b/.agents/skills/gh-triage-dependabot-prs/SKILL.md index 820f201..45f878b 100644 --- a/.agents/skills/gh-triage-dependabot-prs/SKILL.md +++ b/.agents/skills/gh-triage-dependabot-prs/SKILL.md @@ -80,17 +80,25 @@ to inspect or report never authorizes reviews, pushes, or merges. 7. Approve or merge only when all of these are true (the merge gate): - The PR is not a draft and has at least one completed GitHub Actions CI check; an external-only status (for example CodeRabbit) does not satisfy - this requirement. + this requirement. Do not trust the snapshot's classification for this + condition: verify the source again immediately before approving or + merging with + `gh api repos///commits//check-runs` and require + at least one completed, successful check run whose `app.name` is + "GitHub Actions"; check runs owned by other apps (for example CodeQL + under "GitHub Advanced Security") do not count. - Every check run is successful, skipped, or neutral, and every external status context is successful; no check is pending, action-required, cancelled, timed out, or failing. GitHub's `statusCheckRollup` uses `status`/`conclusion` for check runs but `state` for status contexts, so do not treat a context's absent `status` as pending. - The tested head SHA still equals the current head SHA. - - The authenticated user still has a pending review request on the PR, or - has already approved it — `gh pr review --approve` consumes the review - request, so a re-checked gate must accept a prior approval — and the PR - uses the Dependabot GitHub App identity. + - The authenticated user still has a pending review request on the PR, has + already approved it — `gh pr review --approve` consumes the review + request, so a re-checked gate must accept a prior approval — or the user + explicitly named and directed this PR for processing in the current + session and that authorization is recorded; and the PR uses the + Dependabot GitHub App identity. - The changed files are consistent with Dependabot dependency maintenance, including manifests, lockfiles, hook configuration, GitHub Actions workflow/action version bumps, or Dependabot-managed grouped updates. @@ -192,4 +200,4 @@ to inspect or report never authorizes reviews, pushes, or merges. PR was left alone. State explicitly that non-Dependabot PRs were untouched and whether all commits lack co-author trailers. - `scripts/snapshot.py` stays read-only: it may query GitHub but must not - submit reviews, edit branches, or merge PRs. \ No newline at end of file + submit reviews, edit branches, or merge PRs. diff --git a/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py index 638aa3c..6034ef0 100755 --- a/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py +++ b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py @@ -52,24 +52,57 @@ def is_dependency_file(path: str) -> bool: return name in DEPENDENCY_FILES or name.startswith("requirements") and name.endswith(".txt") -def classify_ci(checks: list[dict[str, Any]]) -> str: +def fetch_check_runs(repository: str, head_sha: str) -> list[dict[str, Any]]: + """Read-only fetch of a commit's check runs with each run's owning app. + + The pull request rollup omits which app created a check run, so query the + commit's check runs directly; ``app`` distinguishes GitHub Actions from + other check-reporting apps (for example CodeQL under "GitHub Advanced + Security"). Statuses are normalized to the uppercase spellings the rollup + uses. If the page truncates the result, a sentinel incomplete run keeps + the classification non-passing. + """ + data = gh_json( + ["api", f"repos/{repository}/commits/{head_sha}/check-runs?per_page=100"] + ) + runs: list[dict[str, Any]] = [] + for run in data.get("check_runs", []): + runs.append( + { + "name": run.get("name"), + "app": (run.get("app") or {}).get("name"), + "status": (run.get("status") or "").upper() or None, + "conclusion": (run.get("conclusion") or "").upper() or None, + } + ) + if data.get("total_count", 0) > len(runs): + runs.append({"name": None, "app": None, "status": "QUEUED", "conclusion": None}) + return runs + + +def classify_ci(checks: list[dict[str, Any]], check_runs: list[dict[str, Any]]) -> str: + """Classify CI state from the PR rollup and the head commit's check runs. + + "passing" requires at least one completed run owned by the GitHub + Actions app; green check runs from other apps, or statuses alone, + classify as "no_checks" rather than "passing". + """ if not checks: return "no_checks" - runs = [check for check in checks if check.get("__typename") == "CheckRun"] contexts = [check for check in checks if check.get("__typename") == "StatusContext"] - if len(runs) + len(contexts) != len(checks): + if len(check_runs) + len(contexts) != len(checks): return "pending" if any( - check.get("status") == "COMPLETED" - and check.get("conclusion") not in ALLOWED_CHECK_CONCLUSIONS - for check in runs + run.get("status") == "COMPLETED" + and run.get("conclusion") not in ALLOWED_CHECK_CONCLUSIONS + for run in check_runs ) or any(check.get("state") in {"FAILURE", "ERROR"} for check in contexts): return "failing" - if any(check.get("status") != "COMPLETED" for check in runs) or any( + if any(run.get("status") != "COMPLETED" for run in check_runs) or any( check.get("state") != "SUCCESS" for check in contexts ): return "pending" - if not runs: + if not any(run.get("app") == "GitHub Actions" for run in check_runs): return "no_checks" return "passing" @@ -134,6 +167,10 @@ def main() -> int: continue paths = [item["path"] for item in details.get("files", [])] checks = details.get("statusCheckRollup") or [] + check_runs = fetch_check_runs( + result["repository"]["nameWithOwner"], details["headRefOid"] + ) + apps_by_name = {run["name"]: run["app"] for run in check_runs} candidates.append( { "url": details["url"], @@ -147,13 +184,16 @@ def main() -> int: "merge_state": details["mergeStateStatus"], "review_decision": details.get("reviewDecision"), "review_requests": sorted(review_requests), - "ci": classify_ci(checks), + "ci": classify_ci(checks, check_runs), "risk_hints": risk_hints(details["title"], paths, details.get("body") or ""), "changed_paths": paths, "checks": [ { "name": check.get("name") or check.get("context"), "type": check.get("__typename"), + "app": apps_by_name.get(check.get("name")) + if check.get("__typename") == "CheckRun" + else None, "status": check.get("status"), "conclusion": check.get("conclusion"), "state": check.get("state"), From 293f0672dbb485a76e5ed1d68dec99b2499384a1 Mon Sep 17 00:00:00 2001 From: d33bs Date: Fri, 9 Oct 2026 15:27:20 -0600 Subject: [PATCH 3/3] fixes --- .../skills/gh-triage-dependabot-prs/SKILL.md | 18 +++++++++++++----- .../scripts/snapshot.py | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.agents/skills/gh-triage-dependabot-prs/SKILL.md b/.agents/skills/gh-triage-dependabot-prs/SKILL.md index 45f878b..79f74f6 100644 --- a/.agents/skills/gh-triage-dependabot-prs/SKILL.md +++ b/.agents/skills/gh-triage-dependabot-prs/SKILL.md @@ -111,14 +111,19 @@ to inspect or report never authorizes reviews, pushes, or merges. source-change, and merge-conflict PRs untouched. Report the reason. 8. For each candidate that passes the merge gate: record the exact head SHA and inspect changed files enough to confirm the PR is Dependabot - dependency maintenance; re-read the check rollup immediately before - mutation; approve with + dependency maintenance; re-read the PR `state` and the check rollup + immediately before mutation and proceed only if `state` is `OPEN`; + approve with `gh pr review --approve --body "CI checks pass."`; merge with a merge commit — `gh pr merge --merge --match-head-commit ` — to avoid synthesized co-author trailers. Never use squash merge. If repository policy forbids merge commits, stop and report instead of silently choosing another method. Verify `state`, `mergedBy`, `mergedAt`, - and `mergeCommit` with `gh pr view`. + and `mergeCommit` with `gh pr view`, then inspect the resolved merge + commit with `gh api repos///commits/` and + confirm `.parents` has at least two entries and `.commit.message` + contains no `Co-authored-by:` trailers; merge status or `mergeCommit` + alone proves neither. 9. Stuck mergeability: if `gh pr merge` fails with "Head branch is out of date" but `gh api repos///compare/...` reports `behind_by` 0 and the base branch is unprotected, the PR is stuck in @@ -190,8 +195,11 @@ to inspect or report never authorizes reviews, pushes, or merges. ## Verification - For every merged PR, read back `state`, `mergedBy`, `mergedAt`, and - `mergeCommit` with `gh pr view` and confirm the merge method was a merge - commit with no co-author trailers. + `mergeCommit` with `gh pr view`, then inspect the resolved merge commit + with `gh api repos///commits/`: report that + `.parents` has at least two entries and that `.commit.message` contains + no `Co-authored-by:` trailers — merge status or `mergeCommit` alone is + not proof of either. - For every fixed-and-merged PR, confirm the final CI rollup is green on the exact pushed SHA. - Report per PR: URL, dependency scope, risk tier and (for majors) the diff --git a/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py index 6034ef0..45e9d66 100755 --- a/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py +++ b/.agents/skills/gh-triage-dependabot-prs/scripts/snapshot.py @@ -162,6 +162,9 @@ def main() -> int: "url,number,title,state,isDraft,author,reviewRequests,headRefName,headRefOid,headRepository,baseRefName,mergeStateStatus,reviewDecision,files,statusCheckRollup,body", ] ) + # A PR can close between the search snapshot and this detail fetch. + if details.get("state") != "OPEN": + continue review_requests = {item["login"].lower() for item in details.get("reviewRequests", [])} if user.lower() not in review_requests: continue