Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): fix env override run command #28156

Merged
merged 1 commit into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 39 additions & 88 deletions packages/nx/src/executors/run-commands/run-commands.impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,35 +783,30 @@ describe('Run Commands', () => {
describe('env', () => {
afterAll(() => {
delete process.env.MY_ENV_VAR;
unlinkSync('.env');
});

it('should add the env to the command', async () => {
const root = dirSync().name;
it('should use value from process.env', async () => {
const f = fileSync().name;
process.env.MY_ENV_VAR = 'from-env';
const result = await runCommands(
{
commands: [
{
command: `echo "$MY_ENV_VAR" >> ${f}`,
},
],
env: {
MY_ENV_VAR: 'my-value',
},
parallel: true,
__unparsed__: [],
},
{ root } as any
context
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toEqual('my-value');
expect(result.success).toEqual(true);
expect(readFile(f)).toContain('from-env');
});

it('should prioritize env setting over local dotenv files', async () => {
writeFileSync('.env', 'MY_ENV_VAR=from-dotenv');
const root = dirSync().name;
it('should add the env to the command', async () => {
process.env.MY_ENV_VAR = 'from-env';
const f = fileSync().name;
const result = await runCommands(
{
Expand All @@ -821,22 +816,20 @@ describe('Run Commands', () => {
},
],
env: {
MY_ENV_VAR: 'from-options',
MY_ENV_VAR: 'my-value',
},
parallel: true,
__unparsed__: [],
},
{ root } as any
context
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toEqual('from-options');
expect(result.success).toEqual(true);
expect(readFile(f)).toContain('my-value');
});

it('should prioritize env setting over dotenv file from envFile option', async () => {
const devEnv = fileSync().name;
writeFileSync(devEnv, 'MY_ENV_VAR=from-dotenv');
const root = dirSync().name;
it('should prioritize env setting over local process.env', async () => {
process.env.MY_ENV_VAR = 'from-env';
const f = fileSync().name;
const result = await runCommands(
{
Expand All @@ -847,117 +840,75 @@ describe('Run Commands', () => {
],
env: {
MY_ENV_VAR: 'from-options',
envFile: devEnv,
},
parallel: true,
__unparsed__: [],
},
{ root } as any
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toEqual('from-options');
});
});

describe('dotenv', () => {
beforeAll(() => {
writeFileSync('.env', 'NRWL_SITE=https://nrwl.io/');
});

beforeEach(() => {
delete process.env.NRWL_SITE;
delete process.env.NX_SITE;
});

afterAll(() => {
unlinkSync('.env');
});

it('should load the root .env file by default if there is one', async () => {
const f = fileSync().name;
const result = await runCommands(
{
commands: [
{
command: `echo $NRWL_SITE >> ${f}`,
},
],
__unparsed__: [],
},
context
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toEqual('https://nrwl.io/');
expect(result.success).toEqual(true);
expect(readFile(f)).toContain('from-options');
});

it('should load the specified .env file instead of the root one', async () => {
it('should prioritize process.env over envFile option', async () => {
process.env.MY_ENV_VAR = 'from-env';
const devEnv = fileSync().name;
writeFileSync(devEnv, 'NX_SITE=https://nx.dev/');
writeFileSync(devEnv, 'MY_ENV_VAR=from-dotenv');
const f = fileSync().name;
let result = await runCommands(
const result = await runCommands(
{
commands: [
{
command: `echo $NX_SITE >> ${f} && echo $NRWL_SITE >> ${f}`,
command: `echo "$MY_ENV_VAR" >> ${f}`,
},
],
envFile: devEnv,
env: {
envFile: devEnv,
},
parallel: true,
__unparsed__: [],
},
context
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toContain('https://nx.dev/');

appendFileSync(devEnv, 'NX_TEST=$NX_SITE');
await runCommands(
{
commands: [
{
command: `echo $NX_TEST >> ${f}`,
},
],
envFile: devEnv,
__unparsed__: [],
},
context
);
expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toContain('https://nx.dev/');
expect(result.success).toEqual(true);
expect(readFile(f)).toContain('from-env');
});

it('should override environment variables that are present in both the specified .env file and other loaded ones', async () => {
it('should prioritize env setting over dotenv file from envFile option', async () => {
process.env.MY_ENV_VAR = 'from-env';
const devEnv = fileSync().name;
writeFileSync(devEnv, 'NRWL_SITE=https://nrwl.io/override');
writeFileSync(devEnv, 'MY_ENV_VAR=from-dotenv');
const f = fileSync().name;
let result = await runCommands(
const result = await runCommands(
{
commands: [
{
command: `echo $NRWL_SITE >> ${f}`,
command: `echo "$MY_ENV_VAR" >> ${f}`,
},
],
envFile: devEnv,
env: {
MY_ENV_VAR: 'from-options',
envFile: devEnv,
},
parallel: true,
__unparsed__: [],
},
context
);

expect(result).toEqual(expect.objectContaining({ success: true }));
expect(readFile(f)).toContain('https://nrwl.io/override');
expect(result.success).toEqual(true);
expect(readFile(f)).toContain('from-options');
});

it('should error if the specified .env file does not exist', async () => {
const f = fileSync().name;
try {
await runCommands(
{
commands: [
{
command: `echo $NX_SITE >> ${f} && echo $NRWL_SITE >> ${f}`,
command: `echo $MY_ENV_VAR`,
},
],
envFile: '/somePath/.fakeEnv',
Expand Down
37 changes: 16 additions & 21 deletions packages/nx/src/executors/run-commands/run-commands.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,12 @@ const childProcesses = new Set<ChildProcess | PseudoTtyProcess>();

function loadEnvVarsFile(path: string, env: Record<string, string> = {}) {
unloadDotEnvFile(path, env);
const result = loadAndExpandDotEnvFile(path, env, true);
const result = loadAndExpandDotEnvFile(path, env);
if (result.error) {
throw result.error;
}
}

function loadEnvVars(path?: string, env: Record<string, string> = {}) {
if (path) {
loadEnvVarsFile(path, env);
} else {
try {
loadEnvVarsFile('.env', env);
} catch {}
}
}

export type Json = {
[k: string]: any;
};
Expand Down Expand Up @@ -478,25 +468,30 @@ function calculateCwd(
return path.join(context.root, cwd);
}

/**
* Env variables are processed in the following order:
* - env option from executor options
* - env file from envFile option if provided
* - local env variables
*/
function processEnv(
color: boolean,
cwd: string,
env: Record<string, string>,
envOptionFromExecutor: Record<string, string>,
envFile?: string
) {
const localEnv = appendLocalEnv({ cwd: cwd ?? process.cwd() });
let res = {
let localEnv = appendLocalEnv({ cwd: cwd ?? process.cwd() });
localEnv = {
...process.env,
...localEnv,
};
// env file from envFile option takes priority over process env
if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false') {
loadEnvVars(envFile, res);

if (process.env.NX_LOAD_DOT_ENV_FILES !== 'false' && envFile) {
loadEnvVarsFile(envFile, localEnv);
}
// env variables from env option takes priority over everything else
res = {
...res,
...env,
let res: Record<string, string> = {
...localEnv,
...envOptionFromExecutor,
};
// need to override PATH to make sure we are using the local node_modules
if (localEnv.PATH) res.PATH = localEnv.PATH; // UNIX-like
Expand Down