Skip to content

Commit 8fe7984

Browse files
horghclaude
andcommitted
Ignore Dependabot security updates in the failure watcher
GitHub runs both Dependabot version updates and Dependabot security updates under one workflow name, "Dependabot Updates", and the watcher counted both. Security updates routinely fail for reasons no pull request can fix -- the advisory is against a dependency this project does not declare directly, or no patched version is reachable. Left alone the watcher stays red every week on those and trains everyone to ignore it. Filter those runs out by title. Version updates are unaffected. The title check is subtler than it looks, so document it properly. A security job is marked by "/." AND a " for " suffix together; version updates are either "/." with no " for " (the scheduled scan) or "/" with one (the pull request). Both halves of " in /. for " are therefore load-bearing -- matching on " in /." alone would discard every scan run, which is most of the version-update runs and the shape the failures this watcher was written for actually took. That "/." spelling only separates the two at the repo root. In a subdirectory a security update and a version update's pull request render identically, so drop " in /e2e/{js,ts} for " by name as well. The Node repos this workflow is shared with carry committed lockfiles under e2e/js and e2e/ts, whose transitive dev dependencies attract advisories no pull request can fix, and nothing in either is shipped code. That is 16 unactionable failures in each of GeoIP2-node and minfraud-api-node over retained history; repos without those directories are unaffected. Unlike the root filter, this one is not free. Both Node repos configure npm with directories: ["/", "**/*"], and that glob does match e2e/js and e2e/ts, so those directories do get version updates -- there is an open version-update pull request under e2e/ts in both repos as this is written. Dropping the pattern discards their pull-request refresh failures along with the security jobs, and the ecosystem label is no help because Dependabot writes "npm_and_yarn" for both. Taken anyway: the scheduled scan is what this watcher primarily exists to catch and is still reported for those directories, so what is given up is the narrower "one open pull request has gone stale" signal for two directories of test scaffolding. After filtering, 4 genuine failures remain reported in GeoIP2-node and 3 in minfraud-api-node. Reading the directories out of dependabot.yml would look more general and was the earlier plan here, but it fails green. Entries may use globs, and minfraud-api-dotnet's directories: ["**/*"] yields titles like "nuget in /**/*" for the scan and "nuget in /MaxMind.MinFraud for System.Net.Http.Json" for the pull request, neither of which any literal comparison against the configured value matches -- so its two real nuget failures would have been dropped without a word. A stale denylist re-introduces noise, which is loud; a stale allowlist hides failures. Name the three kinds of run in the comment while here, because the scheduled scan and the per-pull-request refresh are easy to conflate: the refresh runs are one per open pull request and are triggered by pushes to the base branch or by rebases, not by the schedule, so they arrive in bursts after merges. The scan is the kind this watcher primarily exists to catch, which is what makes hiding a hypothetical refresh failure under e2e an acceptable cost rather than a hole. Bound the query server-side with --created instead of fetching all of history and filtering by date locally, so --limit now caps an already-narrowed window rather than standing in for one, and the run list drops from several API pages to one. --limit rises 100 -> 500 as a backstop: it still applies before the title filter, and reaching it would silently drop the oldest in-window runs. This workflow is shared verbatim across MaxMind repos. The change was developed in maxmind/device-android and is applied here unmodified; see that repo's commit for the measurements it was derived from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 256e59b commit 8fe7984

1 file changed

Lines changed: 90 additions & 10 deletions

File tree

.github/workflows/dependabot-failure-watcher.yml

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)