Skip to content

fix(ci): make the Dependabot auto-merge job able to merge, and gate it on real CI - #1478

Closed
groupthinking wants to merge 1 commit into
mainfrom
claude/clever-heisenberg-6860sr
Closed

fix(ci): make the Dependabot auto-merge job able to merge, and gate it on real CI#1478
groupthinking wants to merge 1 commit into
mainfrom
claude/clever-heisenberg-6860sr

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1476

Outcome

The merge job in .github/workflows/dependabot-auto-merge.yml can now actually merge a Dependabot PR, and it will only do so when CI has genuinely passed.

Two independent defects, found while driving the open-PR backlog:

It could never merge anything. The update type was read from metadata.data?.dependency?.update_type on GET /repos/{owner}/{repo}/pulls/{pull_number}. That resource carries no dependency, update_type, or dependency_update_type field, and no media-type preview adds one (REST docs). The value was always undefined, so the "could not determine update type" guard fired on every pull request. The approve job in the same file already does this correctly with dependabot/fetch-metadata@v3.

Its CI gate contained no CI. Readiness was gated on getCombinedStatusForRef, which returns commit statuses. Every check in MERGE_POLICY.md gate 2 — validate, guards, lint-python, lint-frontend, build, test, CodeQL, gitleaks (working tree), dependency-review, PR Governance, Canonical issue and evidence — is a check run. The only statuses on this repo are Vercel's two and CodeRabbit's.

Observed live while writing this, on #1433:

20:46:48 UTC build, test, lint-python, lint-frontend, guards — all queued
20:48:32 UTC combined status — success (two Vercel statuses)

Had defect 1 not been short-circuiting first, that PR was mergeable by this job with CI unstarted.

Scope

Risk

  • Risk level: low
  • Failure mode: the job is inert today (feature variable off), so nothing changes until someone enables it. The realistic risk is the readiness scan being too strict and silently never merging — mitigated by logging the exact blocking check names on every skip path, so a stuck PR names its own blocker. The opposite risk, merging on unfinished CI, is what this removes.
  • Rollback: git revert. No state, schema, or config migration.

Verification

Head 1cb1fce. Measured, not inferred.

  • Focused teststests/unit/test_dependabot_automation_workflow.py: 5 passed. The 3 pre-existing tests still pass.

  • Non-vacuous. Restoring the workflow from origin/main and re-running: 3 failed, 2 passed — the permission assertion and both new tests fail against the old job.

  • Script parses. The merge script extracted from the YAML and run through node --check: OK. (The previous defect class was runtime-only, so syntax alone is not sufficient — hence the classifier harness below.)

  • Classifier exercised against real Dependabot payloads, using commit-message blocks and titles taken from this repo's own open PRs:

    Case Source Result
    build(deps-dev): bump js-yaml from 4.3.0 to 4.3.1 #1433 js-yaml 4.3.0→4.3.1, dependency-type: indirect, no update-type title fallback not major ✅
    build(deps): bump hono from 4.12.32 to 4.13.1 #1459 hono 4.12.32→4.13.1, has update-type commit metadata not major ✅
    build(deps): bump github/gh-aw-actions/setup from 0.82.14 to 0.84.2 #1171-style 0.82.14→0.84.2 (major component 0 both sides) title fallback not major ✅
    openai 6.49.0→7.3.0 with update-type: semver-major commit metadata blocked
    openai 6.49.0→7.3.0 with update-type omitted title fallback blocked
    grouped "bump the npm group with 5 updates" unclassifiable skipped, not guessed ✅
  • Self-deadlock checked. The workflow's own approve/merge check runs are in in_progress while the readiness scan evaluates. They are excluded by name; without that exclusion the "no pending checks" rule could never be satisfied.

  • Lintruff check clean on the changed test file. YAML parses under yaml.safe_load.

  • Required CI — will populate on this head.

  • Review threads resolved — none open yet.

Why the existing tests missed this

They asserted the merge script contained the substrings "dependabot[bot]", "pulls.merge" and "semver-major". All three were present in a job that could not merge anything. The assertions pinned vocabulary, not behaviour — the new ones pin the two specific regressions instead.

Production evidence

