Sometimes you want to dynamically generate some jobs in a Github Action. The matrix pattern is a powerful tool for that. But the way you can interact with variables are limited, you can not use environment variables directly to define the job matrix.

Motivation

A use case where this can be relevant is building or deploying services with different configurations, for different tenants etc. For example, imagine you have an application that polls events from different queues. For a proper isolation you want to deploy one polling service for each queue. In different environments (e.g. dev/staging/prod) there exist different queues (different names, properties and number of queues). We can define the job matrix for the list of services (for simplicity we just pass the service names here, in reality we might want to use more complex objects with other information and use more than one matrix dimension):

name: Continuous Delivery

on:
  push:
    branches:
      - main

jobs:
  deploy-service:
    runs-on: ubuntu-latest
    name: "Update service"
    strategy:
      matrix:
        service: [abc-service, def-service, xyz-service]
    steps:
      - name: Deploy service
        run: echo "Deploying service ${{ matrix.service }} ..."
      # Deployment code ...
Job matrix
Job matrix
Example job prints value from matrix
Example job prints value from matrix

But how can we pass different services for different environments?

Github lets us define environments in the repository settings and set environment variables that we can use in the actions. We create an environment “dev” and define a variable SERVICES (with properly escaped strings):

[\"abc-service\", \"def-service\", \"xyz-service\"]
Defining the env var
Defining the env var

We reference the environment: dev and can use the env var in a job:

...
jobs:
  echo-services:
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - name: Echo SERVICES
        id: echo-services
        run: echo "services=${{ vars.SERVICES }}"
Job retrieves and prints value from env var
Job retrieves and prints value from env var

Problem

We cannot use the env var to define the matrix! E.g. trying

...
jobs:
  deploy-service:
    runs-on: ubuntu-latest
    environment: dev
    name: "Update service"
    strategy:
      matrix:
        service: ${{ fromJson(vars.SERVICES) }}
    steps:
      - name: Deploy service
        run: echo "Deploying service ${{ matrix.service }} ..."
      # Deployment code ...

… yields an error:

Error when evaluating 'strategy' for job 'deploy-service'. .github/workflows/deploy.yml (Line: 23, Col: 18): Error from function 'fromJson': empty input, .github/workflows/deploy.yml (Line: 23, Col: 18): Unexpected value ''
Error: can not access env var
Error: can not access env var

This is a Github Action limitation: Env vars are “available to the steps of all jobs in the workflow” (source), but not outside of steps.

Solution

Here is a pattern that I found works well, inspired by this stackoverflow answer.

In the Github yaml file, define a helper job dispatch-deployments that copies the environment variable to the job output. While the matrix strategy can not reference environment variables, it can reference job outputs! We reference the job another job depends on with needs.

name: Continuous Delivery

on:
  push:
    branches:
      - main

jobs:
  dispatch-deployments:
    runs-on: ubuntu-latest
    environment: dev
    outputs:
      services: ${{ steps.set-services.outputs.services }}
    steps:
      - name: Extract SERVICES
        id: set-services
        run: echo "services=${{ vars.SERVICES }}" >> $GITHUB_OUTPUT

  deploy-service:
    runs-on: ubuntu-latest
    environment: dev
    name: "Update service"
    needs: dispatch-deployments
    strategy:
      matrix:
        service: ${{ fromJson(needs.dispatch-deployments.outputs.services) }}
    steps:
      - name: Deploy service
        run: echo "Deploying service ${{ matrix.service }} ..."
      # Deployment code ...

Voilà, the jobs are spawned based on the values in the environment variable:

Dispatch helper job and job matrix
Dispatch helper job and job matrix

Thinking beyond env vars, the dispatch helper job could be expanded to perform service discovery and more.