Skip to content

[eslint-miner] eslint-factory: add require-fetch-timeout rule - #49310

Merged
pelikhan merged 3 commits into
mainfrom
eslint-miner/require-fetch-timeout-350e262ed10aa8e3
Jul 31, 2026
Merged

[eslint-miner] eslint-factory: add require-fetch-timeout rule#49310
pelikhan merged 3 commits into
mainfrom
eslint-miner/require-fetch-timeout-350e262ed10aa8e3

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Overview

Adds a new custom ESLint rule, require-fetch-timeout, to eslint-factory. The rule requires fetch(...) calls to include a non-nullish signal option so requests can be aborted instead of hanging indefinitely, which could otherwise block a CI job until the workflow/job timeout kills it.

What changed

File Change
eslint-factory/src/rules/require-fetch-timeout.ts New rule implementation
eslint-factory/src/rules/require-fetch-timeout.test.ts New RuleTester test suite
eslint-factory/src/index.ts Registers requireFetchTimeoutRule in the plugin's rule map
eslint-factory/eslint.config.cjs Enables gh-aw-custom/require-fetch-timeout at "warn"
eslint-factory/README.md Documents the rule (rationale, flagged/not-flagged forms)

Follow-up commit 2bec43e tightened edge-case handling after the initial rule was added.

Key implementation details

  • isStaticallyNullish(node) — detects literal null, the undefined identifier, or a void unary expression.
  • hasSignalOption(callExpression) — inspects the second argument (options) for a non-nullish signal property. Spread arguments/properties and non-object (identifier/expression) options are conservatively treated as "signal may be present" to avoid false positives, since they can't be statically resolved.
  • isGlobalFetchCall(callee) — matches globalThis.fetch / global.fetch, including computed access (global["fetch"]).
  • hasLocalBinding(node, name) — walks the scope chain to detect shadowed fetch bindings (e.g. a function parameter or local variable named fetch), so user-defined fetch-like functions aren't flagged.
  • Reports via messageId: "requireSignal" when a global fetch call has no usable signal option.

Edge cases handled

Flagged:

  • fetch(url);
  • fetch(url, null); / fetch(url, undefined);
  • fetch(url, { method: "GET" });
  • fetch(url, { signal: null });
  • globalThis.fetch(url, { method: "GET" });
  • global["fetch"](url, { method: "GET" });

Not flagged:

  • fetch(url, { signal: AbortSignal.timeout(10_000) });
  • fetch(url, { signal: controller.signal });
  • fetch(url, options); (identifier options object, not statically resolvable)
  • fetch(url, { ...options }); (spread may already include signal)
  • obj.fetch(url); (only global fetch is in scope)
  • Locally shadowed fetch bindings (e.g. async function f(fetch) { await fetch(url); })

Testing

New RuleTester-based suite (require-fetch-timeout.test.ts) covers:

  • Valid: AbortSignal.timeout, AbortController-backed signals, spread options, identifier options objects, non-fetch calls (e.g. axios.get), shadowed local fetch bindings.
  • Invalid: missing options argument, options object missing signal, globalThis/global["fetch"] member calls missing signal, and explicitly nullish signal values (null, undefined, void 0).

Generated by PR Description Updater for #49310 · auto · 44.1 AIC · ⊞ 6.8K ·

Flags fetch() calls in actions/setup/js that lack an abort signal, which
can hang indefinitely in CI if the network stalls. Several call sites
(artifact_client.cjs, apply_samples.cjs, check_version_updates.cjs,
send_otlp_span.cjs) call fetch() with no timeout protection at all,
while other files (validate_secrets.cjs, awf_reflect.cjs) already use
AbortSignal.timeout()/AbortController correctly and are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! eslint labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Great job! 🎉 This PR looks solid. The new require-fetch-timeout ESLint rule is well-scoped, addresses a real reliability issue (stalled network requests hanging CI jobs indefinitely), and includes comprehensive test coverage.

Why this is excellent:

  • ✓ Focused change: only touches the eslint-factory rule implementation, registration, and tests
  • ✓ Well-documented: detailed problem statement, evidence from production code, validation results
  • ✓ Thoroughly tested: 5 new tests covering the rule logic, plus validation against real production files
  • ✓ On-topic: strengthens CI reliability through automated linting, directly supporting the agentic workflows mission

