ci(deps): keep major bumps out of the grouped pull request, and split the cadence - #484
Merged
Conversation
#472 gave each ecosystem one grouped pull request a month with `patterns: ['*']` and no `update-types` filter. The first run showed what that grouping actually covers: | PR | contents | crossed | |------|-----------|-----------------------------------------------------| | #476 | 4 actions | major tags v4 -> v7, merged | | #477 | 11 npm | TypeScript 5.6 -> 7.0, Vite 6 -> 8 | | #478 | 17 crates | tauri-plugin-prevent-default 2 -> 5, notify 6 -> 8 | Nothing failed. Dependabot's schema check accepts the config, and the result is a `chore(deps)` title wrapped around a toolchain migration -- a diff whose per-dependency changelogs are the review, presented as a batch that cannot be split, bisected, or reverted per dependency. So each group now takes `update-types: [minor, patch]`. Majors fall outside every group and arrive one pull request per dependency, each carrying its own changelog. That is the documented shape: "Example: Individual pull requests for major updates and grouped for minor/patch updates" in GitHub's guide to optimizing pull request creation, whose stated result is "All major updates will continue to be raised as individual pull requests." `update-types` alone does not suppress anything. The next example in the same guide, the one titled "no pull requests for major updates", starts from this exact group and reaches that outcome by *adding* an `ignore` condition on `version-update:semver-major`. That condition is deliberately not written here: an `ignore` is a permanent, repo-invisible mute, and the point of this change is to make majors visible one at a time, not to stop hearing about them. `open-pull-requests-limit` goes 2 -> 4 because the shape of a run changed. Two was sized for "one grouped pull request, plus headroom for a stale one". With majors opening individually the file now holds one grouped pull request plus n majors, and a limit of 2 leaves room for exactly one of them. One caveat, which is in the config comment because it is invisible from the config: on a run where a grouped pull request is already open, Dependabot marks every dependency matching the group's `patterns` as handled before it consults `update-types`, so that run opens no individual major pull requests at all. They appear on the first run after the grouped pull request is merged or closed. Majors are deferred by an unreviewed batch, not dropped by it. Separately, and recorded in the config comment because nothing else in the repository says it: `tauri build` fails when a Tauri plugin's crate and npm halves disagree on major *or minor*, and Dependabot puts those two halves in two different pull requests. #478 is red on Linux and macOS for exactly that -- `tauri-plugin-dialog (v2.7.2) : @tauri-apps/plugin-dialog (v2.6.0)`, the npm half sitting in #477. That coupling is not a semver problem and this change does not fix it; the pair has to be merged as a pair. The github-actions comment is corrected while it is being rewritten: it claimed all five actions are tracked by major tag. Four are; `dtolnay/rust-toolchain@stable` carries no version for Dependabot to compare. scripts/dependabotConfig.test.ts locks the four properties this file has that a reader cannot see: no group lists or admits `major`, every group stays `applies-to: version-updates` so a security advisory never waits for the monthly batch, the `interval` matches the cadence the header comment promises, and the limit stays above the point where the queue becomes invisible. Each was verified to fail by breaking the property it claims to protect. `ecosystemBlocks()` asserts it found blocks, because every check is a `for` over its result and a `for` over an empty list passes. Measured: an extra space in each `- package-ecosystem:` line left four of the five checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
This was referenced Aug 6, 2026
PathGao
added a commit
that referenced
this pull request
Aug 6, 2026
….0 npm (#490) Follow-up to #484, which is now doing its job -- the recreated cargo batch (#489) came back with 7 minor/patch crates instead of 16 with majors inside. Three things that first pass did not cover, and one that was planned and is not here. **Cadence.** npm and cargo move to `quarterly`; github-actions stays `monthly`. Security advisories never use this schedule -- they are a repository setting and open their own pull requests as they land -- so version updates exist only to stop drift, and drift costs what somebody else's deadline costs. On Actions somebody else always sets it: runner images retire toolchains on their own timetable, which is how `build.yml` came to build releases on a Node pulled from the image toolcache (#485). An Actions bump is also usually a one-line tag move. Nothing external forces a TypeScript or a serde upgrade, and a quarter of them in one grouped pull request is less interrupting and more reviewable than three monthly ones. **`minor` does not mean compatible below 1.0, and the two ecosystems disagree about it.** Dependabot types an npm update from the literal position of the number, so katex 0.16.47 -> 0.18.1 is a `minor` and went straight into #487 -- the batch whose whole premise is that it is safe to skim. It is not: KaTeX 0.18.0 renamed every internal CSS class and 0.17.0 changed `__defineFunction`, both flagged BREAKING upstream. npm's own caret rule agrees, since `^0.16.47` stops before 0.17. Cargo does not have this bug. `Dependabot::Cargo::Version` implements the pre-1.0 rule, so windows 0.61.3 -> 0.62.2 counts as a major -- which is visible in #489, where those crates are absent from the batch. The npm side has no equivalent subclass. There is no `update-types` value for "pre-1.0 minor", and `patterns` match names rather than versions, so this cannot be written as a rule. It can be written as a list: `exclude-patterns` names the three pre-1.0 npm dependencies, and they get individual pull requests. The test derives that list from package.json, so a new 0.x dependency -- or an existing one reaching 1.0 -- fails the suite instead of quietly rejoining the batch. **Two crates held.** `tauri` requires `windows ^0.61` and `webview2-com ^0.38`, still true at 2.11.5. Taking `windows` 0.62 or `webview2-com` 0.39 puts two `windows-core` versions in one graph and the build stops in our own WebView2 call with E0599. Grouping already keeps them out of the batch; what is left is an individual pull request every quarter that cannot be merged at any published tauri 2.x. Bounded to one minor series each rather than written as a semver level, so the entries lapse. The semver-level form would work -- `Cargo::Version` overrides `ignored_major_versions` so ">= 0.62" is what major means for a 0.61 crate -- but it would also swallow 0.63 and 0.64 in silence. `windows` 0.63 does not exist yet; the day it does, Dependabot proposes it and we find out whether tauri has moved. The Tauri packages themselves are deliberately not ignored, and the file says so: the lockfile is on 2.10.2 while 2.11.5 shipped 2026-07-01, and the red pull request is the only notification there is. **The cooldown is not here, because the case for it does not survive being checked.** The mechanism is fine -- `cooldown` is valid alongside `groups`, dependabot-core passes it on both the grouped and the individual path, and it cannot touch a security update (`update_cooldown: job.security_updates_only? ? nil : job.cooldown` in both). What fails is the premise that the red pull requests are red because their targets are fresh: - typescript 7.0.2 shipped 2026-07-08; svelte-check 4.7.4 shipped 2026-07-27, nineteen days later, and narrowed its peer range to `^5.0.0 || ^6.0.0` -- excluding 7 on purpose. - windows 0.62.2 has been out since 2025-10-06 and webview2-com 0.39.1 since 2026-03-11. Ten months and five. - katex 0.18.1 is three weeks old, but it is breaking at any age. - the rest is the Tauri parity guard, which is not about age at all. None of them is a freshness problem. And on a quarterly schedule a major caught by an N-day window is not delayed N days, it is delayed a quarter. Also worth recording: GitHub Actions does not support `semver-major-days` at all, so a cooldown could never have applied to the one ecosystem that stays monthly. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
.github/dependabot.ymlgroups each ecosystem withpatterns: ['*']and noupdate-typesfilter, so a major version bump lands inside the monthly grouped pull request. The first run showed what that covers:tauri-plugin-prevent-default2 → 5,notify6 → 8Nothing failed — Dependabot's schema check accepts the config. It just produced a
chore(deps)title wrapped around a toolchain migration, in a shape that cannot be split, bisected, or reverted per dependency.Each group now takes
update-types: [minor, patch], andopen-pull-requests-limitgoes 2 → 4.Does this make majors individual, or does it drop them?
That distinction decides whether this fixes the problem or buries it, so I checked it rather than assuming.
Documentation. GitHub's Optimizing the creation of pull requests for Dependabot version updates carries two adjacent examples:
patterns+update-types: [minor, patch], i.e. this config's exact shape. Its stated result: "All major updates will continue to be raised as individual pull requests."ignorecondition onversion-update:semver-major.Suppressing majors takes that extra
ignore.update-typesalone does not, and noignoreis added here.Implementation, read in
dependabot-coreatmain, because the docs and the behaviour have been reported to diverge:DependencyGroup#contains?(common/lib/dependabot/dependency_group.rb) checkspatterns,exclude-patternsanddependency-type— notupdate_types.GroupUpdateCreation#compile_updates_forcallssemver_rules_allow_grouping?, which returnsfalsefor a major whenupdate_typesis[minor, patch], and then returns early viamark_handled_for_group_by_name, which is a no-op unless the group usesgroup-by: dependency-name. We don't. So the dependency is never added tohandled_dependencies.DependencySnapshot#ungrouped_dependenciesisallowed_dependenciesminushandled_dependencies, andGroupUpdateAllVersions#performrunsrun_ungrouped_dependency_updatesafter the grouped pass, delegating toUpdateAllVersions— one pull request per dependency.Majors are excluded from the group and opened individually. The change is right.
What I could not verify
dependabot-coreatmain, not the revision GitHub's hosted Dependabot runs. That revision isn't publicly pinned.GroupUpdateAllVersionscallsmark_group_handled, which doesadd_handled_dependencies(group.dependencies.map(&:name))— andgroup.dependencieswas populated bycontains?, which ignoresupdate-types. So on such a run, no individual major pull requests open at all; they appear on the first run after the grouped PR is merged or closed. Majors are deferred by an unreviewed batch, not dropped by it — but they are deferred. The reporter's own job log shows exactly this path. I could not reproduce it, only trace it.open-pull-requests-limit: 4is a judgement, not a measurement. One slot for the grouped PR, three for majors. I have not observed how many majors a real run opens at once.Also found, and recorded in the config comment
#477 and #478 are both red, and one cause is a coupling Dependabot structurally cannot see. The Tauri plugins ship as coupled crate+npm pairs, and
tauri buildrefuses to run when the halves disagree — from #478's Linux and macOS legs:The crate half is in #478, the npm half in #477; neither is green alone. The guard compares major and minor, so this change does not avoid it — a minor-only grouped batch can split a plugin pair the same way. That is why it is written into
dependabot.ymlrather than left to be rediscovered.One thing I deliberately did not do
#478's Windows leg fails differently, and it is not a grouping problem:
src-tauri/src/lib.rs:1966and:1972.windows-core0.61.2 (via tauri →webview2-com) and 0.62.2 (via our ownwindowsbump) are both in the graph. I checked crates.io: tauri 2.11.5, the current latest, still requireswindows ^0.61andwebview2-com ^0.38. So bumping ourwindows = "0.61.3"to 0.62 is unmergeable at any published tauri 2.x, and it will come back every month.An
ignoreentry would silence it, and I did not add one — that is a compatibility call, and a configignorewith no expiry goes stale silently the moment tauri moves. Three options, for you rather than for me:ignorewindowsandwebview2-comwith a comment naming the unblocking condition (tauri requiringwindows ^0.62). Quiet, but needs a human to notice when it expires.Cargo.toml(windows = "0.61",webview2-com = "0.38") so Dependabot stops proposing an incompatible bump. Puts the constraint where the constraint actually lives.Tests
scripts/dependabotConfig.test.tslocks the properties that are invisible from reading the file: no group lists or admitsmajor, every group staysapplies-to: version-updates, theintervalmatches the cadence the header comment promises, and the limit stays above the point where the queue becomes invisible.Each was verified by breaking the property it protects and confirming that test goes red:
Expected values to be strictly deep-equal: - 'github-actions'update-typesthe npm group must limit itself to minor and patch; without that, a major lands inside a grouped pull request and stops being reviewable- majorthe npm group must not list major among its update typesapplies-tothe cargo group must apply to version updates onlythe header comment promises one grouped pull request a month, so npm must be on \interval: monthly``the header comment promises one grouped pull request a week, so npm must be on \interval: weekly``npm has a limit of 2; majors open one pull request each, so anything under 3 hides the queue rather than shortening itTwo of those tests changed as a result of running this:
interval: monthlywhile being named for the header comment. Editing the comment to promise a weekly bot left it green — the name was a claim the test did not make. It now reads the cadence out of the comment, which is what makes it a contract between two copies of one fact rather than a constant checking itself.- package-ecosystem:line madeecosystemBlocks()return[], and four of the five checks passed on the empty list — every one of them is aforover that result.ecosystemBlocks()now asserts it found something, so the detector states its own precondition instead of relying on a sibling test to notice.npm run check— 0 errors.npm test— 733/733 locally. Those numbers are from before thisbranch was rebased onto current
master; the run on this PR is the one to trust.Second commit: cadence split, two held crates, and one change left out
Cadence
npmandcargo→quarterly;github-actionsstaysmonthly.Security advisories never use this schedule, so version updates exist only to stop drift — and drift costs what someone else's deadline costs. On Actions somebody else always sets it: runner images retire toolchains on their own timetable, which is exactly how
build.ymlcame to build releases on a Node pulled from the toolcache (#485). An Actions bump is also usually a one-line tag move. Nothing external forces a TypeScript or serde upgrade.quarterlyverified as a documentedschedule.intervalvalue — "run on the first day of each quarter (January, April, July, and October)". (It isfpt/GHEC plus GHES ≥ 3.19; this repo is on github.com.)Two crates held, and the shape the obvious version would have got wrong
taurirequireswindows ^0.61andwebview2-com ^0.38— still true at 2.11.5, the current latest. #478's bumps arewindows 0.61.3 → 0.62.2andwebview2-com 0.38.2 → 0.39.1, which collide twowindows-coreversions and fail atsrc-tauri/src/lib.rs:1966.An
ignoreonversion-update:semver-majorwould have been a no-op here, and I nearly wrote one. These are 0.x crates.IgnoreCondition#versions_by_typedelegates toVersion#ignored_major_versions, which for0.61.3builds[">= 1.a"]— 0.62.2 passes straight through. The bump Dependabot needs to skip is classified minor (new_minor > current_minor). So the entries use boundedversions:ranges instead:That also gives the lapse property:
windows0.63 andwebview2-com0.40 do not exist yet, so on the day they ship Dependabot proposes them and the question gets asked again.One hazard worth naming:
ignoreis the only option here that also filters security updates —ignored_versionsdoesreturn versions if security_updates_only, so aversions:entry applies to advisories while anupdate-types:entry does not. The exposure is nil in practice becauseCargo.tomlpinswindows = "0.61.3"(^0.61.3=>=0.61.3, <0.62.0), so 0.62.x is already outside what Cargo will resolve; advisories against the 0.61.x we actually build are untouched. It is in the config comment regardless.The Tauri packages are not ignored, and the file now says why: the lockfile is on tauri 2.10.2 while 2.11.5 shipped 2026-07-01, and the red PR is the only notification that exists.
The cooldown is not in this PR
The mechanism checks out.
cooldownis valid alongsidegroups; dependabot-core passesjob.cooldownon the grouped path (group_update_creation.rb:404) and the individual path (update_all_versions.rb:249) — so it would reach the individual major PRs, the case that matters; and it cannot delay a security update, both byupdate_cooldown: job.security_updates_only? ? nil : job.cooldownon both paths and by the docs ("only available for version updates, not security updates").What does not hold is the premise. None of the current red PRs is red because its target is fresh:
^5.0.0 || ^6.0.0— a deliberate exclusionwindows0.62.2^0.61webview2-com0.39.1^0.38And the cadence change makes a cooldown actively costly: on a quarterly schedule a major caught by an N-day window is not delayed N days, it is delayed to the next quarter.
quarterlyalready means a proposed major is typically weeks old.Also worth recording: GitHub Actions does not support
semver-major-daysat all (docs table:default-daysonly), so a cooldown could never have applied to the one ecosystem staying monthly.Happy to add it if you'd rather —
semver-major-days: 7on npm and cargo is the shape — but I'd be shipping something I can't show a benefit for.Falsification for every test touched
Per-ecosystem, as asked — each cadence mutation names the block it broke:
monthlythe header comment promises npm runs quarterly, but its block says monthlymonthlythe header comment promises cargo runs quarterly, but its block says monthlyquarterlythe header comment promises github-actions runs monthly, but its block says quarterlymonthlythe header comment promises npm runs monthly, but its block says quarterlythe header comment must state a cadence for every ecosystem and no othersversions:every ignore entry under cargo must carry a versions: list; an entry without one silences the dependency permanently, including its security updates'>= 0.62'ignoring windows at '>= 0.62' does not name a bounded version series, so nothing will ever reopen the question'0.*'ignoring windows at '0.*' does not name a bounded version series, so nothing will ever reopen the questionignore:cargo has an \ignore:` key this test cannot parse, so it cannot vouch for it`The four earlier properties were re-falsified after the rewrite and still fail correctly, and re-indenting
- package-ecosystem:still turns all six red rather than passing vacuously.The ignore test had the same vacuity hole as the first round — it
continued when its parser found nothing. It now asks "does this block ignore anything" separately from "can I read it", which is what the last mutation above exercises.npm run check— 0 errors, 652 files.npm test— 752/752.🤖 Generated with Claude Code