Github Actions
What are Github actions?
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline.
It can trigger workflows based on a good variety of events, such as pull requests created, labels added, releases published and so on.
Workflows
A workflow is a configurable automated process that will run one or more jobs. They are defined by YAML files defined in .github/workflows
and each workflow/file can be used to a specific purpose.
A workflow must contain the following components:
- One or more events that will trigger the workflow.
- One or more jobs, each of which will execute on a runner machine and run a series of one or more steps.
- Each step can either run a script that you define or run an action, which is a reusable extension that can simplify your workflow.
Example of an action:
name: deploy-frontend
run-name: ${{ github.actor }} is deploying version ${{ github.run_number }}
on:
release:
types: [published]
jobs:
deploy-new-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: npm ci
- run: npm run build
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-central-1
- run: aws s3 sync dist/ s3://my-project/${{ github.run_number }} --recursive --quiet
Triggers
Workflow triggers are events that cause a workflow to run. There are more than 36 main types of triggers and each one has specific activities associated, so we are going to focus on the main ones.
- pull_request - Runs your workflow when activity on a pull request in the workflow's repository occurs.
name: deploy-frontend
run-name: ${{ github.actor }} is deploying version ${{ github.run_number }}
on:
pull_request:
types: [published, synchronize]
- push - Runs your workflow when you push a commit or tag, or when you clone a repository.
on:
push:
branches:
- 'main'
- 'releases/**'
- workflow_dispatch - Enables your workflow to be run manually and also allows users to add inputs to this workflow manually.
run-name: Running Deployment on ${{ github.event.inputs.environment }}
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
type: choice
options:
- info
- warning
- debug
tags:
description: 'Test scenario tags'
required: false
type: boolean
environment:
description: 'Environment to run tests against'
type: environment
required: true
- release - Runs your workflow when release activity in your repository occurs.
on:
release:
types: [published]
- schedule - The
schedule
event allows you to trigger a workflow at a scheduled time.
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 5 * * 1,3'
- cron: '30 5 * * 2,4'
Jobs
A workflow contains one or more jobs which execute a set of steps in a given machine.
jobs:
my-job:
runs-on: ubuntu-latest
steps:
- name: say_hello
run: echo Hello world!
- name: say_bye
run: echo Bye!
If you use a github hosted runner each job will run using the image specified on runs-on property. The types available are:
- windows-latest / windows-2022
- windows-2019
- ubuntu-latest / ubuntu-22.04
- ubuntu-20.04
- macos-13 / macos-13-xl (beta)
- macos-latest / macos-12
- macos-11
In case you are not interested on having github hosted runners it is also possible to run github actions on self-hosted environments.
Creating dependent jobs
By default, the jobs in your workflow all run in parallel at the same time. If you have a job that must only run after another job has completed, you can use the needs
keyword to create this dependency.
jobs:
setup:
runs-on: ubuntu-latest
steps:
- run: echo "preparing environment"
build:
needs: setup
runs-on: ubuntu-latest
steps:
- run: echo "building application"
test:
needs: setup
runs-on: ubuntu-latest
steps:
- run: echo "testing application"
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: echo "deploying application"
Outputting values from jobs
Sometimes jobs that depend on each other might also require not only the order to be preserved, but also extra data, to handle this situation you can create outputs for a job and consume it on another.
jobs:
job1:
runs-on: ubuntu-latest
outputs:
greeting: ${{ steps.step1.outputs.test }}
target: ${{ steps.step2.outputs.test }}
steps:
- id: step1
run: echo "test=hello" >> "$GITHUB_OUTPUT"
- id: step2
run: echo "test=world" >> "$GITHUB_OUTPUT"
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- env:
GREETING: ${{needs.job1.outputs.greeting}}
TARGET: ${{needs.job1.outputs.target}}
run: echo "$GREETING $TARGET"
Environment variables and secrets
You are able to set environment variables to keep your code flexible by not using fixed values. This environment variables can be set on Job, workflow, repository or even on organisation level.
name: Auto Deploy
run-name: Deploying
on:
release:
types: [published]
env:
VERSION_TAG: GA_${{ github.run_number }}
jobs:
name: deploy
runs_on: ubuntu-latest
env:
BUCKET: s3://my-project
steps:
- run: echo "Deploying version $VERSION_TAG on $BUCKET"
And in case you are dealing with sensitive information you can create a secret to store it, this will prevent this value from being directly accessed/viewed by other developers and will obfuscate it on logs.
name: top_secret
on:
workflow_dispatch: {}
env:
GLOBAL_CONSPIRACY: ${{ secrets.GLOBAL_CONSPIRACY }}
jobs:
name: deploy
runs_on: ubuntu-latest
steps:
- run: echo "The government doesn't want you to know that $GLOBAL_CONSPIRACY"
Job permissions
In some cases you might want to use resources within Github, for example, to fetch packages or add a label to a pull request. You can use permissions
to modify the permissions granted, adding or removing access as required, so that you only allow the minimum required access.
The permissions property can be defined either in the workflow level (which will set those permissions to all jobs) or on job level.
permissions:
actions: read|write|none
checks: read|write|none
contents: read|write|none
deployments: read|write|none
id-token: read|write|none
issues: read|write|none
discussions: read|write|none
packages: read|write|none
pages: read|write|none
pull-requests: read|write|none
repository-projects: read|write|none
security-events: read|write|none
statuses: read|write|none
Details on one of the permissions here
Going deeper on workflows
Reusing workflows
Rather than copying and pasting from one workflow to another, you can make workflows reusable. A workflow that uses another workflow is referred to as a "caller" workflow. The reusable workflow is a "called" workflow.
If you reuse a workflow from a different repository, any actions in the called workflow run as if they were part of the caller workflow. For example, if the called workflow uses actions/checkout
, the action checks out the contents of the repository that hosts the caller workflow, not the called workflow.
You can connect a maximum of four levels of workflows - that is, the top-level caller workflow and up to three levels of reusable workflows.
Creating a reusable workflow
Reusable workflows are created by adding the trigger workflow_call
which also allows you to pass inputs to the workflow.
on:
workflow_call:
inputs:
config-path:
required: true
type: string
secrets:
envPAT:
required: true
And those inputs/secrets can be used by referencing inputs.[property] or secrets.[properties]:
jobs:
reusable_workflow_job:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/labeler@v4
with:
repo-token: ${{ secrets.envPAT }}
configuration-path: ${{ inputs.config-path }}
Calling a reusable workflow
jobs:
call-workflow-passing-data:
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
with:
config-path: .github/labeler.yml # inputs
secrets:
envPAT: ${{ secrets.envPAT }} # Passing secrets individually
jobs:
call-workflow-passing-data:
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
with:
config-path: .github/labeler.yml
secrets: inherit # Passing all secrets from caller
Caching Dependencies
To make your workflows faster and more efficient, you can create and use caches for dependencies and other commonly reused files. This can be achieved by using the cache action which will allow a workflow to cache data or fetch data as long as it is on the default branch (usually main) or in the workflow's branch.
YAML
name: Caching with npm
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
name: Only install dependencies if there is no cache
run: npm ci
Storing Artifacts
Artifacts allow you to share data between jobs in a workflow and store data once that workflow has completed. This can be used, for example, to share test results with another job.
The main difference between artefacts and cache is that artifacts are supposed to be values that can change between each workflow run, while cache tends to be more long lasting.
To handle artifacts there are 2 actions, upload-artifact
and download-artifact
name: Node CI
on: [push]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: npm install, build, and test
run: |
npm install
npm run build --if-present
npm test
- name: Archive code coverage results
uses: actions/upload-artifact@v3
with:
name: code-coverage-report
path: output/test/code-coverage.html
sonar_cloud:
runs_on: ubuntu-latest
steps:
- name: Download a single artifact
uses: actions/download-artifact@v3
with:
name: code-coverage-report
Testing Github Actions Locally
One of the biggest complains when developing workflows is that the feedback loop is too long. You need to do your changes, commit files, push them, run the workflow and if the change doesn't work as expected do the whole cycle again.
Fortunately there is a small program called act which allows you to run your github actions locally. It reads the workflows on .github/workflows/
uses Docker API to either pull or build the necessary and when you run the action it runs the container following your workflow. The environment variables and filesystem are all configured to match what GitHub provides.
Installing Act
brew install act
# OR
gh extension install https://github.com/nektos/gh-act
Listing actions
# List all actions for all events:
act -l
# List the actions for a specific event:
act workflow_dispatch -l
Running events
act pull_request
act -s MY_SECRET=somevalue --input NAME=somevalue