Dynamic Secrets for GitHub Actions

David Ricardo Dager Mora
Globant
Published in
6 min readNov 6, 2023
GitHub Actions Logo

When working with GitHub Actions, sometimes we want to run the same steps in our workflows but change the secrets and variables used based on the target branch, environments, or application that we are using.

One reasonable approach is to copy and paste your whole action, create a new workflow with a different name, and reference those changes. Unfortunately, this adds an operational burden to our daily work because we now have to maintain two (or even more) workflows that are basically the same. This is the least D.R.Y. (Don’t Repeat Yourself) possible, and you should try to avoid this scenario.

Luckily, GitHub Action allows us to keep using the same workflow but dynamically change which parameters to use with some help of built-in functions and a few configurations.

In this post, I’ll teach you two different ways about how to dynamically change your secrets/variables with functional code examples and how to set the requirements for them to work.

Let’s dive in!

Definitions

Let’s start with some concepts that will help set a common ground to understand the benefits and capabilities of GitHub Actions.

Workflows: Workflows are Yaml files that define all the steps and configurations of our CI/CD process. You can set events to trigger them, do it manually, or even do something like scheduling the execution. If you would like to know more, you can see the official documentation.

Secrets: GitHub Secrets is a secure way of managing sensible information within your workflows. You can set SSH keys, username/passwords, secret strings, and almost any sensible information securely, and you will have the confidence that these secrets are ciphered and no one can see their content, but you can safely use them inside your workflows without exposing them because they are masked in the output. This adds a security layer to your CI/CD pipelines which is a good practice; managing sensitive information securely is always a thing to keep in mind in all your work.

In order to access a secret called MY_SECRET , you’ll have to write something like this in your workflow:

echo "${{ secrets.MY_SECRET }}"

Variables: Variables work like secrets, but their content isn’t masked. You can check their value, change it, and use them every time you need to. Variables aren’t ciphered, so if you need to set sensible content, please use secrets. Using variables in your workflow is similar to using secrets; here’s an example with a variable called MY_VAR:

echo "${{ vars.MY_VAR }}"

Environments: GitHub Environments is a logic group of resources, branches, secrets, and variables inside your repository. You can set protection for those environments and control how and when they are used. This helps your team to deploy into your desired environments securely and have a trace of all the changes. If you want to use an environment, you have to create it first in your repository’s configuration (I will show you how to do it later) and call it in your workflow with the environment keyword in GitHub Actions like this:

environment: MY_ENV_NAME

# Another way to use it is as a map, the URL parameter is optional

environment:
name: MY_ENV_NAME
url: MY_ENV_URL

Hierarchy: Both secrets and variables work on three different levels; Organization, Repository, and Environment. Each of these levels carries a certain hierarchy or preference, and the order from the least preference goes like this:

  1. Organization
  2. Repository
  3. Environment

This order means that if you have the same secret/variable on more than one level, GitHub will use the one with the highest preference. As an example, imagine that we set up the same variable MY_VAR in all levels, and each one has a different value:

MY_VAR: Foo — Org level
MY_VAR: Bar — Repo level
MY_VAR: Baz — Env level

If we print the variable value in a workflow, we will get the following:

jobs:
example_job:
environment: my_env
steps:
- run: echo "Value: ${{ var.MY_VAR }}"

#OUTPUT
Value: Baz

As we can see, the environment level has the highest preference.

Inputs: GitHub Actions allows us to have input on our manual workflows, and we can set several input types. Currently, there are only 4 possible types; string, boolean, choice, and environment. In our case, we will use the environment type, which is a special input that links all the environments defined in your repository. If you would like to know more about input types, you can read the official documentation.

How To Set Environments, Secrets, and Vars

All of the above can be configured in each repository’s configuration. We can set them with just a few clicks:

Repository configuration. Author: David Dager

If you are inside a GitHub Organization, you will see another section at the end for that level. If there are any organization-level secrets or variables, they would appear there. From this page, you can set secrets, variables, and environments in all three levels, but to set organization-level configurations, you will need escalated permissions.

Code Examples

Now, with the previous concepts and configurations, we can start with some working examples. The first one is a manual workflow that uses an option list generated by any environment that you configure in your repository and updates automatically:

name: 'Manual Workflow'
on:
workflow_dispatch:
inputs:
environment:
type: environment
description: 'Choose your environment'
jobs:
manual_job:
name: 'My Manual Job'
runs-on: ubuntu-latest
environment:
name: ${{ inputs.environment }}
steps:
- name: 'Print env name'
shell: bash
run: echo '${{ inputs.environment }}'
- name: 'Use var and secret from env'
shell: bash
run: echo '${{ vars.USERNAME }}:${{ secrets.PASSWORD}}'

The previous workflow will print the env name and username and password defined in either the org, repo, or env level.

Manual workflow with prod env as target. Author: David Dager

But what if you want your workflow to run automatically on any push event and to use the corresponding environment? Here’s an example of a workflow that doesn’t need a manual input:

jobs:
get_target_env:
name: 'Get target env'
runs-on: ubuntu-latest
outputs:
target_env: ${{ steps.set_result.outputs.target_env }}
steps:
- name: 'Set env based on branch'
id: set_results
shell: bash
run: |
echo "Branch: ${{ github.ref_name }}"
echo "target_env=${{ github.ref_name }}" >> $GITHUB_OUTPUT

automated_job:
name: 'My Automated Job'
runs-on: ubuntu-latest
needs: get_target_env
environment:
name: ${{ needs.get_target_env.outputs.target_env }}
steps:
- name: 'Print env name'
shell: bash
run: echo 'Using ${{ github.ref_name }}'
- name: 'Use var and secret from env'
shell: bash
run: echo '${{ vars.USERNAME }}:${{ secrets.PASSWORD}}'

In this case, we use another job to set the environment as an output and use it in our job automatically. For this to work, your branch name should be equal to your environment name. You can modify the first step if you need to:

Conditional Jobs in GitHub Actions. Author: David Dager

In the next image, we can see that the secrets are being masked, the value of the username changes based on the environment defined, and if the variable/secret is defined in that level if it doesn’t exist, it will try to use the content in the level one hierarchy above and so on. In this case, we have an environment called main matching the branch we were using.

Automated output of second job. Author: David Dager

Conclusions

With the hierarchy levels, we can have the same variable/secret defined on different environments, and our workflow code would remain the same; just changing the target environment allows us enough flexibility to use the same workflow for different environments. GitHub Actions comes with several tools and functions that allow us to use best practices and to keep our code D.R.Y., but we need to figure out how to combine and adapt them to our specific needs.

Resources

--

--