Post

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:

  1. It allows you to store the variables in the repository as code, so it’s easier to diff/review changes
  2. Non-admins can view/modify the variables
  3. 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.

This post is licensed under CC BY 4.0 by the author.