You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dependabot security updates always open against the default branch (main). target-branch in .github/dependabot.yml scopes version updates only — the file's own comment block says so, and the record agrees: version updates land on v2/main (#1976, #1921–#1927), security updates land on main (#1833, #1840, #1913, #1925, #2004).
Since main is never merged back into v2/main, every one of those fixes stalls on the release branch. That is exactly how #2057 happened: v2/main sat on vulnerable hono / undici / body-parser / ip-address / fast-uri for weeks, and Dependabot would not re-raise them because it considers them fixed on the branch it watches. The failure is silent and compounding — nothing surfaces it, and each new security PR widens the gap.
The manual workaround is one command (gh pr edit <n> --base v2/main). The problem is remembering it, every time, forever. Automate it.
What the workflow does
For each open PR authored by dependabot[bot] whose base is main:
Retarget it to v2/main.
Label it v2 — Dependabot stamps only its own dependencies / javascript, and Label by version requires exactly one of v1 / v2 on every PR.
Create a tracking issue for it and put it on the board (below).
Link the two — rewrite the PR body so its first line is Closes #<issue>.
The predicate is pleasantly simple: version updates already target v2/main, so any Dependabot PR against main is by definition a security update — the thing we want moved. No further classification needed.
Schedule: hourly
on:
schedule: [{ cron: "17 * * * *" }]workflow_dispatch:
Hourly, plus manual dispatch for testing and for catching up after an outage.
A scheduled sweep is not just convenient here, it is required. Workflows triggered by Dependabot events run with a read-only GITHUB_TOKENand no access to secrets — and this job needs both write permission and a secret (see Tokens). A pull_request_target job is the usual escape hatch but is security-sensitive and would need a standing rule never to check out the PR head. The sweep sidesteps both, is trivially testable via workflow_dispatch, and self-heals if a run is missed.
Merge guard — nobody knee-jerks it into main
An hourly sweep leaves a window of up to an hour where the PR is sitting against main looking perfectly mergeable. Shortening the window does not close it; a blocking check does.
Add a small workflow on pull_request that fails when actor == 'dependabot[bot]' && base_ref == 'main', and register its context as a required status check on main's ruleset (repo ruleset 3187937, which today requires only build). Merging is then impossible until the PR is retargeted, at which point the check re-runs against v2/main and passes.
This works despite the Dependabot token restriction: the job needs no write permission and no secrets — it only has to exit non-zero. It is also the durable half of this issue. The sweep is convenience; the guard is what makes the mistake unavailable, and it keeps protecting us if the sweep breaks, is disabled, or has not run yet.
Have it fail with an explanatory message pointing at this issue, so whoever hits it understands why rather than re-running it.
Tracking issue + board placement
The repo's rule is that every PR references an issue, because only issues go on the board — a PR with no issue has no card and is invisible to the project. Dependabot PRs have quietly been exempt from this. They should not be.
For each retargeted PR, create an issue and:
Field
Value
Labels
v2 + chore (dependency work, no user-facing behavior change)
Milestone
the current milestone — the open one with the nearest due date, resolved at runtime
Todo, not Incoming — correctly, since Incoming means "nobody has evaluated this" and a security fix arriving through this pipeline is approved by policy. That is also why it gets a milestone: Incoming ⇔ no milestone, everything past it ⇔ milestoned.
Priority High is a standing decision, not a rubric result. Scored the usual way a routine dependency bump would land Medium at best. We are overriding that deliberately: these carry security fixes for a branch that has already been left exposed once (#2057), and the whole point of the pipeline is that they do not sit. Record the override in the issue body so it does not read as an unscored assertion — the rubric explicitly allows overriding the band when it is plainly wrong, provided the reason is stated.
Resolve "current milestone" at runtime rather than hardcoding:
gh api repos/modelcontextprotocol/inspector/milestones --jq \
'map(select(.state=="open")) | sort_by(.due_on) | .[0].title'
Handle the empty case (no open milestone) by creating the issue unmilestoned and saying so, rather than failing the run.
Tokens — the part that will bite
GITHUB_TOKENcannot write to an organization Project. Projects v2 is an org-level resource and needs a fine-grained PAT or a GitHub App installation token carrying organization projects: write, plus issues: write and pull requests: write on this repo. Store it as a repository secret and use it for the gh project calls.
Scheduled workflows can read secrets, which is another reason the sweep shape is the right one — a Dependabot-triggered job could not have done this at all.
Idempotency — mandatory, given it runs hourly
The sweep re-runs every hour and will keep seeing PRs it has already handled until they merge. It must not create a second issue, a duplicate card, or re-edit a body each pass.
Make the PR body the state marker: if it already begins with Closes #, the PR is fully processed — skip it entirely. That keeps the check to a single field with no external bookkeeping. Whatever marker is chosen, the test that matters is that a second run within the hour is a complete no-op.
Validate this before building it
The whole approach rests on one unverified assumption: that Dependabot tolerates having its PR's base changed out from under it. It may instead close the PR as superseded, or recreate it against main on its next run — in which case the sweep would fight Dependabot in a loop and be worse than nothing.
So: retarget the next security PR by hand first and watch it for a full Dependabot cycle. Build the automation only if the PR survives, keeps its branch, and still merges. If it does not, fall back to having the workflow open a mirror PR against v2/main (cherry-picking the bump) and leave the original alone — the merge guard and the tracking-issue machinery are unaffected either way.
Precondition: the branches must stay content-identical
A Dependabot branch is cut from main's tip, so retargeting computes its diff against merge-base(v2/main, branch). When the branches carry no other divergence, that base is v2/main's own tip and the diff collapses to the bump itself. When they have drifted, retargeting drags every main-only commit into the PR — which is why this was not viable before #2059 closed the gap.
Have the sweep detect this rather than assume it: if the retargeted PR's changed-file count exceeds a small threshold, comment and skip instead of leaving a misleading PR open. Drift is a signal that something else needs fixing.
Decide: retarget only, or land on both?
Retargeting moves the fix to v2/main, and main does not get it until the next milestone merge. That is the right default — main's content only reaches users when a release is cut, and releases come from milestone merges, so routing to v2/main loses nothing for published users while avoiding the drift that routing to main creates.
The exception is a hotfix: patching the released line without waiting for the next milestone. v2 has no documented hotfix flow (v1 has one — it publishes straight from v1/main). If we want one, it should be written down separately rather than assumed to be what these PRs are for.
Acceptance
A Dependabot PR opened against main ends up based on v2/main, labeled v2, with Closes #<issue> as the first line of its body, mergeable, and with a diff limited to the dependency change.
Merging such a PR into main is blocked by a required check, both before the sweep runs and if the sweep is broken or disabled.
A second sweep within the hour is a complete no-op — no duplicate issue, card, or body edit.
The sweep is a no-op when there is nothing to retarget, and does not loop or fight Dependabot across a full cycle.
Drift between main and v2/main is reported rather than silently producing a huge PR.
Documented in AGENTS.md / the README beside the existing Dependabot notes, including the Dependabot-PR carve-out from the "every PR references an issue" rule now being closed rather than tolerated.
Context
Alternatives considered and why they are not this issue:
Make v2/main the default branch — the real fix, and it would also make Closes #N auto-close work and issue-template edits go live. Blocked today by the org-level ~DEFAULT_BRANCH ruleset, which carries an empty bypass list and would import a mandatory approval onto v2/main. Viable if inspector-maintainers (id 14642559, already admin on this repo) is added as a bypass actor at the org level.
A scheduled npm audit fix --package-lock-only PR against v2/main — sidesteps Dependabot entirely, at the cost of duplicating what Dependabot already does well.
Dependabot security updates always open against the default branch (
main).target-branchin.github/dependabot.ymlscopes version updates only — the file's own comment block says so, and the record agrees: version updates land onv2/main(#1976, #1921–#1927), security updates land onmain(#1833, #1840, #1913, #1925, #2004).Since
mainis never merged back intov2/main, every one of those fixes stalls on the release branch. That is exactly how #2057 happened:v2/mainsat on vulnerablehono/undici/body-parser/ip-address/fast-urifor weeks, and Dependabot would not re-raise them because it considers them fixed on the branch it watches. The failure is silent and compounding — nothing surfaces it, and each new security PR widens the gap.The manual workaround is one command (
gh pr edit <n> --base v2/main). The problem is remembering it, every time, forever. Automate it.What the workflow does
For each open PR authored by
dependabot[bot]whose base ismain:v2/main.v2— Dependabot stamps only its owndependencies/javascript, and Label by version requires exactly one ofv1/v2on every PR.Closes #<issue>.The predicate is pleasantly simple: version updates already target
v2/main, so any Dependabot PR againstmainis by definition a security update — the thing we want moved. No further classification needed.Schedule: hourly
Hourly, plus manual dispatch for testing and for catching up after an outage.
A scheduled sweep is not just convenient here, it is required. Workflows triggered by Dependabot events run with a read-only
GITHUB_TOKENand no access to secrets — and this job needs both write permission and a secret (see Tokens). Apull_request_targetjob is the usual escape hatch but is security-sensitive and would need a standing rule never to check out the PR head. The sweep sidesteps both, is trivially testable viaworkflow_dispatch, and self-heals if a run is missed.Merge guard — nobody knee-jerks it into
mainAn hourly sweep leaves a window of up to an hour where the PR is sitting against
mainlooking perfectly mergeable. Shortening the window does not close it; a blocking check does.Add a small workflow on
pull_requestthat fails whenactor == 'dependabot[bot]' && base_ref == 'main', and register its context as a required status check onmain's ruleset (repo ruleset3187937, which today requires onlybuild). Merging is then impossible until the PR is retargeted, at which point the check re-runs againstv2/mainand passes.This works despite the Dependabot token restriction: the job needs no write permission and no secrets — it only has to exit non-zero. It is also the durable half of this issue. The sweep is convenience; the guard is what makes the mistake unavailable, and it keeps protecting us if the sweep breaks, is disabled, or has not run yet.
Have it fail with an explanatory message pointing at this issue, so whoever hits it understands why rather than re-running it.
Tracking issue + board placement
The repo's rule is that every PR references an issue, because only issues go on the board — a PR with no issue has no card and is invisible to the project. Dependabot PRs have quietly been exempt from this. They should not be.
For each retargeted PR, create an issue and:
v2+chore(dependency work, no user-facing behavior change)PVT_kwDOCt2Azc4BJVxt)Todo(fbdaf21e)High(0a877460)Todo, not Incoming — correctly, since Incoming means "nobody has evaluated this" and a security fix arriving through this pipeline is approved by policy. That is also why it gets a milestone: Incoming ⇔ no milestone, everything past it ⇔ milestoned.
Priority
Highis a standing decision, not a rubric result. Scored the usual way a routine dependency bump would land Medium at best. We are overriding that deliberately: these carry security fixes for a branch that has already been left exposed once (#2057), and the whole point of the pipeline is that they do not sit. Record the override in the issue body so it does not read as an unscored assertion — the rubric explicitly allows overriding the band when it is plainly wrong, provided the reason is stated.Resolve "current milestone" at runtime rather than hardcoding:
gh api repos/modelcontextprotocol/inspector/milestones --jq \ 'map(select(.state=="open")) | sort_by(.due_on) | .[0].title'Handle the empty case (no open milestone) by creating the issue unmilestoned and saying so, rather than failing the run.
Tokens — the part that will bite
GITHUB_TOKENcannot write to an organization Project. Projects v2 is an org-level resource and needs a fine-grained PAT or a GitHub App installation token carryingorganization projects: write, plusissues: writeandpull requests: writeon this repo. Store it as a repository secret and use it for thegh projectcalls.Scheduled workflows can read secrets, which is another reason the sweep shape is the right one — a Dependabot-triggered job could not have done this at all.
Idempotency — mandatory, given it runs hourly
The sweep re-runs every hour and will keep seeing PRs it has already handled until they merge. It must not create a second issue, a duplicate card, or re-edit a body each pass.
Make the PR body the state marker: if it already begins with
Closes #, the PR is fully processed — skip it entirely. That keeps the check to a single field with no external bookkeeping. Whatever marker is chosen, the test that matters is that a second run within the hour is a complete no-op.Validate this before building it
The whole approach rests on one unverified assumption: that Dependabot tolerates having its PR's base changed out from under it. It may instead close the PR as superseded, or recreate it against
mainon its next run — in which case the sweep would fight Dependabot in a loop and be worse than nothing.So: retarget the next security PR by hand first and watch it for a full Dependabot cycle. Build the automation only if the PR survives, keeps its branch, and still merges. If it does not, fall back to having the workflow open a mirror PR against
v2/main(cherry-picking the bump) and leave the original alone — the merge guard and the tracking-issue machinery are unaffected either way.Precondition: the branches must stay content-identical
A Dependabot branch is cut from
main's tip, so retargeting computes its diff againstmerge-base(v2/main, branch). When the branches carry no other divergence, that base isv2/main's own tip and the diff collapses to the bump itself. When they have drifted, retargeting drags everymain-only commit into the PR — which is why this was not viable before #2059 closed the gap.Have the sweep detect this rather than assume it: if the retargeted PR's changed-file count exceeds a small threshold, comment and skip instead of leaving a misleading PR open. Drift is a signal that something else needs fixing.
Decide: retarget only, or land on both?
Retargeting moves the fix to
v2/main, andmaindoes not get it until the next milestone merge. That is the right default —main's content only reaches users when a release is cut, and releases come from milestone merges, so routing tov2/mainloses nothing for published users while avoiding the drift that routing tomaincreates.The exception is a hotfix: patching the released line without waiting for the next milestone. v2 has no documented hotfix flow (v1 has one — it publishes straight from
v1/main). If we want one, it should be written down separately rather than assumed to be what these PRs are for.Acceptance
mainends up based onv2/main, labeledv2, withCloses #<issue>as the first line of its body, mergeable, and with a diff limited to the dependency change.v2+chore, carries the current milestone, and sits on board Add tab and approval flow for server -> client sampling #28 in Todo with Priority High.mainis blocked by a required check, both before the sweep runs and if the sweep is broken or disabled.mainandv2/mainis reported rather than silently producing a huge PR.AGENTS.md/ the README beside the existing Dependabot notes, including the Dependabot-PR carve-out from the "every PR references an issue" rule now being closed rather than tolerated.Context
Alternatives considered and why they are not this issue:
v2/mainthe default branch — the real fix, and it would also makeCloses #Nauto-close work and issue-template edits go live. Blocked today by the org-level~DEFAULT_BRANCHruleset, which carries an empty bypass list and would import a mandatory approval ontov2/main. Viable ifinspector-maintainers(id14642559, already admin on this repo) is added as a bypass actor at the org level.npm audit fix --package-lock-onlyPR againstv2/main— sidesteps Dependabot entirely, at the cost of duplicating what Dependabot already does well.