actions / actions/setup-node

Volta extends resolution in getNodeVersionFromFile has no cycle detection, recursing until stack overflow

Open
#1,628 2 comments 0 reactions 1 assignee View on GitHub

@v-gowridurgad is already working on this.

Since Sep 11, 2026.

Dominant language
TypeScript
Stars
5k
Forks
1.7k
Avg merge
3d 12h
Merged PRs (30d)
2

Description

Summary

getNodeVersionFromFile follows Volta extends recursively with no cycle detection, so a self-referential or mutually-referential volta.extends chain causes unbounded recursion and crashes the action with a stack overflow instead of a clear error.

Location

  • File: src/util.ts
  • Function: getNodeVersionFromFile(versionFilePath: string)
  • Code path:
if (manifest.volta?.extends) {
  const extendedFilePath = path.resolve(
    path.dirname(versionFilePath),
    manifest.volta.extends
  );
  core.info('Resolving node version from ' + extendedFilePath);
  return getNodeVersionFromFile(extendedFilePath);
}

No visited-path set or depth limit is threaded through the recursion.

Problem

Volta workspaces support {"volta": {"extends": "./base/package.json"}}. If the target (transitively) points back to a file already on the resolution stack — e.g. package.json with "extends": "./package.json" (typo), or a.json → b.json → a.json — the function recurses forever until V8 throws RangeError: Maximum call stack size exceeded. The user gets an opaque stack-overflow failure rather than a diagnostic naming the cycle.

Trigger / Reproduction

Based on static analysis (no workflow run performed):

  1. Set node-version-file to a package.json containing:
    {"volta": {"extends": "./package.json"}}
    
    or a two-file cycle a.jsonb.json via volta.extends.
  2. Run the action. getNodeVersionFromFile resolves extends, re-enters itself with the same path, and never terminates normally.

Note: this is a static-analysis finding; I did not execute a workflow against a cyclic fixture.

Expected Behavior

Cyclic volta.extends should fail fast with a clear error naming the files in the cycle (e.g. Detected cyclic volta.extends: a.json -> b.json -> a.json), consistent with how missing files already throw The specified node version file at: ... does not exist.

Actual Behavior

Unbounded recursion until stack exhaustion, producing an unactionable RangeError with no mention of the offending extends chain.

Impact

  • A one-character typo in volta.extends turns a configuration mistake into an opaque action crash, costing debugging time.
  • No data-loss risk, but the failure mode hides the actual cause (cycle) behind a generic engine error.

Suggested Direction

  • Thread a seen: Set<string> (resolved absolute paths) through getNodeVersionFromFile, checking before recursing and throwing a descriptive cycle error. Alternatively cap recursion depth with the same diagnostic. Either preserves current behavior for acyclic chains.

Evidence

  • Source via API: src/util.ts Volta-extends branch shows direct unconditional recursion with no guard; neighboring branches (missing file, non-JSON/TOML fallthrough) all have explicit handling, highlighting the gap.
  • Duplicate check: issue search for volta extends cycle recursion returns total_count: 0, and the open-issue list contains no extends-cycle report (nearest is .nvmrc comment parsing) — no apparent duplicate.
What happened

Unbounded recursion on cyclic Volta extends as detailed above.

Expected behavior

Fast, descriptive cycle error instead of stack overflow.

Steps to reproduce

Point node-version-file at a self- or mutually-referential Volta extends fixture and observe getNodeVersionFromFile re-enter indefinitely (static path; runtime stack overflow implied).

reproducible code
// package.json
{"volta": {"extends": "./package.json"}}
- uses: actions/setup-node@v4
  with:
    node-version-file: package.json
manifest.yaml

N/A

Versions
  • actions/setup-node: current main (verified via API, src/util.ts)
  • Node: N/A (static analysis finding)

Classification

  • FACT: Volta extends recursion has no visited-set or depth cap (verified in source via API).
  • INFERENCE: cyclic fixtures therefore recurse until stack exhaustion.
  • HYPOTHESIS: a visited-path guard produces a clear error with no change to valid chains.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.