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):
- Set
node-version-file to a package.json containing:
{"volta": {"extends": "./package.json"}}
or a two-file cycle a.json ↔ b.json via volta.extends.
- 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.
Summary
getNodeVersionFromFilefollows Voltaextendsrecursively with no cycle detection, so a self-referential or mutually-referentialvolta.extendschain causes unbounded recursion and crashes the action with a stack overflow instead of a clear error.Location
src/util.tsgetNodeVersionFromFile(versionFilePath: string)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.jsonwith"extends": "./package.json"(typo), ora.json → b.json → a.json— the function recurses forever until V8 throwsRangeError: 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):
node-version-fileto apackage.jsoncontaining:{"volta": {"extends": "./package.json"}}a.json↔b.jsonviavolta.extends.getNodeVersionFromFileresolvesextends, 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.extendsshould 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 throwThe specified node version file at: ... does not exist.Actual Behavior
Unbounded recursion until stack exhaustion, producing an unactionable
RangeErrorwith no mention of the offendingextendschain.Impact
volta.extendsturns a configuration mistake into an opaque action crash, costing debugging time.Suggested Direction
seen: Set<string>(resolved absolute paths) throughgetNodeVersionFromFile, 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
src/util.tsVolta-extends branch shows direct unconditional recursion with no guard; neighboring branches (missing file, non-JSON/TOML fallthrough) all have explicit handling, highlighting the gap.volta extends cycle recursionreturnstotal_count: 0, and the open-issue list contains no extends-cycle report (nearest is.nvmrccomment parsing) — no apparent duplicate.What happened
Unbounded recursion on cyclic Volta
extendsas detailed above.Expected behavior
Fast, descriptive cycle error instead of stack overflow.
Steps to reproduce
Point
node-version-fileat a self- or mutually-referential Voltaextendsfixture and observegetNodeVersionFromFilere-enter indefinitely (static path; runtime stack overflow implied).reproducible code
manifest.yaml
N/A
Versions
main(verified via API,src/util.ts)Classification
extendsrecursion has no visited-set or depth cap (verified in source via API).