I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Eight days left until this first milestone. Hopefully better days are ahead.
Yesterday I suddenly remembered that I still had to create a GitHub Actions to build and deploy the editor of our project DeckDeckGo.
Even though most of the integrations are already automated, this feature is still on my Todo list because I will have to obfuscate some production tokens before being able to properly finish this task.
When I thought about it, I asked my self if I actually had not already solved such feature in another project recently? Guess what, indeed I have đ, but in an Angular prototype. A small project I developed for my self in order to help me find a flat in ZĂźrich a couple of weeks ago (Watamato if interested, check it out).
Thatâs why I am sharing with you today this new tricks.
Concept
Angular, out of the box, let us handle environments variables thanks to the property fileReplacements
of our angular.json
. Per default, most probably, your project contains two files, an environment.ts
and another one for your productive build, environment.prod.ts
.
The idea is the following: In environment.prod.ts
we are going to define keys without any values, allowing us to push these in our public GitHub repo safely. Then, with the help of system variables, set these before build within our GitHub Actions.
Setup Environment.ts
To begin with, letâs setup first our environment.ts
files. Our goal is to obfuscate a token, letâs say for example that we want to hide our Firebase Api key.
Not really related to the solution but letâs say a goodie, we also inject the version
and name
of our application in your configuration. Note that this requires the activation of the compiler options resolveJsonModule
to true
in your tsconfig.json.
Our environment.ts
:
import { name, version } from "../../package.json";
export const environment = {
production: false,
firebase: {
apiKey: "the-key-you-can-expose"
},
name,
version
};
And our environment.prod.ts
which contains 'undefined'
for the hidden value. The reason behind this being a string is the fact that our upcoming parser is going to inject such value if the key is not defined at build time.
export const environment = {
production: true,
firebase: {
apiKey: "undefined"
},
name: "enviro-replace",
version: "0.0.1"
};
Hide Development Variables
In the previous setting, I amend the fact that we are agree to expose our key in our development configuration, but you might also want to hide it. In such case, what I recommend, is extracting the values in a separate local file which you explicitly ignore in your .gitignore
.
For example, letâs say we create a new file firebase.environment.ts
in which we move our configuration and which add to the list of Git ignored files.
export const firebase = {
firebase: {
apiKey: "the-key-you-can-expose"
}
};
Then we can update our environment.ts
as following:
import { firebase } from "./firebase.environment";
import { name, version } from "../../package.json";
export const environment = {
production: false,
...firebase,
name,
version
};
Update Variables Before Build
Our productive environment contains at this point an hidden value 'undefined'
which we have to replace before building our application.
For such purpose we can use the âmagic fileâ described in the article of Riccardo Andreatta đ.
We create a new script ./config.index.ts
. Basically what it does is overwriting our environment.prod.ts
file with new values and notably these we are going to define in your environment or GiHub Actions secret store.
In this parser we notice two interesting things:
- It contains the environment variables too. That means that if you would add a new key to your configuration, you will have to update the script too.
- We are using the environment process
process.env.FIREBASE_API_KEY
to inject a value we would path from our environment or from GitHub Actions to overwrite the environment with the effective key we were looking to hide.
import { writeFile } from "fs";
import { name, version } from "../package.json";
const targetPath = "./src/environments/environment.prod.ts";
const envConfigFile = `export const environment = {
production: true,
firebase: {
apiKey: '${process.env.FIREBASE_API_KEY}'
},
name: '${name}',
version: '${version}'
};
`;
writeFile(targetPath, envConfigFile, "utf8", (err) => {
if (err) {
return console.log(err);
}
});
Finally we can add the execution of the script to our package.json
:
"scripts": {
"config":
"ts-node -O '{\"module\": \"commonjs\"}' ./config.index.ts",
"build": "npm run config && ng build --prod",
}
Testing
We are all set, we can now give it a try. Letâs first run a build without doing anything.
As you can notice, our apiKey
remains equals to 'undefined'
and therefor not valid for our build.
Letâs now try to define an environment variable (export FIREBASE_API_KEY="this is my prod key"
) and run our build again.
Tada, our environment variable has been set and use for our build đ.
At this point you may ask yourself âyes but David, if we do so, then each time we run a build our environment.prod.ts
file is going to be modifiedâ. To which I would answer âyes you are right ⌠but our goal is to automate the build with a GitHub Actions in order to not run productive build locally anymore, therefore the modification is not that a problem for our daily workflow đâ.
GitHub Actions
The very final piece, the automation with GitHub Actions.
I am not going to cover how is it possible to create such script, Julien Renaux covers well the subject in one of his blog post or alternatively you can check out of my Angular related app.yml GitHub actions.
I assume that your script is ready and that you have defined a FIREBASE_API_KEY
in your reposâ secrets.
The related build sequence of your application probably looks like the following:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build
To which we now âonlyâ need to add the following:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build
env:
FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
Thatâs already it. Doing so, GitHub Actions will set the related environment variable for our build and our above script and configuration will take care of the rest.
Summary
GitHub Actions are so handy, there were and are a big asset to my continuous integration workflow.
Stay home, stay safe!
David