Skip to content

Repository files navigation

GitHub Workflow Guide

Warning

This is an advanced guide and assumes you already know the basics of GitHub Workflows. Think of this more like an advanced cheat sheet. I went through the documentation and captured any notes that I felt were important, and organized them into the README file you see here. If you are new to GitHub Workflows, then I would suggest going through the GitHub docs first.

Important

This is a live document. Some of the sections are still a work in progress. I will be continually updating it over time.

Tip

AI was not used in the creation of this guide.


Table of Contents


Workflow Settings

# name of the workflow as shown in the GitHub UIname: 'string'# optional, default is the path & name of the yaml file# name to use for each run of the workflowrun-name: 'string'# optional, default is specific to how your workflow was triggered
  • run-name can use expressions, and can reference the contexts of github and inputs

Triggers

Documentation - Triggering a workflow

Documentation - Events that trigger workflows

# option 1: single event with no optionson: push# option 2: multiple events with no optionson:
- push
- fork# option 3: multiple events with optionson:
push:
branches:
- blahblahissues:
types:
- openedschedule:
- cron: '30 5,17 * * *'timezone: 'America/New_York'# option 4: manual trigger where you can specify a max of 25 inputson:
workflow_dispatch:
inputs:
someInputName:
description:
required: true | falsedefault: 'defaultValue'type: boolean | number | string | choice | environmentoptions: # only when type: choice
- option1
- option2someOtherInput:
description:
required: truetype: string# option 5: if this workflow is used as a reusable workflow (job-level template)on:
workflow_call:
inputs: # input parametersinputName1:
description:
required: true | falsetype: boolean | number | string # requireddefault: something # if omitted, boolean = false, number = 0, string = ""secrets: # input secretssecretName1:
description:
required: true | falseoutputs: # output valuesoutputName1:
description:
value:
  • If multiple events are specified, only 1 event needs to occur to trigger the workflow
  • If multiple events happen at the same time, then multiple runs of the workflow will trigger

Permissions for the GITHUB_TOKEN

Documentation - Modifying the permissions for the GITHUB_TOKEN

Documentation - Workflow Syntax - Permissions

  • Use this if you want to modify the default permissions granted to the GITHUB_TOKEN
  • Optional, the default can be set in the repo settings (by an admin) to either a permissive preset or a restricted preset
  • As a good security practice, you should grant the GITHUB_TOKEN the least required access
  • When the permissions key is used, all unspecified permissions are set to none, with the exception of the metadata scope, which always gets read access.
  • Supported scopes for permissions: workflow-level, job-level
# option 1: full syntaxpermissions:
actions: read | write | noneartifact-metadata: read | write | noneattestations: read | write | nonechecks: read | write | nonecontents: read | write | nonedeployments: read | write | nonediscussions: read | write | noneid-token: read | write | noneissues: read | write | nonemodels: read | nonepackages: read | write | nonepages: read | write | nonepull-requests: read | write | nonesecurity-events: read | write | nonestatuses: read | write | none# option 2: shortcut syntax to provide read or write access for all scopespermissions: read-all | write-all# option 3: shortcut syntax to disable permissions to all scopespermissions: {}

More Info:

  • When you enable GitHub Actions, then a GitHub App will be installed on your repo
    • The GITHUB_TOKEN secret is used to hold an installation access token for that app
  • Before each job begins, GitHub fetches an unique installation access token for the job
    • The token expires when a job finishes or after a maximum of 24 hours.
    • The token can authenticate on behalf of the GitHub App installed on your repo
    • The token's permissions are limited to the repo that contains your workflow
  • My blog post all about GitHub Apps and the GITHUB_TOKEN

Default Settings

Documentation - Setting a default shell and working directory

  • Creates a map of default settings that will be inherited downstream
  • Supported scopes for defaults: workflow-level, job-level
    • The most specific defaults wins
defaults:
run:
shell: bashworking-directory: scripts

Concurrency Settings

Documentation - Control the concurrency of workflows and jobs

  • Ensures that only one Workflow (or only one Job) from the specified concurrency group can run at a time
  • Optional
  • Supported scopes for concurrency: workflow-level, job-level
