The quick and easy way to design a GitHub Actions workflow
Table of contents
See Contributing guide.
If you want to know available options for a workflow and if you YAML syntax is valid, edit your workflow on GitHub rather than in a local file. Click on a field or at an indent level where you want to add a field, the press CTRL+SPACE. Youโll see available options for that context. This can save reading through the docs.
build-deploy
.Example names for your workflow:
name: CI
name: CI Build
name: CI Test
name: Node CI
name: Deno CI
name: Python CI
name: Deno CI
name: Deploy GH Pages
name: Release
For more options such as building on schedule, see my Triggers cheatsheet.
You can combine the sections below - for example, you can run a task on pushes main
, on a nightly schedule and also whenever you press the run button (without having to make a commit).
Push a local commit or commit using the GitHub UI to trigger these on
conditions.
main.yml
- The simplest approach. Builds on any branch, regardless of Pull Requests.
on: push
main.yml
- Builds on a chosen branch only.
on:
push:
branches: main
main.yml
- Triggered on a commit or push to your main branch or any branch with a Pull Request. Ignore changes to markdown files (such as docs) at all levels (Iโve tested and this rule works, without having to use **/*.md
). Note that anything in docs
directory that is not markdown (such as an image or YAML) can still trigger a run.
on:
push:
branches: main
paths-ignore:
- "*.md"
pull_request:
branches: main
paths-ignore:
- "*.md"
main.yml
- Similar to above but still watches for changes in markdown files outside the docs
directory. Such as if you have a static site with markdown content in the root of the repo. If you have a directory within docs/
, you need the double globalstar as below - I found docs/
was not sufficient as a rule.
on:
push:
branches: main
paths-ignore:
- "docs/**"
- README.md
pull_request:
branches: main
paths-ignore:
- "docs/**"
- README.md
The workflow above has been set up to not run if there are just changes in your docs
directory. This is useful to reduce a run that gives no benefit and would still take up processing minutes allocate to your account. If you actually have content in your docs
directory that matters like for a documentation site, then of course you can remove the ignore parts.
Create a tag or a release to trigger your workflow.
release.yml
on:
release:
types: created
tag.yml
on:
push:
tags:
- 'v*'
Add this to your workflow.
This allows you to run this workflow manually by a button pressed in the Actions tab.
No arguments.
on: workflow_dispatch
Here the job expects to inputs.
on:
workflow_dispatch:
inputs:
name:
description: 'Person to greet'
required: true
default: 'Mona the Octocat'
home:
description: 'location'
required: false
default: 'The Octoverse'
See Workflow dispatch in my Dev Cheatsheets for more info.
Run on a given frequency, using Crontab notation.
Daily at midnight:
on:
schedule:
- cron: "0 0 * * *"
This is useful for building a site, deploying an application or publishing to a package registry. Also, if you have any code quality or security scans such as a CodeQL workflow.
Give a job a name using a key under jobs
and a pretty name under name
field.
The job name might be similar to the workflow fileโs name at the top, but I find it better to be more specific. Like โNode CIโ for the workflow and โBuild and deployโ as a job name.
Some samples:
jobs:
build:
name: Build
jobs:
build-test:
name: Build and test
jobs:
build-deploy:
name: Build and deploy
Define the operating system for a job.
Run on Ubuntu - this is the most common flow:
jobs:
build:
name: Build
runs-on: ubuntu-latest
Run on a matrix of operating systems - useful if you need to test your app can install and run on all.
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
runs-on: ${{ matrix.os }}
Note that quotes are not needed in YAML, whether for a string or array of strings.
You can also pin like ubunutu-18.04
.
For a list of available systems, see GitHub Hosted Runners in the docs.
env:
VAR_A: Hello
jobs:
build:
env:
VAR_B: World
Define steps for a job to run
This section covers some common snippets across languages, for some of my common flows that build, test and deploy.
A note on wording - โsetupโ is a noun, while โset upโ is a verb. See both under the setup definition on the Merriam-Webster online dictionary.
For more details, please explore the Workflows section in my Code Cookbook.
That covers:
This is an outline of a generic workflow.
It includes emojis to brighten up the log and make it easier to scan visually. For set up, you might use something related to the language, like a snake for Python.
steps:
- name: Checkout ๐๏ธ
uses: actions/checkout@v2
- name: Set up Foo โ๏ธ # e.g. Python ๐, Node or Ruby ๐
uses: actions/setup-foo@v2
with:
foo-version: '1.x'
- name: Cache dependencies ๐พ
uses: actions/cache@v2
with:
# ...
- name: Install dependencies ๐ง
run: # ...
- name: Check formatting ๐จ
run: # ...
- name: Lint ๐ง
run: # ...
- name: Test ๐จ
run: # ...
- name: Build ๐๏ธ
run: # ...
- name: Deploy to GitHub Pages ๐
uses: # ...
Using the checkout action.
steps:
- name: Checkout ๐๏ธ
uses: actions/checkout@v2
GH Actions comes with Node and Yarn set up already. But you can use an action if you ned more recent version, or to use a matrix of Node or Yarn versions.
Using the setup-node action.
steps:
- name: Set up Node.js โ๏ธ
uses: actions/setup-node@v2
with:
node-version: '14.x'
Use a matrix of Node versions.
steps:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- name: Checkout ๐๏ธ
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} โ๏ธ
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
Using the setup-deno action.
steps:
- name: Set up Deno โ๏ธ ๐ฆ
uses: denolib/setup-deno@v2
with:
deno-version: v1.x
Using the setup-go action.
steps:
- name: Set up Go โ๏ธ โฉ
uses: actions/setup-go@v2
with:
go-version: 1.15
steps:
- name: Install rustfmt
run: rustup component add rustfmt
- name: Check formatting
run: cargo fmt --all --check
- name: Build
run: cargo build --tests --workspace
- name: Test
run: cargo test --workspace
Using the setup-python action.
steps:
- name: Set up Python โ๏ธ ๐
uses: actions/setup-python@v2
with:
python-version: 3.x
Using the setup-ruby action.
steps:
- name: Set up Ruby โ๏ธ ๐
uses: actions/setup-ruby@v1
with:
ruby-version: 2.7
This section is not needed for Go or Deno where packages are installed on running, building, testing, etc.
See GH Actions Cache guide.
The cache step is optional but makes the build faster. It will load dependencies from the cache, if the dependencies file is unchanged. Also adds a clean-up step for you to save dependencies to the cache.
steps:
- name: Cache Python packages ๐พ
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install dependencies ๐ง
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
See related workflows here.
For NPM projects:
steps:
- name: Cache NPM packages ๐พ
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-
${{ runner.OS }}-
- name: Install dependencies ๐ง
run: npm install
For Yarn projects:
steps:
- name: Get Yarn cache dir ๐พ
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1 ๐พ
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies ๐ง
run: yarn install
This uses Yarnโs cache directory. On Ubuntu this is ~/.cache/yarn/v6
.
This will setup Ruby in the environment, install gems with Bundler and even cache the gems for faster builds.
steps:
- name: Set up Ruby ๐
uses: actions/setup-ruby@v1
with:
ruby-version: '2.7'
bundler-cache: true
This Ruby setup flow works great for Jekyll projects too. Just make sure to add jekyll
gem to your Gemfile
and add a build step as below.
steps:
- name: Build ๐
run: bundle exec jekyll build --trace
Then your workflow will set up Ruby and Jekyll then build _site
directory.
If you want to persist the _site
directory output to serve as a GH Pages site, see the GitHub Pages section.
See more Jekyll samples and info in my Jekyll CI recipes.
Create your production build output
Bundle your package or app so it can be installed, or build your website assets.
This depends on setting up Makefile
with a build
target.
steps:
- name: Build ๐๏ธ
run: make build
steps:
- name: Build ๐๏ธ
run: npm run build
steps:
- name: Build ๐๏ธ
run: yarn build
After setting up Ruby and installing dependencies with Bundler.
Build the site with Jekyll.
steps:
- name: Build ๐๏ธ
run: bundle exec jekyll build --trace
If you want to persist the _site
directory output to serve as a GH Pages site, see the GitHub Pages section.
This action will take a given build output directory (like dist
, build
or _site
) and commit it as a single commit on the gh-pages
branch at the root path. This can then be served as static assets (HTML, CSS and JS) on a GH Pages site.
For more info and related workflows and actions, see GH Pages in my Code Cookbook.
- name: Deploy to GitHub Pages ๐
if: ${{ github.event_name != 'pull_request' }}
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: _site
Steps:
npm run build
(for React, Vue or Next.js)._site
or build
.gh-pages
branch (this it is default behavior).Note the use of the if
condition on this step. This means that your entire workflow can run on a push to your main branch. But on a push to a Pull Request branch, the earlier build steps will run but the deploy step at the end will be skipped. This is useful to avoid deploy your work in progress branch to your production site.
Here are some workflows I have set up for my projects which Iโd like to share.