angular-cli
angular-cli copied to clipboard
Request: Method to pass environment variables during build vs file.
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
}
};
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
}
};
@hansl we were just discussing this, thought you might have something to add
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.
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 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, 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.
Injecting environment variables during build seems essential. Any ETA for supporting this in the ng cli?
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?
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.
very needed for dockerizing the apps indeed
@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.
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.
+1
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
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.
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
.
How about the webpack.DefinePlugin
mentioned in this answer? How do we integrate it into Angular CLI?
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
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
We wound up sing this Visual Studio Team Services extension in our release. Very happy with it.
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.
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.
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
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.
This CLI approach generates a file that doesn't messes up with the version control and keeps them on environment.ts
are there any news? it would be nice to have this feature, I guess there are plenty of user's cases
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.
@k10der, looking at your guidance. Could you please reply to the following if possible?
-
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" }
-
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.
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:
- Update the ./scripts/prebuild.js file by removing the first line of code (
#!/usr/bin/env node
). - Update
prebuild
script definition in package.json (removepostinstall
script, since it won't work on Windows; changeprebuild
; thebuildsuite
is actually not required at all).
"scripts": {
"build": "ng build --prod",
"prebuild": "node ./scripts/prebuild.js"
}
- 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
).
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.<
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
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.