# option 1: specify a concurrency group with default settingsconcurrency: groupName# option 2: specify a concurrency group with custom settingsconcurrency:
group: groupNamecancel-in-progress: true # this will cancel any currently running workflows/jobs first
  • groupName can be any string or expression (but limited to the github context only)
  • Default behavior: If a Workflow/Job in the concurrency group is currently running, then any new Workflows/Jobs will be placed into a pending state and will wait for the original Workflow/Job to finish. Only the most recent Workflow/Job is kept in the pending state, and all others will be cancelled.

Variables

Documentation - Store information in variables

Environment Variables

  • Cannot reference other variables in the same map
  • Supported scopes for env: workflow-level, job-level, step-level
    • The most specific variable wins
# defining environment variables (workflow-level)env:
KEY1: valueKEY2: value# defining environment variables (job-level)jobs:
someJobId:
env:
KEY1: valueKEY2: value# defining environment variables (step-level)jobs:
someJobId:
steps:
- name: someStepNameenv:
KEY1: valueKEY2: value# use an environment variable in the workflow yaml:${{ env.KEY }}# use an environment variable inside of a script by just accessing the shell variable as usual:linux: $KEYwindows powershell: $env:KEYwindows cmd: %KEY%# there are many default environment variables (see link above)# most also have a matching value in the github context so you can use them in the workflow yaml$GITHUB_REF and ${{ github.ref }}

Configuration Variables

  • Defined in the GitHub UI
  • Can be shared by multiple Workflows
  • Supported scopes for vars: organization-level, repo-level, repo environment-level
    • The most specific variable wins
  • Configuration variable naming restrictions:
    • Can only contain alphanumeric characters or underscores
    • Must not start with the GITHUB_ prefix or a number
    • Case insensitive
    • Must be unique at the level they are created at
  • Configuration variable limits: 1,000 per Organization, 500 per Repo, 100 per Repo Environment
# use a configuration variable in the workflow yaml:${{ vars.KEY }}# use a configuration variable inside of a script by just accessing the shell variable as usual:linux: $KEYwindows powershell: $env:KEYwindows cmd: %KEY%

Secrets

Documentation - Using secrets in GitHub Actions

  • Defined in the GitHub UI
  • Can be shared by multiple Workflows
  • Supported scopes for secrets: organization-level, repo-level, repo environment-level
    • The most specific variable wins
  • Secrets naming restrictions:
    • Can only contain alphanumeric characters or underscores
    • Must not start with the GITHUB_ prefix or a number
    • Case insensitive
    • Must be unique at the level they are created at
  • Secrets limits: 1,000 per Organization, 100 per Repo, 100 per Repo Environment
  • Avoid using structured data (like JSON) as the value of your Secret. This helps to ensure that GitHub can properly redact your Secret in logs.
  • Secrets cannot be directly referenced in if: conditionals
    • Instead, consider setting secrets as Job-level environment variables, then referencing the environment variables to conditionally run Steps in the Job
# Actions can't directly use secrets that are defined via the GitHub UI# However, you can use the secret as an input or environment variablesteps:
- name: Hello world actionenv: # Set the secret as an environment variableSOME_VAR: ${{ secrets.Key }uses: action/something@v1with: # Set the secret as a value to an inputsomeInput: ${{ secrets.Key }}

Jobs / Defining the work

Normal Jobs:

Documentation - Using jobs in a workflow

