https://github.com/nnellans/github-workflows-guide
GitHub Workflows Guide
https://github.com/nnellans/github-workflows-guide
ci-cd cicd continuous-delivery continuous-deployment continuous-integration github github-actions github-workflow workflows
Last synced: 2 days ago
JSON representation
GitHub Workflows Guide
- Host: GitHub
- URL: https://github.com/nnellans/github-workflows-guide
- Owner: nnellans
- License: gpl-3.0
- Created: 2023-01-29T17:08:38.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2025-04-25T14:17:13.000Z (9 months ago)
- Last Synced: 2025-04-25T15:27:18.964Z (9 months ago)
- Topics: ci-cd, cicd, continuous-delivery, continuous-deployment, continuous-integration, github, github-actions, github-workflow, workflows
- Homepage: https://www.nathannellans.com
- Size: 8.61 MB
- Stars: 8
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# GitHub Workflow Guide
- Version: 1.1.0
- Author:
- Nathan Nellans
- Email: me@nathannellans.com
- Web:
- https://www.nathannellans.com
- https://github.com/nnellans/github-workflows-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.
---
# Table of Contents
- [Workflow Settings](#workflow-settings)
- [Triggers](#triggers)
- [Permissions for the GitHub Token](#permissions-for-the-github_token)
- [Default Settings](#default-settings)
- [Concurrency Settings](#concurrency-settings)
- [Variables](#variables)
- [Secrets](#secrets)
- [Jobs and Steps](#jobs--defining-the-work)
- [Normal Jobs](#normal-jobs)
- [Calling a Reusable Workflow](#jobs-that-call-a-reusable-workflow-job-level-template)
- [Reusable Actions vs. Reusable Workflows](#reusable-actions-vs-reusable-workflows)
- [Workflow Commands](#workflow-commands)
- [YAML Anchors & Aliases](#yaml-anchors--aliases)
- [Links](#links)
---
# Workflow Settings
```yaml
# name of the workflow as shown in the GitHub UI
name: 'string' # optional, default is the path & name of the yaml file
# name to use for each run of the workflow
run-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](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)
[Documentation - Events that trigger workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)
```yaml
# option 1: single event with no options
on: push
# option 2: multiple events with no options
on:
- push
- fork
# option 3: events with options
on:
push:
branches:
- blahblah
issues:
types:
- opened
schedule:
- cron: '30 5,17 * * *'
# option 4: manual trigger where you can specify a max of 25 inputs
on:
workflow_dispatch:
inputs:
someInputName:
description:
required: true | false
default: 'defaultValue'
type: boolean | number | string | choice | environment
options: # only when type: choice
- option1
- option2
someOtherInput:
description:
required: true
type: string
# option 5: if this workflow is used as a reusable workflow (job-level template)
on:
workflow_call:
inputs: # input parameters
inputName1:
description:
required: true | false
type: boolean | number | string # required
default: something # if omitted, boolean = false, number = 0, string = ""
secrets: # input secrets
secretName1:
description:
required: true | false
outputs: # output values
outputName1:
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](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token#modifying-the-permissions-for-the-github_token)
[Documentation - Workflow Syntax - Permissions](https://docs.github.com/en/actions/reference/workflows-and-actions/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
```yaml
# option 1: full syntax
permissions:
actions: read | write | none
artifact-metadata: read | write | none
attestations: read | write | none
checks: read | write | none
contents: read | write | none
deployments: read | write | none
discussions: read | write | none
id-token: read | write | none
issues: read | write | none
models: read | none
packages: read | write | none
pages: read | write | none
pull-requests: read | write | none
security-events: read | write | none
statuses: read | write | none
# option 2: shortcut syntax to provide read or write access for all scopes
permissions: read-all | write-all
# option 3: shortcut syntax to disable permissions to all scopes
permissions: {}
```
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`](https://www.nathannellans.com/post/github-apis-github-tokens-and-github-action-workflows)
# Default Settings
[Documentation - Setting a default shell and working directory](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/set-default-values-for-jobs)
- Creates a map of default settings that will be inherited downstream
- Supported scopes for `defaults`: workflow-level, job-level
- The most specific defaults wins
```yaml
defaults:
run:
shell: bash
working-directory: scripts
```
# Concurrency Settings
[Documentation - Control the concurrency of workflows and jobs](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)
- 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
```yaml
# option 1: specify a concurrency group with default settings
concurrency: groupName
# option 2: specify a concurrency group with custom settings
concurrency:
group: groupName
cancel-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](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-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
```yaml
# defining environment variables (workflow-level)
env:
KEY1: value
KEY2: value
# defining environment variables (job-level)
jobs:
someJobId:
env:
KEY1: value
KEY2: value
# defining environment variables (step-level)
jobs:
someJobId:
steps:
- name: someStepName
env:
KEY1: value
KEY2: 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: $KEY
windows powershell: $env:KEY
windows 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
```yaml
# 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: $KEY
windows powershell: $env:KEY
windows cmd: %KEY%
```
---
# Secrets
[Documentation - Using secrets in GitHub Actions](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets)
- 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
```yaml
# Actions can't directly use secrets that are defined via the GitHub UI
# However, you can use the secret as an input or environment variable
steps:
- name: Hello world action
env: # Set the secret as an environment variable
SOME_VAR: ${{ secrets.Key }
uses: action/something@v1
with: # Set the secret as a value to an input
someInput: ${{ secrets.Key }}
```
# Jobs / Defining the work
## Normal Jobs:
[Documentation - Using jobs in a workflow](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-jobs)
```yaml
jobs:
symbolicJobName: # must be unique, start with a letter or underscore, and only contain letters, numbers, dashes, and underscores
name: 'string' # friendly name that is shown in the GitHub UI
runs-on: windows-latest | ubuntu-slim | ubuntu-latest | macos-latest | self-hosted # specifies the Agent to run on
needs: # Job dependencies
if: # Job conditions, ${{ ... }} can optionally be used to enclose your condition
continue-on-error: true # allows the Workflow to pass if this Job fails
timeout-minutes: 10 # max time a Job can run before being cancelled. optional, default is 360
permissions: # job-level GITHUB_TOKEN permissions
defaults: # job-level defaults
concurrency: # job-level concurrency group
env: # job-level variables
KEY: value
environment: # see more below
container: # see more below
snapshot: # see more below
services: # see more below
strategy: # see more below
outputs: # see more below
# list the Steps of this Job
steps:
# Use a GitHub Action
- id: 'symbolicStepName' # optional
name: 'string' # optional. friendly name that is shown in the GitHub UI
if: # Step conditions, ${{ ... }} can optionally be used to enclose your condition
continue-on-error: true # allows the Job to pass if this Step fails
timeout-minutes: 10 # max time to run the Step before killing the process
env: # Step-level variables
KEY: value
# option 1: use a public action
uses: 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 repo
uses: ./.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 supported
uses: docker://alpine:3.8 # from Docker Hub
uses: docker://ghcr.io/owner/image # from GitHub Packages Container Registry
uses: docker://gcr.io/cloud-builders/gradle # from Google Container Registry
# parameters to pass to the action, must match what is defined in the action
with:
param1: value1
param2: value2
# when using an action from a public container image (option 3)
args: 'something' # this overwrites the CMD instruction in your Dockerfile
entrypoint: 'something' # this overwrite the ENTRYPOINT instruction in your Dockerfile
# Run a single-line Script
- name: something2
run: single-line command
shell: bash | pwsh | python | sh | cmd | powershell
working-directory: ./temp
# Run a multi-line Script
- name: something3
run: |
multi-line
command
```
### Job.Environment
[Documentation - Managing environments for deployment](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments)
- Specifies a GitHub environment to deploy to
```yaml
jobs:
symbolicJobName:
# option 1 - specify just an environment name
environment: envName
# option 2 - specify environment name and url
environment:
name: envName
url: someUrl
```
- `envName` can be a string or any expression (except for the `secrets` context)
### Job.Container
[Documentation - Running jobs in a container](https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container)
- Defines a container that will run all Steps in this Job
```yaml
jobs:
symbolicJobName:
# option 1 - shortcut syntax specifying just the image
container: node:14.16
# option 2 - full syntax
container:
image: node:14.16
credentials: # used to login to the container registry
username:
password:
env: # specify environment variables inside the container
KEY: value
ports: # array of ports to expose on the container
- 8080:80 # maps port 8080 on the docker host to port 80 on the container
volumes: # array of volumes for the container to use, you can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host
- source:destinationPath
options: --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](https://docs.github.com/en/actions/how-tos/manage-runners/larger-runners/use-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
```yaml
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 syntax
snapshot: customImageName
# option 2 - mapping syntax
# lets you specify a version number, only major & minor versions are supported, patch version are not supported
snapshot:
image-name: customImageName
version: 2.*
```
### Job.Services
[Documentation - Communicating with Docker service containers](https://docs.github.com/en/actions/tutorials/use-containerized-services/use-docker-service-containers)
- Defines service container(s) that are used by your Job
```yaml
jobs:
symbolicJobName:
services:
symbolicServiceName: # label used to access the service container
image: nginx
credentials: # used to login to the container registry
username:
password:
env: # specify environment variables inside the service container
KEY: value
ports: # an array of ports to expose on the service container
- 80
volumes: # array of volumes for the container to use, you can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host
- source:destinationPath
options: --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](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations)
- Use variables to make one Job run multiple different times
```yaml
jobs:
symbolicJobName:
strategy:
fail-fast: boolean # optional, default is true
max-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 permutations
KEY1: [valueA, valueB]
KEY2: [valueX, valueY, valueZ]
include: # an extra list of objects to include
exclude: # 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](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/pass-job-outputs)
- Specify outputs of this Job
```yaml
jobs:
symbolicJobName:
outputs: # map of outputs for this job
key: value
key: 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](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows)
[Documentation - Reusing workflow configurations](https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations)
- Only the following parameters are supported in such a Job
```yaml
jobs:
symbolicJobName: # must be unique, start with a letter or underscore, and only contain letters, numbers, dashes, and underscores
name: 'string' # friendly name that is shown in the GitHub UI
needs: # Job dependencies
if: # Job conditions, ${{ ... }} can optionally be used to enclose your condition
permissions: # job-level GITHUB_TOKEN permissions
concurrency: # job-level concurrency group
strategy: # 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 repo
uses: ./.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 template
with:
param1: value1
param2: value2
secrets: # secrets to pass to the template, must match what is defined in the template
param1: ${{ 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 Actions | Reusable Workflows |
| --- | --- | --- |
| Scope | Step-level | Job-level |
| Supports `env` variables
defined in parent Workflow | Yes | No |
| Input types | none (string) | boolean, number, string |
| Input Secrets | No[^1] | Yes |
| Supports Service Containers | No | Yes |
| Can specify Agent
(`runs-on`) | No | Yes |
| Filename | Must be `action.yml`
(so, 1 per folder) | Can be anything `.yml`
(must be in `.github/workflows/` -
no subfolders) |
| Nesting | 10 levels | 10 levels |
| Logging | Summarized | Logging for each Job and Step |
[^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.
> [!TIP]
> - Example [action-composite.yaml](./action-composite.yaml) file showing the complete syntax for a reusable Composite Action
> - Example [action-docker.yaml](./action-docker.yaml) file showing the complete syntax for a reusable Docker Action
> - Example [action-javascript.yaml](./action-javascript.yaml) file showing the complete syntax for a reusable JavaScript Action
---
# Workflow Commands
[Documentation - Workflow commands for GitHub Actions](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands)
- 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
```bash
# Some examples (all using Bash), see the docs for a full reference
# Print a debug message to the log
echo "::debug::This is a debug message"
# Masking a string value so it's not shown in the logs
echo "::add-mask::This value will be masked"
# Setting an environment variable
echo "KEY=value" >> "$GITHUB_ENV"
# Setting an output parameter
echo "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](https://github.com/orgs/community/discussions/13082) on this topic
> - The [official docs](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#example-masking-and-passing-a-secret-between-jobs-or-workflows) 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:
```bash
SENSITIVE="$(command that outputs a sensitive, multi-line value)"
while read -r line
do
echo "::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:
```bash
# Make sure the delimiter you're using won't occur on a line of its own within the value
{
echo 'KEY<> "$GITHUB_ENV"
```
---
# YAML Anchors & Aliases
[Documentation - YAML anchors and aliases](https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations)
- 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
```yaml
jobs:
firstSymbolicJobName:
env: &anchorName # this defines the section that will become the anchor
KEY1: value1
KEY2: value2
KEY3: value3
secondSymbolicJobName:
env: *anchorName # this defines an alias (the values in the anchor will be copied here)
```
---
# Links
- official github actions: https://github.com/orgs/actions/repositories
- official azure actions: https://github.com/marketplace?query=Azure&type=actions&verification=verified_creator