angular-cli icon indicating copy to clipboard operation
angular-cli copied to clipboard

Request: Method to pass environment variables during build vs file.

Open DennisSmolek opened this issue 8 years ago • 129 comments

OS?

Any, mostly a build/CI question

Versions.

1.0.0-beta.26

Reasoning

I really like the environment variables setup with the CLI and we have switched to using them. One problem though is we no longer have access to server set ENV variables which is how we pass information to certain things during deployment. Consider:

Bugsnag.releaseStage = environment.releaseStage;
// can't access process.env anymore with the CLI
  const commit = process.env.CI_COMMIT_ID;
  if (commit) Bugsnag.metaData = {
        commits: {
          id: commit
        }
      };

Codeship ENV variables

Being able to add the release stage, branch, and commit ID allows us to segment and track bugs/issues very accurately. My problem is I know the build takes whatever stage I pass and replaces the environment.ts file so I can't just edit the file with a bash script (well, if I know EXACTLY the build before hand I could) but it would be nice to be able to pass env variables in the build line.

Consider: ng build --prod could be: ng build --prod --envVar:commitId: CI_COMMIT_ID

which would append the variable after the file gets merged

export const environment =  {
    production: true, 
    commitId: 'ca82a6dff817ec66f44342007202690a93763949'
}

something like this would ensure it gets added to the right file and at the right time/place..

Then we could do:

  const commit = environment.commidId;
  if (commit) Bugsnag.metaData = {
        commits: {
          id: commit
        }
      };

DennisSmolek avatar Feb 01 '17 14:02 DennisSmolek

@hansl we were just discussing this, thought you might have something to add

Brocco avatar Feb 01 '17 14:02 Brocco

Why not make ENV variables available in process.env again?

I'm really feeling comfortable with Heroku Config Vars and Codeship environment variables, I already set up them, moved all my sensitive data like secret keys to angular-cli environment.dev.ts, BUT I can't use them.(

export const environment = {
  production: false,
  GOOGLE_RECAPTCHA_SITE_KEY: '6Le...Zq'
};

Because there is no way to access variables from test env or production like:

export const environment = {
  production: false,
  GOOGLE_RECAPTCHA_SITE_KEY: process.env.GOOGLE_RECAPTCHA_SITE_KEY
};

in my environment.test.ts and environment.prod.ts

Need to thinking on using third party packages like dotenv or env2

P.S: Is it hard to implement configuration like this:

...
"scripts": [],
"environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.dev.ts",
        "test": "environments/environment.test.ts",
        "prod": "environments/environment.prod.ts"
      },
"processEnv": [
         "CUSTOM_ENV_VAR",
         "GOOGLE_RECAPTCHA_SITE_KEY",
         ...
]

So then there will be opportunity access them from process.env.CUSTOM_ENV_VAR, etc.

yuzhva avatar Feb 08 '17 00:02 yuzhva

I ran into the same issue a day ago. And for a temporary workaround, that works (just checked it with Codeship), I just added the "prebuild" script, that generates the appropriate environment file using current env variables. ejs is used as a template processor due to it's simplicity.

scripts/prebuild.js - this is a script with the required logic

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const ejs = require('ejs');

const environmentFilesDirectory = path.join(__dirname, '../src/environments');
const targetEnvironmentTemplateFileName = 'environment.prod.ts.template';
const targetEnvironmentFileName = 'environment.prod.ts';

// Define default values in case there are no defined ones,
// but you should define only non-crucial values here,
// because build should fail if you don't provide the correct values
// for your production environment
const defaultEnvValues = {
  PREFIX_STORAGE_TYPE: 'localStorage',
  PREFIX_USER_TOKEN_FIELD_NAME: 'userToken',
};

// Load template file
const environmentTemplate = fs.readFileSync(
  path.join(environmentFilesDirectory, targetEnvironmentTemplateFileName),
  {encoding: 'utf-8'}
);

// Generate output data
const output = ejs.render(environmentTemplate, Object.assign({}, defaultEnvValues, process.env));
// Write environment file
fs.writeFileSync(path.join(environmentFilesDirectory, targetEnvironmentFileName), output);

process.exit(0);

src/environments/environment.prod.ts.template - this is a template file, that generates the required environment file. Notice, that PREFIX_BACKEND_URL isn't provided in scripts/prebuild.js script in defaultEnvValues, because in production it should be defined only by build environment variables.

