Skip to main content

How to Structure/Manage Jobs in the Workflow

Parallelization of Jobs

By default all jobs in a workflow run in parallel. You can control the order of jobs by specifying dependencies.

Matrices

A matrix strategy is a great way to run the same job multiple times with different inputs. This is useful if you want to run your tests on multiple versions of a language, or if you want to run your tests on multiple operating systems.

note

The maximum number of jobs that can be used in a matrix strategy is 256.

Example of a matrix strategy
YML
jobs:
  example_matrix:
    strategy:
      matrix:
        version: [10, 12, 14]
        os: [ubuntu-latest, windows-latest]

Ordering Jobs

You can define the order of the jobs using the needs keyword. This is useful if you want to run a job that depends on the output of another job.

Example of linking jobs
YML
jobs:
  job1:
  job2:
    needs: job1
  job3:
    needs: [job1, job2]
    steps:
      - run: echo ${{ needs.job1.outputs.myOutput }}

Job Timeouts

You can define a timeout for a job, and if the job takes longer than the timeout to run, the job will be cancelled.

The default timeout for a job is 6 hours or 360 minutes.

note

The GITHUB_TOKEN expires after the job finishes or 24 hours. This is a limiting factor for SHRs.

Running Jobs in Containers / Service Containers

Running in a container will not always be faster than running on a GHR. The time it takes to download the container image and start the container can be longer than the time it takes to start a job on a GHR.

Containers

Use jobs.<job_id>.container to create a container to run any steps in a job that don't already specify a container.

Example of running a job within a container
YML
name: CI
on:
  push:
    branches: [ main ]
jobs:
  container-test-job:
    runs-on: ubuntu-latest
    container: 
      image: node:18
      env:
        NODE_ENV: development
      ports:
        - 80
      volumes:
        - my_docker_volume:/volume_mount
      options: --cpus 1
    steps:
      - name: Check for dockerenv file
        run: (ls /.dockerenv && echo Found dockerenv) || (echo No dockerenv)
tip

You can omit the image keyword and use the short version container: node:18 if you don't need to specify parameters.

Service Containers

Service containers let you run a container parallel to your job. This can be helpful if your job needs to talk to a database, for example.

Example of using a service container
YML
name: Redis container example
on: push

jobs:
  # Label of the container job
  container-job:
    # Containers must run in Linux based operating systems
    runs-on: ubuntu-latest
    # Docker Hub image that `container-job` executes in
    container: node:16-bullseye

    # Service containers to run with `container-job`
    services:
      # Label used to access the service container
      redis:
        # Docker Hub image
        image: redis

Authenticating with a Container Registry

Sometimes you will need to authenticate with a container registry to pull an image. You can use the credentials keyword to do this.

Example of authenticating with a container registry
YML
jobs:
  build:
    services:
      redis:
        # Docker Hub image
        image: redis
        ports:
          - 6379:6379
        credentials:
          username: ${{ secrets.dockerhub_username }}
          password: ${{ secrets.dockerhub_password }}
      db:
        # Private registry image
        image:  ghcr.io/octocat/testdb:latest
        credentials:
          username: ${{ github.repository_owner }}
          password: ${{ secrets.ghcr_password }}

Environments: Controls How/When a Job is Run Based on Protection Rules Set, Limits Branches, Scopes Secrets

You can create environments and secure those environments with deployment protection rules. A job that references an environment must follow any protection rules for the environment before running or accessing the environment's secrets.

Scoping secrets to an environment is very powerful because of the controls it gives you. You can limit which branches can access the secrets, and you can leverage the environment protection rules to control when a job can access the secrets.

Environment Protection Rules

Deployment protection rules require specific conditions to pass before a job referencing the environment can proceed.

Required Reviewers

You can require that specific individuals or teams review a pull request before a job can proceed.

Wait timer

You can delay a job for a specific amount of time before it can proceed.

Branch restrictions

You can restrict which branches or tags can access the environment.

Admin bypass

You can allow or disallow repository administrators to bypass the protection rules.

Custom deployment protection rules

You can create custom deployment protection rules to gate deployments with third-party services.

Conditional Jobs/Steps

You can use the if keyword to conditionally run a job or step.

YML
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
Example of conditional jobs
YML
name: example-workflow
on: [push]
jobs:
  production-deploy:
    if: github.repository == 'octo-org/octo-repo-prod'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '14'
      - run: npm install -g bats

Permissions for Jobs

There is a default token called GITHUB_TOKEN which by default has the permissions defined in your repositories Actions settings.

It's a good idea to limit permissions as much as possible by being explicit.

Example of limiting permissions
YML
jobs:
  stale:
    runs-on: ubuntu-latest

    permissions:
      issues: write
      pull-requests: write

    steps:
      - uses: actions/stale@v5

GitHub Apps

Using actions/create-github-app-token you can get a token for a GitHub App. This is better than using a PAT because you get more control and you don't need to consume a license.

Example of using a GitHub App token
YML
name: Run tests on staging
on:
  push:
    branches:
      - main

jobs:
  hello-world:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/create-github-app-token@v1
        id: app-token
        with:
          app-id: ${{ vars.APP_ID }}
          private-key: ${{ secrets.PRIVATE_KEY }}
      - uses: ./actions/staging-tests
        with:
          token: ${{ steps.app-token.outputs.token }}