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.
Comment on lines +55 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove time-sensitive facts from permanent workflow documentation.

The “open pull request” statement and exact count of 16 failures will become stale as PRs merge and retention changes. Keep the durable rationale, but remove these snapshot details or record them in dated external documentation.

🤖 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 55 - 69,
Update the workflow comment around the Dependabot directory-pattern tradeoff to
remove time-sensitive claims about currently open pull requests and exact
failure counts. Preserve the durable rationale that excluding the patterns also
drops kind 2 and kind 3 signals, while retaining kind 1 reporting and accepting
the narrower coverage tradeoff.

#
# 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 \
Comment on lines +104 to +108
--json conclusion,createdAt,displayTitle,url)
Comment on lines 98 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching workflow:"
fd -a 'dependabot-failure-watcher\.yml$' . || true

if [ -f .github/workflows/dependabot-failure-watcher.yml ]; then
  echo
  echo "Relevant workflow section:"
  cat -n .github/workflows/dependabot-failure-watcher.yml | sed -n '80,140p'
fi

echo
echo "gh cli availability:"
if command -v gh >/dev/null 2>&1; then
  gh --version | head -20
else
  echo "gh not installed"
fi

echo
echo "Search for gh run list usage in workflow:"
rg -n "gh run list|--limit|--created|run list" .github/workflows/dependabot-failure-watcher.yml || true

Repository: maxmind/minfraud-api-node

Length of output: 2738


🌐 Web query:

GitHub CLI gh run list --limit documentation

💡 Result:

The --limit flag (shorthand -L) is used with the gh run list command in the GitHub CLI to specify the maximum number of workflow runs to fetch [1][2]. Usage: gh run list --limit or gh run list -L Key details: - Default value: The default limit is 20 [1][3]. - Functionality: It constrains the number of recent workflow runs retrieved from the repository [1][4]. - Compatibility: This flag can be combined with other filtering flags such as --workflow, --branch, --status, and --user to refine the list of results [1][5]. For example, to list only the 5 most recent workflow runs, you would use: gh run list --limit 5 [4]

Citations:


🌐 Web query:

GitHub CLI gh run list return code truncated response pagination --limit

💡 Result:

The GitHub CLI command gh run list uses the --limit (-L) flag to control the number of workflow runs fetched, with a default value of 20 [1][2]. Regarding your concerns about pagination, truncation, and return codes: Pagination and Truncation The gh run list command does not perform automatic pagination across all available history to provide a complete list; instead, it fetches a single batch of runs up to the specified --limit [3]. If you require a complete history or a total count, you must use gh api with GraphQL to query totalCount, or use gh api --paginate to iterate through all pages [3]. Note that in some scenarios, such as when using custom filters or tools that filter the output, pagination might stop prematurely if a batch is filtered to zero results, which can lead to silent truncation of your expected dataset [4]. Additionally, while some users have reported perceived string truncation when using the --json flag, the GitHub CLI does not typically truncate long strings in its output; verify your pipe commands (like head or jq) if you are encountering truncated data, as they can cause broken pipe errors (often resulting in exit code 141) [5][6]. Return Codes - Exit code 0: The command executed successfully [7]. - Exit code 1: A general error occurred [7]. - Exit code 141: This is a SIGPIPE error that frequently occurs when piping output to commands like head or tail [6]. The head command may exit after receiving the required number of lines, leaving the gh process with nowhere to send the remaining data [6]. You can often resolve this by using jq functions such as first instead of piping to head [6]. If no runs are found for a repository, the command generally handles this gracefully without returning a non-zero error code [8].

Citations:


🌐 Web query:

GitHub Actions workflow runs API list run count per workflow in range limit

💡 Result:

