Skip to content
Merged
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
100 changes: 90 additions & 10 deletions .github/workflows/dependabot-failure-watcher.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,77 @@ name: Dependabot Failure Watcher

# 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
# triangle in the Dependabot tab that nobody checks. Security-update runs share
# that workflow name and are deliberately excluded -- see below.
#
# GitHub reuses the "Dependabot Updates" name for three different kinds of run:
#
# 1. A version update's scheduled scan: one run per .github/dependabot.yml
# entry, on the schedule set there. It works out what is out of date and
# opens or updates pull requests. This is the kind this watcher primarily
# exists to catch -- when a scan breaks, the whole ecosystem quietly stops
# being updated and nothing else tells anyone.
# 2. A version update's per-pull-request refresh: one run per already-open
# Dependabot pull request, rebasing or re-checking it. These are not driven
# by the schedule at all -- a push to the base branch, a rebase, or an
# "@dependabot recreate" comment triggers them, so they arrive in bursts
# after merges rather than at the scheduled time. A failure here means one
# open pull request has gone stale, which is worth knowing but is much
# narrower than a broken scan.
# 3. A security update: one ad-hoc job per vulnerable package, triggered by a
# Dependabot alert rather than by dependabot.yml at all.
#
# Kind 3 routinely fails 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. Counting those would keep this workflow permanently red
# and train everyone to ignore it, so they are filtered out below.
#
# Of the fields "gh run list --json" exposes, only the title separates the three
# -- event, headBranch and actor are identical. Titles come in these shapes:
#
# - "<eco> in /." -- kind 1 at the repo root, which
# has no " for " suffix
# - "<eco> in <configured-dir>" -- kind 1 elsewhere, path verbatim
# - "<eco> in / for <deps>" -- kind 2 at the repo root
# - "<eco> in <configured-dir> for <deps>" -- kind 2 elsewhere
# - "<eco> in /. for <one-dep>" -- kind 3 at the repo root
# - "<eco> in <manifest-dir> for <one-dep>" -- kind 3 elsewhere, where
# <manifest-dir> is wherever the vulnerable manifest was discovered
#
# At the root, then, kind 3 is marked by "/." AND a " for " suffix together, and
# BOTH HALVES of " in /. for " are load-bearing -- do not shorten it. Matching on
# " in /." alone would also discard every kind 1 run, which is most of the runs
# here and the shape both failures this watcher was written for actually took.
#
# Outside the root, kinds 2 and 3 cannot be told apart by title, so the filter
# has to name directories instead. e2e/js and e2e/ts (in the Node repos this
# workflow is shared with) are consumer smoke tests carrying committed
# lockfiles, so their transitive dev dependencies attract advisories that no
# pull request can fix, and nothing in them is shipped code.
#
# Be clear about the cost, because it is not zero: both Node repos configure npm
# with directories: ["/", "**/*"], and that glob does match e2e/js and e2e/ts,
# so those directories DO get version updates. Dropping the pattern therefore
# discards their kind 2 failures as well as their kind 3 ones -- there is an
# open version-update pull request under e2e/ts in both repos as this is
# written. The npm ecosystem label does not rescue the distinction either:
# Dependabot writes "npm_and_yarn" for both kinds, so "npm_and_yarn in /e2e/ts
# for js-yaml" could be either a security job or the refresh of a
# version-update pull request.
#
# Accepted deliberately anyway. Kind 1 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, in exchange for dropping 16 unactionable failures in each of
# the two Node repos over retained history.
#
# Reading the directories out of dependabot.yml instead looks more general but is
# worse: entries may use globs (directories: ["**/*"]), which never match a title
# literally, so genuine failures would be dropped without a word. Prefer a
# denylist: when it goes stale it re-introduces noise, which is loud, whereas a
# stale allowlist hides failures, which is silent.
#
# Runs entirely within this repo (no external service). A failed scheduled run
# emails the person who last edited the cron below. Note: GitHub auto-disables
Expand All @@ -22,22 +90,34 @@ jobs:
check-dependabot-runs:
runs-on: ubuntu-latest
steps:
- name: Fail if any Dependabot update failed in the last 8 days
- name: Fail if any Dependabot version update failed in the last 8 days
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
since=$(date -u -d '8 days ago' +%Y-%m-%dT%H:%M:%SZ)
failures=$(gh run list \
# --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)
failures=$(echo "$runs" | jq '
[.[]
| select((.displayTitle | contains(" in /. for ")) | not)
| select((.displayTitle | test(" in /e2e/(js|ts) for ")) | not)
Comment on lines +104 to +113

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 "== locate workflow =="
fd -a 'dependabot-failure-watcher.yml' . || true

echo "== relevant section =="
if [ -f ".github/workflows/dependabot-failure-watcher.yml" ]; then
  nl -ba .github/workflows/dependabot-failure-watcher.yml | sed -n '90,145p'
fi

echo "== repo action versions / gh availability clues =="
git ls-files .github/workflows | xargs -r sed -n '/runs-as: ubuntu|actions\/setup-node|setup-node|apt-get update|gh run list/p' 2>/dev/null | head -120

Repository: maxmind/GeoIP2-java

Length of output: 242


🌐 Web query:

GitHub CLI gh run list default limit --json behavior truncated latest run count

💡 Result:

In the GitHub CLI (gh), the gh run list command retrieves workflow runs with a default limit of 20 [1][2][3]. You can adjust this count using the -L or --limit flag [1][4]. Regarding the behavior of --json and truncation: 1. Data Truncation: There is no inherent, documented behavior in the GitHub CLI that forces truncation of data when using the --json flag [5]. If data appears truncated in output, it is generally not a standard feature of the --json output mechanism itself [5]. Users have occasionally reported issues regarding long string truncation, but these are typically treated as bugs or specific formatting issues rather than intended CLI behavior [5]. 2. JSON Fields: The --json flag requires a comma-separated list of fields [1][6]. If you do not know which fields are available, you can omit the arguments to the --json flag (e.g., gh run list --json) to see a list of valid fields for that command [6]. Valid fields commonly supported by the GitHub API for workflow runs include databaseId, number, url, status, conclusion, workflowName, createdAt, startedAt, updatedAt, event, headBranch, headSha, and displayTitle [7]. The gh run list command fetches data from the GitHub Actions REST API; therefore, the consistency and completeness of the returned data are primarily governed by that API rather than by client-side truncation in the GitHub CLI [5][7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'pwd=%s\n' "$(pwd)"
printf 'files:\n'
find .github/workflows -maxdepth 1 -type f 2>/dev/null | sort | head -100 || true

printf 'dependabot failure watcher path candidates:\n'
find . -name 'dependabot-failure-watcher.yml' -o -name 'dependabot-failure-watcher.yaml' 2>/dev/null | sort

printf 'workflow size:\n'
wc -l .github/workflows/dependabot-failure-watcher.yml 2>/dev/null || true

printf 'relevant section:\n'
sed -n '90,145p' .github/workflows/dependabot-failure-watcher.yml 2>/dev/null || true

printf 'gh run list related searches:\n'
rg -n "gh run list|limit 500|500|no runs failed|all-clear|fails" .github/workflows/dependabot-failure-watcher.yml .github/workflows 2>/dev/null || true

Repository: maxmind/GeoIP2-java

Length of output: 3951


🌐 Web query:

site:cli.github.com/manual/gh_run_list --limit gh run list returns runs created since

💡 Result:

The --limit flag for the gh run list command does not filter runs by date; instead, it specifies the maximum number of workflow runs to fetch [1]. The default value for this flag is 20 [1]. To filter runs by the date they were created, you must use the --created flag [1]. The --created flag accepts date ranges and specific dates in formats such as YYYY-MM-DD [1].

Citations:


🌐 Web query:

GitHub Actions run list API default 30 max 100 gh run list pagination

💡 Result:

When interacting with GitHub's REST API and the GitHub CLI (gh), pagination behavior and limits differ as follows: GitHub REST API (Workflow Runs) For endpoints such as List workflow runs for a repository or List workflow runs for a workflow, the API supports pagination using the per_page and page parameters [1][2][3]. - per_page default: 30 [1][2][3] - per_page maximum: 100 [1][2][3] - The API may return up to 1,000 results when specific filtering parameters (such as actor, branch, event, or status) are used [1][2][3]. GitHub CLI (gh run list) The GitHub CLI command gh run list abstracts these API calls. - --limit (-L) default: 20 [4][5] - The --limit flag allows you to specify the maximum number of runs to fetch [4][5]. - When using the lower-level gh api command, you can use the --paginate flag to automatically fetch all pages of results [6]. Note that when using --paginate with certain API endpoints or filters, there may be a hard limit of 1,000 results returned [7].

Citations:


🌐 Web query:

CLI gh run list 500 limit GitHub Actions API pagination

💡 Result:

For the gh run list command, the --limit flag allows you to specify the maximum number of workflow runs to fetch [1]. The current maximum value supported by this flag is 1,000 [2]. There is no native support for an "unlimited" option or a value higher than 1,000 for this specific command [3][2]. If you need to process or retrieve more than 1,000 workflow runs, you have two primary options: 1. Iterative batching: You can run gh run list in a loop, filtering by creation date or processing runs in batches of 1,000 until no more results are returned [4][2]. 2. Using gh api with pagination: The gh api command serves as a direct interface to the GitHub REST API and supports an --paginate flag [5][6]. Unlike gh run list, this flag allows the CLI to automatically follow pagination links until all available results are retrieved [5][7]. When using gh api for this purpose, you can target the workflow runs endpoint directly: gh api --paginate "/repos/{owner}/{repo}/actions/runs?per_page=100" [6] Note that when using gh api --paginate, you should be aware that it makes sequential HTTP requests, which will count against your GitHub API rate limits [7]. Additionally, some specific API endpoints—particularly those involving certain search parameters—may have their own server-side result caps (e.g., 1,000 results) regardless of client-side pagination [8]. If you encounter this, refining your query with more specific filters (such as status or date ranges) is recommended to retrieve the complete data set [4][8].

Citations:


Detect when gh run list hits its result cap.

The workflow already notes that gh run list returns runs newest-first and can drop the oldest in-window runs when --limit is reached. If the 500-run cap is reached, filter only applies to the fetched subset, so older failed version updates can be omitted and the all-clear can still be reported. Fail closed on query saturation or paginate until the full date-filtered result set is collected.

🤖 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 104 - 113,
Update the run collection around gh run list so it detects when the 500-result
limit is saturated and does not report an all-clear from an incomplete
newest-first subset. Fail closed when the cap is reached, or paginate through
all date-filtered runs before applying the existing jq failure filters; preserve
the current exclusions and failure reporting for the complete result set.

| select(.conclusion == "failure"
or .conclusion == "startup_failure"
or .conclusion == "timed_out")]')
count=$(echo "$failures" | jq 'length')
if [ "$count" -gt 0 ]; then
echo "::error::$count failed Dependabot update run(s) in the last 8 days:"
echo "::error::$count failed Dependabot version update run(s) in the last 8 days:"
echo "$failures" | jq -r '.[] | "- \(.displayTitle) (\(.createdAt))\n \(.url)"'
exit 1
fi
echo "No failed Dependabot update runs in the last 8 days."
echo "No failed Dependabot version update runs in the last 8 days."
Loading