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.
The maximum number of jobs that can be used in a matrix strategy is 256.
Example of a matrix strategy
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
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.
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
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)
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
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
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.
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
Example of conditional jobs
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
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
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 }}