To list workflow runs for a specific workflow via the GitHub Actions REST API, you use the endpoint: GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs [1][2]. Regarding the limits on the number of results returned: 1. Per-Page Pagination: You can control the number of results returned in a single request using the per_page parameter, which has a maximum value of 100 [1][3][4]. 2. Search Result Limit: When using specific filtering parameters (such as actor, branch, event, or status), the GitHub API documentation notes that these endpoints will return a maximum of 1,000 results [1][2][3]. This is a known limitation when filtering [5]. 3. Retrieving All Results: If you need to retrieve more than the 1,000-result limit, it is recommended to avoid using the filter parameters directly in the API call [5]. Instead, you should fetch the data without filters (if possible) and use client-side logic—such as tools like jq or programming language filters—to perform the filtering after the data has been retrieved [5]. To navigate through large sets of results, you must use pagination by incrementing the page parameter in your requests [1][3][4]. top_results: 1, 3, 4, 6, 9

Citations:


🌐 Web query:

site:github.com/cli/cli gh run list --created pagination limit

💡 Result:

The gh run list command does not have a specific "pagination limit" parameter for the --created flag itself, but it uses the --limit flag to control the total number of workflow runs retrieved [1][2][3]. When you use --created, it filters results based on a date-time range using GitHub's search syntax (e.g., ">=2024-01-01" or "2024-01-01..2024-02-01") [1][4][5]. The command fetches runs from the API and handles pagination automatically until it reaches the total count specified by the --limit flag [6]. Key technical details: - --limit [int]: Specifies the maximum number of runs to fetch (default is 20) [1][3]. - --created [date]: Filters runs created within a specific date-time range. It supports GitHub's standard date search syntax [1][4]. - Pagination: The CLI internally manages pagination via the API (using per_page parameters) and stops once the requested --limit is satisfied [6]. To fetch more runs than the default, simply use the --limit flag in combination with your --created filter: gh run list --created ">=2026-07-01" --limit 100

Citations:


🌐 Web query:

site:github.com/cli/cli --created pagination "gh_run_list" fetchUntilLimit

💡 Result:

The GitHub CLI command gh run list does not currently support a feature named fetchUntilLimit or a mechanism to automatically fetch all runs regardless of quantity via a single flag [1]. 1. Current Pagination Mechanism: The gh run list command retrieves workflow runs using a --limit flag (aliased as -L), which defaults to 20 or 30 depending on the version and configuration [2][3]. This flag restricts the total number of items fetched from the GitHub API [4]. 2. Fetching All Runs: There is no native flag (like --all) to bypass this limit [1]. Users wishing to retrieve all runs must either: - Provide a sufficiently large integer to the --limit flag [3]. - Use the gh api command combined with the --paginate flag, which automatically fetches all pages of results from the GitHub API [4][5]. 3. Technical Context: The internal implementation of gh run list typically handles pagination by requesting specific page sizes (e.g., 50 or 100) from the API [6]. While developers have discussed allowing --limit to accept 0 or -1 to signify "all," this functionality has not been implemented as of July 2026 [1]. If you need to retrieve every workflow run, the recommended approach is using gh api --paginate: gh api --paginate repos/:owner/:repo/actions/runs You can then filter or format the output using --jq or --template [4][5][7]. Note that fetching all runs on very active repositories may be subject to API rate limits [8].

Citations:


🌐 Web query:

"Search API" "maximum limit" "1000" GitHub REST workflow runs

💡 Result:

The GitHub REST API endpoints for workflow runs impose a limit of 1,000 results when specific query parameters are used, effectively acting as a search interface [1][2]. Specifically, when you use parameters such as actor, branch, check_suite_id, created, event, head_sha, or status to filter your request, the API restricts the total number of retrievable items to 1,000, regardless of pagination [1][2]. To retrieve more than 1,000 workflow runs, you must avoid using these filtering parameters in your API request [2]. If you need to retrieve a large history of runs, you can fetch them without filters (or with fewer filters) and perform the necessary filtering on the client side [2]. For standard pagination, the per_page parameter has a maximum value of 100, which allows you to efficiently iterate through results beyond the first page until you reach the 1,000-item search limit or the end of the total result set when filters are not applied [1][3].

Citations:


🌐 Web query:

"Search API" "maximum limit" "1000" GitHub Actions workflow runs

💡 Result:

When using the GitHub REST API to list workflow runs, the behavior regarding result limits depends on whether you are using filter parameters. Unfiltered requests: If you do not use specific filter parameters, the endpoint generally supports standard pagination [1], allowing you to retrieve more than 1,000 results by iterating through pages using the page parameter [2][3]. Some sources indicate an effective limit of up to 40,000 items in unfiltered mode [4]. Filtered requests: If you use certain filter parameters—such as actor, branch, check_suite_id, created, event, head_sha, or status—the API switches to a search-based index [1][5]. When these filters are applied, the endpoint is subject to a hard limit of 1,000 results [1][4][5]. Even if you paginate through the results, you will not be able to retrieve more than 1,000 items per query when these filters are active [1][6]. To retrieve more than 1,000 results when filters are required, developers typically use strategies such as time-window segmentation (e.g., using the created parameter to query smaller, overlapping, or sequential time ranges) to keep each request under the 1,000-item cap [4]. Additionally, while individual page size can be set using the per_page parameter (up to a maximum of 100) [2][7], this does not bypass the overall 1,000-result limit imposed by the search-indexed filtered queries [1][6].

Citations:


🌐 Web query:

GitHub REST "Search API" maximum results "1000" "total results" "maximum"

💡 Result:

The GitHub REST API enforces a hard limit of 1,000 results for any single search query [1][2][3]. Even when using pagination parameters, you cannot retrieve more than 1,000 items in total for a given search request [2][3]. To work around this limitation, developers typically break their search into smaller, manageable segments by applying filters that restrict the result set to fewer than 1,000 items [2][3]. Common strategies include: - Date Filtering: Splitting the search by specific time ranges (e.g., created_at or updated_at ranges) to reduce the number of results per query [2][3]. - Attribute Filtering: Using additional query parameters—such as file size, language, or repository name—to slice the search space into smaller segments [2]. If a query produces more than 1,000 matches, the API will still return a total_count field indicating the full number of matching items, but the actual data returned will be restricted to the first 1,000 [1][3]. While these limitations apply to the Search API, other non-search endpoints (such as those retrieving lists of pull requests or issues directly from a repository) may not be subject to this same 1,000-result cap [1].

Citations:


🌐 Web query:

GitHub Docs Action workflow runs list --created "1000" "total items"

💡 Result:

When interacting with the GitHub REST API or GitHub CLI (gh) to list workflow runs, you may encounter a limitation of 1,000 items when using filtered searches (such as filtering by creation date or status) [1][2][3][4]. GitHub enforces different pagination caps for the /actions/runs endpoint: 1. Unfiltered mode: In general requests without specific filters, the API allows access to a larger number of items (historically reported as up to 40,000) [1][2]. 2. Filtered mode: When you apply filters such as created or status, GitHub enforces a cap of 1,000 items per query [1][2][3]. If you exceed this number, the API may return an error (such as HTTP 422) or simply truncate the results [1][2][3]. To retrieve more than 1,000 workflow runs, you must work around these pagination limits by "bisection" or adaptive time-windowing [1][2]: - Break your request into smaller time windows using the created parameter (e.g., created=2026-01-01T00:00:00Z..2026-01-15T00:00:00Z) [1][2]. - If a time window still contains more than 1,000 items, further divide that window into even smaller increments until each segment contains fewer than 1,000 runs [1][2]. - Iterate through these segments, collecting the results from each, to build a complete dataset [1][2]. If you are using the GitHub CLI (gh), note that the --paginate flag will automatically follow pagination links, but it cannot bypass the server-side filtered search cap of 1,000 items [3]. You will need to write a script that performs the time-windowed queries described above [1][2].

Citations:


Fail closed when the run list is capped before the window is covered.

gh run list --limit 500 stops after 500 matching runs, and filtered run queries can be capped at 1,000 by the API. If the result set is capped while omitted runs still remain, this script can incorrectly report no failures. Either paginate in smaller --created ranges until the full window is covered, or detect an incomplete result and exit non-zero instead of continuing to Line 123.

🤖 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 98 - 109,
Update the run collection logic surrounding the gh run list invocation to ensure
the entire eight-day window is covered rather than trusting a capped result set.
Either paginate the --created range in smaller intervals and combine all
results, or detect when the API/CLI limit truncates matching runs and exit
non-zero before the subsequent failure-checking logic; do not allow an
incomplete list to report success.

Source: MCP tools

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")]')
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."