jobs:
symbolicJobName: # must be unique, start with a letter or underscore, and only contain letters, numbers, dashes, and underscoresname: 'string'# friendly name that is shown in the GitHub UIruns-on: windows-latest | ubuntu-slim | ubuntu-latest | macos-latest | self-hosted # specifies the Agent to run onneeds: # Job dependenciesif: # Job conditions, ${{ ... }} can optionally be used to enclose your conditioncontinue-on-error: true # allows the Workflow to pass if this Job failstimeout-minutes: 10# max time a Job can run before being cancelled. optional, default is 360permissions: # job-level GITHUB_TOKEN permissionsdefaults: # job-level defaultsconcurrency: # job-level concurrency groupenv: # job-level variablesKEY: valueenvironment: # see more belowcontainer: # see more belowsnapshot: # see more belowservices: # see more belowstrategy: # see more belowoutputs: # see more below# list the Steps of this Jobsteps:
# Use a GitHub Action
- id: 'symbolicStepName'# optionalname: 'string'# optional. friendly name that is shown in the GitHub UIif: # Step conditions, ${{ ... }} can optionally be used to enclose your conditioncontinue-on-error: true # allows the Job to pass if this Step failstimeout-minutes: 10# max time to run the Step before killing the processenv: # Step-level variablesKEY: value# step concurrencybackground: # see more belowwait: # see more belowwait-all: # see more belowcancel: # see more below# option 1: use a public actionuses: actions/checkout@v3 # owner/repo@ref, or owner/repo/folder@ref, where ref can be a branch, tag, or SHA# option 2: use an action file from a checked out repouses: ./.github/actions/someFolder # make sure to checkout the repo first, no ref is supported as it uses the ref that you checked out# option 3: use an action from a public container image (only on Linux runners)# there is currently no way to authenticate to the specified registry, so be careful of rate limits. also, that means private registries are not supporteduses: docker://alpine:3.8 # from Docker Hubuses: docker://ghcr.io/owner/image # from GitHub Packages Container Registryuses: docker://gcr.io/cloud-builders/gradle # from Google Container Registry# parameters to pass to the action, must match what is defined in the actionwith:
param1: value1param2: value2# when using an action from a public container image (option 3)args: 'something'# this overwrites the CMD instruction in your Dockerfileentrypoint: 'something'# this overwrite the ENTRYPOINT instruction in your Dockerfile# Run a group of steps concurrently, see more below
- parallel: # see more below
- name: step1run: some command
- name: step2run: some command
- name: step3run: some command# Run a single-line Script
- name: something2run: single-line commandshell: bash | pwsh | python | sh | cmd | powershellworking-directory: ./temp# Run a multi-line Script
- name: something3run: | multi-line command

Step Concurrency

GitHub Changelog Announcement

  • Allows you to run multiple steps in parallel, with various options:
jobs:
symbolicJobName:
steps:
# use the background keyword to run a step asynchronously# the job will immediately continue to the next step without waiting for this one to finish
- name: someStepNameid: someStepIDbackground: true# use the wait keyword to pause the job until the given background step(s) finish
- name: anotherStepNamewait: someBackgroundStepID # option 1: wait on a single background stepwait: # option 2: wait on multiple background steps
- someBackgroundStepID
- someBackgroundStepID# use the wait-all keyword to pause the job until ALL background steps finish
- name: yetAnotherStepNamewait-all: # this keyword takes no arguments# use the cancel keyword to gracefully stop a single background step
- name: oneMoreStepNamecancel: someBackgroundStepID# use the parallel keyword as a convenient shorthand to group multiple background steps together# all steps will run as background steps# the job will automatically wait for all steps in the group to finish before moving on
- parallel:
- name: backgroundStep1run: command1
- name: backgroundStep2run: command2
- name: backgroundStep3run: command3
  • A maximum of 10 background steps can run concurrently in a single job. Additional background steps will be queued up.
  • The background keyword works on steps that use the run or uses keywords.
  • Outputs from a background step are only available once the matching wait step completes.
  • If a background step fails, then the matching wait or wait-all steps fail as well.
  • The cancel keyword will first try a SIGTERM termination signal, and if the step does not exit within a short grace period then it will send a SIGKILL termination signal.

Job.Environment

Documentation - Managing environments for deployment

  • Specifies a GitHub environment to deploy to
jobs:
symbolicJobName:
# option 1 - specify just an environment nameenvironment: envName# option 2 - specify environment name and urlenvironment:
name: envNameurl: someUrl # optionaldeployment: true # optional, default: true. setting to false will not create a deployment object, not compatible with custom deployment protection rules
  • envName can be a string or any expression (except for the secrets context)

Job.Container

Documentation - Running jobs in a container

  • Defines a container that will run all Steps in this Job
jobs:
symbolicJobName:
# option 1 - shortcut syntax specifying just the imagecontainer: node:14.16# option 2 - full syntaxcontainer:
image: node:14.16credentials: # used to login to the container registryusername:
password:
env: # specify environment variables inside the containerKEY: valueports: # array of ports to expose on the container
- 8080:80# maps port 8080 on the docker host to port 80 on the containervolumes: # array of volumes for the container to use, you can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host
- source:destinationPathoptions: --cpus 1 # specifies additional options for the docker create command, --network is not supported
  • Optional, if omitted the Job will run directly on the Agent and not inside a Container
  • Only for Steps that don't already use their own Container
  • Only supported on Microsoft-hosted Ubuntu runners, or self-hosted Linux runners
  • run Steps inside of a Container will default to the sh shell, but you can override with jobid.defaults.run or step.shell

