GitHub Actions Deployment to Different Environments

tack,

Learn how to deploy to different target environments using GitHub Actions.

Intro

If you are using more than one environment for your project, you may be wondering how to deploy to each of the environments (staging/production) using GitHub Actions (GHA). We will show you a simple setup using self-hosted GHA runners which should get you going in no time.

gha-workflow

Fig.: GitHub Actions manual trigger with an environment selector.

GitHub Actions Runners

You will need to have one GitHub Runner installed per environment (host machine). This way, you can target a specific runner inside of your workflow to deploy to the runner's host machine.

To learn how to install self-hosted GHA runners, see the official documentation.

Workflow File

Adding Environment Selector

In your workflow file, add a workflow_dispatch trigger, which will enable you to run this workflow manually (see Fig. 1). Afterwards, you can specify the inputs using the inputs key.

We specify an environment input, give it a display name, mark it as required and turn it into a select by including type: choice.

.github/workflows/deployment.yml
name: deployment

on:
  workflow_dispatch:
    inputs:
      environment:
            description: 'Environment'
            required: true
            type: choice
            options:
              - 'TEST'
              - 'PROD'
            default: 'TEST'

Now we will be able to select whether we wish to deploy to the TEST or PROD environment.

Adding Build & Deployment Steps

Now we can add the build & deployment jobs. This is a simplified example building and deploying our project on the same machine using a Makefile to automate the process further.

The file will then look like this:

.github/workflows/deployment.yml
name: deployment

on:
  workflow_dispatch:
    inputs:
      environment:
            description: 'Environment'
            required: true
            type: choice
            options:
              - 'UAT'
              - 'PROD'
            default: 'UAT'

jobs:

  build:
    runs-on: ${{ vars[format('{0}_RUNNER_LABEL', inputs.environment)] }}
    steps:
    - uses: actions/checkout@v4
    - name: Build Project
      run: make build

  deploy:
    runs-on: ${{ vars[format('{0}_RUNNER_LABEL', inputs.environment)] }}
    needs: [ build ]
    steps:
    - uses: actions/checkout@v4
    - name: Start Docker Container
      run: make run

Setting Up GHA Variables

Now we store the names of each of our runners (test and production) in GHA Variables so that they can be used in the workflow. We will create 2 variables: TEST_RUNNER_LABEL and PROD_RUNNER_LABEL.

To create a new GHA variable, go to your repository > Settings > Security > Secrets and variables > Actions > Variables > New repository variable:

gha-variables

Fig.: Creating a new GHA variable.

Determining Which Runner to Use

As you can see in our workflow above, we are using the variables we have created to determine which runner should be used. When the workflow dispatches a job, the runner which should pick it up is determined using the runs-on key.

We can use the format() expression to determine whether TEST_RUNNER_LABEL or PROD_RUNNER_LABEL should be used to fill in the name of the runner to pick up the job. We do this by filling in the value of the selected environment from the inputs (inputs.environment). The value from the input (TEST or PROD) is concatenated with _RUNNER_LABEL which creates the final name of the GHA variable to load the name of the runner from.

More on GHA expressions can be found here.

Note: It would be safer to store these variables in GHA secrets, however, for some reason, it seems that GHA do not support using secrets for the runs-on value.

gha-runs-on-secrets-error

Fig.: Using secrets for the runs-on value leads to an error.

Conclusion

Now you should be able to manually dispatch a workflow and select the target environment/runner to execute it. This is a basic but effective setup, which can be improved for example by using centralized GHA runners which can deploy to a selected host using custom scripts. That way you can have a "fleet" of runners, each of which can deploy to any other host machine in your setup.