Tokenization / Replacing Environment Tokens in GitHub Actions
Replacing environment-specific configuration at deployment time
Overview
I’ve been thinking about the “build once, deploy many” concept lately, and how to accomplish this in GitHub Actions. We definitely don’t want to be creating a “dev” build, testing it, then creating a new “prod” build and deploying that. They are two separate binaries; there is no way to verify that what we tested in dev is what is shipped to prod. We want to create a single build and deploy that to multiple environments.
In Azure DevOps, I primarily used Colin’s ALM Corner Build & Release Tools Replace Tokens task or the standalone Replace Tokens extension and task to find/replace environment-specific files during a deployment. I was exploring how to do this in GitHub Actions, and I think I found a way to mostly recreate this pattern.
I’m using the lindluni/actions-variable-groups and cschleiden/replace-tokens actions to accomplish this. The actions-variable-groups
action allows you to store the values of the (non-secret) variables to a file (variables-as-code!) in the repository, and the replace-tokens
action does the find-and-replacing.
The Workflow
Here is the workflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dev:
runs-on: ubuntu-latest
environment: dev
needs: build
steps:
- uses: actions/checkout@v3
- name: Inject Variables
uses: lindluni/actions-variable-groups@v2
with:
groups: |
.github/variables/dev.yml
- uses: cschleiden/replace-tokens@v1
with:
tokenPrefix: '#{'
tokenSuffix: '}#'
files: '["**/*_tokenized.json"]'
- name: replace app settings
run: |
rm appsettings.json
mv appsettings_tokenized.json appsettings.json
There is an alternative to the actions-variable-groups
action where you instead use configuration variables defined on a repository’s environment. I prefer the actions-variable-groups
action for a few reasons:
- It allows you to store the variables in the repository as code, so it’s easier to diff/review changes
- Non-admins can view/modify the variables
- You have to map in each environment variable to the workflow, which is a bit more cumbersome than just specifying the file to inject
If you wanted to see how it would look using configuration variables, it would look something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dev:
runs-on: ubuntu-latest
environment: dev
needs: build
steps:
- uses: actions/checkout@v3
- uses: cschleiden/replace-tokens@v1
env:
APIURL: ${{ vars.APIURL }}
VAR2: ${{ vars.VAR2 }}
VAR3: ${{ vars.VAR3 }}
with:
tokenPrefix: '#{'
tokenSuffix: '}#'
files: '["**/*_tokenized.json"]'
- name: replace app settings
run: |
rm appsettings.json
mv appsettings_tokenized.json appsettings.json
Secrets?
Secrets should not be stored in the repository (obviously). Create these as secrets and map them in as environment variables, like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
dev:
runs-on: ubuntu-latest
environment: dev
needs: build
steps:
- uses: actions/checkout@v3
- name: Inject Variables
uses: lindluni/actions-variable-groups@v2
with:
groups: |
.github/variables/dev.yml
- uses: cschleiden/replace-tokens@v1
env:
APIKEY: ${{ secrets.APIKEY }}
with:
tokenPrefix: '#{'
tokenSuffix: '}#'
files: '["**/*_tokenized.json"]'
- name: replace app settings
run: |
rm appsettings.json
mv appsettings_tokenized.json appsettings.json
Note that if you are injecting a secret into a raw file, ensure that wherever this file is ending up is not somewhere where it can be accessed by the general viewing public. If using self-hosted runners, it is a good idea to have a step that always runs to delete the tokenized file.
iOS?
You can even do this with compiled iOS IPA files. An IPA is just a fancy zip, so we can extract it, replace values, re-zip as a .ipa
file, and re-sign it.
I’m going to briefly outline this here; I don’t have an app to test with at the moment, so some things might need to be tweaked slightly (such as unzip/zip folder paths):
- Here is a sample gist as to not bog down this post too much
Note: You can see how I did this with a real-world app in Azure DevOps in a deployment pipeline here.
Summary
This is essentially the “GitHub” flavor of Colin’s Config Per Environment vs Tokenization in Release Management and End to End Walkthrough: Deploying Web Applications Using Team Build and Release Management posts from many years ago. Hopefully this helps give you an idea of how you can accomplish this in GitHub Actions.