Dependabot Consolidation
SkillCommunicationCombine several open Dependabot pull requests into a single branch and PR: enumerate the open Dependabot PRs, cherry-pick them onto fresh main, rewrite commit messages to satisfy .github/commit-guidelines.md, verify locally, push, open the combined PR, wait for CI, and close the superseded PRs once it is green. Use when asked to combine, consolidate, batch, roll up, or squash together existing Dependabot PRs.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Dependabot Consolidation skill
What this skill tells your AI
The instructions your AI receives, as published by sillsdev/fieldworks in .claude/skills/dependabot-consolidation/SKILL.md and read by ahel’s review.
Turn several open Dependabot pull requests into one reviewable pull request, so
the repository pays for one CI cycle instead of one per bump. In scope: open
PRs authored by app/dependabot against one base. Out of scope: human-authored
dependency PRs and edits to .github/dependabot.yml (recommend those, do not
make them).
Three gates
The run stops and asks three times. Do not collapse them.
- Plan - after enumerating, before any branch or worktree exists.
- Push - after all local work and checks, before anything leaves the machine.
- Close - after CI goes green, before closing the superseded PRs.
Anything that deviates from the approved plan stops and reports rather than improvising: a dropped PR, a different base, a conflict, a red CI.
Step 1 - Enumerate
gh pr list --author "app/dependabot" --state open --limit 100 --json number,title,baseRefName,headRefName
Zero or one PR: nothing to combine, report and stop. More than one base:
stop and ask. Then fetch once - Dependabot branches live in this repository,
so no refs/pull/N/head plumbing is needed:
git fetch origin --prune
Read every commit body in each PR's range (git log --format=%B origin/<base>..origin/<headRefName> - never -1; a force-pushed PR can carry
several commits). From the updated-dependencies: trailers: count
dependency-name: lines for the PR title, and flag any
update-type: version-update:semver-major in the plan. A major in
github-actions is normal - the config only ignores nuget majors.
Step 2 - Gate 1: the plan
Plan:
Combine #1035, #1036, #1037 -> chore/dependabot-combined-20260812
Title: Chore: Bump 15 dependencies from 3 dependabot PRs
#1035 actions-minor group (2) minor/patch
#1036 actions/setup-dotnet 5->6 MAJOR
#1037 nuget-minor group (12) minor/patch
Local: cherry-pick, reword, gitlint + whitespace, build (nuget in batch)
Push: origin chore/dependabot-combined-20260812
Open: PR against main, label 'dependencies'
Then: wait for CI; if green, ask before closing #1035 #1036 #1037
Proceed?
Do not present file overlap as conflict - overlapping files usually apply
cleanly, and real conflicts are discovered by the cherry-picks themselves.
Branch name: chore/dependabot-combined-<yyyyMMdd> (chore/ is in live use
on the remote even though Docs/workflows/pull-request-workflow.md omits it).
Step 3 - Construct the branch
Work in a dedicated worktree. scripts/Worktree-CreateFromBranch.ps1 decides
the location; use a flat directory name, because the branch name contains a
slash. Creating one by hand instead:
git worktree add --no-track <worktreeRoot>/dependabot-combined-<yyyyMMdd> -b <branch> origin/<base>
--no-track prevents the branch tracking origin/<base> (else git status
reads "ahead of main" and a stray git pull rebases it). Branching off current
origin/<base> makes Dependabot-branch staleness a non-issue.
For each PR in ascending order, for each of its commits in order:
git cherry-pick <sha>, then git commit --amend -F <msgfile> when the
message needs rewriting. --amend preserves the original author and date.
Write <msgfile> with the Write tool, never a shell heredoc (PowerShell 5.1
mangles the backticks and quotes these bodies contain).
On conflict, abort the whole run: cherry-pick --abort, remove the
worktree, delete the branch, and report the conflicting PR, the paths, and the
exact retry command with that PR excluded.
Rewriting the message
Dependabot bodies fail gitlint's B1 (body line <= 80) on their markdown link lines, and Dependabot writes at least three body formats - read the actual body, never pattern-match on one form:
- nuget group:
Bumps <name> from A to Bprose lines, no links. Already compliant - do not amend at all. - actions group: a
Bumps the <group> with N updates: [a](url)...summary,Updates `name` from A to Blines, and link bullets. - single dependency: one
Bumps [name](url) from A to B.line plus link bullets - the only prose naming the dependency.
Apply in order, never deleting a line whose information survives nowhere else:
- Delete every
- [Release notes](url)/- [Changelog](url)/- [Commits](url)bullet. - Strip markdown links in surviving lines:
[text](url)becomestext. - Delete a still-long line only if it is a group summary restated by the per-dependency lines below; if it is the only place a dependency is named, stop and ask.
- Keep every
Updates .../Bumps ...dependency line, the---separator, the wholeupdated-dependencies:trailer with its closing..., andSigned-off-by:. - Collapse blank-line runs to one.
Before committing, assert: subject <= 72, no trailing punctuation, blank second line, body lines <= 80, no hard tabs, no trailing whitespace.
Step 4 - Verify locally
git -C <wt> log --check --pretty=format:"---% h% s" origin/<base>..
gitlint --ignore body-is-missing --commits origin/<base>..
gitlint is the exact tool CI runs. If absent, point at the install in
.github/commit-guidelines.md and check the rules directly (do not pip
install without asking). When checking directly, a check that cannot execute
is a failure, not a pass - e.g. grep -P dies outside UTF-8 locales in Git
Bash and reports nothing; prefer grep -q "$(printf '\t')" and confirm each
check ran.
Check for deliberate pins before accepting any package bump
Dependabot cannot tell a dependency from a pin. Read the comment block above
every changed PackageVersion/version-property line
(git diff -U6 origin/<base>..HEAD -- Directory.Packages.props Build/Src/Directory.Packages.props Build/SilVersions.props). Stop on any
comment that names a version, says pin/stays/do-not-bump, cites a PR or issue,
or gives a retest procedure. Then classify:
- Constraint-driven (comment cites a range): verify from the depending
package's
.nuspecunderpackages/. Unbracketedversion="X"is a floor - bumping above it is always safe (pinning too LOW is what causes NU1109); bracketed[X]/[X,Y)ranges are the only blocking kind and are rare. Take a floor-cleared bump; refresh a comment that names a stale number. - Empirically-driven (pinned because a version was observed to break
something, e.g.
Microsoft.Extensions.DependencyModelat 9.0.16 from PR #1000): no manifest expresses this, so the graph will never block it - these belong in dependabot'signore:list. Never clear one on a green CI: PR #984 was green at the documented-broken version, so CI is not evidence either way. Only the pin's own retest procedure counts, and the procedure must first be validated against the known-bad version (a positive control); if the known-bad version also passes, the repro is lost - keep the pin, report that, and never declare the bump safe. Re-run any suspected failure before attributing it to a bump; flaky tests mimic signal.
Two more structural checks, both silent when violated:
- No property-to-literal rewrites: ~a third of root entries read
Version="$(SilLcmVersion)"etc. with the value inBuild/SilVersions.props.git diff ... | grep -E '^[-+].*Version="\$\('- any hit needs a look. - Build/Src layering:
Build/Src/Directory.Packages.propsimports the root and layersInclude(build-only packages) /Update(overrides a root pin), so one package can be deliberately at two versions (DependencyModel 9.0.16 root / 2.1.0 Build/Src; Microsoft.Bcl.AsyncInterfaces 9.0.16 root / 10.0.4 Build/Src). When a changed package appears in both files, say which sites moved.Build/Src/NativeBuild/NativeBuild.csprojis the only CPM opt-out; a literal version in any other csproj is a violation, not a bump.
Second-order: a batch can bump a pinned package AND the package whose comment
justifies the pin (ParatextData is in Dependabot's scope). Re-derive ranges
from the new .nuspec, not the comment.
Anticipate Paratext integration impact
Paratext loads the ILRepack-merged FwParatextLexiconPlugin.dll into its own
net48 process, where FieldWorks' binding redirects do not apply and the
plugin's config carries only dllmaps. External references resolve there at
exact strong-name versions or through a version-ignoring fallback over
Paratext's own directory - so a bump can break or silently downgrade the
plugin inside Paratext while every FieldWorks build and test stays green.
Nothing in CI covers this; the merged plugin first executes inside a real
Paratext.
This skill does not fix Paratext-contract breaks; it anticipates them. Treat a
batch as Paratext-sensitive when it touches any of: SIL.LCModel*, SIL.Core*
or the other SIL packages internalized by
Src/FwParatextLexiconPlugin/ILRepack.targets, ParatextData, SIL.Machine*,
icu.net, CommonServiceLocator, or the packages pinned "so the copies
ILRepack internalizes are deterministic"
(Microsoft.Extensions.DependencyInjection*, Microsoft.Bcl.AsyncInterfaces).
Then:
-
Say in the plan (gate 1) that the batch is Paratext-sensitive and why.
-
After the local build, diff the merged plugin's external references between the base build and the branch build - run this against each and compare:
powershell -Command "[System.Reflection.Assembly]::ReflectionOnlyLoadFrom('<dir>\Output\Debug\FwParatextLexiconPlugin.dll').GetReferencedAssemblies() | Sort-Object Name | ForEach-Object { \"$($_.Name) $($_.Version)\" }" -
Report any difference to the user and stop there: an added or version-shifted strong-named reference is a change to what Paratext's process must resolve. New externals usually mean a package swapped its internals (as liblcm's StructureMap-to-DI swap did); the fix - extending the ILRepack list or adjusting pins - is deliberate follow-up work with its own verification, not part of this run.
-
ParatextDatabumps deserve a release-notes read even when green:ParatextDataIntegrationTestsself-skips without an installed Paratext, so CI coverage of that package is partial. -
When a batch changed what gets internalized into the plugin, recommend a manual smoke of the lexicon plugin inside a real Paratext before release - there is no automated coverage of the merged assembly anywhere.
Build, only when the batch touches packages
If any picked commit touches *.props, *.json, or another package manifest,
build - the nuget group matches *, so it can carry build-critical
(Microsoft.Build.Utilities.Core backs FwBuildTasks) or test-only packages.
Seed native artifacts first (a nuget bump cannot invalidate them), noting
robocopy's exit codes 0-7 all mean success:
robocopy <mainRepo>\Output\<Config> <wt>\Output\<Config> /E /NFL /NDL /NJH /NJS /R:1 /W:1
build.ps1 needs vswhere.exe; prepend its directory in the same invocation:
$env:PATH = "$env:PATH;C:\Program Files (x86)\Microsoft Visual Studio\Installer"
.\build.ps1 -BuildTests -SkipNative -StartedBy agent
With the seed this is ~90 seconds, so never skip it on cost grounds; only the
unseeded native fallback is expensive. Afterward confirm the bumped versions
actually restored rather than being shadowed by LocalDevPackages:
find <wt> -name project.assets.json | xargs grep -ohE '"SIL\.(Core|LCModel)/[0-9][^"]*"' | sort -u
If resolved versions disagree with the picked commits, the build proved nothing about the bump - say so. For an actions-only batch skip the build entirely; it says nothing about workflow YAML. Either way the local build is a smoke test, not a stand-in for CI.
Step 5 - Gate 2: push and open
git -C <wt> push -u origin <branch>
gh pr create --base <base> --head <branch> --title "..." --body-file <file> --label dependencies
Open ready, not draft (drafts run CI anyway). Write a purpose-built body with
--body-file, not the PR template: what it combines and why (one CI cycle
instead of N); how it was built (original commits cherry-picked onto current
main, authors preserved, which bodies were trimmed and which left
byte-identical); a table of actual old -> new versions per source PR; any
major bump called out in prose; and an itemized what-was-verified /
what-was-not section - never claim a build that did not run. Add the reviewer
note that Dependabot PRs show red Build Debug and run tests because they
cannot read secrets.CODECOV_TOKEN, while this PR runs from a repo-owned
branch.
Step 6 - Wait for CI
gh pr checks <new> --watch --interval 30
Run it in the background; it exits non-zero on any failed check - that is a verdict, not a tool error. A full cycle is roughly 13 minutes.
If CI is red: report the failing job, step, and log URL, say whether it reads as a dependency failure or infrastructure, leave everything open, and stop. Do not guess which bump caused it and do not re-run.
Step 7 - Gate 3: close the superseded PRs
Only after CI is green, and only after asking:
gh pr close <N> --comment "Superseded by #<new>."
Then git worktree remove <wt>.
Final report
- Combined PR number and URL; which PRs went in, which were excluded and why.
- What was verified locally and what was left to CI; CI verdict.
- Paratext-sensitivity assessment and contract-test verdict, when applicable.
- Which originals were closed.
Signals
- GitHub stars
- 111
- Forks
- 42
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
dependabot-consolidation- Source
- github.com/sillsdev/fieldworks