export const environment = {
  production: true,
  backendUrl: '<%= PREFIX_BACKEND_URL %>',
  storageType: '<%= PREFIX_STORAGE_TYPE %>',
  userTokenFieldName: '<%= PREFIX_USER_TOKEN_FIELD_NAME %>'
};

package.json - there are some changes here. In scripts/prebuild.js shebang (#!) style is used, so "prebuild" is just a pointer to the script file. And to make it work it must be marked as executable, that's why "postinstall" script is added either.

{
  ...
  "scripts": {
    ...
    "build": "ng build --prod --aot",
    "prebuild": "./scripts/prebuild.js",
    ...
    "postinstall": "chmod +x ./scripts/*.js"
  },
  ...
  "devDependencies": {
    ...
    "ejs": "^2.5.6",
    ...
  }
}

k10der avatar Apr 13 '17 08:04 k10der

@k10der this is great, this is similar to what we did. What we do is actually move the environment scripting out of angular totally. We have a single environment.ts file for all builds and use a custom script to generate the correct values via the env variables.

I still think this should be something within the CLI but there are of course workarounds.

DennisSmolek avatar Apr 14 '17 13:04 DennisSmolek

@DennisSmolek, I also think, that handling of process.env variables should be built-in in cli. And your approach to use a single environment file, that is generated by external tool makes sense: my dev environment variables are currently stored in the project repository, which is OK unless I'll start using some 3rd party APIs with private keys. So I guess one-environment-file approach should be used in future angular-cli versions.

k10der avatar Apr 14 '17 13:04 k10der

Injecting environment variables during build seems essential. Any ETA for supporting this in the ng cli?

AmirSasson avatar Apr 20 '17 16:04 AmirSasson

Why would common cross platform method for OS configuration support of env variables be excluded?

Unless there's a strong argument against 12factor that is being proposed for angular?

davidwalter0 avatar May 12 '17 04:05 davidwalter0

I'd also really like to be able to use process.env variables. I'm deploying to docker swarms and the capability to be able to include variables in a compose file is important to my application.

zpydee avatar May 13 '17 11:05 zpydee

very needed for dockerizing the apps indeed

avatsaev avatar May 15 '17 07:05 avatsaev

@k10der Do you have any repository on github that is using the solution you proposed? I'd like to check it because I could not find any real solution on the internet.

carlosthe19916 avatar May 20 '17 13:05 carlosthe19916

Hey, @carlosthe19916. I have a sample project, that I use just for practicing in Angular2+. It's not a production ready and you probably won't get any benefit from running it, but I use the proposed solution there. And it's exactly as I described it in this topic.

k10der avatar May 20 '17 14:05 k10der

+1

emreavsar avatar Jun 09 '17 07:06 emreavsar

Hi friends... What I think I want is to access environment variables at run time (in addition to build time as under discussion here.) For continuous integration purposes, I want to build only once, then deploy that same dist folder to my staging, qa, and prod environments using environment variables set with my CI provider on my runtime box. At first noodling process.env seems to equal {}. Do you feel I'm doing something wrong either technically or with the build-once-deploy-many approach?

GaryB432 avatar Jun 22 '17 17:06 GaryB432

@GaryB432

This is also my use case. I'm thinking to ignore the Angular CLI environments and use a plain old JavaScript include that sets my environment vars in a window.environment var.

Then, I can just deploy a different JavaScript include file and have different variable values with the same build output.

michaelarnauts avatar Jun 22 '17 17:06 michaelarnauts

Workaround ahead. Might work fine in your setup, might not.

I've used two angular cli environments (dev and prod). Values that need to exists at build time are defined in environment.ts or environment.prod.ts. Values that need to be available during runtime are fetched from the window._env array in the environment.*.values.js file.

Production and Staging are using the same environment.prod.ts file, since this is used at build time, and you only want to do one build.

src/environment/environment.ts

export const environment = {
    production: false,
    backendUrl: (<any>window)._env.backendUrl,
};

src/environment/environment.prod.ts

export const environment = {
    production: true,
    backendUrl: (<any>window)._env.backendUrl,
};

src/environment/environment.values.js (development variables)

window._env = {
    backendUrl: 'https://localhost:7000',
};

src/environment/environment.prod.values.js (production variables)

window._env = {
    backendUrl: 'https://api.example.com/',
};

src/environment/environment.stag.values.js (staging variables)

window._env = {
    backendUrl: 'https://api-staging.example.com/',
};

Next, you need to make sure that you add this line to your index.html. We will place this file there during deployment.

...
<head>
  ...
  <script src="/assets/env.js"></script>
</head>
...

The tests also need to have these values, so I've loaded them at the top of the test.ts bootstrap file.

src/test.ts

// Load default environment values
import 'environments/environment.values.js';
...

Finally, you need to change your deployment/development scripts so that you execute this command:

For production:

cp src/environments/environment.prod.values.js dist/assets/env.js

For staging:

cp src/environments/environment.stag.values.js dist/assets/env.js

For development:

cp src/environments/environment.values.js dist/assets/env.js

I'm using Gitlab CI for deployment, and I execute this command before copying the dist/ to the production/staging server. For local development, I'm using a Makefile to set everything up, so I execute the copy the file there right before I'm running the ng serve.

You might also want to add the dist/assets/env.js to your .gitignore.

michaelarnauts avatar Jun 23 '17 11:06 michaelarnauts

How about the webpack.DefinePlugin mentioned in this answer? How do we integrate it into Angular CLI?

FranklinYu avatar Jul 04 '17 16:07 FranklinYu

Another quick solution (any shell command in a template could break everything)

package.json:

"prebuild": "eval \"echo \\\"$(cat src/environments/environment.ts.template)\\\"\" > src/environments/environment.ts",

environment.ts.template:

export const environment = {
  production: false,
  clientId: \"${CLIENT_ID}\",
  apiId: \"${API_ID}\",
  authDiscoveryEndpoint: \"${AUTH_DISCOVERY_ENDPOINT}\"
};

Also, something like this could work better

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < src/environments/environment.ts.template | tee src/environments/environment.ts

jaguardev avatar Jul 23 '17 20:07 jaguardev

My current workaround is a pre and post build event which replaces a version-tag in my environment.prod.ts file export const environment = { production: true, system: 'prod', version: '%VERSION%' };

install npm-replace: npm install replace --save-dev

package.json "build:prod": "ng build --env=prod --output-hashing=all --output-path=dist/prod --aot", "prebuild:prod": "replace '%VERSION%' $VERSION src/environments/environment.prod.ts", "postbuild:prod": "replace $VERSION '%VERSION%' src/environments/environment.prod.ts",

Jenkins runs it with: ($Tag is my release-tag, for example "1.0.0") VERSION="$Tag" npm run build:prod

nh-nico avatar Jul 27 '17 13:07 nh-nico

We wound up sing this Visual Studio Team Services extension in our release. Very happy with it.

GaryB432 avatar Jul 27 '17 15:07 GaryB432

Adding my voice to this request--in the middle of doing DevOps work on three different Angular 4 applications and the lack of access to environment variables has made this much harder than it needs to be. I'm hopeful this can be rectified soon.

I agree with @GaryB432 that access at both build and runtime would be great, but I could settle with build time if need be.

wvh-shanee avatar Aug 03 '17 13:08 wvh-shanee

To add to this, on Heroku I currently need to commit new code to change the env variables. Having access to process.env would let us change the environment variables through the browser and command line without needing to commit anything.

JWesorick avatar Aug 08 '17 15:08 JWesorick

If anyone is interested we made a little command line util to add to your CD step. It takes an EJS template and processes with process.env variables context. We use it to grab our build provider's environment vars and stick them into a small global object script which is then loaded into index.html

GaryB432 avatar Aug 16 '17 16:08 GaryB432

Just adding to my comment above, if you guys are using Azure we created a build/release step that plugs environment variables into a small shim script. Very simple and meets our requirement, viz: build one dist folder and deploy it to multiple environments. I'm sure the underlying package on which it is based, the one I mentioned in the previous comment, is adaptable to other CI/CD ecosystems.

GaryB432 avatar Sep 01 '17 14:09 GaryB432

This CLI approach generates a file that doesn't messes up with the version control and keeps them on environment.ts

kopz9999 avatar Sep 11 '17 22:09 kopz9999

are there any news? it would be nice to have this feature, I guess there are plenty of user's cases

FrancescoBorzi avatar Oct 15 '17 14:10 FrancescoBorzi

i'm interested in this for post-build, like @GaryB432 mentioned, build once/deploy many. I'm not running on azure so need to plug the vars during deployment from vsts Release. CI Process builds creates artifacts from dist folder. I suppose @michaelarnauts way would work for me. What are the drawbacks to having variables in plain js from assets/env.js? At least when webpack runs on the files now its garbled a bit to find the environment const.

wolfpackt99 avatar Nov 07 '17 16:11 wolfpackt99

@k10der, looking at your guidance. Could you please reply to the following if possible?

  1. In which order do we need to execute the npm scripts within a CI environment i.e. Azure CI. Something like below PS: buildsuite? `"scripts": { "build": "ng build --prod", "prebuild": "./scripts/prebuild.js", "postinstall": "chmod +x ./scripts/*.js" "buildsuite": "npm run postinstall && npm run prebuild && npm run build" }

  2. If have multiple env variables defined in Azure CI app settings then what the following line of code will do?
    const output = ejs.render(environmentTemplate, Object.assign({}, defaultEnvValues, process.env)); Thanks - Ravindra.

ravivit9 avatar Nov 15 '17 15:11 ravivit9

Hey, @ravivit9. I'm not aware about Azure CI, but I guess it's using Windows. So the scripts I shared earlier won't fully work.

Usually you define build steps in CI/CD service, so I don't see a need to create a separate buildsuite script. Also this buildsuite script doesn't give you any value, because npm run postinstall is automatically run after you run (or CI/CD server executes) npm install. Same thing for npm run prebuild - it's run before you run (or CI/CD server executes) npm build.

So taking this into account you need to:

  1. Update the ./scripts/prebuild.js file by removing the first line of code (#!/usr/bin/env node).
  2. Update prebuild script definition in package.json (remove postinstall script, since it won't work on Windows; change prebuild; the buildsuite is actually not required at all).
  "scripts": {
    "build": "ng build --prod",
    "prebuild": "node ./scripts/prebuild.js"
  }
  1. Create tasks in your CI/CD service:
  • Install dependencies (run npm install)
  • Run tests (if you have any)
  • Build the project (run npm run build)
  • Do something with the new build artifacts

Regarding your second question.

const output = ejs.render(environmentTemplate, Object.assign({}, defaultEnvValues, process.env));

It creates a constant (output), that will contain the result of the template rendering (ejs.render(environmentTemplate, ...)), and that template (environmentTemplate) will take it's values from an object, that is generated from default values (detaulfEnvValues) and your current environment variables, that you define in your CI/CD (process.env).

k10der avatar Nov 15 '17 16:11 k10der

Thanks for getting back to me so quickly.

As per step 3, when does "prebuild" task will be run if I don't run. How and where will I invoke prebuild before ng build.

My requirement is I have four different environment.<>.ts files under environments directory environment.dev.ts environment.ci.ts environment.tes.ts environment.uat.ts environment.prod.ts

So the prebuild script will literally read one of the above field based on process.env.CURRENT_ENVIRONMENT value and overwrite all contents of environment.ts file.

package.json

scripts: { "build": "ng build", "prebuild": "node ./server/prebuild.js", "buildsuite": "npm run prebuild && npm run build" }

In Azure CI the command after npm install will be npm run buildsuite to ensure to run prebuild first and then build.

scripts/prebuild.js

`const fs = require('fs'); const env = process.env; const targetEnvFileObj = {}; targetEnvFileObj.dev = 'environment.dev.ts'; targetEnvFileObj.ci = 'environment.ci.ts'; targetEnvFileObj.test = 'environment.test.ts'; targetEnvFileObj.prod = 'environment.prod.ts';

const currentEnvironment = process.env.CURRENT_ENVIRONMENT; // possible values ci /test/uat/prod

function replaceContents(sourceEnvFile, cb) { fs.readFile(sourceEnvFile, (err, contents) => { if (err) return cb(err); fs.writeFile('environment.ts', contents, cb); }); }

replaceContents(targetEnvFileObj[currentEnvironment] , err => { if (err) { // handle errors here throw err; } console.log('done'); }); ` Appreciate your view on this.

ravivit9 avatar Nov 15 '17 16:11 ravivit9

@ravivit9

As per step 3, when does "prebuild" task will be run if I don't run. How and where will I invoke prebuild before ng build.

You can define pre and post scripts for any npm script, so prebuild will be invoked by npm before build script when you run npm run build (https://docs.npmjs.com/misc/scripts#description - the last paragraph in the Description section).

And regarding your question about how to best implement environment changing. In this topic users discuss how to provide custom variable to the resulting file. And it seems you have all the files pre-defined. So you need just to configure a command in CI, that will take environment variable and pass it's value to ng build --prod --environment=$ENVIRONMENT_NAME command or something like this (https://github.com/angular/angular-cli/wiki/build#build-targets-and-environment-files). And I guess your type of question fits better for StackOverflow.

k10der avatar Nov 15 '17 16:11 k10der