Skip to content

Conversation

@pgrzesik
Copy link
Contributor

@pgrzesik pgrzesik commented Oct 19, 2020

Add support for autoloading env variables from .env and .env.${stage} files.

The support is pretty basic and is meant to be similar to support implemented in serverless/components - https://github.com/serverless/components/blob/master/src/cli/index.js#L64-L76

There are still a few small questions that I think are better to discuss directly on a PR:

  1. Where should the documentation of that feature live? I know that there's an open issue to change the current documentation structure to have a provider-agnostic section(s), but at the moment I don't see a good place to document that feature. Should it live as a top-level piece of documentation, similar to e.g. configuration-validation.md?
  2. How should the path for .env files be resolved? Should it take into account servicePath if one is set/provider?
  3. By following the similar approach as serverless/components, only one .env file can be loaded - would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env?

Closes: #7907

@pgrzesik pgrzesik force-pushed the add-support-for-loading-env-files branch from 65bc9bb to bcdedc9 Compare October 19, 2020 13:54
Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik that looks really good! Please see my comments

Comment on lines 30 to 35
if (doesStageEnvFileExists) {
dotenv.config({ path: stageEnvFilePath });
} else if (doesDefaultEnvFileExists) {
dotenv.config({ path: defaultEnvFilePath });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested but I assume that dotenv.config() is safe to call when env file at path doesn't exist.

If it's the case I would just run it unconditionally

Technically we need a file exists check only to eventually display deprecation message so with v3 it'll be nice to remove this checks.

I take it can be constructed as follows

if (useDotEnv) {
  dotenv.config(...)
} else {
  cost hasDotEnvFile = ...
  if (hasDotEnvFile) showDeprecation()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that approach, however, that suggests that you'd like the behavior to be a bit different than the one in serverless/components - please see my question from the above

By following the similar approach as serverless/components, only one .env file can be loaded - would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env?

So, do you think that we should load both env files (default one first, then stage) if they exist?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik what about that one

If I read correctly from dotenv implementation, they return object with error property if file was not found: https://github.com/motdotla/dotenv/blob/7301ac9be0b2c766f865bbe24280bf82586d25aa/lib/main.js#L108

This also reminds me, that it'll be good to signal any parsing errors to the users. I think it'll be good to crash then with meaningful error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read correctly from dotenv implementation, they return object with error property if file was not found: motdotla/dotenv@7301ac9/lib/main.js#L108

Do you suggest using that to avoid explicit checks if the file exists? If yes, then, unfortunately, we still have to check for the existence of files in case the useDotenv is not set to true.

This also reminds me, that it'll be good to signal any parsing errors to the users. I think it'll be good to crash then with meaningful error.

That's a good idea, however, I couldn't really come up with any actual possible parsing errors from dotenv.config that are not related to nonexistent file under provided path. I still think it's a good idea to check for any error. How would you like the framework to handle it? Crash or rather emit a warning to the user and continue processing as "normal"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If yes, then, unfortunately, we still have to check for the existence of files in case the useDotenv is not set to true

Yes, but it's temporary, it's only for deprecation reporting and will be removed with next major

How would you like the framework to handle it? Crash or rather emit a warning to the user and continue processing as "normal"?

I think we should crash. If env setup is broken it's unlikely things will go well

@pgrzesik
Copy link
Contributor Author

pgrzesik commented Oct 20, 2020

Thank you @medikoo for a swift review 🙇 There are still a few questions that I'm not sure about and I was hoping if you would be able to help make a proper decision:

  1. Where should the documentation of that feature live? I know that there's an open issue to change the current documentation structure to have a provider-agnostic section(s), but at the moment I don't see a good place to document that feature. Should it live as a top-level piece of documentation, similar to e.g. configuration-validation.md?
  2. How should the path for .env files be resolved? Should it take into account servicePath and search for .env files in the same dir as servicePath if one is set/provider?

Thanks in advance 🙇

@medikoo
Copy link
Contributor

medikoo commented Oct 20, 2020

@pgrzesik great questions, sorry I forgot to answer them in initial review:

Where should the documentation of that feature live?

Yes, let's put them top-level as environment-variables.md (I'll ensure site is configured to take that)

How should the path for .env files be resolved? Should it take into account servicePath if one is set/provider?

Simply against process.cwd(), `servicePath will in all cases i point it afaik

would it make sense to load both .env and .env.stage if there're present in a way that .env.stage will be loaded later to override overlapping variables from .env

I imagined supporting both (as you describe), but let me confirm with Components authors on why it works differently there.

@codecov-io
Copy link

codecov-io commented Oct 22, 2020

Codecov Report

Merging #8413 (76b99c3) into master (fef389b) will increase coverage by 0.02%.
The diff coverage is 93.75%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #8413      +/-   ##
==========================================
+ Coverage   87.25%   87.27%   +0.02%     
==========================================
  Files         255      256       +1     
  Lines        9501     9516      +15     
==========================================
+ Hits         8290     8305      +15     
  Misses       1211     1211              
Impacted Files Coverage Δ
lib/configSchema.js 100.00% <ø> (ø)
lib/Serverless.js 96.42% <93.33%> (-0.52%) ⬇️
lib/loadEnv.js 94.11% <94.11%> (ø)
...ib/plugins/aws/package/lib/generateCoreTemplate.js 94.87% <0.00%> (-0.26%) ⬇️
lib/plugins/aws/invokeLocal/index.js 68.53% <0.00%> (-0.09%) ⬇️
lib/plugins/aws/customResources/index.js 98.57% <0.00%> (-0.03%) ⬇️
lib/plugins/aws/common/index.js 100.00% <0.00%> (ø)
lib/plugins/aws/lib/validate.js 100.00% <0.00%> (ø)
lib/plugins/aws/package/index.js 100.00% <0.00%> (ø)
lib/plugins/aws/common/lib/artifacts.js 100.00% <0.00%> (ø)
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fef389b...76b99c3. Read the comment docs.

@pgrzesik
Copy link
Contributor Author

Hello @medikoo 👋 Did you hear back from serverless/components team? I've made the suggested updates and if we have that clear I think we will be able to wrap up that PR soon 👍

@medikoo
Copy link
Contributor

medikoo commented Oct 26, 2020

@pgrzesik very sorry for late response. After looking into it deeper, I think we should support only one env file. It's also recommended approach by dotenv authors.

So .env.{stage} should take precedence over .env

@pgrzesik
Copy link
Contributor Author

Hello @medikoo, thanks a lot for your response 🙇 I'll apply the changes to once again use only one env file and push the changes, hopefully later today 💯

@pgrzesik pgrzesik force-pushed the add-support-for-loading-env-files branch from 702c8af to 274ac08 Compare October 29, 2020 16:52
@pgrzesik pgrzesik requested a review from medikoo October 29, 2020 16:52
@pgrzesik
Copy link
Contributor Author

Updated, any feedback will be much appreciated 🙇

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik ! It looks very promising. Please see my suggestions and let me know what yo think

layout: Doc
-->

# Preloading environment variables
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe name it as "Resolution of environment variables"

And add another section header as:

## Support for `.env` files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed 👍


# Preloading environment variables

The framework automatically loads environment variables from `.env` files with the help of [dotenv](https://www.npmjs.com/package/dotenv).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be good to inform upfront that it's an opt-in. So maybe let's put it as:

With useDotenv: true set in your serverless.yml file, framework automatically... (from next major version .env files will be loaded by default and useDotenv setting will be ignored)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed 👍


The framework automatically loads environment variables from `.env` files with the help of [dotenv](https://www.npmjs.com/package/dotenv).

The framework looks for `.env` and `.env.{stage}` files in current directory and then tries to load them using `dotenv`. If `.env.{stage}` is found, `.env` will not be loaded. If stage is not explicitly defined, it defaults to `dev`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe change current directory to service directory, it'll be a bit more specific

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed 👍


The framework looks for `.env` and `.env.{stage}` files in current directory and then tries to load them using `dotenv`. If `.env.{stage}` is found, `.env` will not be loaded. If stage is not explicitly defined, it defaults to `dev`.

**Note**: There are a few differences between above functionality and [serverless-dotenv-plugin](https://github.com/colynb/serverless-dotenv-plugin):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe make it a subsection as:

###  Differences against  `serverless-dotenv-plugin`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed 👍

const path = require('path');
const _ = require('lodash');

class EnvLoader {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if creating a class is justified in this case. It looks as relatively simple loadEnv function encapsulated into class, which doesn't feel right.

Let's host it as function, same as e.g. eventuallyUpdate.js logic

Still I notice that utils folder also doesn't seem right, to me it can also be placed directly in lib (we anyway need at some point to improve internal organization)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also not sure but I decided to follow similar approach as with YamlParser class. I currently changed implementation from class-based to function-based, however, it still depends on serverless instance. I was thinking on how to separate it, but it actually requires a few dependencies such as access to serverlessConfigFile, options and logDeprecation. I'm on the fence here and I see a few options:

  • keep the dependency on serverless
  • keep the dependency on specific pieces - instead of passing serverless instance, just pass the needed pieces
  • resolve everything in Serverless and only pass stage, useDotenv to loadEnv function

Which one do you think makes the most sense here? I personally like the option #3, but that moves some of the responsibilities of loadEnv directly to Serverless class, which is not ideal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also not sure but I decided to follow similar approach as with YamlParser class

Totally understood, we have a lot of legacy conventions. Still it's good to abort some of them, especially when they resemble noisy not well justified practices.

it still depends on serverless instance

That's true. I think the cleanest approach for now, would be to follow with something as:

InServerless.js (inline in init or in new method):

if (serviceContext && serviceConfig.useDotEnv) {
  const stage = // resolve stage
  loadDotEnv(stage);
} else {
  isDotFileEnvExisting = // fs exists checks
  if (isDotFileEnvExisting) showDeprecation();
}

And in lib/resolveDotEnv.js just load eventually existing config for given stage

Note, that if we do it that way, then we would be able to use lib/resolveDotEnv.js in as is form, in refactor we plan to do here: #8364 (it is mostly about separating CLI and programmatic logic, so Framework core is purely programmatic (there will be then no dotenv resolution in its core), it'll also allow to integrate Components better)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yes, option 3 as you pointed, seems best :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But without passing useDotenv (let's call the util when we know we want to resolve env from .env)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for a comprehensive explanation, I wasn't sure if moving some of the env-related logic makes sense in this case, but thanks to your explanation I see the reasoning and I also think that this is the most appropriate solution moving forward 🙇

Comment on lines 30 to 35
if (doesStageEnvFileExists) {
dotenv.config({ path: stageEnvFilePath });
} else if (doesDefaultEnvFileExists) {
dotenv.config({ path: defaultEnvFilePath });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik what about that one

If I read correctly from dotenv implementation, they return object with error property if file was not found: https://github.com/motdotla/dotenv/blob/7301ac9be0b2c766f865bbe24280bf82586d25aa/lib/main.js#L108

This also reminds me, that it'll be good to signal any parsing errors to the users. I think it'll be good to crash then with meaningful error.

describe('EnvLoader', () => {
describe('with useDotenv', () => {
it('should load matching stage env file if present', async () => {
const result = await runServerless({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it as util (which just takes servicePath as an input) we may write simpler test not involving runServerless.

Then we can write temporary files to home dir (it's a temporary folder in test run, not real user home dir), and handle process.env with process-uils/override-env as:

overrideEnv(() => {
  // Load env from .env
  // Confirm on `process.env` it's effective
});

It'll be also helpful to have it detached from Serverless instance, as it's likely we will refactor whole process follow up to this proposal: #8364

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the decision on dependencies of loadEnv func, I will either refactor it to not depend on serverless or keep it in current form

@pgrzesik
Copy link
Contributor Author

pgrzesik commented Nov 8, 2020

Hello @medikoo - I'm really sorry about radio silence - I was not available for pretty much whole last week and I'm only now catching up. I hope to address all the comments at the beginning of the week.

@medikoo
Copy link
Contributor

medikoo commented Nov 9, 2020

No worries, and great thanks for heads up @pgrzesik !

@pgrzesik pgrzesik force-pushed the add-support-for-loading-env-files branch 2 times, most recently from d7b545b to b14d97b Compare November 10, 2020 19:45
@pgrzesik pgrzesik requested a review from medikoo November 10, 2020 19:56
Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks @pgrzesik for valuable input I hope my answers helped to clarify my point.

Sorry, if it's too confusing. We plan a bigger refactor improvement to the Framework (so it's more composable and fits along Components better), and that roadmap also influences implementation choices for PR's like this.

@pgrzesik
Copy link
Contributor Author

Thanks a lot @medikoo for additional clarification - that was definitely very valuable to me and helped me understand a bit better the vision for upcoming changes/refactoring of the framework. My initial gut feeling was to encapsulate the whole logic related to loading env (including all needed checks, etc) in separate util to keep Serverless class "cleaner", but I definitely see that it wouldn't make sense here, given the upcoming changes. I'll follow up with a PR, according to your suggestions 👍

@pgrzesik pgrzesik force-pushed the add-support-for-loading-env-files branch from b14d97b to c26ae95 Compare November 18, 2020 21:14
let restoreEnv;

beforeEach(() => {
const tmpDir = createTmpDir();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a place that I would love to get your feedback @medikoo - I was trying to just setup .env files once in before, however, after first test, the files were no longer accessible, so I switched to beforeEach/afterEach combo that I'm not super happy about due to unnecessary creation of .env files before each test. Do you have any suggestions on how to properly set it up so these files setup in before will be visible to all tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik As we're preparing it as standalone module (that takes stage), the unit test may simply put needed .env files to process.cwd(), and confirm that after running util expected env vars are seen on process.env

Important notes:

  • process.env is reset of each test file, therefore we don't have to worry about side effects (it's ensured by: restore-env Mocha extension we have loaded
  • process.cwd() is set to home dir, which is set to temporary dir (again ensured by Mocha extensions: mock-cwd and mock-homedir - note that homedir is cleanup up for other test files, so we also do not need to take care of cleaning it.

Additionally if you'd like to have process.env or process.cwd isolated for specific test runs (so there are no side effects for following tests in same test file). check: process-utils/override-env an process-utils/override-cwd

When having it isolated, and tested as above with unit test, not sure if it's worth testing that resolution works in context of Serverless instance. We might have some one sanity test, and such can be constructed as:

  1. Setup a chosen fixture (like here:
    const { servicePath, updateConfig } = await fixtures.setup('function');
    )
  2. Then in returned servicePath setup needed .env files
  3. Invoke runServerless run on fixture (as here:
    const { cfTemplate: originalTemplate } = await runServerless({
    cwd: servicePath,
    cliArgs: ['package'],
    });
    )
  4. Confirm on the outcome

If that doesn't answer your questions, let me know. I know I've provided an indirect answer, but I hope that above information clarifies how to setup test efficiently

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've posted above comment, without looking into test file, which looks very good imo.

I believe you don't need to create tmp dir, you may just put files in process.cwd() (once) as is, and all that you need to reset is process.env (which you already do with process-utils/override-env).

.env files should definitely not be removed between single tests, so you should be able to setup them once in before (they will however be cleaned after whole test file runs)


const { error: stageEnvResultError } = dotenv.config({ path: stageEnvFilePath });

if (!stageEnvResultError) return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, as suggested, I changed the approach to try to load files and based on error returned by dotenv.config decide how to proceed further. I'd love to hear your opinion on the chosen approach and I'm open to suggestions on how to improve it further

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks great!

@pgrzesik pgrzesik requested a review from medikoo November 18, 2020 21:24
Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik that looks great! I've posted just few clarifications towards proposed tests

let restoreEnv;

beforeEach(() => {
const tmpDir = createTmpDir();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've posted above comment, without looking into test file, which looks very good imo.

I believe you don't need to create tmp dir, you may just put files in process.cwd() (once) as is, and all that you need to reset is process.env (which you already do with process-utils/override-env).

.env files should definitely not be removed between single tests, so you should be able to setup them once in before (they will however be cleaned after whole test file runs)

@pgrzesik
Copy link
Contributor Author

Thanks a lot for very valuable suggestions @medikoo.

Initially, I wanted to setup files in before hook, relying on the mock-cwd addition to mocha, but I was experiencing suspicious errors and failing tests - when I was running full suite, there were no errors, but when I was running only newly added tests, one of them was failing. So I started digging a bit. What I found out is that, most likely, there's a small race condition in the current mocha runner setup, which makes some of the tests and/or before/beforeEach hooks to run without mocked cwd.

loadEnv
  cwd in before: /home/pgrzesik/Resources/opensource/serverless
  cwd in first test: /home/pgrzesik/Resources/opensource/serverless
    ✓ should load matching stage env file if present
  cwd in second test: /tmp/tmpdirs-serverless/17eb/f19d17
    1) should load from default env file if present and no matching stage file found
  cwd in third test: /tmp/tmpdirs-serverless/17eb/f19d17
    ✓ should throw ServerlessError if dotenv returns error other than missing file

Here, I added simple logging statements to log process.cwd in before hook as well as in each of the tests. As can be seen, in before hook and first test, the cwd is not changed, which results in creation of the .env files in non-mocked directory. An additional side effect of that are leftover .env files in project dir. When running all tests with npm test, mentioned loadEnv tests didn't have that problem, because they weren't run at the beginning of the whole test suite. The problem surfaces again when all tests are run with mocha-isolated - please see the failed build: https://github.com/serverless/serverless/pull/8413/checks?check_run_id=1437876395

I'm not very familiar with mocha, but it seems like the problem (if that race condition is the actual problem with the setup) could be solved with the use of root hooks or even better, with global fixtures. That, however, would require an upgrade to at least v.8.2.0 of mocha. I would love to hear your thoughts on this one, maybe you already encountered that issue before?

As for the changes relevant to the main topic of this PR I did the following:

  • Changed to setup in before, but I reverted to previous approach due to issues described above
  • Added an "integration" sanity test(s) in lib/Serverless.test.js

@medikoo
Copy link
Contributor

medikoo commented Nov 23, 2020

@pgrzesik thanks for this insight. Still I'm not able to reproduce the race condition you're describing.

I've created following test file:

'use strict';

console.log('MODULE', process.cwd());

describe('test', () => {
  console.log('DESCRIBE', process.cwd());

  before(() => {
    console.log('BEFORE', process.cwd());
  });

  beforeEach(() => {
    console.log('BEFORE EACH', process.cwd());
  });
  it('test', () => {
    console.log('IT', process.cwd());
  });
});

I've put it in Framework folder into test.js

and then I've simply run npx mocha test.js, and outcome is as follows:

% npx mocha test.js  
MODULE /Users/medikoo/npm-packages/serverless
DESCRIBE /Users/medikoo/npm-packages/serverless


  test
BEFORE /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
BEFORE EACH /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
IT /private/var/folders/wx/nx84lt4j59q46pdq879v9j540000gn/T/tmpdirs-serverless/8a5a/68dcfd
    ✓ test


  1 passing (5ms)

While in scope of module body cwd is not yet tweaked (and that's known limitation), it's in place in before call.

Are you sure you're relying on Mocha v6 as referenced in package.json ?

Concerning Mocha v8. Upgrade is pending, I've already started work on it (check: https://github.com/serverless/test/pull/59 - it'll be ready if not today, than in next days)

@pgrzesik
Copy link
Contributor Author

Thanks for checking that out @medikoo. I've confirmed on my side once again, and you're right that with npx mocha test.js it works, but when using npx mocha-isolated or running it via npm test -- -g "testname", it resurfaces again, which is very strange. Please see the following examples, I took the test you mentioned and added it to lib/something.test.js so it would be picked up by npm test. I can also confirm that I'm on mocha@6.2.3.

Running with npx mocha lib/something.test.js

➜  serverless git:(add-support-for-loading-env-files) ✗ npx mocha lib/something.test.js
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /tmp/tmpdirs-serverless/85f6/f6ff40
BEFORE EACH /tmp/tmpdirs-serverless/85f6/f6ff40
IT /tmp/tmpdirs-serverless/85f6/f6ff40
    ✓ test


  1 passing (5ms)

Running with npx mocha-isolated lib/something.test.js

➜  serverless git:(add-support-for-loading-env-files) ✗ npx mocha-isolated lib/something.test.js
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /home/pgrzesik/Resources/opensource/serverless
BEFORE EACH /home/pgrzesik/Resources/opensource/serverless
IT /home/pgrzesik/Resources/opensource/serverless
    ✓ test


  1 passing (3ms)

Running with npm test -- -g "RaceCondition"

➜  serverless git:(add-support-for-loading-env-files) ✗ npm test -- -g "RaceCondition"

> serverless@2.11.1 test /home/pgrzesik/Resources/opensource/serverless
> mocha "!(test|node_modules)/**/*.test.js" "-g" "RaceCondition"

DESCRIBE /home/pgrzesik/Resources/opensource/serverless
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /home/pgrzesik/Resources/opensource/serverless
BEFORE EACH /home/pgrzesik/Resources/opensource/serverless
IT /home/pgrzesik/Resources/opensource/serverless
    ✓ test


  1 passing (11ms)

As can be seen above, the test uses non-mocked cwd if ran with npm test and mocha-isolated. I also did another "experiment", where I modified the test to wait for a second in before hook.

'use strict';

const wait = require('timers-ext/promise/sleep');

console.log('MODULE', process.cwd());

describe('RaceCondition', () => {
  console.log('DESCRIBE', process.cwd());

  before(async () => {
    console.log('BEFORE', process.cwd());
    await wait(1000)
    console.log('BEFORE', process.cwd());
  });

  beforeEach(() => {
    console.log('BEFORE EACH', process.cwd());
  });

  it('test', () => {
    console.log('IT', process.cwd());
  });
});

Running it with npm test -- -g "RaceCondition"

➜  serverless git:(add-support-for-loading-env-files) ✗ npm test -- -g "RaceCondition"
> serverless@2.11.1 test /home/pgrzesik/Resources/opensource/serverless
> mocha "!(test|node_modules)/**/*.test.js" "-g" "RaceCondition"

DESCRIBE /home/pgrzesik/Resources/opensource/serverless
MODULE /home/pgrzesik/Resources/opensource/serverless
DESCRIBE /home/pgrzesik/Resources/opensource/serverless


  RaceCondition
BEFORE /tmp/tmpdirs-serverless/aa19/738736
BEFORE EACH /tmp/tmpdirs-serverless/aa19/738736
IT /tmp/tmpdirs-serverless/aa19/738736
    ✓ test


  1 passing (1s)

After waiting for a second, cwd is properly mocked.

Another example of this behavior, not related to my machine surfaced in mocha-isolated tests on CI on the commit where I didn't create a temporary dir manually: https://github.com/serverless/serverless/pull/8413/checks?check_run_id=1437876395
It additionally includes information that lib/loadEnv.test.js didn't clean created temporary files which suggests that the .env files were created in "real" dir and not the mocked one.

Would you be able to verify it on your side as well?

@pgrzesik pgrzesik requested a review from medikoo November 23, 2020 16:18
@medikoo
Copy link
Contributor

medikoo commented Nov 24, 2020

@pgrzesik indeed, I was able to reproduce it locally, great thanks for thorough explanations.

Interestingly it can be reproduced already by node node_modules/.bin/mocha test.js . I've diagnosed and patched the issue here: https://github.com/serverless/test/pull/62. It's already released and I've bump @serverless/test version in package.json, to ensure patch is taken.

Therefore after you update against master issue should be gone.

@pgrzesik pgrzesik force-pushed the add-support-for-loading-env-files branch from e6d9df3 to 7c5e72a Compare November 24, 2020 21:53
@pgrzesik
Copy link
Contributor Author

Thanks a lot @medikoo 🙇 Indeed, your changes resolved the issue. I've pushed version rebased on top of current master and changed the approach to setting up the files in before hook, without creating yet another temporary dir.

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgrzesik that looks great! I've proposed just few minor, mostly style improvements, and we're good to go!

).servicePath;

const defaultFileContent = 'DEFAULT_ENV_VARIABLE=valuefromdefault';
writeFileSync(path.join(servicePath, '.env'), defaultFileContent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use async function (and rely on fs.promises directly).

(Unfortunately in Framework we have some not great old patterns that dates Node.js v4 and before, we will slowly recover from that, and ideally if new code is free from them)

before(() => {
const stage = 'testing';
const stageFileContent = 'FROM_STAGE=valuefromstage';
writeFileSync(path.join(process.cwd(), `.env.${stage}`), stageFileContent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here let's rely on fs.promises.writeFile


it('should load matching stage env file if present', async () => {
await loadEnv('testing');
expect(process.env.FROM_DEFAULT).to.be.undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More logical check is to.not.have.property() (theoretically undefined can be assigned as value, and that should be not expected)


There are a few differences between above functionality and [serverless-dotenv-plugin](https://github.com/colynb/serverless-dotenv-plugin):

- the framework only loads environments variables locally and does not pass them to your functions' environment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably start each sentence capitalized.

also let's fix functions' into function's

lib/loadEnv.js Outdated
Comment on lines 11 to 12
`Encountered: "${error}" while trying to load environment variables`,
` from filepath: ${filePath}.`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably reword it to:

Failed to load environment variables from "${filePath}": ${error}"
  • Original error message can be verbose, so it's better to put this at the end
  • It's also convention in few other places, to first provide short context info, and then trail it with original error message.

@pgrzesik
Copy link
Contributor Author

@medikoo thank you, all should be fixed now 🙇

@pgrzesik pgrzesik requested a review from medikoo November 25, 2020 14:13
Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pgrzesik ! It's great to have this long waited functionality finally in

@medikoo medikoo merged commit d1a22c8 into serverless:master Nov 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support loading env variables from .env files

3 participants