Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 43 additions & 17 deletions .github/scripts/dependabot-janitor.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
#!/usr/bin/env bash
#
# Dependabot janitor: across all wyre-technology mcp-* and node-* repos, auto-merge
# Dependabot janitor: across all mcp-* and node-* repos in scope, auto-merge
# Dependabot patch/minor PRs — AND major bumps of dev/CI tooling (eslint, vitest,
# typescript, @types/*, GitHub Actions, etc.) — whose CI is green. Major bumps of
# RUNTIME dependencies, red CI, conflicts, and code-owner-blocked PRs are reported
# but never merged. A green test suite is treated as sufficient proof for dev/CI
# tooling (it doesn't ship at runtime); runtime majors always need a human.
#
# Requires: gh CLI authenticated via GH_TOKEN (a GitHub App installation token with
# contents:write + pull_requests:write across the org).
# contents:write + pull_requests:write across every org in ORGS).
#
# Env:
# ORG GitHub org (default: wyre-technology)
# ORGS space-separated GitHub orgs to scan (default: "wyre-technology
# WYRE-AI"). 2026-08-25: the *-mcp fleet moved from being entirely
# under wyre-technology to being split across both orgs (63
# repos total, only 13 remain under wyre-technology, 50 now under
# WYRE-AI, conduit included) — a single-org $ORG silently covered
# only 13/63 repos with no error, which is almost certainly why
# the dependabot backlog looked like permanent steady-state
# rather than something the janitor was actually working through.
# REPOS entries are now "org/name" pairs so every downstream gh
# call (-R "$repo") targets the repo's ACTUAL org, not a single
# global one — per-repo gh calls already worked across the split
# via GitHub's transfer redirect, but the enumeration step never
# did, since org-level listing doesn't follow transferred repos.
# ORG back-compat single-org override — if set, used as the sole
# entry in ORGS instead of the default two-org list. Prefer ORGS.
# DRY_RUN if "true", classify and report but do not approve/merge
# EXCLUDE_REPOS space-separated repo names to skip regardless of the
# in-scope regex match below (default: empty). 2026-08-21:
# in-scope regex match below (default: empty). Matched against
# the bare repo name, not "org/name" — a name is excluded on
# whichever org it's found in. 2026-08-21:
# used to hold out repos still pinned to the pre-fix
# mcp-server-release.yml (vacuous CI — the same bug class
# that got this janitor disabled 07-21) until each one's
Expand All @@ -26,23 +42,33 @@
# the regex.
set -uo pipefail

ORG="${ORG:-wyre-technology}"
ORGS="${ORG:-${ORGS:-wyre-technology WYRE-AI}}"
DRY_RUN="${DRY_RUN:-false}"
EXCLUDE_REPOS="${EXCLUDE_REPOS:-}"

work="$(mktemp -d)"
for cat in merged majors red pending conflicts blocked errors nocheck; do : > "$work/$cat"; done

# Repos in scope: names ending in -mcp, starting with mcp, or starting with node-,
# minus anything in EXCLUDE_REPOS.
# Repos in scope, across every org in ORGS: names ending in -mcp, starting with
# mcp, or starting with node-, minus anything in EXCLUDE_REPOS. Each entry is
# "org/name" so downstream `-R` calls target the repo's real org directly —
# no repo-name collisions expected across these two orgs, but if one ever
# occurs both entries survive (sort -u dedupes exact "org/name" pairs, not
# bare names), which is the conservative direction to fail in.
mapfile -t REPOS < <(
gh api --paginate "/orgs/$ORG/repos?per_page=100" \
--jq '.[] | select(.archived==false) | .name' \
| grep -E '(-mcp$|^mcp|^node-)' \
| { if [[ -n "$EXCLUDE_REPOS" ]]; then grep -vxF -f <(tr ' ' '\n' <<<"$EXCLUDE_REPOS"); else cat; fi; } \
for _org in $ORGS; do
gh api --paginate "/orgs/$_org/repos?per_page=100" \
--jq '.[] | select(.archived==false) | .name' \
| grep -E '(-mcp$|^mcp|^node-)' \
| sed "s|^|$_org/|"
done \
| awk -F/ -v exclude="$EXCLUDE_REPOS" '
BEGIN { n = split(exclude, ex, " "); for (i = 1; i <= n; i++) skip[ex[i]] = 1 }
!($2 in skip)
' \
| sort -u
)
echo "Scanning ${#REPOS[@]} repositories in scope."
echo "Scanning ${#REPOS[@]} repositories in scope across: $ORGS"
[[ -n "$EXCLUDE_REPOS" ]] && echo "Excluded (EXCLUDE_REPOS): $EXCLUDE_REPOS"