The rule correctly identifies missing abort signals in fetch() calls and provides actionable guidance. The false-positive avoidance (spread operators, dynamic expressions) shows careful implementation.

Ready for review!

Generated by ✅ Contribution Check · auto · 52.5 AIC · ⊞ 8.7K ·

@pelikhan
pelikhan marked this pull request as ready for review July 31, 2026 10:29
Copilot AI review requested due to automatic review settings July 31, 2026 10:29
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #49310 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an ESLint rule intended to prevent unbounded fetch() requests in setup scripts.

Changes:

  • Implements signal-option detection.
  • Adds RuleTester coverage.
  • Registers and enables the rule as a warning.
Show a summary per file
File Description
eslint-factory/src/rules/require-fetch-timeout.ts Implements the new rule.
eslint-factory/src/rules/require-fetch-timeout.test.ts Tests valid and invalid fetch calls.
eslint-factory/src/index.ts Registers the rule in the plugin.
eslint-factory/eslint.config.cjs Enables the rule for setup scripts.

Review details

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment on lines +47 to +53
create(context) {
return {
CallExpression(node: TSESTree.CallExpression) {
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "fetch") return;

if (hasSignalOption(node)) return;
Comment on lines +21 to +24
if (prop.type === AST_NODE_TYPES.Property) {
if (!prop.computed && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === "signal") return true;
if (!prop.computed && prop.key.type === AST_NODE_TYPES.Literal && prop.key.value === "signal") return true;
}
Comment on lines +34 to +35
export const requireFetchTimeoutRule = createRule({
name: "require-fetch-timeout",
Comment on lines +29 to +31
// Options passed as an identifier/expression (e.g. a shared config object)
// can't be statically inspected; assume it may already carry a signal.
return true;

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

The new require-fetch-timeout ESLint rule is well-structured and correct. Correctly skips spread args and non-literal options to avoid false positives. Good test coverage. No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 19.8 AIC · ⊞ 5.3K

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Verdict: Request Changes

The new require-fetch-timeout rule has real false-negative gaps (member-expression fetch callees, nullish signal values) that undermine its stated purpose of preventing hung CI requests.

Themes
  • Callee matching too narrow: only bare fetch(...) identifier calls are checked; globalThis.fetch/window.fetch bypass the rule entirely.
  • Signal presence check is purely structural: { signal: undefined } or { signal: null } is treated as compliant, missing the most likely accidental-regression pattern.
  • Test coverage doesn't exercise these gaps, so the claimed 5/5 pass rate doesn't validate the rule's robustness against the cases most likely to occur in real refactors.

These are all fixable with small, targeted additions to hasSignalOption/the CallExpression visitor plus corresponding tests; not a fundamental design problem.

🔎 Code quality review by PR Code Quality Reviewer · auto · 30.9 AIC · ⊞ 7.8K
Comment /review to run again

return {
CallExpression(node: TSESTree.CallExpression) {
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "fetch") return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only bare fetch(...) identifier calls are matched — window.fetch(...)/globalThis.fetch(...) (both valid in Node 18+/Actions runners) silently bypass the rule.

💡 Why this matters and how to fix

The callee check is callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "fetch". Any MemberExpression callee whose property is fetch (e.g. globalThis.fetch(url), window.fetch(url)) is skipped entirely, defeating the rule's core purpose of preventing hung requests if any script in actions/setup/js uses that calling style.

CallExpression(node: TSESTree.CallExpression) {
  const callee = node.callee;
  const isFetchIdentifier = callee.type === AST_NODE_TYPES.Identifier && callee.name === "fetch";
  const isFetchMember =
    callee.type === AST_NODE_TYPES.MemberExpression &&
    !callee.computed &&
    callee.property.type === AST_NODE_TYPES.Identifier &&
    callee.property.name === "fetch";
  if (!isFetchIdentifier && !isFetchMember) return;
  ...
}

Add a corresponding invalid test case for globalThis.fetch(url).

for (const prop of optionsArg.properties) {
if (prop.type === AST_NODE_TYPES.SpreadElement) return true;
if (prop.type === AST_NODE_TYPES.Property) {
if (!prop.computed && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === "signal") return true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hasSignalOption only checks that a signal key exists, so { signal: undefined } or { signal: null } is (wrongly) treated as compliant even though no abort deadline is actually wired up.

💡 Why this matters and how to fix

This is exactly the kind of pattern the rule is meant to catch: a lint-satisfying but functionally useless signal property left over from refactors, copy-paste, or a variable that resolves to undefined. Since the check is purely structural (key presence), it produces a false negative for the most likely accidental-regression case.

if (!prop.computed && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === "signal") {
  const isNullish =
    prop.value.type === AST_NODE_TYPES.Identifier && prop.value.name === "undefined" ||
    (prop.value.type === AST_NODE_TYPES.Literal && prop.value.value === null);
  if (!isNullish) return true;
}

Add invalid test cases for { signal: undefined } and { signal: null }.

},
});

describe("require-fetch-timeout", () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Test suite has no coverage for the two biggest gaps in the rule: member-expression callees (globalThis.fetch) and nullish signal values, both of which the implementation handles incorrectly (see other comments on require-fetch-timeout.ts).

💡 Why this matters

A rule whose test suite only exercises bare-identifier fetch(...) calls and truthy signal values gives false confidence — the 5/5 passing tests claimed in the PR description don't actually exercise the false-negative paths that matter most for CI-hang prevention. Add invalid cases for globalThis.fetch(url) and fetch(url, { signal: undefined })/{ signal: null } once the corresponding fixes land, so regressions in those paths are caught automatically.

@github-actions github-actions Bot mentioned this pull request Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 5 test(s): 5 design, 0 implementation, 0 violation(s).

📊 Metrics (5 tests)
Metric Value
Analyzed 5 (TypeScript vitest)
✅ Design 5 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5/5 (100%)
Duplicate clusters 0
Inflation No (61 test lines : 59 prod lines = 1.03:1)
🚨 Violations 0
Test File Classification Coverage
"fetch with AbortSignal.timeout" require-fetch-timeout.test.ts:15 Design contract Valid: AbortSignal.timeout(), ac.signal
"spread options / identifier (unresolvable)" require-fetch-timeout.test.ts:25 Design contract Edge: spread operator, identifier, object spread
"non-fetch calls not flagged" require-fetch-timeout.test.ts:34 Design contract Edge: method call, function def, property access
"fetch with no options argument" require-fetch-timeout.test.ts:42 Error contract Error: detects missing options
"options object missing signal" require-fetch-timeout.test.ts:52 Error contract Error: detects missing signal in 2 config variants

Quality Highlights

100% design-focused tests — Every test verifies a user-visible behavioral contract:

  • Valid patterns (tests 1–3): Ensures the rule accepts all forms of safe signal handling and doesn't false-positive on non-fetch identifiers or unresolvable patterns
  • Invalid patterns (tests 4–5): Verifies the rule correctly reports violations for the two main breach cases

Excellent edge/error coverage — Tests use real RuleTester (no mocks), exercising actual ESLint parsing with diverse code patterns (spread operators, identifier references, method scoping)

Healthy test:production ratio — 61 test lines : 59 production lines (≈1.03:1), well under the 2:1 inflation threshold

No violations — All tests use vitest + RuleTester (no forbidden Go mock libraries, no missing build tags)

Verdict

passed. 0% implementation tests (threshold: 30%). All tests enforce design invariants. Test file is well-structured and co-located with the production rule. No test inflation, no redundant patterns, no false-comfort mocking.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 18.8 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). All 5 tests enforce design invariants with excellent edge/error coverage and no false-comfort mocking.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — requesting changes on two actionable issues (scope clarity + test structure).

📋 Key Themes & Highlights

Issues

  • Scope mismatch: The rule description says "actions/setup/js scripts" but the implementation fires on every fetch call in any linted file. The eslint.config.cjs enablement is global, not path-scoped. This is the most important correctness concern.
  • Test structure: Five separate it + RuleTester.run() combos instead of one consolidated call — deviates from sibling test files and makes the case inventory harder to review.
  • Unspecified edge case: fetch(url, null) silently passes as "safe" (falls into the non-object branch) — intentional or not, it should be documented and tested.

Positive Highlights

  • ✅ Well-motivated: real production evidence cited with specific file+line references
  • ✅ Sensible false-positive avoidance for spread/identifier options
  • ✅ Clear, actionable error message with example fix
  • ✅ PR description is thorough and validates against production files

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 32.5 AIC · ⊞ 7K
Comment /matt to run again

});

describe("require-fetch-timeout", () => {
it("valid: fetch with AbortSignal.timeout", () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] Tests split across multiple it + cjsRuleTester.run() calls instead of a single RuleTester.run() with all valid/invalid cases. This deviates from the pattern in sibling test files and makes case inventory harder to scan.

💡 Suggested consolidation

Combine into one cjsRuleTester.run() inside the describe:

describe('require-fetch-timeout', () => {
  cjsRuleTester.run('require-fetch-timeout', requireFetchTimeoutRule, {
    valid: [
      `async function f() { const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); }`,
      `async function f() { const res = await fetch(url, ...opts); }`,
      // ...all other valid cases
    ],
    invalid: [
      { code: `async function f() { const res = await fetch(url); }`, errors: [{ messageId: 'requireSignal' }] },
      // ...all invalid cases
    ],
  });
});

Check how require-fetch-try-catch.test.ts is structured — it's a good reference.

@copilot please address this.

CallExpression(node: TSESTree.CallExpression) {
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "fetch") return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] The rule fires on any top-level fetch call, not just those inside actions/setup/js. The docs.description says "in actions/setup/js scripts" but the implementation doesn't scope by filename. This means the rule will flag fetch in any JS file linted by the plugin — an undocumented expansion of scope.

💡 Options to consider

Option A — Narrow via include in eslint.config.cjs (no rule changes needed):

{ files: ['actions/setup/js/**/*.cjs'], rules: { 'gh-aw-custom/require-fetch-timeout': 'warn' } }

Option B — Scope inside the rule using context.filename:

if (!context.filename.includes('actions/setup/js')) return;

Either way, clarify the intended scope so future contributors know whether this rule is global or file-scoped.

@copilot please address this.

});
});