Job.Snapshot

Documentation - Using custom images

  • For creating custom images that you can use with GitHub-hosted larger runners
  • Lets you preinstall tools, dependencies, and configurations into your runner image
  • Each job that includes the snapshot keyword creates a separate image
  • Each successful run of a job that includes the snapshot keyword creates a new version of that image
jobs:
symbolicJobName:
# option 1 - string syntax# just specify an image name, this either creates a new image (1.0.0) or adds a new (minor) version to the existing image# can not specify a version number with this syntaxsnapshot: customImageName# option 2 - mapping syntax# lets you specify a version number, only major & minor versions are supported, patch version are not supportedsnapshot:
image-name: customImageNameversion: 2.*

Job.Services

Documentation - Communicating with Docker service containers

  • Defines service container(s) that are used by your Job
jobs:
symbolicJobName:
services:
symbolicServiceName: # label used to access the service containerimage: nginxcredentials: # used to login to the container registryusername:
password:
env: # specify environment variables inside the service containerKEY: valueports: # an array of ports to expose on the service container
- 80volumes: # array of volumes for the container to use, you can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host
- source:destinationPathoptions: --cpus 1 # specifies additional options for the docker create command, --network is not supported
  • Optional
  • Only supported on Microsoft-hosted Ubuntu runners, or self-hosted Linux runners
  • Not supported inside a composite action

Job.Strategy

Documentation - Running variations of jobs in a workflow

  • Use variables to make one Job run multiple different times
jobs:
symbolicJobName:
strategy:
fail-fast: boolean # optional, default is truemax-parallel: 5# max number of matrix Jobs to run in parallel. optional, default is to run all Jobs in parallel (if enough runners are available)matrix: # the variables that will define the different permutationsKEY1: [valueA, valueB]KEY2: [valueX, valueY, valueZ]include: # an extra list of objects to includeexclude: # an extra list of objects to exclude
  • Optional
  • A different Job will run for each combination of KEYs, in this example that would be 6 different Jobs
  • There is a max of 256 Jobs
  • This will create a matrix context which lets you use matrix.KEY1 and matrix.KEY2 to reference the current iteration
  • exclude is processed first before include, this allows you to add back combinations that were previously excluded
  • When fail-fast is set to true, if any job in the matrix fails, then all in-progress and queued jobs in the matrix will be cancelled

Job.Outputs

Documentation - Passing information between jobs

  • Specify outputs of this Job
jobs:
symbolicJobName:
outputs: # map of outputs for this jobkey: valuekey: value
  • These outputs are available to all downstream Jobs that depend on this Job
  • Max of 1 MB per Output, and 50 MB total per Workflow
  • Any expressions in an Output are evaluated at the end of a Job
  • Any secrets in an Output are redacted and not sent to GitHub Actions

Jobs that call a reusable workflow (job-level template):

Documentation - Reuse Workflows

Documentation - Reusing workflow configurations

  • Only the following parameters are supported in such a Job
jobs:
symbolicJobName: # must be unique, start with a letter or underscore, and only contain letters, numbers, dashes, and underscoresname: 'string'# friendly name that is shown in the GitHub UIneeds: # Job dependenciesif: # Job conditions, ${{ ... }} can optionally be used to enclose your conditionpermissions: # job-level GITHUB_TOKEN permissionsconcurrency: # job-level concurrency groupstrategy: # define a matrix for parallel jobs# option 1: a reusable workflow from another repo (public or private)uses: org/repo/.github/workflows/file.yaml@ref # where ref can be a branch, tag, or SHA# option 2: a reusable workflow file from the same repouses: ./.github/workflows/file.yaml # no ref is supported, it uses the same ref that triggered the parent workflow# parameters to pass to the template, must match what is defined in the templatewith:
param1: value1param2: value2secrets: # secrets to pass to the template, must match what is defined in the templateparam1: ${{ secrets.someSecret }}param2: ${{ secrets.someOtherSecret }}secrets: inherit # pass all of the secrets from the parent workflow to the template. this includes org, repo, and environment secrets from the parent workflow

