feat(core): add NX_PROJECT_ROOT environment variable to runtime cache inputs (#31428)

Add NX_PROJECT_ROOT environment variable to runtime cache inputs

## Current Behavior

Runtime cache input commands do not have access to project-specific
context, making it impossible to create project-aware runtime inputs.

## Expected Behavior

Runtime cache input commands can access `$NX_PROJECT_ROOT` environment
variable containing the project's root directory path.

## Related Issue(s)

Fixes #20949

Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
This commit is contained in:
Craigory Coppola
2025-09-04 16:04:55 -04:00
committed by GitHub
parent 2c678a1d51
commit 5679b3ea8c
4 changed files with 74 additions and 5 deletions
@@ -761,4 +761,62 @@ describe('native task hasher', () => {
// );
// console.dir(hashes, { depth: null });
// });
it('should provide NX_PROJECT_ROOT environment variable to runtime commands', async () => {
const workspaceFiles = await retrieveWorkspaceFiles(tempFs.tempDir, {
'libs/parent': 'parent',
});
const builder = new ProjectGraphBuilder(
undefined,
workspaceFiles.fileMap.projectFileMap
);
builder.addNode({
name: 'parent',
type: 'lib',
data: {
root: 'libs/parent',
targets: {
build: {
executor: 'nx:run-commands',
inputs: [
// Use a runtime command that outputs the NX_PROJECT_ROOT
{ runtime: 'echo "NX_PROJECT_ROOT: $NX_PROJECT_ROOT"' },
],
},
},
},
});
const projectGraph = builder.getUpdatedProjectGraph();
const taskGraph = createTaskGraph(
projectGraph,
{ build: ['^build'] },
['parent'],
['build'],
undefined,
{}
);
const hash = await new NativeTaskHasherImpl(
tempFs.tempDir,
nxJson,
projectGraph,
workspaceFiles.rustReferences,
{ selectivelyHashTsConfig: false }
).hashTask(taskGraph.tasks['parent:build'], taskGraph, {});
// The runtime command should have access to NX_PROJECT_ROOT
// and the hash should include that output
expect(hash.details).toHaveProperty(
'runtime:echo "NX_PROJECT_ROOT: $NX_PROJECT_ROOT"'
);
// Verify the hash is deterministic and non-empty, proving NX_PROJECT_ROOT was available
const runtimeHash =
hash.details['runtime:echo "NX_PROJECT_ROOT: $NX_PROJECT_ROOT"'];
expect(runtimeHash).toBeDefined();
expect(runtimeHash).not.toBe('3244421341483603138'); // Should not be empty hash
expect(runtimeHash).toBe('17104739612706417536'); // Should be consistent hash for "libs/parent"
});
});
+4 -1
View File
@@ -359,7 +359,10 @@ impl HashPlanner {
)]
};
let runtime_and_env_inputs = self_inputs.iter().filter_map(|i| match i {
Input::Runtime(runtime) => Some(HashInstruction::Runtime(runtime.to_string())),
Input::Runtime(runtime) => Some(HashInstruction::Runtime(
project_name.to_string(),
runtime.to_string(),
)),
Input::Environment(env) => Some(HashInstruction::Environment(env.to_string())),
_ => None,
});
+9 -2
View File
@@ -179,11 +179,18 @@ impl TaskHasher {
trace!(parent: &span, "hash_workspace_files: {:?}", now.elapsed());
hashed_workspace_files?
}
HashInstruction::Runtime(runtime) => {
HashInstruction::Runtime(project_name, runtime) => {
// Create a modified environment with NX_PROJECT_ROOT
let mut env_with_project_root = js_env.clone();
if let Some(project) = self.project_graph.nodes.get(project_name) {
env_with_project_root
.insert("NX_PROJECT_ROOT".to_string(), project.root.clone());
}
let hashed_runtime = hash_runtime(
&self.workspace_root,
runtime,
js_env,
&env_with_project_root,
Arc::clone(&self.runtime_cache),
)?;
trace!(parent: &span, "hash_runtime: {:?}", now.elapsed());
+3 -2
View File
@@ -47,7 +47,7 @@ pub struct TaskGraph {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum HashInstruction {
WorkspaceFileSet(Vec<String>),
Runtime(String),
Runtime(String, String), // (project_name, runtime)
Environment(String),
ProjectFileSet(String, Vec<String>),
ProjectConfiguration(String),
@@ -89,7 +89,8 @@ impl fmt::Display for HashInstruction {
}
HashInstruction::WorkspaceFileSet(file_set) =>
format!("workspace:[{}]", file_set.join(",")),
HashInstruction::Runtime(runtime) => format!("runtime:{}", runtime),
HashInstruction::Runtime(project_name, runtime) =>
format!("{}:runtime:{}", project_name, runtime),
HashInstruction::Environment(env) => format!("env:{}", env),
HashInstruction::TaskOutput(task_output, dep_outputs) => {
let dep_outputs = dep_outputs.join(",");