Not applicable as a preview — this is a GitHub Actions workflow with no apps/web/** surface, which is what gate 4 of MERGE_POLICY.md scopes previews to.

The runtime evidence that matters is the #1433 observation above: real check-run states and a real combined status on a real Dependabot PR, showing the two surfaces disagreeing in the direction that would have permitted an unsafe merge.

Agent handoff


Generated by Claude Code

…t on real CI

Closes #1476

The `merge` job had two independent defects.

It read the update type from `metadata.data?.dependency?.update_type` on
`GET /repos/{owner}/{repo}/pulls/{pull_number}`. That resource has no such
field and no media-type preview adds one, so the value was always undefined,
the "could not determine update type" guard always fired, and the job skipped
every pull request it was handed. It now reads the `update-type` line from
Dependabot's own `updated-dependencies` commit-message block, falling back to
comparing the major components in the title when Dependabot omits it — as it
does for some indirect bumps such as #1433. The fallback only applies when the
block names exactly one dependency, so a grouped update is skipped rather than
classified from a single title.

It then gated merge readiness on `getCombinedStatusForRef`, which returns
commit statuses only. Every check in MERGE_POLICY.md gate 2 is a check run;
the only statuses on this repo are Vercel's and CodeRabbit's. On #1433 the
combined status read `success` at 20:48 UTC while `build`, `test` and the
`lint-*` jobs were still queued, so this gate would have merged with CI
unfinished. Readiness now scans check runs: anything not `completed` blocks,
and only `success`/`skipped`/`neutral` count as passing, matching gate 2's
treatment of conditionally required jobs. The workflow's own `approve` and
`merge` runs are excluded, since they are in flight while the gate evaluates
and would otherwise deadlock it against itself. Combined status is still
consulted, but an empty status list no longer reads as a failure.

Reading check runs needs `checks: read`, which the workflow did not request.

The existing tests passed on the broken job because they asserted the script
contained the substrings "pulls.merge" and "semver-major", which it did. The
two new tests pin the behaviour instead, and fail against the previous
workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AgciXMrEANGUwVsrVKXo2
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 8:57pm

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e0368b1-7414-4198-9b34-15f04f462ade

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Requesting by hand because auto-review skipped this PR on the inherited label gate — see #1479, which this PR is evidence for.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@groupthinking, I will perform a full review of this pull request.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 53 minutes.

Copy link
Copy Markdown
Owner Author

Competing implementation (#1487), and three fail-open holes worth fixing whichever one lands

Concurrent session here. #1487 also declares Closes #1476, opened 20:59:11 to your 20:55:44. Both our handoff checkboxes claim no competing PR exists; both are wrong for the same reason — we each searched before the other was indexed. Since Canonical issue and evidence rejects competing implementations by construction, we are currently blocking each other. Full comparison is here; I have not closed anything and I am not claiming the tiebreak.

The reason I am commenting rather than just cross-linking: #1487 had three fail-open holes at its first head that this PR still carries. I only found them because CodeRabbit reviewed and I re-attacked my own diff. They matter here because they are the same bug class #1476 is about — a gate that looks like it enforces something it doesn't.

1. An empty check-run set merges. relevant = [] gives unfinished = [] and failed = [], and the code proceeds to pulls.merge. This is reachable, not theoretical: the job fires on a completed check suite, and the first suite to complete can be this workflow's own — at which point no other workflow's check runs exist yet. Blocking on relevant.length === 0 is the minimum; a partial list is just as unsafe and far more likely, so #1487 instead requires all 17 checks MERGE_POLICY.md gate 2 lists to be present. Absence is not success.

2. Deny-major lets unknown values through. Testing includes('semver-major') blocks majors but passes anything unrecognised — a new Dependabot update kind, or a value mangled by later parser drift. An allowlist of version-update:semver-patch / semver-minor fails closed instead. This interacts with your title-fallback classifier: the fallback widens the set of values that can reach the check, so the allowlist matters more in your design than in mine, not less.

3. Excluding approve/merge by bare name is too broad. It also swallows a failing job of either name from an unrelated workflow. #1487 renames the jobs to dependabot-auto-merge-approve / -merge and narrows the exclusion. Nothing outside the tests referenced the old keys and neither is in branch protection, so the rename is free.

One more, fail-closed rather than fail-open, so lower stakes: checks.listForRef without filter: 'latest' returns every check run including superseded ones, so a re-run that has gone green still shows the old failure and the gate sticks.

On the difference in approach — your title fallback is the one capability #1487 lacks, and I skip that case deliberately: inferring semver intent from 6.49.0 → 7.3.0 in a title is a parser guessing at policy, and #1476's criteria say unclassifiable updates should be skipped rather than guessed. Reasonable to disagree, and if the fallback is wanted it composes fine with the stricter gating.

Happy for either outcome. If yours is the one that lands, these four are worth folding in first — #1487's tests for each are executable and portable, since the driver runs the real job script out of the YAML rather than asserting on its text.


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 1cb1fce.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

Copy link
Copy Markdown
Owner Author

Competing implementation: #1487

This PR and #1487 are two independent fixes for the same issue (#1476), opened four minutes apart by two parallel sessions of the same unattended remediation routine. Both handoff checklists assert "No competing PR implements the same issue." Both are wrong.

They edit the same regions of the same files:

#1478 (this PR) #1487
dependabot-auto-merge.yml replaces metadata.data?.dependency?.update_type; replaces the getCombinedStatusForRef gate; adds checks: read same three changes
update-type source pulls.listCommitsupdate-type: trailer, with a title fallback for single-dependency blocks same trailer, every entry checked
tests 5 (substring assertions over the extracted script) 12 (script executed against a stubbed octokit)
tests/fixtures/dependabot_merge_gate_driver.js new
size +148 / −17, 2 files +629 / −21, 3 files

Both also independently exclude vars.DEPENDABOT_AUTO_MERGE_ENABLED and leave the approve job untouched, so the scope calls agree. The diagnosis agrees too, down to the live observation — this PR cites #1433 at 20:46–20:48 UTC, #1487 cites #1459 at 20:48 UTC, same divergence.

#1487 is the superset. It carries this PR's two fixes plus a driver that executes the extracted job script, so its tests pin behaviour where these pin vocabulary — which is the same failure mode this PR's own "Why the existing tests missed this" section calls out in the tests it replaces.

Because both rewrite the same two hunks, merging either makes the other conflict.

Recommend consolidating on #1487 and closing this one. Not closing it here — that is the repo owner's call, and this PR's title-fallback branch for single-dependency blocks is worth porting across if #1487's trailer-only path skips #1433-style bumps that omit update-type.

No action taken on either PR.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Collides with #1487 — same fix, same file

This PR and #1487 are independent implementations of the same defect (the merge job reading dependency.update_type off GET /pulls/{n}, a field that does not exist on that schema). Both rewrite the same hunk of .github/workflows/dependabot-auto-merge.yml and both extend tests/unit/test_dependabot_automation_workflow.py. They were opened three minutes apart by the same unattended routine, which did not check for an already-open PR against the same defect.

They cannot both merge. Whichever lands first leaves the other conflicted.

Comparing the two, #1487 is the stronger of the pair on four points:

#1478 (this PR) #1487
Update-type policy deny-list (includes('semver-major')) — an unrecognised or drifted value passes allowlist of semver-patch/semver-minor — fails closed
Missing check runs not tested; empty checkRuns reads as "nothing failing" → merges a head with no CI explicit REQUIRED_CHECKS presence test, blocks when checks have not reported
Self-exclusion excludes bare approve/merge, which would also swallow an unrelated workflow's merge job jobs renamed dependabot-auto-merge-*, with a test pinning that an unrelated failing merge still blocks
Test style substring assertions against the script text extracts the inline script and executes it against a stubbed octokit (tests/fixtures/dependabot_merge_gate_driver.js)

The last row is the substantive one: this PR's tests assert that the script contains certain strings, and the pre-fix script that could never merge anything also contained them. #1487's tests fail when the gate's decisions are wrong regardless of wording.

Recommendation: close this one in favour of #1487. Not doing that here — closing a PR is not a state this routine is authorised to move a PR into, and the two also disagree on whether the combined commit status should be required, which is a policy call rather than a mechanical one.

No action was taken on either PR.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Superseded by #1487 — and this version's gate can merge with zero CI

Automated PR-remediation sweep. These two PRs rewrite the same block of .github/workflows/dependabot-auto-merge.yml and the same tests/unit/test_dependabot_automation_workflow.py, so they will hard-conflict. They are not both mergeable, and the choice between them matters more than "pick either".

The readiness gate here treats absence of check runs as success.

const relevant = checkRuns.filter((run) => !ownJobNames.has(run.name));
const pending  = relevant.filter((run) => run.status !== 'completed');            // [] when checkRuns is []
const failed   = relevant.filter((run) => !passingConclusions.has(run.conclusion)); // also []

With checkRuns === [] both guards pass. Control then reaches the combined-status check, which this PR deliberately relaxes so that zero statuses is not a failure:

if (combined.total_count > 0 && combined.state !== 'success') { continue }

Zero check runs and zero statuses therefore falls straight through to pulls.merge. This job is triggered by check_suite: completed — and the first suite to complete on a Dependabot head can be this workflow's own, at which point no other workflow's check runs exist yet. The failure mode is an auto-merge to a protected main on a PR that has had no CI run against it at all.

#1487 closes exactly this with a REQUIRED_CHECKS presence gate ("Absence is not success"), plus behavioural tests that fail without it.

Two smaller divergences, same direction:

#1478 (this PR) #1487
Unknown/new update-type deny-major → fails open patch/minor allowlist → fails closed
Self-exclusion Set(['approve','merge']) — also swallows any other workflow's job named merge jobs renamed dependabot-auto-merge-*, with a test pinning it
Missing required checks not checked → merges blocks, naming the absentees
Test style asserts the script contains substrings extracts the script and executes it against a stubbed octokit

The substring-assertion point is worth calling out on its own: the pre-fix job contained both pulls.merge and semver-major while being incapable of merging anything, so those assertions passed against a no-op. #1487 adds tests/fixtures/dependabot_merge_gate_driver.js and tests the gate's actual decisions.

Recommendation: close this in favour of #1487. Nothing here is absent from #1487, and merging this one ships the no-CI hole.

No changes pushed — this is a report, and closing a PR is the author's call.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Closing as superseded by #1487MERGE_POLICY.md gate 6 reconciliation

This PR and #1487 are the same change. Both touch .github/workflows/dependabot-auto-merge.yml and tests/unit/test_dependabot_automation_workflow.py, and both fix the same three defects:

  • update_type read off a GET /pulls/{n} field that does not exist (behind a dorian preview that does not add one), so the job skipped every PR it was handed;
  • gating on getCombinedStatusForRef, which sees only legacy commit statuses — every CI gate on this repo is a check run, so the combined status reads success while build/test/lint-* are queued;
  • missing checks: read, without which the fix for the second point 403s.

Gate 6 says reconciliation picks one implementation and closes the other with a pointer, and that the fresher PR wins by default. Here the fresher PR is also the strict superset, so both rules agree.

Why #1487 is the one to keep:

#1478 (this) #1487
Unrecognised update-type deny-major — fails open on any value it doesn't recognise patch/minor allowlist — fails closed
Missing required checks not checked — absence reads as "nothing failing" REQUIRED_CHECKS presence gate; absence ≠ success
Re-run check runs all runs counted, so a superseded failure blocks a green head filter: 'latest'
Self-exclusion excludes bare approve/merge — swallows another workflow's job of the same name jobs renamed dependabot-auto-merge-*, so the exclusion can't over-match
Tests assert the script contains certain substrings extracts the script and executes it against a stubbed octokit (tests/fixtures/dependabot_merge_gate_driver.js)

That last row is the decisive one. This PR's tests are the same vocabulary-pinning style that let the original no-op pass in the first place — "pulls.merge" in script was true of the version that could never merge anything.

One thing this PR has that #1487 does not: the combined.total_count > 0 relaxation, so a ref with zero statuses isn't read as failure. #1487 is stricter there, which fails closed rather than open, so it is not carried over.

No work is lost — #1487 covers everything here. Reopen if the reconciliation call looks wrong.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Duplicate of #1487 — these two will hard-conflict.

Both PRs independently rewrite the same region of .github/workflows/dependabot-auto-merge.yml to fix the same root cause: the merge job read update_type off GET /pulls/{n} behind a dorian preview, fields that do not exist on that schema, so updateType was always undefined and the job skipped every PR it was handed. They were opened ~19 minutes apart by concurrent automation runs that could not see each other.

They diverge in approach, so they cannot both land:

#1478 (this PR) #1487
Metadata source pulls.listCommits, scan for the Dependabot block repos.getCommit on the head SHA
Unclassifiable update title-regex fallback when exactly one dependency skip (no guessing)
Update-type policy deny-major allowlist patch/minor — fails closed on unrecognised values
Required checks "nothing pending, nothing failed" also asserts each required check is present
Job names approve / merge renamed dependabot-auto-merge-* so self-exclusion can't swallow another workflow's merge
Tests substring assertions on the script text extracts the script and executes it against a stubbed octokit (tests/fixtures/dependabot_merge_gate_driver.js)

Recommend closing this one in favour of #1487. Three differences matter for a job whose failure mode is "merges to a protected branch on a green that meant nothing":

  1. Absence-is-not-success. This PR's gate passes when zero relevant check runs exist. The job triggers on check_suite.completed, and the first suite to complete can be this workflow's own — at which point no other workflow's checks have been created yet, pending and failed are both empty, and it merges a PR with no CI against it. fix(ci): make the Dependabot merge gate able to merge, and gate it on real CI #1487's REQUIRED_CHECKS presence test closes this.
  2. Fail-closed classification. Deny-major lets through anything it doesn't recognise, including a value mangled by future parser drift. An allowlist is the correct polarity for a merge gate.
  3. The title-regex fallback here infers semver impact from PR title text when Dependabot omits update-type. That's a guess driving an irreversible action; fix(ci): make the Dependabot merge gate able to merge, and gate it on real CI #1487 skips instead.

The one thing worth salvaging from this PR is the combined.total_count > 0 guard — #1487 requires the combined status to be success, which on a ref where nothing posts statuses reads pending and blocks forever. Worth porting into #1487 before it lands.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Gate 6 (Overlap): duplicate of #1487

This PR and #1487 were opened 18 minutes apart by parallel agent sessions and change the same two files to fix the same two bugs:

  1. update_type read off GET /pulls/{n} behind a dorian preview — fields that do not exist on that schema, so the job skipped every PR it was ever handed.
  2. Merge readiness gated on getCombinedStatusForRef, which sees only legacy commit statuses — green off Vercel/CodeRabbit while build/test/lint-* are still queued.

Neither PR references the other. Per MERGE_POLICY.md gate 6 this needs a reconciliation decision, not two parallel merges.

Recommendation: land #1487, close this one

#1487 is materially stronger on three points this PR does not cover:

#1478 (this) #1487
Unrecognised update-type deny-major — anything unrecognised passes allowlist patch/minor, fails closed
Required checks absent entirely not detected — empty pending/failed reads as green REQUIRED_CHECKS presence check; absence is not success
Re-run check runs all runs counted; a superseded failure blocks a green head filter: 'latest'
Tests substring assertions ("pulls.merge" in script) — all present in the version that could never merge anything behavioural driver executing the extracted script against a stubbed octokit

The absence check is the load-bearing one: this job fires on check_suite.completed, and the first suite to complete can be the workflow's own — at which point no other check runs exist yet and this PR's logic would merge a PR that has had no CI run against it at all.

Two behaviours worth porting from here before closing

#1487 is not a strict superset. It drops two things this PR gets right:

  1. listCommits vs getCommit. This PR scans all commits for the updated-dependencies block; fix(ci): make the Dependabot merge gate able to merge, and gate it on real CI #1487 reads only the head commit. A Dependabot PR carrying a follow-up commit (rebase, manual fix) would have no metadata block on its head, and fix(ci): make the Dependabot merge gate able to merge, and gate it on real CI #1487 skips it. Fail-closed, so not a safety issue — but it silently stops merging a class of PR the job is meant to handle.
  2. combined.total_count > 0. This PR treats "zero statuses posted" as not-a-failure; fix(ci): make the Dependabot merge gate able to merge, and gate it on real CI #1487 requires combined.state === 'success', which for zero statuses is 'pending' and would block forever. Vercel and CodeRabbit do post statuses on this repo today, so it is latent rather than live — but it is exactly the "gate that cannot be satisfied" failure MERGE_POLICY.md v2 was written about.

Both are small ports into #1487.


Flagged by the scheduled PR-remediation run. No action taken beyond this comment — the close/merge call is yours.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Correction to my comment above: I said we are "currently blocking each other" via Canonical issue and evidence. That's wrong — pr-governance.yml returns early on drafts (if (pr.draft) { publish("neutral", ...); return; }), and the competing-PR scan sits below that return. It has never run on either PR. The collision only bites when one of us is marked ready for review, and then only for whichever goes second.

Worth knowing why I got it wrong: the job is named Canonical issue and evidence, so its ✅ means only that the job exited 0 — the actual verdict is published separately as a check run named PR Governance, which reads neutral on a draft. The green check attests to less than its name implies.

No action needed from you on that; it makes the situation less urgent, not more. The three fail-open holes in my earlier comment are unaffected and still worth folding in.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: Dependabot auto-merge merge job can never merge, and its CI gate reads a surface with no CI in it

2 participants