Skip to content

Ignore Dependabot security updates in the failure watcher - #421

Merged
oschwald merged 1 commit into
mainfrom
wstorey/stf-1245-dependabot-failure-watcher-emails-go-to-the-correct-person
Jul 30, 2026
Merged

Ignore Dependabot security updates in the failure watcher#421
oschwald merged 1 commit into
mainfrom
wstorey/stf-1245-dependabot-failure-watcher-emails-go-to-the-correct-person

Conversation

@horgh

@horgh horgh commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

GitHub runs 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 the project does not declare directly, or no patched
version is reachable -- so the watcher stays red every week on those and trains
everyone to ignore it.

This filters security-update runs out by title, documents why both halves of
" in /. for " are load-bearing, and bounds the gh run list query server-side
with --created instead of fetching all of history and filtering locally.

See the commit message for the full reasoning, including why reading the
directory list out of dependabot.yml was tried and rejected.

This workflow is shared verbatim across MaxMind repos. The change was developed
in maxmind/device-android (maxmind/device-android#71)
and is applied here unmodified; the resulting file is byte-identical in every
repo.

Summary by CodeRabbit

  • Bug Fixes
    • Improved detection of failed Dependabot version-update runs by expanding recent-run coverage and accounting for additional failure states (startup failures and timeouts).
    • Reduced false alarms by excluding security-update and consumer smoke-test runs from failure alerts using clearer title-pattern rules.

Copilot AI review requested due to automatic review settings July 30, 2026 16:59
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Dependabot failure watcher documents run-title classifications and updates recent-run filtering to exclude security and selected end-to-end advisory runs while preserving failure detection.

Changes

Dependabot failure watcher

Layer / File(s) Summary
Classify and filter Dependabot failures
.github/workflows/dependabot-failure-watcher.yml
Documents Dependabot title patterns, retrieves up to 500 runs from the last 8 days, excludes security and selected e2e advisory runs, and counts failure-oriented conclusions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit checks the runs at night,
Filtering titles left and right.
Failed scans hop into the queue,
Security blooms are skipped from view.
“All clear!” says Bunny, bright and spry.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: the failure watcher now ignores Dependabot security-update runs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wstorey/stf-1245-dependabot-failure-watcher-emails-go-to-the-correct-person

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Dependabot Failure Watcher workflow to avoid reporting known-unactionable Dependabot security update failures, keeping the signal focused on Dependabot version update health.

Changes:

  • Filters out Dependabot security-update workflow runs based on displayTitle patterns, with detailed rationale documented inline.
  • Switches gh run list time bounding to server-side filtering via --created (instead of fetching history and filtering by createdAt locally).
  • Refactors the run-query step to fetch runs once, then apply filtering with jq.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 3 to +6
# Dependabot version updates run as GitHub Actions workflow runs named
# "Dependabot Updates". This scheduled job looks back over the past week for any
# of those runs that failed and fails itself if it finds one, so a silently-broken
# ecosystem surfaces as a red scheduled run instead of only a red triangle in the
# Dependabot tab that nobody checks.
# version-update run that failed and fails itself if it finds one, so a
# silently-broken ecosystem surfaces as a red scheduled run instead of only a red

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/dependabot-failure-watcher.yml:
- Around line 99-105: Add regression coverage for the jq title classifier used
to build failures, with fixtures covering documented root, non-root, and e2e/js
and e2e/ts title shapes. Assert that root security runs and denylisted e2e runs
are excluded, while root version-update runs remain included.
- Around line 88-98: Update the Dependabot run query in the watcher step so it
cannot silently omit older runs when the 8-day window contains more than 500
results. Use pagination or narrower daily queries to retrieve the complete
window, or detect a capped result set and fail closed instead of reporting
all-clear; preserve the existing failure evaluation for all retrieved runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 86b83d44-8a16-4fd2-bcd5-94cd9cd5a8bc

📥 Commits

Reviewing files that changed from the base of the PR and between 293fff4 and 9aa1bd3.

📒 Files selected for processing (1)
  • .github/workflows/dependabot-failure-watcher.yml

Comment on lines +88 to +98
# --created filters server-side, so --limit applies to runs already
# narrowed to the window rather than to all of history. Runs come back
# newest-first, so reaching the limit would drop the oldest in-window
# runs and this step would report all-clear without them -- hence a
# limit far above any plausible week's worth of runs.
runs=$(gh run list \
--repo "$REPO" \
--workflow "Dependabot Updates" \
--limit 100 \
--json conclusion,createdAt,displayTitle,url \
--jq "[.[] | select((.conclusion == \"failure\" or .conclusion == \"startup_failure\" or .conclusion == \"timed_out\") and .createdAt >= \"$since\")]")
--created ">=$since" \
--limit 500 \
--json conclusion,createdAt,displayTitle,url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== matching workflow =="
fd -a 'dependabot-failure-watcher.yml' . || true

echo "== relevant workflow section =="
for f in $(fd 'dependabot-failure-watcher.yml' .); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,180p'
done

echo "== uses gh run list in workflows =="
rg -n "gh run list|limit 500|Dependabot Updates|conclusion|created" .github/workflows || true

echo "== docs/context references =="
if command -v gh >/dev/null 2>&; then
  gh --version || true
else
  echo "gh CLI not installed"
fi

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 1787


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file size =="
wc -l .github/workflows/dependabot-failure-watcher.yml

echo "== workflow top and result handling =="
nl -ba .github/workflows/dependabot-failure-watcher.yml | sed -n '1,150p'

echo "== gh CLI availability/version =="
if command -v gh >/dev/null 2>&1; then
  gh --version || true
else
  echo "gh CLI not installed"
fi

echo "== deterministic window/limit behavior probe =="
python3 - <<'PY'
from datetime import datetime, timedelta

# As modeled from the snippet comment/window: an eight-day window and only the first 500 runs are kept.
window_start = datetime(2026, 7, 1)
running_window_start = window_start - timedelta(days=8)
window_days = 8

total_runs = 501
failed_at_window_start_conclusion = "failure"
# Results are newest-first; index 0 is newest.
runs = [{"conclusion": failed_at_window_start_conclusion, "title": "version update"} for _ in range(total_runs)]
# gh run list with --limit 500 would keep runs[:500], discarding the oldest in-window run.
kept = runs[:500]
failures_kept = [r for r in kept if r["conclusion"] in {"failure", "startup_failure", "timed_out"}]

print({
    "window_start": str(window_start),
    "window_days": window_days,
    "running_window_start": str(running_window_start),
    "total_in_window": total_runs,
    "kept": len(kept),
    "dropped": total_runs - len(kept),
    "dropped_failure_conclusion": kept[0]["conclusion"] if kept else None,
    "failed_resulting_report_would_see": "failure" if failures_kept else "none",
})
PY

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 316


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== file size =="
wc -l .github/workflows/dependabot-failure-watcher.yml

echo "== workflow top and result handling =="
awk '{printf "%5d\t%s\n", NR, $0}' .github/workflows/dependabot-failure-watcher.yml | sed -n '1,150p'

echo "== gh CLI availability/version =="
if command -v gh >/dev/null 2>&1; then
  gh --version || true
else
  echo "gh CLI not installed"
fi

echo "== deterministic window/limit behavior probe =="
python3 - <<'PY'
from datetime import datetime, timedelta

# As modeled from the snippet comment/window: an eight-day window and only the first 500 runs are kept.
window_start = datetime(2026, 7, 1)
running_window_start = window_start - timedelta(days=8)
window_days = 8

total_runs = 501
failed_at_oldest_in_window_conclusion = "failure"
# Results are newest-first; index 0 is newest.
runs = [{"conclusion": failed_at_oldest_in_window_conclusion, "title": "version update"} for _ in range(total_runs)]
# gh run list with --limit 500 would keep runs[:500], discarding the oldest in-window run.
kept = runs[:500]
failures_kept = [r for r in kept if r["conclusion"] in {"failure", "startup_failure", "timed_out"}]

print({
    "window_start": str(window_start),
    "window_days": window_days,
    "running_window_start": str(running_window_start),
    "total_in_window": total_runs,
    "kept": len(kept),
    "dropped": total_runs - len(kept),
    "dropped_failure_conclusion": kept[0]["conclusion"] if kept else None,
    "failed_resulting_report_would_see": "failure" if failures_kept else "none",
})
PY

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 7358


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== gh run list limit flag docs =="
gh run list --help | sed -n '/--limit/,+8p' || true

echo "== repository run volume for Dependabot workflow (last 90 days) =="
gh run list \
  --repo "$GITHUB_REPOSITORY" \
  --workflow "Dependabot Updates" \
  --created ">=2026-04-01T00:00:00Z" \
  --limit 1000 \
  --json completedAt,status,conclusion | \
jq -r '.[] | "\(.completedAt)\t\(.status)\t\(.conclusion)"' | \
sed -n '1,120p'

echo "== exact count/version for a stable 7-day lookback window =="
count=$(gh run list \
  --repo "$GITHUB_REPOSITORY" \
  --workflow "Dependabot Updates" \
  --created ">=2026-06-22T00:00:00Z" \
  --limit 1 \
  --json createdAt | jq length)
echo "$count runs total last 7 days (sample capped at 1 to avoid large output)"

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 961


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== repository run volume for Dependabot workflow (last 90 days) =="
gh run list \
  --repo maxmind/MaxMind-DB-Reader-java \
  --workflow "Dependabot Updates" \
  --created ">=2026-04-01T00:00:00Z" \
  --limit 1000 \
  --json createdAt,endCandidate,completedAt,status,conclusion \
  --order asc | jq 'length'

echo "== exact count/version for a stable 7-day lookback window =="
total_7d=$(gh run list \
  --repo maxmind/MaxMind-DB-Reader-java \
  --workflow "Dependabot Updates" \
  --created ">=2026-06-22T00:00:00Z" \
  --limit 1000 \
  --json completedAt,status,conclusion)
echo "$total_7d" | jq 'length'
echo "$total_7d" | jq '.[] | select(.completedAt != null) | {createdAt:.createdAt,status:.status,conclusion:.conclusion,url:.url}' | jq -s '
  {
    total: length,
    first: .[0],
    last: .[-1],
    failures: [.[] | select(.conclusion == "failure" or .conclusion == "startup_failure" or .conclusion == "timed_out")] | length
  }'

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 1204


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== repository run volume for Dependabot workflow (last 90 days) =="
gh run list \
  --repo maxmind/MaxMind-DB-Reader-java \
  --status previous \
  --workflow "Dependabot Updates" \
  --created ">=2026-04-01T00:00:00Z" \
  --limit 1000 \
  --json createdAt,status,conclusion \
  | jq 'length'

echo "== exact count/version for a stable 7-day lookback window =="
total_7d=$(gh run list \
  --repo maxmind/MaxMind-DB-Reader-java \
  --status previous \
  --workflow "Dependabot Updates" \
  --created ">=2026-06-22T00:00:00Z" \
  --limit 1000 \
  --json createdAt,status,conclusion)
echo "$total_7d" | jq 'length'
echo "$total_7d" | jq '.[] | select(.createdAt != null) | {createdAt:.createdAt,status:.status,conclusion:.conclusion,title:.displayTitle}' | jq -s '
  {
    total: length,
    first: .[0],
    last: .[-1],
    failures: [.[] | select(.conclusion == "failure" or .conclusion == "startup_failure" or .conclusion == "timed_out")] | length
  }'

Repository: maxmind/MaxMind-DB-Reader-java

Length of output: 1399


Do not silently treat a capped result set as complete.

If more than 500 Dependabot runs exist in the 8-day window, older in-window runs are discarded and the watcher can report “all-clear” even though a version-update failure was dropped from the query result. Use pagination/offset-based queries, a narrower daily window, or fail closed when the limit is reached.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-failure-watcher.yml around lines 88 - 98,
Update the Dependabot run query in the watcher step so it cannot silently omit
older runs when the 8-day window contains more than 500 results. Use pagination
or narrower daily queries to retrieve the complete window, or detect a capped
result set and fail closed instead of reporting all-clear; preserve the existing
failure evaluation for all retrieved runs.

Source: MCP tools

Comment on lines +99 to +105
failures=$(echo "$runs" | jq '
[.[]
| select((.displayTitle | contains(" in /. for ")) | not)
| select((.displayTitle | test(" in /e2e/(js|ts) for ")) | not)
| select(.conclusion == "failure"
or .conclusion == "startup_failure"
or .conclusion == "timed_out")]')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the title classifier.

Add fixtures for the documented root, non-root, and e2e/{js,ts} title shapes, asserting that root security runs and denylisted e2e runs are excluded while root version-update runs remain included.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-failure-watcher.yml around lines 99 - 105, Add
regression coverage for the jq title classifier used to build failures, with
fixtures covering documented root, non-root, and e2e/js and e2e/ts title shapes.
Assert that root security runs and denylisted e2e runs are excluded, while root
version-update runs remain included.

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>
@horgh
horgh force-pushed the wstorey/stf-1245-dependabot-failure-watcher-emails-go-to-the-correct-person branch from 9aa1bd3 to e5b7609 Compare July 30, 2026 17:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.github/workflows/dependabot-failure-watcher.yml (1)

99-109: ⚠️ Potential issue | 🟠 Major

The 500-run cap can still report a false all-clear.

--created narrows the query, but --limit 500 still silently drops older in-window runs when there are more than 500. Paginate, query narrower windows, or fetch an extra row and fail closed when the cap is reached. This repeats the unresolved prior review finding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependabot-failure-watcher.yml around lines 99 - 109,
Update the run collection logic in the Dependabot Updates query so it cannot
silently omit older runs within the since window. Replace the fixed 500-run cap
with complete pagination or narrower-window queries; alternatively fetch one
additional row and fail closed when the cap is reached. Ensure all in-window
runs are evaluated before reporting all-clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In @.github/workflows/dependabot-failure-watcher.yml:
- Around line 99-109: Update the run collection logic in the Dependabot Updates
query so it cannot silently omit older runs within the since window. Replace the
fixed 500-run cap with complete pagination or narrower-window queries;
alternatively fetch one additional row and fail closed when the cap is reached.
Ensure all in-window runs are evaluated before reporting all-clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 128a4476-aee3-4be7-9b45-c83b2300f063

📥 Commits

Reviewing files that changed from the base of the PR and between 9aa1bd3 and e5b7609.

📒 Files selected for processing (1)
  • .github/workflows/dependabot-failure-watcher.yml

@oschwald
oschwald merged commit bbdbedb into main Jul 30, 2026
31 checks passed
@oschwald
oschwald deleted the wstorey/stf-1245-dependabot-failure-watcher-emails-go-to-the-correct-person branch July 30, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants