diff --git a/packages/nx/src/tasks-runner/__snapshots__/task-env.spec.ts.snap b/packages/nx/src/tasks-runner/__snapshots__/task-env.spec.ts.snap new file mode 100644 index 00000000000000..5dc89f0a8613a9 --- /dev/null +++ b/packages/nx/src/tasks-runner/__snapshots__/task-env.spec.ts.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`getEnvFilesForTask should return the correct env files for a standard task 1`] = ` +[ + "libs/test-project/.env.build.local", + "libs/test-project/.env.build", + "libs/test-project/.build.local.env", + "libs/test-project/.build.env", + "libs/test-project/.env.local", + "libs/test-project/.local.env", + "libs/test-project/.env", + ".env.build.local", + ".env.build", + ".build.local.env", + ".build.env", + ".env.local", + ".local.env", + ".env", +] +`; + +exports[`getEnvFilesForTask should return the correct env files for a standard task with configurations 1`] = ` +[ + "libs/test-project/.env.build.development.local", + "libs/test-project/.env.build.development", + "libs/test-project/.build.development.local.env", + "libs/test-project/.build.development.env", + "libs/test-project/.env.development.local", + "libs/test-project/.env.development", + "libs/test-project/.development.local.env", + "libs/test-project/.development.env", + "libs/test-project/.env.build.local", + "libs/test-project/.env.build", + "libs/test-project/.build.local.env", + "libs/test-project/.build.env", + "libs/test-project/.env.local", + "libs/test-project/.local.env", + "libs/test-project/.env", + ".env.build.development.local", + ".env.build.development", + ".build.development.local.env", + ".build.development.env", + ".env.development.local", + ".env.development", + ".development.local.env", + ".development.env", + ".env.build.local", + ".env.build", + ".build.local.env", + ".build.env", + ".env.local", + ".local.env", + ".env", +] +`; + +exports[`getEnvFilesForTask should return the correct env files for an atomized task 1`] = ` +[ + "libs/test-project/.env.e2e-ci.local", + "libs/test-project/.env.e2e-ci", + "libs/test-project/.e2e-ci.local.env", + "libs/test-project/.e2e-ci.env", + "libs/test-project/.env.e2e.local", + "libs/test-project/.env.e2e", + "libs/test-project/.e2e.local.env", + "libs/test-project/.e2e.env", + "libs/test-project/.env.local", + "libs/test-project/.local.env", + "libs/test-project/.env", + ".env.e2e-ci.local", + ".env.e2e-ci", + ".e2e-ci.local.env", + ".e2e-ci.env", + ".env.e2e.local", + ".env.e2e", + ".e2e.local.env", + ".e2e.env", + ".env.local", + ".local.env", + ".env", +] +`; + +exports[`getEnvFilesForTask should return the correct env files for an atomized task with configurations 1`] = ` +[ + "libs/test-project/.env.e2e-ci.staging.local", + "libs/test-project/.env.e2e-ci.staging", + "libs/test-project/.e2e-ci.staging.local.env", + "libs/test-project/.e2e-ci.staging.env", + "libs/test-project/.env.e2e.staging.local", + "libs/test-project/.env.e2e.staging", + "libs/test-project/.e2e.staging.local.env", + "libs/test-project/.e2e.staging.env", + "libs/test-project/.env.staging.local", + "libs/test-project/.env.staging", + "libs/test-project/.staging.local.env", + "libs/test-project/.staging.env", + "libs/test-project/.env.e2e-ci.local", + "libs/test-project/.env.e2e-ci", + "libs/test-project/.e2e-ci.local.env", + "libs/test-project/.e2e-ci.env", + "libs/test-project/.env.e2e.local", + "libs/test-project/.env.e2e", + "libs/test-project/.e2e.local.env", + "libs/test-project/.e2e.env", + "libs/test-project/.env.local", + "libs/test-project/.local.env", + "libs/test-project/.env", + ".env.e2e-ci.staging.local", + ".env.e2e-ci.staging", + ".e2e-ci.staging.local.env", + ".e2e-ci.staging.env", + ".env.e2e.staging.local", + ".env.e2e.staging", + ".e2e.staging.local.env", + ".e2e.staging.env", + ".env.staging.local", + ".env.staging", + ".staging.local.env", + ".staging.env", + ".env.e2e-ci.local", + ".env.e2e-ci", + ".e2e-ci.local.env", + ".e2e-ci.env", + ".env.e2e.local", + ".env.e2e", + ".e2e.local.env", + ".e2e.env", + ".env.local", + ".local.env", + ".env", +] +`; diff --git a/packages/nx/src/tasks-runner/task-env-paths.ts b/packages/nx/src/tasks-runner/task-env-paths.ts new file mode 100644 index 00000000000000..124a96ba7392fd --- /dev/null +++ b/packages/nx/src/tasks-runner/task-env-paths.ts @@ -0,0 +1,49 @@ +export function getEnvPathsForTask( + projectRoot: string, + target: string, + configuration?: string, + nonAtomizedTarget?: string +): string[] { + const identifiers: string[] = []; + // Configuration-specific identifier (like build.development, build.production) + if (configuration) { + identifiers.push(`${target}.${configuration}`); + if (nonAtomizedTarget) { + identifiers.push(`${nonAtomizedTarget}.${configuration}`); + } + identifiers.push(configuration); + } + // Non-configuration-specific identifier (like build, test, serve) + identifiers.push(target); + if (nonAtomizedTarget) { + identifiers.push(nonAtomizedTarget); + } + // Non-deterministic identifier (for files like .env.local, .local.env, .env) + identifiers.push(''); + + const envPaths = []; + // Add DotEnv Files in the project root folder + for (const identifier of identifiers) { + envPaths.push(...getEnvFileVariants(identifier, projectRoot)); + } + // Add DotEnv Files in the workspace root folder + for (const identifier of identifiers) { + envPaths.push(...getEnvFileVariants(identifier)); + } + + return envPaths; +} + +function getEnvFileVariants(identifier: string, root?: string) { + const path = root ? root + '/' : ''; + if (identifier) { + return [ + `${path}.env.${identifier}.local`, + `${path}.env.${identifier}`, + `${path}.${identifier}.local.env`, + `${path}.${identifier}.env`, + ]; + } else { + return [`${path}.env.local`, `${path}.local.env`, `${path}.env`]; + } +} diff --git a/packages/nx/src/tasks-runner/task-env.spec.ts b/packages/nx/src/tasks-runner/task-env.spec.ts new file mode 100644 index 00000000000000..4191b2d9a8f04d --- /dev/null +++ b/packages/nx/src/tasks-runner/task-env.spec.ts @@ -0,0 +1,144 @@ +import { getEnvFilesForTask } from './task-env'; +import { Task } from '../config/task-graph'; +import { ProjectGraph } from '../config/project-graph'; + +describe('getEnvFilesForTask', () => { + it('should return the correct env files for a standard task', () => { + const task = { + projectRoot: 'libs/test-project', + target: { + project: 'test-project', + target: 'build', + }, + } as any as Task; + const graph = { + nodes: { + 'test-project': { + data: { + targets: { + build: {}, + }, + }, + }, + }, + } as any as ProjectGraph; + const envFiles = getEnvFilesForTask(task, graph); + expect(envFiles).toMatchSnapshot(); + }); + it('should return the correct env files for a standard task with configurations', () => { + const task = { + projectRoot: 'libs/test-project', + target: { + project: 'test-project', + target: 'build', + configuration: 'development', + }, + } as any as Task; + const graph = { + nodes: { + 'test-project': { + data: { + targets: { + build: { + configurations: { + development: {}, + }, + }, + }, + }, + }, + }, + } as any as ProjectGraph; + const envFiles = getEnvFilesForTask(task, graph); + expect(envFiles).toMatchSnapshot(); + }); + it('should return the correct env files for an atomized task', () => { + const task = { + projectRoot: 'libs/test-project', + target: { + project: 'test-project', + target: 'e2e-ci--i/am/atomized', + }, + } as any as Task; + const graph = { + nodes: { + 'test-project': { + data: { + targets: { + 'e2e-ci--i/am/atomized': {}, + 'e2e-ci--tests/run-me.spec.ts': {}, + 'e2e-ci--tests/run-me-2.spec.ts': {}, + 'e2e-ci--merge-reports': {}, + 'e2e-ci': { + metadata: { + nonAtomizedTarget: 'e2e', + }, + }, + e2e: {}, + }, + metadata: { + targetGroups: { + 'E2E (CI)': [ + 'e2e-ci--i/am/atomized', + 'e2e-ci--tests/run-me.spec.ts', + 'e2e-ci--tests/run-me-2.spec.ts', + 'e2e-ci--merge-reports', + 'e2e-ci', + ], + }, + }, + }, + }, + }, + } as any as ProjectGraph; + const envFiles = getEnvFilesForTask(task, graph); + expect(envFiles).toMatchSnapshot(); + }); + it('should return the correct env files for an atomized task with configurations', () => { + const task = { + projectRoot: 'libs/test-project', + target: { + project: 'test-project', + target: 'e2e-ci--i/am/atomized', + configuration: 'staging', + }, + } as any as Task; + const graph = { + nodes: { + 'test-project': { + data: { + targets: { + 'e2e-ci--i/am/atomized': { + configurations: { + staging: {}, + }, + }, + 'e2e-ci--tests/run-me.spec.ts': {}, + 'e2e-ci--tests/run-me-2.spec.ts': {}, + 'e2e-ci--merge-reports': {}, + 'e2e-ci': { + metadata: { + nonAtomizedTarget: 'e2e', + }, + }, + e2e: {}, + }, + metadata: { + targetGroups: { + 'E2E (CI)': [ + 'e2e-ci--i/am/atomized', + 'e2e-ci--tests/run-me.spec.ts', + 'e2e-ci--tests/run-me-2.spec.ts', + 'e2e-ci--merge-reports', + 'e2e-ci', + ], + }, + }, + }, + }, + }, + } as any as ProjectGraph; + const envFiles = getEnvFilesForTask(task, graph); + expect(envFiles).toMatchSnapshot(); + }); +}); diff --git a/packages/nx/src/tasks-runner/task-env.ts b/packages/nx/src/tasks-runner/task-env.ts index 22d66ae976de23..da0ccb26fcfb27 100644 --- a/packages/nx/src/tasks-runner/task-env.ts +++ b/packages/nx/src/tasks-runner/task-env.ts @@ -3,6 +3,8 @@ import { config as loadDotEnvFile } from 'dotenv'; import { expand } from 'dotenv-expand'; import { workspaceRoot } from '../utils/workspace-root'; import { join } from 'node:path'; +import { ProjectGraph } from '../config/project-graph'; +import { getEnvPathsForTask } from './task-env-paths'; export function getEnvVariablesForBatchProcess( skipNxCache: boolean, @@ -20,11 +22,11 @@ export function getEnvVariablesForBatchProcess( }; } -export function getTaskSpecificEnv(task: Task) { +export function getTaskSpecificEnv(task: Task, graph: ProjectGraph) { // Unload any dot env files at the root of the workspace that were loaded on init of Nx. const taskEnv = unloadDotEnvFiles({ ...process.env }); return process.env.NX_LOAD_DOT_ENV_FILES === 'true' - ? loadDotEnvFilesForTask(task, taskEnv) + ? loadDotEnvFilesForTask(task, graph, taskEnv) : // If not loading dot env files, ensure env vars created by system are still loaded taskEnv; } @@ -165,64 +167,45 @@ export function unloadDotEnvFile( }); } -function getEnvFilesForTask(task: Task): string[] { - // Collect dot env files that may pertain to a task - return [ - // Load DotEnv Files for a configuration in the project root - ...(task.target.configuration - ? [ - `${task.projectRoot}/.env.${task.target.target}.${task.target.configuration}.local`, - `${task.projectRoot}/.env.${task.target.target}.${task.target.configuration}`, - `${task.projectRoot}/.env.${task.target.configuration}.local`, - `${task.projectRoot}/.env.${task.target.configuration}`, - `${task.projectRoot}/.${task.target.target}.${task.target.configuration}.local.env`, - `${task.projectRoot}/.${task.target.target}.${task.target.configuration}.env`, - `${task.projectRoot}/.${task.target.configuration}.local.env`, - `${task.projectRoot}/.${task.target.configuration}.env`, - ] - : []), - - // Load DotEnv Files for a target in the project root - `${task.projectRoot}/.env.${task.target.target}.local`, - `${task.projectRoot}/.env.${task.target.target}`, - `${task.projectRoot}/.${task.target.target}.local.env`, - `${task.projectRoot}/.${task.target.target}.env`, - `${task.projectRoot}/.env.local`, - `${task.projectRoot}/.local.env`, - `${task.projectRoot}/.env`, - - // Load DotEnv Files for a configuration in the workspace root - ...(task.target.configuration - ? [ - `.env.${task.target.target}.${task.target.configuration}.local`, - `.env.${task.target.target}.${task.target.configuration}`, - `.env.${task.target.configuration}.local`, - `.env.${task.target.configuration}`, - `.${task.target.target}.${task.target.configuration}.local.env`, - `.${task.target.target}.${task.target.configuration}.env`, - `.${task.target.configuration}.local.env`, - `.${task.target.configuration}.env`, - ] - : []), - - // Load DotEnv Files for a target in the workspace root - `.env.${task.target.target}.local`, - `.env.${task.target.target}`, - `.${task.target.target}.local.env`, - `.${task.target.target}.env`, - - // Load base DotEnv Files at workspace root - `.local.env`, - `.env.local`, - `.env`, - ]; +function getOwnerTargetForTask( + task: Task, + graph: ProjectGraph +): [string, string?] { + const project = graph.nodes[task.target.project]; + if (project.data.metadata?.targetGroups) { + for (const targets of Object.values(project.data.metadata.targetGroups)) { + if (targets.includes(task.target.target)) { + for (const target of targets) { + if (project.data.targets[target].metadata?.nonAtomizedTarget) { + return [ + target, + project.data.targets[target].metadata?.nonAtomizedTarget, + ]; + } + } + } + } + } + return [task.target.target]; +} + +export function getEnvFilesForTask(task: Task, graph: ProjectGraph): string[] { + const [target, nonAtomizedTarget] = getOwnerTargetForTask(task, graph); + + return getEnvPathsForTask( + task.projectRoot, + target, + task.target.configuration, + nonAtomizedTarget + ); } function loadDotEnvFilesForTask( task: Task, + graph: ProjectGraph, environmentVariables: NodeJS.ProcessEnv ) { - const dotEnvFiles = getEnvFilesForTask(task); + const dotEnvFiles = getEnvFilesForTask(task, graph); for (const file of dotEnvFiles) { loadAndExpandDotEnvFile(join(workspaceRoot, file), environmentVariables); } diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index 128bd8c644c395..8f4ca4155bb672 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -225,7 +225,7 @@ export class TaskOrchestrator { // region Processing Scheduled Tasks private async processTask(taskId: string): Promise { const task = this.taskGraph.tasks[taskId]; - const taskSpecificEnv = getTaskSpecificEnv(task); + const taskSpecificEnv = getTaskSpecificEnv(task, this.projectGraph); if (!task.hash) { await hashTask(