Reusable Actions vs. Reusable Workflows

This list of features changes quite often. For example, Reusable Workflows being able to call other Reusable Workflows is fairly new.

Reusable ActionsReusable Workflows
ScopeStep-levelJob-level
Supports env variables
defined in parent Workflow
YesNo
Input typesnone (string)boolean, number, string
Input SecretsNo1Yes
Supports Service ContainersNoYes
Can specify Agent
(runs-on)
NoYes
FilenameMust be action.yml
(so, 1 per folder)
Can be anything .yml
(must be in .github/workflows/ -
no subfolders)
Nesting10 levels10 levels
LoggingSummarizedLogging for each Job and Step

Tip


Workflow Commands

Documentation - Workflow commands for GitHub Actions

  • These are special commands that can be used to communicate with the runner machine
  • They can do multiple different things, such as set environment variables, set output values, set debug messages, and more
  • Depending on the specific Workflow Command, it can be used in one of two ways:
    • Using the echo command with a specific format
    • Writing to a file
# Some examples (all using Bash), see the docs for a full reference# Print a debug message to the logecho"::debug::This is a debug message"# Masking a string value so it's not shown in the logsecho"::add-mask::This value will be masked"# Setting an environment variableecho"KEY=value">>"$GITHUB_ENV"# Setting an output parameterecho"KEY=value">>"$GITHUB_OUTPUT"

Warning

For reusable workflows, any variables you set in the env context inside of the reusable workflow will NOT be available in the parent workflow. To get around this, the reusable workflow could create an output which can then be consumed by the parent workflow.

Warning

A masked value can NOT be passed from one Job to another Job in GitHub Actions

  • GitHub Discussion on this topic
  • The official docs want you to use a secret store, such as Azure KeyVault, to solve this problem. In effect, Job 1 uploads the value to the secret store, and then Job 2 downloads the value from the secret store.

Multi-Line Values

If you need to mask a sensitive, multi-line value, then you can do the following:

SENSITIVE="$(command that outputs a sensitive, multi-line value)"whileread -r line
doecho"::add-mask::${line}"done<<<"$SENSITIVE"# In this example, the sensitive value will be assigned to the variable called SENSITIVE# The command used on line 1 will be logged in plain-text, so it must not include sensitive values (but, this is a plain-text YAML file, so you would never do that in the first place, right?)# The value assigned to the variable is then read, line-by-line, and a mask is applied to each line's value# An example of a safe command you could use:
SENSITIVE="$(az keyvault secret show --name MySecretName --vault-name MyVaultName --query value --output tsv)"

If you need to set an environment variable or an output to use a multi-line value, then you can do the following:

# Make sure the delimiter you're using won't occur on a line of its own within the value
{
echo'KEY<<DELIMETER'
command(s) that produce multiple lines of output
echo DELIMETER
} >>"$GITHUB_ENV"

YAML Anchors & Aliases

Documentation - YAML anchors and aliases

  • GitHub Actions supports a limited set of YAML features like anchors and aliases
  • Use &symbolicName to define the anchor (the section you want to capture)
  • Use *symbolicName to define one or more aliases, where each one will be a copy of the anchor
  • YAML Merge Keys, specified by <<: are not yet supported by GitHub. This means each anchor must be copied exactly as-is, with no way to add an override of a single value
jobs:
firstSymbolicJobName:
env: &anchorName # this defines the section that will become the anchorKEY1: value1KEY2: value2KEY3: value3secondSymbolicJobName:
env: *anchorName # this defines an alias (the values in the anchor will be copied here)

Expressions

Documentation - Evaluate expressions in workflows and actions

case expression

case( pred1, val1, pred2, val2, ..., default )
  • Evaluates predicates in order and returns the value corresponding to the first predicate that evaluates to true. If no predicate matches, it returns the last argument as the default value.

Links

Footnotes

  1. You can not directly pass GitHub Secrets to an Action. However, you could use a Secret for the value of one of the Action's input parameters, or you could use a Secret as the value of an environment variable that the Action could then read.