# Return the leading integer (major version) of a semver-ish string.
Expand Down Expand Up @@ -81,7 +107,7 @@ is_dev_major() {
}

for repo in "${REPOS[@]}"; do
prs="$(gh pr list -R "$ORG/$repo" --author 'app/dependabot' --state open \
prs="$(gh pr list -R "$repo" --author 'app/dependabot' --state open \
--json number,title,mergeable 2>/dev/null)" || { echo "$repo: pr list failed" >>"$work/errors"; continue; }
[[ "$(jq 'length' <<<"$prs")" == "0" ]] && continue

Expand All @@ -103,7 +129,7 @@ for repo in "${REPOS[@]}"; do

# CI status. gh pr checks exit codes: 0=all pass, 8=pending, 1=failing,
# non-zero+"no checks" => repo has no checks for this PR.
checks_out="$(gh pr checks "$num" -R "$ORG/$repo" 2>&1)"; rc=$?
checks_out="$(gh pr checks "$num" -R "$repo" 2>&1)"; rc=$?
if [[ $rc -eq 8 ]]; then echo "$label" >>"$work/pending"; continue; fi
if [[ $rc -ne 0 ]]; then
if grep -qi 'no checks' <<<"$checks_out"; then
Expand All @@ -125,7 +151,7 @@ for repo in "${REPOS[@]}"; do
# have auto-merged a TS7 major with zero flag -- worse than
# node-datto-rmm#46's already-flagged "(no CI)" case, since that one
# at least surfaced in the run summary.
buckets_json="$(gh pr checks "$num" -R "$ORG/$repo" --json bucket 2>/dev/null)"
buckets_json="$(gh pr checks "$num" -R "$repo" --json bucket 2>/dev/null)"
total="$(jq 'length' <<<"${buckets_json:-[]}" 2>/dev/null || echo 0)"
skipping="$(jq '[.[] | select(.bucket=="skipping")] | length' <<<"${buckets_json:-[]}" 2>/dev/null || echo 0)"
if [[ "$total" -gt 0 && "$total" == "$skipping" ]]; then
Expand Down Expand Up @@ -158,9 +184,9 @@ for repo in "${REPOS[@]}"; do
fi

# Approve (satisfies non-code-owner review requirements) then squash-merge.
gh pr review "$num" -R "$ORG/$repo" --approve \
gh pr review "$num" -R "$repo" --approve \
-b "Auto-approved by Dependabot janitor: CI green (patch/minor, or dev/CI-tooling major)." >/dev/null 2>&1
if merge_err="$(gh pr merge "$num" -R "$ORG/$repo" --squash --delete-branch 2>&1)"; then
if merge_err="$(gh pr merge "$num" -R "$repo" --squash --delete-branch 2>&1)"; then
echo "$label$flag" >>"$work/merged"
else
if grep -qiE 'review|code ?owner|protected|required|base branch policy|not mergeable|auto.?merge' <<<"$merge_err"; then
Expand Down
38 changes: 34 additions & 4 deletions .github/workflows/dependabot-janitor.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
name: Dependabot Janitor

# Daily sweep that auto-merges green Dependabot patch/minor PRs across all
# wyre-technology mcp-* and node-* repos. Majors, red CI, conflicts, and PRs
# blocked by required code-owner review are reported, never merged.
# mcp-* and node-* repos in ORGS (wyre-technology and WYRE-AI as of 2026-08-25
# — the fleet split across both orgs; see dependabot-janitor.sh's own header
# for the full story). Majors, red CI, conflicts, and PRs blocked by required
# code-owner review are reported, never merged.
#
# Auth: a GitHub App installation token (APP_ID / APP_PRIVATE_KEY org secrets).
# The app must have Contents: Read & write and Pull requests: Read & write.
# The app must have Contents: Read & write and Pull requests: Read & write,
# and — as of the dual-org fix — must be INSTALLED on both wyre-technology
# and WYRE-AI, or token minting below will fail for whichever org it's not
# installed on. Per boss (2026-08-25), the WYRE-AI install is a pending Aaron
# ask already tracked outside this repo — if this workflow starts failing on
# "Mint org-wide installation token" with a WYRE-AI-related error, that's the
# likely cause, not a bug in this file.