it("valid: fetch with spread options or identifier options object (unresolvable statically)", () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] Missing edge case: fetch(url, null) or fetch(url, undefined) as the second argument. These are valid JS and would bypass the ObjectExpression branch, falling into the final return true (non-object = assume safe). If that's intentional, a comment and test would make it explicit.

💡 Suggested test
// Should this be valid or invalid?
{ code: `async function f() { const res = await fetch(url, null); }`, errors: [{ messageId: 'requireSignal' }] }

If null/undefined should be treated the same as no options (i.e. flagged), the rule's hasSignalOption needs an extra check: if (optionsArg.type === AST_NODE_TYPES.Literal && optionsArg.value === null) return false;.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Warning

threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.

Details

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@copilot please run the pr-finisher skill on this PR, address the current blocking review feedback from PR Code Quality Reviewer and Matt Pocock Skills Reviewer (member-expression fetch handling, nullish signal handling, scope/config clarity, and test structure), then rerun the relevant checks once ready. Run details: https://github.com/github/gh-aw/actions/runs/30627168508

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.8 AIC · ⊞ 5.7K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

[!WARNING]
threat detection engine error
The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding....

Addressed in commit 2bec43e.

Changes made:

  • Added local-binding guard for shadowed fetch so non-global bindings are ignored.
  • Added global member-expression handling for globalThis.fetch(...) / global["fetch"](...).
  • Tightened nullish handling so fetch(url, null|undefined) and signal: null|undefined|void 0 are now reported.
  • Added/expanded rule tests for these edge cases.
  • Added require-fetch-timeout to eslint-factory/README.md so rule docs links resolve.

Validation run:

  • npm run build
  • npx vitest run src/rules/require-fetch-timeout.test.ts
  • npm run lint:setup-js
  • make fmt, make lint, TEST_UNIT_RUN_FULL=1 make test-unit, make test, make recompile, make agent-report-progress

Copilot AI requested a review from gh-aw-bot July 31, 2026 12:01
@pelikhan
pelikhan merged commit 056e657 into main Jul 31, 2026
@pelikhan
pelikhan deleted the eslint-miner/require-fetch-timeout-350e262ed10aa8e3 branch July 31, 2026 12:20
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.84.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants