You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
prefer-actions-exec-over-child-process (eslint-factory/src/rules/prefer-actions-exec-over-child-process.ts) treats all four OUTPUT_CAPTURING_METHODS (exec, execSync, execFile, execFileSync) identically and always suggests migrating to @actions/exec's exec()/getExecOutput().
But child_process.exec()/execSync() (unlike execFile/execFileSync) run the given command through a shell, accepting a single string with full shell syntax: pipes, &&/||, redirection (2>/dev/null), globbing, etc. @actions/exec's exec()/getExecOutput() do not invoke a shell by default — they take a commandLine string split into program + args, or an explicit args array; shell metacharacters in the command string are not interpreted unless the caller explicitly passes { shell: true } (which changes semantics and reintroduces the same shell-injection surface @actions/exec is usually chosen to avoid).
So for exec/execSync call sites whose single command-string argument contains shell operators, the rule's message ("Prefer @actions/exec's exec()/getExecOutput() over child_process.{{method}}()...") is actively misleading: a literal drop-in swap breaks the command's behavior, rather than needing just an async-conversion — a different, more serious caveat than the one already carved out for non-async callers.
The rule resolves execAsync back through promisify(exec) to { method: "exec", promisified: true } and (correctly, per current logic) reports it with the plain preferActionsExec message, since a promisified binding is treated as real @actions/exec-equivalent territory. But the 2>/dev/null suppression and the || echo "" fallback only work because child_process.exec() runs the string through /bin/sh -c. Rewriting this literally as exec.getExecOutput("which", ["copilot"]) silently drops both behaviors - the migration is not the mechanical swap the message implies.
Ask
Detect when the resolved method is exec/execSync (shell-based) and the command argument is a single string containing shell metacharacters (pipes, &&/||, redirection, globbing, subshells, etc.) - either suppress the suggestion for that shape or emit a distinct message noting the shell-syntax caveat (that @actions/exec needs { shell: true } or a rewrite into an args-array plus explicit logic to replicate the shell behavior).
At minimum, document this caveat in the rule's docs.description and README entry so the suggestion isn't read as a literal 1:1 replacement for shell-syntax commands.
Acceptance criteria
Add a test case modeled on validate_secrets.cjs's promisified exec('which copilot 2>/dev/null || echo ""') (or a direct execSync("a | b")) demonstrating either a suppressed report or a distinct shell-caveat message.
Add a test case confirming a shell-metacharacter-free single-string command (e.g. execSync("git status")) is unaffected - still reported with the existing message.
No regressions in the existing prefer-actions-exec-over-child-process.test.ts suite.
Scope note
Filed under the ESLint Refiner mission for eslint-factory/**, targeting actions/setup/js/** as the grounding corpus.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
Summary
prefer-actions-exec-over-child-process(eslint-factory/src/rules/prefer-actions-exec-over-child-process.ts) treats all fourOUTPUT_CAPTURING_METHODS(exec,execSync,execFile,execFileSync) identically and always suggests migrating to@actions/exec'sexec()/getExecOutput().But
child_process.exec()/execSync()(unlikeexecFile/execFileSync) run the given command through a shell, accepting a single string with full shell syntax: pipes,&&/||, redirection (2>/dev/null), globbing, etc.@actions/exec'sexec()/getExecOutput()do not invoke a shell by default — they take acommandLinestring split intoprogram+args, or an explicitargsarray; shell metacharacters in the command string are not interpreted unless the caller explicitly passes{ shell: true }(which changes semantics and reintroduces the same shell-injection surface@actions/execis usually chosen to avoid).So for
exec/execSynccall sites whose single command-string argument contains shell operators, the rule's message ("Prefer@actions/exec's exec()/getExecOutput() over child_process.{{method}}()...") is actively misleading: a literal drop-in swap breaks the command's behavior, rather than needing just an async-conversion — a different, more serious caveat than the one already carved out for non-async callers.Grounded evidence
actions/setup/js/validate_secrets.cjs (github-script module, marker present)
Lines 18-19 and 252:
The rule resolves
execAsyncback throughpromisify(exec)to{ method: "exec", promisified: true }and (correctly, per current logic) reports it with the plainpreferActionsExecmessage, since a promisified binding is treated as real@actions/exec-equivalentterritory. But the2>/dev/nullsuppression and the|| echo ""fallback only work becausechild_process.exec()runs the string through/bin/sh -c. Rewriting this literally asexec.getExecOutput("which", ["copilot"])silently drops both behaviors - the migration is not the mechanical swap the message implies.Ask
exec/execSync(shell-based) and the command argument is a single string containing shell metacharacters (pipes,&&/||, redirection, globbing, subshells, etc.) - either suppress the suggestion for that shape or emit a distinct message noting the shell-syntax caveat (that@actions/execneeds{ shell: true }or a rewrite into an args-array plus explicit logic to replicate the shell behavior).docs.descriptionand README entry so the suggestion isn't read as a literal 1:1 replacement for shell-syntax commands.Acceptance criteria
validate_secrets.cjs's promisifiedexec('which copilot 2>/dev/null || echo ""')(or a directexecSync("a | b")) demonstrating either a suppressed report or a distinct shell-caveat message.execSync("git status")) is unaffected - still reported with the existing message.prefer-actions-exec-over-child-process.test.tssuite.Scope note
Filed under the ESLint Refiner mission for
eslint-factory/**, targetingactions/setup/js/**as the grounding corpus.Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.