on:
schedule:
Expand Down Expand Up @@ -34,6 +42,19 @@ jobs:
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# NOT yet expanded to WYRE-AI: verified live (2026-08-25) via
# `gh api orgs/WYRE-AI/installations` that the wyre-projects-bot App
# (app_id 3588278, the one APP_ID/APP_PRIVATE_KEY mint tokens for) is
# NOT installed on WYRE-AI — only digitalocean, blacksmith-sh, vanta,
# and two infisical apps are. Adding `WYRE-AI` to `owner:` here before
# that install exists would very likely fail token minting outright
# (untested whether create-github-app-token degrades gracefully to a
# partial-org token or hard-fails on a missing installation — not
# worth risking on the currently-working wyre-technology half to find
# out). Script-side ORGS support (dependabot-janitor.sh) is ready for
# WYRE-AI; this `owner:` line is the one line still gating it. Add
# `WYRE-AI` here once the App install is confirmed, nothing else
# in this file needs to change.
owner: ${{ github.repository_owner }}

- name: Check out scripts
Expand All @@ -45,7 +66,16 @@ jobs:
- name: Run janitor
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
ORG: ${{ github.repository_owner }}
# Was ORG: ${{ github.repository_owner }} — that pinned this to
# wyre-technology only and silently overrode dependabot-janitor.sh's
# own ORGS default (see that file's header). Removed so the script's
# "wyre-technology WYRE-AI" default takes effect. The token above is
# NOT yet valid for WYRE-AI (App not installed there — see the
# token-minting step's comment), so every WYRE-AI repo will show up
# in this run's Errors section ("pr list failed") until that install
# lands — expected, isolated per-repo, and won't affect wyre-technology
# repos. That's the intended interim state: visible and diagnosable
# instead of silently scanning 13 of 63 repos with no error at all.
DRY_RUN: ${{ inputs.dry_run }}
BACKLOG_FILE: dependabot-backlog.md
# 2026-08-21: all 25 repos originally held out (see task_1786765529531
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,44 @@ here. The format is based on
`cancel-in-progress: false` is deliberate — a queued run waits for the
in-flight release/publish to finish rather than cancelling it mid-publish.

- **`dependabot-janitor.sh` / `dependabot-janitor.yml`**: added dual-org
support. The *-mcp/node-* fleet moved from being entirely under
`wyre-technology` to being split across `wyre-technology` (13 repos) and
`WYRE-AI` (50 repos, `conduit` included) sometime around 2026-08-24
evening/night. The janitor's repo enumeration was a single-org API call
(`ORG`, defaulting to `wyre-technology`), so it silently kept scanning
only the 13 repos still there — no error, just 50 of 63 repos never
looked at. Almost certainly the real explanation for a persistent
~80-108-PR "chronic dependabot backlog" that multiple `scan-mcp-repos`
cycles reported as steady-state review-gating rather than what it
actually was: the janitor never reaching those repos at all.

`ORG` is now `ORGS` (space-separated, default `"wyre-technology
WYRE-AI"`, with `ORG` kept as a back-compat single-org override). Each
`REPOS` entry is now an `"org/name"` pair rather than a bare name, so
every downstream `gh ... -R` call targets the repo's real org directly.
Verified live: the new enumeration finds 105 repos in scope (85 WYRE-AI
+ 20 wyre-technology, using the script's actual `-mcp$|^mcp|^node-`
pattern, broader than just the `*-mcp` fleet) vs. the ~20 the old
single-org call would have found.

**Not fully live yet — one line still gates it, left in place and
documented rather than silently forced.** The `wyre-projects-bot` App
(the one `APP_ID`/`APP_PRIVATE_KEY` mint tokens for) is confirmed NOT
installed on `WYRE-AI` (verified via `gh api orgs/WYRE-AI/installations`
— only `digitalocean`, `blacksmith-sh`, `vanta-with-task-management`,
and two `infisical` apps are). Adding `WYRE-AI` to the token-minting
step's `owner:` before that install exists risks failing token minting
outright rather than degrading gracefully (untested, and not worth
risking the currently-working `wyre-technology` half to find out) — so
`owner:` is left single-org for now, with the exact one-line change
documented inline for whoever does the App install. Meanwhile `ORGS`
already includes `WYRE-AI`, so every `WYRE-AI` repo will show up in the
run's Errors section (`pr list failed`, an auth failure) until the
install lands — expected, isolated per-repo (no crash, no effect on
`wyre-technology` repos), and turns a previously-invisible gap into a
visible, diagnosable one in the workflow's own summary output.

- **`mcp-server-release.yml`**: the `mcpb` job did not install the MCPB CLI, so
it failed on 24 of the 26 repos with a `pack:mcpb` script. Pack scripts shell
out to `npx mcpb pack`; only `autotask-mcp` and `blumira-mcp` carry
Expand Down