Basic notes on GitHub Actions
https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
Runner image YAML workflow label Notes
Windows Server 2022 windows-latest or windows-2022 The windows-latest label currently uses the Windows Server 2022 runner image.
Windows Server 2019 windows-2019
Ubuntu 22.04 ubuntu-latest or ubuntu-22.04 The ubuntu-latest label currently uses the Ubuntu 22.04 runner image.
Ubuntu 20.04 ubuntu-20.04
Ubuntu 18.04 [deprecated] ubuntu-18.04 Migrate to ubuntu-20.04 or ubuntu-22.04. For more information, see this GitHub blog post.
macOS Monterey 12 macos-12
macOS Big Sur 11 macos-latest or macos-11 The macos-latest label is currently transitioning to the macOS Monterey 12 runner image. During the transition, the label might refer to the runner image for either macOS 11 or 12. For more information, see this GitHub blog post.
macOS Catalina 10.15 [deprecated] macos-10.15 Migrate to macOS-11 or macOS-12. For more information, see this GitHub blog post.
https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
https://learn.microsoft.com/en-us/training/paths/automate-workflow-github-actions/
Workflow triggered hen a commit is pushed to a branch
on: push
https://learn.microsoft.com/en-us/training/paths/automate-workflow-github-actions/
No notes
Manually trigger a workflow
on: workflow_dispatch
Trigger a workflow by querying a REST API
on: repository_dispatch
Workflow triggered on a schedule
Called on by other workflows.
Context can be accessed using the ${{}}
expression.
E.g. ${{ github }}
or ${{ toJSON(github) }}
https://docs.github.com/en/actions/learn-github-actions/contexts
Context name Type Description
github object Information about the workflow run. For more information, see github context.
env object Contains variables set in a workflow, job, or step. For more information, see env context.
vars object Contains variables set at the repository, organization, or environment levels. For more information, see vars context.
job object Information about the currently running job. For more information, see job context.
jobs object For reusable workflows only, contains outputs of jobs from the reusable workflow. For more information, see jobs context.
steps object Information about the steps that have been run in the current job. For more information, see steps context.
runner object Information about the runner that is running the current job. For more information, see runner context.
secrets object Contains the names and values of secrets that are available to a workflow run. For more information, see secrets context.
strategy object Information about the matrix execution strategy for the current job. For more information, see strategy context.
matrix object Contains the matrix properties defined in the workflow that apply to the current job. For more information, see matrix context.
needs object Contains the outputs of all jobs that are defined as a dependency of the current job. For more information, see needs context.
inputs object Contains the inputs of a reusable or manually triggered workflow. For more information, see inputs context.
https://github.com/marketplace/actions/upload-a-build-artifact
https://github.com/marketplace/actions/download-a-build-artifact
https://docs.github.com/en/actions/learn-github-actions/contexts#about-contexts
needs
Contains the outputs of all jobs that are defined as a dependency of the current job. For more information, see needs context.
build:
needs: ...
runs-on: ubuntu-latest
outputs:
script-file: ${{ steps.publish.outputs.script-file }}
steps:
...
- name: Publish JS filename
id: publish
run: find dist/assets/*.js -type f -execdir echo 'script-file={}' >> $GITHUB_OUTPUT ';'
...
deploy:
needs: build
runs-on: ubuntu-latest
steps:
...
- name: Output filename
run: echo "${{ needs.build.outputs.script-file }}"
...
https://github.com/marketplace/actions/cache
The following should be used in all jobs that use the dependencies
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache deps
uses: actions/cache@v3
with:
path: ~/.npm
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
...
...
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: ${{ steps.cache.outputs.cache-hit != 'true' }}
run: npm ci
...
Environment variables can be accessed in Node through process.env
.
name: Deployment
on:
push:
branches:
- main
- dev
env:
MONGODB_DB_NAME: gha-demo
...
...
jobs:
test:
env:
MONGODB_CLUSTER_ADDRESS: clusterA
PORT: 8080
environment: testing
runs-on: ubuntu-latest
...
Using the environment variables will differ by the runner.
Using $ENV_NAME
or ${{ env.ENV_NAME }}
Using $env:ENV_NAME
jobs:
test:
env:
MONGODB_CLUSTER_ADDRESS: clusterA
PORT: 8080
runs-on: ubuntu-latest
steps:
...
- name: Run server
run: npm start & npx wait-on http://127.0.0.1:$PORT
- name: Print results
run: echo "Running on port: ${{ env.PORT }}"
...
https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
Repo-specific secrets can be used as values in your workflows.
...
jobs:
test:
env:
MONGODB_CLUSTER_ADDRESS: ${{ secrets.MONGODB_CLUSTER_ADDRESS }}
PORT: 8080
runs-on: ubuntu-latest
...
https://docs.github.com/en/actions/learn-github-actions/contexts#vars-context
Environments (Testing, staging, etc) can be created through Settings > Environments
.
Environments can have their own stored secrets and variables so a new repo-specific secrets/variables is not needed for each environment.
Variables are not secret and can be viewed and altered. Secrets are not visible and can only be updated.
Variables could be useful for things such as addresses and ports. Secrets could be useful for things such as database passwords.
Declaring an environment is through the environment
key.
...
jobs:
test:
environment: testing
env:
MONGODB_CLUSTER_ADDRESS: ${{ secrets.MONGODB_CLUSTER_ADDRESS }}
...
When requesting a secret/variable in the job, it will retrieve the environment value instead of an overall repo-specific value.
...
- name: Test code
id: run-tests
run: npm run test
- name: Upload test report
if: ${{ failure() && steps.run-tests.outcome == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: test-report
path: test.json
...
${{ }}
can be ignored in if
https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context
https://docs.github.com/en/actions/learn-github-actions/expressions
https://docs.github.com/en/actions/learn-github-actions/expressions#status-check-functions
Using continue-on-error
to run a step when a previous step(s) has failed.
This could be similar to the previous if: ${{ failure() }}
.
if
will display that a job failed (red failure icon) if you are using the above if
method to continue after a failure.
continue-on-error
will display a successful job icon if a previous step failed.
...
- name: Test code
id: run-tests
run: npm run test
- name: Upload test report
continue-on-error: true
uses: actions/upload-artifact@v3
with:
name: test-report
path: test.json
...
Matrix allows you to run the job multiple times for each value in the matrix. A matrix could have a length of 1, in which case, the job will only run once.
...
on: push
jobs:
build:
strategy:
matrix:
node-version: [12, 14, 16]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Get code
- uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
...
...
jobs:
build:
strategy:
matrix:
node-version: [12, 14, 16]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 18
operating-system: ubuntu-latest
exclude:
- node-version: 12
operating-system: windows-latest
runs-on: ${{ matrix.os }}
...
You can reference another workflow to create reusable workflows.
...
deploy:
needs: build
uses: ./.github/workflows/reusable-demo.yml
...
The reusable workflow should be run with workflow_call
in the on
section.
https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
...
on:
workflow_call:
inputs:
artifact-name:
description: The name of the deployable artifact
required: false
default: dist
type: string
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/download-artifact@v3
with:
name: $ {{ inputs.artifact-name }}
...
...
deploy:
needs: build
uses: ./.github/workflows/reusable-demo.yml
with:
artifact-name: dist-files
...
...
deploy:
needs: build
uses: ./.github/workflows/reusable-demo.yml
secrets:
some-secret: ${{ secrets.some-secret }}
...
name: Reusable
on:
workflow_call:
outputs:
result:
description: The result of the deployment operation
value: ${{ jobs.deploy.outputs.outcome }}
jobs:
deploy:
outputs:
outcome: ${{ steps.step-result.outputs.step-result }}
runs-on: ubuntu-latest
steps:
...
- name: Set result output
id: step-result
run: echo "step-result=success" >> $GITHUB_OUTPUT
...
The following should run AFTER the previous workflow has finished.
...
jobs:
...
print-deploy-result:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Print deploy out
run: echo "${{ needs.deploy.outputs.result }}"
The workflow can run in your specified container so all the following steps will run inside that container.
See https://hub.docker.com/ for a list of more containers.
env
values are provided to the container, if the container needs them.
...
jobs:
test:
environment: testing
runs-on: ubuntu-latest
container:
image: node:16
env:
some-value: value
...
An example of a service container could be for creating a DB that exists for testing purposes only.
E.g. Spinning up a test DB instance that the pipeline could use to test against during pushes to the repo.
...
jobs:
test:
environment: testing
runs-on: ubuntu-latest
env:
MONGODB_CONNECTION_PROTOCOL: mongodb
MONGODB_CLUSTER_ADDRESS: 127.0.0.1:27017 # This address:port must be used if this is not running in a container. This could be 'mongodb' if container was node.
MONGODB_USERNAME: root
MONGODB_PASSWORD: example
PORT: 8080
services:
mongodb:
image: mongo
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
...
https://docs.github.com/en/actions/creating-actions
https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
- Combine multiple existing workflow steps into one single action
- Combine
run
commands anduses
commands - Allows for reusing shared steps
See ./Docker Examples for an example
name: "Get and cache dependencies"
description: "Get the dependencies and cache them."
runs:
using: "composite"
steps:
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
shell: bash
...
name: "Get and cache dependencies"
description: "Get the dependencies and cache them."
inputs:
caching:
description: 'Whether to cache dependencies or not'
required: false
default: 'true'
runs:
using: "composite"
steps:
- name: Cache dependencies
if: inputs.caching == 'true'
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true' && inputs.caching != 'true'
run: npm ci
shell: bash
...
name: "Get and cache dependencies"
description: "Get the dependencies and cache them."
inputs:
caching:
description: 'Whether to cache dependencies or not'
required: false
default: 'true'
outputs:
used-cache:
description: 'Whether cache was used'
value: ${{ steps.install.outputs.cache }}
runs:
using: "composite"
steps:
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
id: install
if: steps.cache.outputs.cache-hit != 'true'
run: |
npm ci
echo "cache=${{ inputs.caching }}" >> $GITHUB_OUTPUT
shell: bash
...
https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action
name: "Deploy to AWS S3"
description: "Deploy a static website to AWS S3"
runs:
using: "node16"
main: "main.js"
JavaScript actions require NPM packages to function and call various processes.
E.g. console.log
becomes core.notice
const core = require('@actions/core')
const github = require('@actions/github')
const exec = require('@actions/exec')
function run() {
core.notice('Hello from my custom JavaScript Action!')
}
run()
Note: Ensure you PUSH your node_modules
folder as GitHub will not grab the packages for you.
Remember, .gitignore
could be preventing the dist
path from being uploaded. Ensure .gitignore
prevents /dist
instead of dist
.
name: "Deploy to AWS S3"
description: "Deploy a static website to AWS S3"
inputs:
bucket:
description: 'The s3 bucket name',
required: true
bucket-region:
description: 'The region of the bucket',
required: false
default: 'us-east-1'
dist-folder:
description: 'The folder containing the deployable files'
required: true
runs:
using: "node16"
main: "main.js"
...
// Get inputs
const bucket = core.getInput('bucket', { required: true })
const bucketRegion = core.getInput('bucket-region', { required: true })
const distFolder = core.getInput('dist-folder', { required: true })
...
exec.exec(`aws s3 sync ${distFolder} ${s3Uri} --region ${bucketRegion}`)
See other examples on how to set env
values
name: "Deploy to AWS S3"
description: "Deploy a static website to AWS S3"
inputs:
bucket:
description: 'The s3 bucket name'
required: true
bucket-region:
description: 'The region of the bucket'
required: false
default: 'us-east-1'
dist-folder:
description: 'The folder containing the deployable files'
required: true
outputs:
website-url:
description: 'The URL of the deployed website'
runs:
using: "node16"
main: "main.js"
const websiteUrl = `http://${bucket}.s3-website-${bucketRegion}.amazonaws.com`
core.setOutput('website-url', websiteUrl)
https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action
name: "Deploy to aws s3"
description: "Deploy a static website to s3"
inputs:
bucket:
description: "The s3 bucket name"
required: true
bucket-region:
description: "The region of the bucket"
required: false
default: "us-east-1"
dist-folder:
description: "The folder containing the deployable files"
required: true
outputs:
website-url:
description: "The URL of the deployed website"
runs:
using: "docker"
image: "Dockerfile" # or image on docker hub
All input values are provided to docker as environment value in all caps, prefixed with INPUT.
E.g. bucket
becomes INPUT_BUCKET
with open(os.environ['GITHUB_OUTPUT'], 'a') as gh_output:
print(f'website-url={website_url}', file=gh_output)
Note: Pathing is relative to the root of the application, not the path of the current workflow.
Alternatively, the path could be to a repo of the action.
...
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Load & cache dependencies
uses: ./.github/actions/cached-deps
...
Note: If using an action from INSIDE your repo, ensure you have used actions/checkout
to retrieve that action.
- A value, set outside a workflow, is used in a workflow
- E.g. Issue title used in a workflow shell command
- Workflow / command behaviour could be changed
Set user-entered data as an environment variable to circumvent this
- Actions can perform any logic, including potentially malicious logic
- E.g. A third-party action that reads and exports your secrets
- Only use trusted actions and inspect code of unknown / untrusted authors
https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
- Consider avoiding overly permissive permissions
- E.g. Only allow checking out code ('read-only')
- Actions supports fine-grained permissions control
By default, workflows run with "full" permissions.
Setting permissions can be on a workflow level or job level.
...
on:
issues:
types:
- opened
permissions:
issues: write
For providing specific, short-lived authentication and authorisation into third-party (Azure, AWS, GCP) services.