Skip to content

Guard setup JS sync fs/exec calls with contextual error handling#47425

Merged
pelikhan merged 4 commits into
mainfrom
copilot/eslint-monster-setup-js-lint-stream-2
Jul 23, 2026
Merged

Guard setup JS sync fs/exec calls with contextual error handling#47425
pelikhan merged 4 commits into
mainfrom
copilot/eslint-monster-setup-js-lint-stream-2

Conversation

Copilot AI commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Daily lint:setup-js surfaced a broad reliability gap in actions/setup/js: several synchronous filesystem and child-process calls could throw unhandled errors and terminate the action abruptly. This change wraps the flagged sync I/O sites so failures surface as contextual, actionable errors instead of raw crashes.

  • Scope: targeted sync I/O hardening

    • Adds local try/catch guards around the flagged fs.statSync, fs.readdirSync, fs.copyFileSync, and execFileSync call sites across the affected setup scripts.
    • Keeps existing success-path behavior intact while converting exceptional paths into explicit script-level failures, warnings, or structured return values as appropriate to each caller.
  • Failure reporting

    • Preserves current control flow where the caller already degrades gracefully.
    • Improves error messages with path/command context so operators can see which file, directory, or subprocess failed without inspecting a raw stack trace.
  • Behavioral adjustments by script

    • Validation-style helpers now return actionable errors when metadata reads fail.
    • Directory scanners and staging/copy helpers now fail with source/destination context instead of propagating unhandled sync I/O exceptions.
    • Setup flows that treat missing or unreadable artifacts as non-fatal continue to do so, but now emit clearer warnings.
  • Regression coverage

    • Adds focused tests for representative wrapped failure modes in comment-memory listing and safe-output artifact staging.

Example of the change pattern:

let stats;
try {
  stats = fs.statSync(filePath);
} catch (err) {
  throw new Error(`Failed to inspect file ${filePath}: ${getErrorMessage(err)}`, { cause: err });
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Wrap sync fs and exec I/O in try/catch Guard setup JS sync fs/exec calls with contextual error handling Jul 22, 2026
Copilot AI requested a review from pelikhan July 22, 2026 23:49
@pelikhan
pelikhan marked this pull request as ready for review July 23, 2026 05:08
Copilot AI review requested due to automatic review settings July 23, 2026 05:08

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

Hardens setup scripts against synchronous filesystem and subprocess failures by adding contextual error handling.

Changes:

  • Wraps sync I/O failures with actionable errors or warnings.
  • Preserves existing fallback behavior where failures are non-fatal.
  • Adds representative regression tests.
Show a summary per file
File Description
actions/setup/js/artifact_client.cjs Handles upload metadata failures.
actions/setup/js/build_checkout_manifest.cjs Adds context to GitHub CLI failures.
actions/setup/js/check_workflow_timestamp.cjs Handles timestamp inspection failures.
actions/setup/js/comment_memory_helpers.cjs Wraps directory-read failures.
actions/setup/js/comment_memory_helpers.test.cjs Tests directory-read failure handling.
actions/setup/js/generate_git_bundle.cjs Handles bundle metadata failures.
actions/setup/js/generate_git_patch.cjs Handles workspace metadata failures.
actions/setup/js/install_frontmatter_skills.cjs Handles skill-directory reads.
actions/setup/js/merge_remote_agent_github_folder.cjs Wraps scanning and copy failures.
actions/setup/js/push_experiment_state.cjs Handles state file and workspace failures.
actions/setup/js/push_repo_memory.cjs Wraps recursive directory scanning.
actions/setup/js/run_evals.cjs Handles agent-output metadata failures.
actions/setup/js/runtime_import.cjs Refetches when cache inspection fails.
actions/setup/js/safe_outputs_handlers.cjs Wraps validation, scanning, and staging I/O.
actions/setup/js/safe_outputs_handlers.test.cjs Tests staging-copy failure handling.
actions/setup/js/setup_threat_detection.cjs Adds contextual artifact inspection handling.

Review details

Tip

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

  • Files reviewed: 16/16 changed files
  • Comments generated: 1
  • Review effort level: Medium

try {
promptStats = fs.statSync(promptPath);
} catch (err) {
core.warning(`⚠️ ${ERR_VALIDATION}: Failed to inspect workflow prompt context at ${promptPath}: ${getErrorMessage(err)}. ` + "Threat detection will continue with fallback workflow context.");
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot avoid emojis in error messages

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR #47425 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 23, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

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

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 0, JS: 2)
✅ Design 2 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 2 (100%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
wraps directory read failures with the memory directory path comment_memory_helpers.test.cjs:93 design_test None
should wrap staging copy failures for absolute-path files safe_outputs_handlers.test.cjs:504 design_test None

Verdict

Passed. 0% implementation tests (threshold: 30%). Both new tests verify behavioral contracts — contextual error messages include path details — directly validating the PR's stated goal of guarding sync fs/exec calls with contextual error handling.

References: §29981333959

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 53.7 AIC · ⌖ 7.64 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot 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.

Review Summary

This PR makes good progress on hardening sync I/O calls with contextual error handling. One blocking issue was found (already flagged as an inline comment): setup_threat_detection.cjs uses getErrorMessage in four new catch blocks but the error_helpers.cjs import is missing — every catch path will throw ReferenceError: getErrorMessage is not defined instead of reporting the I/O failure.

All other changed files that use getErrorMessage have the correct import.

Blocking: Add const { getErrorMessage } = require("./error_helpers.cjs"); to the imports in setup_threat_detection.cjs.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 28.1 AIC · ⌖ 5.58 AIC · ⊞ 5K

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes for a blocking correctness bug and two follow-on improvements.

📋 Key Themes & Highlights

Blocking issue

  • Missing getErrorMessage import in setup_threat_detection.cjs — all four new catch blocks call getErrorMessage which is not imported, so every error path crashes with ReferenceError instead of the intended diagnostic. The fix is one require line.

Follow-on improvements

  • Ambiguous combined try/catch in check_workflow_timestamp.cjs — two statSync calls share a single handler; the error message cannot distinguish which file was missing.
  • Early-exit inside file-collection loop in push_experiment_state.cjs — the first statSync failure aborts the whole scan; if the intent is to report all bad files, use a collect-then-fail pattern; if it's intentional, a test would confirm and document it.

Positive highlights

  • ✅ Consistent error-message format (Failed to <verb> <resource>: <cause>) used throughout — easy to scan in logs.
  • { cause: err } on re-thrown errors preserves the original stack trace for deep debugging.
  • ✅ New tests for comment_memory_helpers and safe_outputs_handlers cover the representative wrapped failure modes as stated in the PR description.
  • ✅ All success-path behaviour is preserved; only exceptional paths are touched.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 51.4 AIC · ⌖ 4.95 AIC · ⊞ 6.7K
Comment /matt to run again

try {
agentOutputStats = fs.statSync(agentOutputPath);
} catch (err) {
core.setFailed(`${ERR_VALIDATION}: Failed to inspect agent output file ${agentOutputPath}: ${getErrorMessage(err)}`);

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.

[/diagnosing-bugs] getErrorMessage is used at lines 55, 105, 125, and 141 but is never imported — every catch path in this file will throw ReferenceError: getErrorMessage is not defined instead of reporting the I/O failure.

💡 Fix: add the missing import

Add this near the top of the file alongside the other require calls:

const { getErrorMessage } = require("./error_helpers.cjs");

All four catch blocks reference getErrorMessage(err) (lines 55, 105, 125, 141), so without this import every error case crashes with a confusing ReferenceError instead of the intended diagnostic message.

@copilot please address this.

lockStat = fs.statSync(lockFile);
} catch (err) {
core.setFailed(`Failed to inspect workflow timestamps: ${getErrorMessage(err)}`);
return;

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.

[/diagnosing-bugs] Both statSync calls share a single try/catch, so the error message can't tell the operator whether the workflow .md or the .lock.yml file was unreadable.

💡 Suggested fix: separate the guards
try {
  workflowStat = fs.statSync(workflowMdFile);
} catch (err) {
  core.setFailed(`Failed to inspect workflow source ${workflowMdFile}: ${getErrorMessage(err)}`);
  return;
}
try {
  lockStat = fs.statSync(lockFile);
} catch (err) {
  core.setFailed(`Failed to inspect lock file ${lockFile}: ${getErrorMessage(err)}`);
  return;
}

The two files have different diagnostic meanings — one missing is a config problem, the other is a build artifact gap. Separate errors make triage unambiguous.

@copilot please address this.

try {
fileInfo = fs.statSync(full);
} catch (err) {
core.setFailed(`Failed to inspect ${stateLabel} file ${full}: ${getErrorMessage(err)}`);

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.

[/tdd] core.setFailed inside a loop followed by return silently skips inspecting the remaining candidate files — only the first failure is reported. If multiple state files are unreadable, operators only learn about the first one.

💡 Options to consider
  1. Collect all errors first, then fail once:
const errors = [];
for (const name of candidateFiles) {
  ...
  try { fileInfo = fs.statSync(full); }
  catch (err) { errors.push(`${full}: ${getErrorMessage(err)}`); continue; }
  ...
}
if (errors.length) { core.setFailed(`Failed to inspect state files:
${errors.join("
")}`); return; }
  1. Or keep the early-exit but add a test that verifies only the first error surfaces — so the behaviour is at least intentional and documented.

@copilot please address this.

@github-actions github-actions Bot 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.

REQUEST_CHANGES — one critical correctness bug must be fixed before merge; two non-blocking observations noted inline.

### Findings summary

🔴 Critical — setup_threat_detection.cjs: getErrorMessage called without being imported

This file calls getErrorMessage() at four catch sites (lines 55, 105, 125, 141) introduced by this PR, but never imports it from ./error_helpers.cjs. Every one of those catch paths will throw ReferenceError: getErrorMessage is not defined at runtime instead of reporting the original I/O failure. The PR's stated goal — converting raw crashes into contextual errors — is completely defeated for this file.

Fix: add const { getErrorMessage } = require('./error_helpers.cjs'); to the imports, as done correctly in the other changed files (check_workflow_timestamp.cjs, run_evals.cjs, comment_memory_helpers.cjs, etc.).

🟡 Medium — check_workflow_timestamp.cjs: combined try/catch loses file identity (see inline comment)

🔵 Low — runtime_import.cjs: empty if (!stats) dead branch (see inline comment)

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 97.7 AIC · ⌖ 4.82 AIC · ⊞ 5.7K
Comment /review to run again

workflowStat = fs.statSync(workflowMdFile);
lockStat = fs.statSync(lockFile);
} catch (err) {
core.setFailed(`Failed to inspect workflow timestamps: ${getErrorMessage(err)}`);

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.

Ambiguous error when either statSync fails: both file stats are batched in one try/catch, so the error message Failed to inspect workflow timestamps cannot identify whether the workflow .md or the .lock.yml was unreadable.

💡 Suggested fix

Wrap each call separately:

try {
  workflowStat = fs.statSync(workflowMdFile);
} catch (err) {
  core.setFailed(`Failed to inspect workflow file ${workflowMdFile}: ${getErrorMessage(err)}`);
  return;
}
try {
  lockStat = fs.statSync(lockFile);
} catch (err) {
  core.setFailed(`Failed to inspect lock file ${lockFile}: ${getErrorMessage(err)}`);
  return;
}

The two files have different roles (source vs generated output), so a missing lock file should produce a different diagnostic than a missing source file. The combined catch makes triage harder.

Comment thread actions/setup/js/runtime_import.cjs Outdated
stats = null;
}
if (!stats) {
// fall through to refetch below

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.

Dead if (!stats) branch adds noise without doing anything: the if (!stats) { // fall through } block has an empty body — it's a no-op that exists solely to introduce the else branch holding the real logic. This is unnecessarily hard to read and inverts the normal control-flow idiom.

💡 Suggested fix

Invert the condition so the happy path is the if branch:

if (stats) {
  const ageInMs = Date.now() - stats.mtimeMs;
  const oneHourInMs = 60 * 60 * 1000;

  if (ageInMs < oneHourInMs) {
    core.info(`Using cached content for URL: ${url}`);
    try {
      return fs.readFileSync(cacheFile, "utf8");
    } catch (err) {
      throw new Error(`Failed to read file ${cacheFile}: ${String(err)}`, { cause: err });
    }
  }
}
// fall through to refetch

The comment // fall through to refetch below belongs at the bottom of this block, not inside the empty branch.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot avoid emojis in error messages

Addressed in b2d01a3.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran it in 309cf21. Local validation passed; CI on this HEAD is stale and needs a human re-trigger before merge.

@pelikhan
pelikhan merged commit 8801f1b into main Jul 23, 2026
8 checks passed
@pelikhan
pelikhan deleted the copilot/eslint-monster-setup-js-lint-stream-2 branch July 23, 2026 06:39
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.83.1

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[eslint-monster] setup/js lint stream 2: wrap sync fs and sync exec I/O in try/catch

3 participants