chores — triage and wrap up dependency-bump PRs
Sweep a repo’s open dependency-bump PRs — the chore(...)-titled, bot-authored PRs from Dependabot/Renovate (pinned GitHub Actions, git submodules, package deps) — and clear them: merge the safe ones, flag the risky ones. These are CI-gated, not review-gated, so they need a different loop than a human PR.
Default policy: merge patch/minor bumps once CI is green; for major bumps, fetch the changelog, summarize the breaking-change risk, and surface it for the user’s call before merging.
When this fires
- “handle chores”, “chores”, “do the chores”, “wrap up the chore PRs”
- “process the dependabot PRs”, “merge the dependency bumps”, “deal with the bump PRs”, “handle the dependency updates”
- A weekly Dependabot batch has piled up and you want it cleared.
What counts as a chore PR
A PR is in scope when either of these holds:
- Its author is one of the dependency bots this skill exists for, matched in the exact login form the source returns:
app/dependabot,dependabot[bot],app/renovate,renovate[bot]. An explicitchorescall names that population, which is what admits those two bots and no other author. - It looks like a chore — the title starts with
chore((e.g.chore(actions):,chore(submodule):,chore(deps):), or the labels includedependencies— and it passesmemories/reviewing-prs.md’s scope test for the invoking user: authored by the GitHub Actions app (github-actions, which openschore(submodule):bumps) or by the invoking user or one of their aliases, assigned to one of them, or one the user explicitly asked this run to work on (a mention such as “do not touch” followed by a number is not a request).
Human-authored feature PRs are out of scope — those go through ardia / gia (review-to-clean), not this skill — and so is a chore-titled or dependencies-labelled PR whose author is another lab member or another bot, unless the invoking user is assigned to it or explicitly asked this run to work on it.
Procedure
0. Establish the target repo
Default to the current repo; accept an explicit owner/name so you can sweep any repo without checking it out:
REPO="${REPO:-$(gh repo view --json nameWithOwner --jq .nameWithOwner)}"
# e.g. to target another repo: REPO=owner/other-repoThis skill is GitHub-first (gh). For a GitLab repo, the same shape applies via glab and @renovate/@dependabot-equivalent commands.
1. List the open chore PRs
Set the three scope inputs first, the way REPO is set above. PR_SCOPE_ALIASES is the comma-separated list of other logins memories/reviewing-prs.md names as the same person as the resolved user (leave it unset when that file lists none for them), and PR_SCOPE_REQUESTED is the comma-separated list of PR numbers the user explicitly asked this run to work on, never a number merely mentioned or excluded (leave it unset when there are none). PR_SCOPE_EXCLUDED is the comma-separated list of PR numbers the user told this run not to touch (“chores, but do not touch” followed by a number); it is a veto checked before every positive arm, bot authors included, so an excluded dependency-bot PR is neither listed nor merged. With all three unset the filter keeps only the resolved login’s own PRs, the assigned ones, and the bots’, which is the fail-closed default.
set -eo pipefail # a failed command, or a failed gh pr list in the pipeline below, stops here
ME=$(gh api user --jq .login 2>/dev/null) || ME="" # WHO_AM_I
if [ -z "$ME" ]; then
# Fail closed, and say so: with no identity the author and assignee arms
# stay unevaluated (aliases included), so only bot-authored and explicitly
# requested PRs pass.
PR_SCOPE_ALIASES=""
echo "::warning::identity lookup failed; author/assignee arms unevaluated (report this)" >&2
fi
# e.g. PR_SCOPE_ALIASES=other-login # from memories/reviewing-prs.md
# e.g. PR_SCOPE_REQUESTED=123,456 # PRs the user asked this run to work on
# e.g. PR_SCOPE_EXCLUDED=789 # PRs the user told this run not to touch
IDS=$(jq -cn --arg me "$ME" --arg al "${PR_SCOPE_ALIASES:-}" \
'[$me] + ($al | split(",") | map(select(length > 0))) | map(select(length > 0)) | unique')
REQ=$(jq -cn --arg r "${PR_SCOPE_REQUESTED:-}" \
'$r | split(",") | map(select(length > 0) | tonumber)')
EXC=$(jq -cn --arg x "${PR_SCOPE_EXCLUDED:-}" \
'$x | split(",") | map(select(length > 0) | tonumber)')
gh pr list --repo "$REPO" --state open --limit 200 \
--json number,title,author,assignees,labels,mergeable \
| jq -r --argjson ids "$IDS" --argjson req "$REQ" --argjson exc "$EXC" '.[] | select(
((.number as $n | $exc | index($n)) == null)
and (
(.author.login | test("^(app/(dependabot|renovate)|(dependabot|renovate)\\[bot\\])$"))
or (
(
(.author.login | test("^(app/github-actions|github-actions\\[bot\\]|github-actions)$"))
or ((.author.login as $a | $ids | index($a)) != null)
or any(.assignees[].login; . as $x | ($ids | index($x)) != null)
or ((.number as $n | $req | index($n)) != null)
) and (
(.title | startswith("chore("))
or (([.labels[].name] | index("dependencies")) != null)
)
)
)
) | "\(.number)\t\(.mergeable)\t\(.title)"' # LIST_PRS--limit 200 because gh pr list defaults to 30 — a piled-up weekly backlog would otherwise be silently truncated.
If there are none, say so and stop.
That listing is a snapshot. Assignment, the title, and the labels can all change while the sweep runs, so re-fetch every input the predicate reads (author, assignees, title, labels) and reapply the same predicate, the PR_SCOPE_EXCLUDED veto included, immediately before each write action in steps 2-5 (closing a bump PR, a @dependabot comment, a merge), and drop and report a PR that no longer passes.
2. Classify each PR by bump size
Record the head, the base, and the title before classifying; on GitHub use the PR pins and check-pr-fully-clean.py, while on GitLab use the MR sha, target_branch, and check-mr-fully-clean.py, since the classification, every read in step 3, and the merge in step 4 are claims about one SHA on one target under one title, and Dependabot can replace the head or retitle the PR between any two of them:
PINNED=$(gh pr view "$N" --repo "$REPO" --json headRefOid -q .headRefOid) # VIEW_PR
BASE=$(gh pr view "$N" --repo "$REPO" --json baseRefName -q .baseRefName) # VIEW_PR; a retarget at the same tip must not pass
TITLE=$(gh pr view "$N" --repo "$REPO" --json title -q .title) # VIEW_PR; the classification below reads this titleShell variables do not survive between tool calls, so print the three values and substitute the literals into every later command, rather than expecting $PINNED to expand later. If the head, the base name, or the title changes before the merge lands, start again from here, classification included. A retitle moves neither SHA and can turn a patch-looking bump into a major one.
Parse the version pair out of the title (... from X to Y) and compare the leading number:
- patch / minor — same major and the major is ≥ 1 (
3.0.2 → 3.0.3,2.4 → 2.7) → safe. 0.xbump (0.4 → 0.5,0.4.1 → 0.4.2) → review; under semver a0.xrelease may break between minor versions, and many0.xmaintainers don’t respect patch semantics either — don’t wave these through as safe.- major — leading number increases (
4 → 7,2 → 3,1 → 2) → review. - submodule (
chore(submodule):) — no semver; it tracks a moving branch by design. Treat a green submodule bump as safe (auto-advancing the pointer is the whole point), unless the diff is unexpectedly large. If the repository has migrated to a native plugin for the vendored tool (e.g.ai-configas a plugin), close the bump PR and remove the redundant submodule instead perremove-redundant-plugin-submodules.md.
When the title has no parseable version (some Renovate digests), fall back to the PR body’s update table or treat it as review.
3. Verify CI is fully green
A bump is only “safe to merge” if every required check passes. skipping is fine (path-filtered jobs); pending means wait, fail means stop.
gh pr checks "$N" --repo "$REPO" # PR_CHECKS
# pass / skipping → ok; pending → not ready yet; fail → do not mergeAlso confirm it isn’t conflicting:
gh pr view "$N" --repo "$REPO" --json mergeable,mergeStateStatus \
--jq '"\(.mergeable) / \(.mergeStateStatus)"' # VIEW_PRIf CONFLICTING / DIRTY, ask the bot to rebase rather than resolving by hand:
gh pr comment "$N" --repo "$REPO" --body "@dependabot rebase" # COMMENT_PR — Dependabot onlyFor a Renovate PR, tick the rebase checkbox in the PR body (or its Dependency Dashboard) — @dependabot comment commands do nothing on Renovate PRs.
4. Safe bumps (patch / minor / submodule + green) → merge
First read whether the base requires a merge queue (the rules probe in fully-clean’s stop bullet). On a base that requires a merge queue, stop and report the bump as blocked: the queue form of the gate is #3030 and is out of scope until it lands. Otherwise run the base-currency check from fully-clean’s stale-base rule (the Do bullets beginning “for a direct merge”), since a green head can still break the base when the base gained a check after the head’s CI ran. That check’s one-liner prints the tested base tip. Record the printed literal as TIP the same way. Immediately before the merge command, require the live head to equal $PINNED, the live base name to equal $BASE, the live title to equal $TITLE, and the live base tip to equal $TIP, and restart from the currency check if the tip moved or from step 2 if anything else did. A regenerated head that already contains the base would otherwise pass a currency check with CI never read for it. When that check finds the base stale, the bot-bump recovery is to update the branch pinned to $PINNED, wait until headRefOid differs from $PINNED, with a deadline of a few minutes (expiry is a failed update: stop and report it rather than restarting), and then start again from the top of step 2: re-record $PINNED and $BASE, re-classify the bump, and rerun the CI and conflict checks against the new pin (review stays skipped on bot PRs). The first different SHA is not necessarily the update’s result, since Dependabot or another writer can replace the head in the same window, so re-classification is what keeps the merge pinned to a head this skill has actually judged. gh api -X PUT "repos/$REPO/pulls/$N/update-branch" -f expected_head_sha="$PINNED" merges the base in, pinned to the head whose CI was read. A 422 whose message names an expected-head mismatch (match on the substring expected head sha, since the live text carries a curly apostrophe and a trailing period that this ASCII rendering cannot show) means the bot or another writer already replaced that head only if headRefOid actually differs from $PINNED — the identical message also appears when $PINNED itself was wrong (built from an abbreviation rather than read in full), so re-reading before touching the branch is what tells the two apart, not merely a precaution. Any other 422 is a failed update: stop and read the message. @dependabot rebase rewrites the head onto the base branch and also clears a conflict. It too replaces the head, so it is followed by the same bounded wait and restart from step 2, never by a direct merge on the old $PINNED. With the pin current and the checks green, merge directly. Dependabot deletes its own branch on merge.
gh pr merge "$N" --repo "$REPO" --squash --match-head-commit "$PINNED" # MERGE_PR; $PINNED is the headRefOid recorded abovePick a merge method the repo actually allows — --squash errors when squash merges are disabled; swap in --merge or --rebase to match the repo’s settings.
Do not arm gh pr merge --auto and do not hand the merge to @dependabot squash and merge. Auto-merge stays enabled across later pushes and fires on required checks alone, so the classified head can be replaced and different content merge without this skill’s scope and bump-risk checks rerunning. Wait for the checks and merge synchronously with the pin instead.
@dependabot ... comment commands do nothing on Renovate PRs. Merge those with the same pinned gh pr merge, not with the merge checkbox in Renovate’s Dependency Dashboard, which hands the merge to Renovate without the pin.
Batch the safe ones — merge them all in one pass, then report.
5. Major bumps → fetch the changelog, summarize, flag
Don’t merge a major bump blind, even when CI is green — a green build can still hide a behavior change. For each:
Read the release notes Dependabot already embedded in the PR body — the fastest source:
gh pr view "$N" --repo "$REPO" --json body --jq .body # VIEW_PR # look for the "Release notes", "Changelog", and "Commits" sectionsIf the body is thin, go to the source. For a GitHub Action the title’s dependency name is the repo (
actions/checkout), so:gh api "repos/<dep-owner>/<dep-repo>/releases" --jq '.[] | "\(.tag_name): \(.name)"' | heador
WebFetchthe project’s releases/CHANGELOG page.Summarize the breaking-change risk in one or two lines per PR — required runtime bumps (e.g. a newer Node for
actions/*v-major jumps), removed inputs, changed defaults — and give a recommendation (merge / hold / needs a workflow tweak first).Surface it for the user’s call. Always get an explicit sign-off before merging a major bump — that human checkpoint is the whole point of flagging it. Don’t self-clear a major because the changelog “looks safe.”
6. Report
A linked wrap-up table — every PR number a markdown link (repo policy) — plus a Pacific-time timestamp (TZ=America/Los_Angeles date "+%Y-%m-%d %H:%M %Z"; the explicit TZ enforces PT on a machine set to any other zone):
## Chores swept — <repo> — <PT timestamp>
| PR | Bump | Type | CI | Action |
|----|------|------|-----|--------|
| [#124](url) | r-spellcheck-action 3.0.2→3.0.3 | patch | ✅ | merged |
| [#120](url) | actions/checkout 4→7 | major | ✅ | held — needs Node 20+ runtime check |
Group as Merged, Flagged (major — your call), and Skipped (failing/pending/conflicting, with why). Never report “all clear” while a major bump is sitting unflagged.
Relationship to other skills
check-dependency-updates/cdu— the audit counterpart.cdufinds stale pins and opens/drives the bumps itself (or recommends adependabot.ymlthat automates them);choresprocesses the bump PRs that land. Usecduto catch what Dependabot misses,choresto clear what it opens.ardia/gia— the human-PR counterpart (drive feature PRs to a clean review verdict).choresis the bot-PR counterpart (CI-gated bumps). Don’t runardion a Dependabot PR —@claudereview is skipped on them by design.pr-status-all— read-only status of every open PR;choresis the acting version scoped to bump PRs.clean-branches/cb— Dependabot deletes its own remote branch on merge, but if you checked any out locally, sweep the stragglers there.defer-issue— if a major bump needs a real code change before it can land (e.g. migrate a removed Action input), file a follow-up issue instead of leaving the PR to rot.wrap-up— a session-end bookend;choresis the focused bump-PR sweep.
Anti-patterns
- ❌ Merging a major bump just because CI is green, or self-clearing one because the changelog “looks safe” — read the changelog and get an explicit sign-off.
- ❌ Running the full
ardireview loop on a bot bump PR (review is skipped on them; they’re gated on CI, not a reviewer). - ❌ Resolving a Dependabot merge conflict by hand — comment
@dependabot rebaseand let the bot redo it. - ❌ Force-merging a PR with
pendingorfailchecks. - ❌ Reporting “chores done” while a flagged major bump is still open with no decision recorded.
- ❌ Treating human feature PRs as chores (or vice-versa) — a dependency bot’s PR is a chore by author; any other PR needs the
chore(title ordependencieslabel and an in-scope author or assignee, never the title or label alone.