@@ -2,9 +2,77 @@ name: Dependabot Failure Watcher
22
33# Dependabot version updates run as GitHub Actions workflow runs named
44# "Dependabot Updates". This scheduled job looks back over the past week for any
5- # of those runs that failed and fails itself if it finds one, so a silently-broken
6- # ecosystem surfaces as a red scheduled run instead of only a red triangle in the
7- # Dependabot tab that nobody checks.
5+ # version-update run that failed and fails itself if it finds one, so a
6+ # silently-broken ecosystem surfaces as a red scheduled run instead of only a red
7+ # triangle in the Dependabot tab that nobody checks. Security-update runs share
8+ # that workflow name and are deliberately excluded -- see below.
9+ #
10+ # GitHub reuses the "Dependabot Updates" name for three different kinds of run:
11+ #
12+ # 1. A version update's scheduled scan: one run per .github/dependabot.yml
13+ # entry, on the schedule set there. It works out what is out of date and
14+ # opens or updates pull requests. This is the kind this watcher primarily
15+ # exists to catch -- when a scan breaks, the whole ecosystem quietly stops
16+ # being updated and nothing else tells anyone.
17+ # 2. A version update's per-pull-request refresh: one run per already-open
18+ # Dependabot pull request, rebasing or re-checking it. These are not driven
19+ # by the schedule at all -- a push to the base branch, a rebase, or an
20+ # "@dependabot recreate" comment triggers them, so they arrive in bursts
21+ # after merges rather than at the scheduled time. A failure here means one
22+ # open pull request has gone stale, which is worth knowing but is much
23+ # narrower than a broken scan.
24+ # 3. A security update: one ad-hoc job per vulnerable package, triggered by a
25+ # Dependabot alert rather than by dependabot.yml at all.
26+ #
27+ # Kind 3 routinely fails for reasons no pull request can fix: the advisory is
28+ # against a dependency this project does not declare directly, or no patched
29+ # version is reachable. Counting those would keep this workflow permanently red
30+ # and train everyone to ignore it, so they are filtered out below.
31+ #
32+ # Of the fields "gh run list --json" exposes, only the title separates the three
33+ # -- event, headBranch and actor are identical. Titles come in these shapes:
34+ #
35+ # - "<eco> in /." -- kind 1 at the repo root, which
36+ # has no " for " suffix
37+ # - "<eco> in <configured-dir>" -- kind 1 elsewhere, path verbatim
38+ # - "<eco> in / for <deps>" -- kind 2 at the repo root
39+ # - "<eco> in <configured-dir> for <deps>" -- kind 2 elsewhere
40+ # - "<eco> in /. for <one-dep>" -- kind 3 at the repo root
41+ # - "<eco> in <manifest-dir> for <one-dep>" -- kind 3 elsewhere, where
42+ # <manifest-dir> is wherever the vulnerable manifest was discovered
43+ #
44+ # At the root, then, kind 3 is marked by "/." AND a " for " suffix together, and
45+ # BOTH HALVES of " in /. for " are load-bearing -- do not shorten it. Matching on
46+ # " in /." alone would also discard every kind 1 run, which is most of the runs
47+ # here and the shape both failures this watcher was written for actually took.
48+ #
49+ # Outside the root, kinds 2 and 3 cannot be told apart by title, so the filter
50+ # has to name directories instead. e2e/js and e2e/ts (in the Node repos this
51+ # workflow is shared with) are consumer smoke tests carrying committed
52+ # lockfiles, so their transitive dev dependencies attract advisories that no
53+ # pull request can fix, and nothing in them is shipped code.
54+ #
55+ # Be clear about the cost, because it is not zero: both Node repos configure npm
56+ # with directories: ["/", "**/*"], and that glob does match e2e/js and e2e/ts,
57+ # so those directories DO get version updates. Dropping the pattern therefore
58+ # discards their kind 2 failures as well as their kind 3 ones -- there is an
59+ # open version-update pull request under e2e/ts in both repos as this is
60+ # written. The npm ecosystem label does not rescue the distinction either:
61+ # Dependabot writes "npm_and_yarn" for both kinds, so "npm_and_yarn in /e2e/ts
62+ # for js-yaml" could be either a security job or the refresh of a
63+ # version-update pull request.
64+ #
65+ # Accepted deliberately anyway. Kind 1 is what this watcher primarily exists to
66+ # catch and is still reported for those directories, so what is given up is the
67+ # narrower "one open pull request has gone stale" signal, for two directories of
68+ # test scaffolding, in exchange for dropping 16 unactionable failures in each of
69+ # the two Node repos over retained history.
70+ #
71+ # Reading the directories out of dependabot.yml instead looks more general but is
72+ # worse: entries may use globs (directories: ["**/*"]), which never match a title
73+ # literally, so genuine failures would be dropped without a word. Prefer a
74+ # denylist: when it goes stale it re-introduces noise, which is loud, whereas a
75+ # stale allowlist hides failures, which is silent.
876#
977# Runs entirely within this repo (no external service). A failed scheduled run
1078# emails the person who last edited the cron below. Note: GitHub auto-disables
@@ -22,22 +90,34 @@ jobs:
2290 check-dependabot-runs :
2391 runs-on : ubuntu-latest
2492 steps :
25- - name : Fail if any Dependabot update failed in the last 8 days
93+ - name : Fail if any Dependabot version update failed in the last 8 days
2694 env :
2795 GH_TOKEN : ${{ github.token }}
2896 REPO : ${{ github.repository }}
2997 run : |
3098 since=$(date -u -d '8 days ago' +%Y-%m-%dT%H:%M:%SZ)
31- failures=$(gh run list \
99+ # --created filters server-side, so --limit applies to runs already
100+ # narrowed to the window rather than to all of history. Runs come back
101+ # newest-first, so reaching the limit would drop the oldest in-window
102+ # runs and this step would report all-clear without them -- hence a
103+ # limit far above any plausible week's worth of runs.
104+ runs=$(gh run list \
32105 --repo "$REPO" \
33106 --workflow "Dependabot Updates" \
34- --limit 100 \
35- --json conclusion,createdAt,displayTitle,url \
36- --jq "[.[] | select((.conclusion == \"failure\" or .conclusion == \"startup_failure\" or .conclusion == \"timed_out\") and .createdAt >= \"$since\")]")
107+ --created ">=$since" \
108+ --limit 500 \
109+ --json conclusion,createdAt,displayTitle,url)
110+ failures=$(echo "$runs" | jq '
111+ [.[]
112+ | select((.displayTitle | contains(" in /. for ")) | not)
113+ | select((.displayTitle | test(" in /e2e/(js|ts) for ")) | not)
114+ | select(.conclusion == "failure"
115+ or .conclusion == "startup_failure"
116+ or .conclusion == "timed_out")]')
37117 count=$(echo "$failures" | jq 'length')
38118 if [ "$count" -gt 0 ]; then
39- echo "::error::$count failed Dependabot update run(s) in the last 8 days:"
119+ echo "::error::$count failed Dependabot version update run(s) in the last 8 days:"
40120 echo "$failures" | jq -r '.[] | "- \(.displayTitle) (\(.createdAt))\n \(.url)"'
41121 exit 1
42122 fi
43- echo "No failed Dependabot update runs in the last 8 days."
123+ echo "No failed Dependabot version update runs in the last 8 days."
0 commit comments