GitHub Actionsο
GitHub Actions is an automation tool that allows developers to automate their workflows, build, test, and deploy their code directly from their GitHub repository.
Components of GitHub Actionsο
A GitHub Actions workflow can be triggered when an event occurs in your repository. A workflow contains one or more jobs which can be run sequentially or in parallel. Each job runs inside a runner (VM or container), and has one or more steps that either runs a script or an action.
- Workflow:
Configurable automated process
Defined by yaml file
Triggered by event, manually or on defined schedule
Defined in
.github/workflows
- Events:
GitHub activity that triggers workflow e.g. pull request, push to branch, issue opening
- Jobs:
A set of steps that will be executed on the same runner
You can share data between steps in the same job
Jobs run in parallel, but can be configured to be dependent on each other
- Actions:
Custom application that does a frequently repeated task
You can write your own, or find them on GitHub Marketplace
- Runners:
Server that executes a job
This can be a GitHub provided VM or you can host your own runner
Workflow YAML fileο
name:: The name of the workflow that appears in the βActionsβ tab in GitHubrun-name:: The name of the workflow run that will appear on GitHubThis can be customised by incrementing a number or showing the username of the person that triggered the action e.g.
run-name: ${{ github.actor }} workflow runon:: Specifies the trigger(s) for the workflowjobs:: Groups together jobs that run in the workflowruns-on:: Specifies where the job will be runsteps:: Lists the steps for the jobuses:: Specifies the pre-defined action that a step will userun:: Tells the job to execute a command on the runnerneeds:: Used when a job needs to wait for another job to complete
Using Variablesο
You can add custom variables to your worflow YAML file. In the example the variables will be available to the given script.
steps:
- run: my_script.scr
env:
MY_VAR_1: 53
MY_VAR_2: hello
Using Scripts and Shell Commandsο
You can either run just a single command, or a script from your local repo, provided with a shell type
steps:
- run: npm install -g bats
- name: Run script
run: ./.github/scripts/build.sh
shell: bash
Workflow Triggers - Eventsο
pull_reqest
You can specify a branch/branches with branches:
You can specify which particular file changes will trigger this with paths:. You can use expressions here like β*,pyβ to trigger on a change in a python file.
workflow_dispatch: Allows you to run the workflow manually from GitHub actions tab
repository_dispatch: Allows you to trigger a workflow using a webhook.
schedule: Can set it to run at specified times, e.g. using the cron tag
on:
schedule:
- cron: "0 4 * * *" # run at 4am (UTC) every day
workflow_call: Uses Githubβs reusable workflow model, workflow is called from another workflow.
Actions Filesο
- The actions you use can be located in:
The same repo as the worflow file
Any public repo
A published Docker container image on Docker GitHub
See GitHub Marketplace for some pre-defined actions.
Adding Actionsο
- From Marketplace:
Copy the uses tag to your worflow e.g.
uses: actions/upload-artifact@v3.1.2The action may require you to provide inputs
- From Same Repo:
You can either use
{owner}/{repo}@{ref}or./path/to/dirsyntax
- uses: ./.github/actions/hello-world-action
- From a Public Repo:
Use
{owner}/{repo}@{ref}
- A container from Docker Hub
Use
docker://{image}:{tag}
Action Release Managementο
You should indicate the version of the action youβd like to use based on your comfort with accepting automatic updates.
Note
It is recommended to use the SHA commit value when using third-party actions
- Using tags
uses: actions/javascript-action@v1.0.1
- Using SHA
uses: actions/javascript-action@a824008085750b8e136effc585c3cd6082bd575f
- Using branches
uses: actions/javascript-action@main
Action Inputs and Outputsο
To see the inputs and outputs of an action, check the action.yml in the root of the repo.
name: "Example"
description: "Receives file and generates output"
inputs:
file-path: # id of input
description: "Path to test script"
required: true
default: "test-file.js"
outputs:
results-file: # id of output
description: "Path to results file"
Action File Fieldsο
See Metadata Syntax for GitHub Actions.
name
author
description
inputs : see above section
outputs : see above section
- runsSpecifies how the action is executed
runs composite actions:
runs: using: "composite" steps: - name: "Run Sphinx Build Script" run: $GITHUB_ACTION_PATH/script.sh shell: bash
runs Docker container:
runs: using: 'docker' image: 'Dockerfile'
Warning
Be careful when your Dockerfile is not in the root of your repo since it cannot access anything above it in the directory tree. If this is the case you will have to use script to run docker from the root pointing at the Dockerfile further down the directory tree
branding: Create an icon that is shown on GitHub marketplace
Running Scriptsο
You can run scripts as part of your composite steps section of either a action file or workflow file.
using: "composite"
steps:
- name: "Run Sphinx Build Script"
run: $GITHUB_ACTION_PATH/script.sh
shell: bash
Note
$GITHUB_ACTION_PATH specifies the path to the action file it is running from.
Note
Even if the script is started from a random place in the repo, it seems that the working directory when the script starts is the root of the github repo.
If you want a script to run, you have to make sure it has itβs permission set to executable. This executable status is included when you commit the file to GitHub.
sudo chmod +x <your_script.sh>
Warning
A script might still exit with a successful exit code even if one of the commands returned an
error code. This could make your workflow seem like it succeeded. You can use the set -e in
your script file to cause the script to exit with error on the first command that returns an error
Expressionsο
Expressions are used to programmatically set environment variables.
You use special syntax to evaluate as an expression: ${{ <expression> }}.
If you use the expression within an if conditional, you can omit the expression syntax above.
Examplesο
steps:
- uses: actions/hello-world-javascript-action@e76147da8e5c81eaf017dede5645551d4b94427b
if: ${{ <expression> }}
env:
MY_ENV_VAR: ${{ <expression> }}
Literalsο
You can use: boolean, null, number or string data types
env:
myNull: ${{ null }}
myBoolean: ${{ false }}
myIntegerNumber: ${{ 711 }}
myFloatNumber: ${{ -9.2 }}
myHexNumber: ${{ 0xff }}
myExponentialNumber: ${{ -2.99e-2 }}
myString: Mona the Octocat
myStringInBraces: ${{ 'It''s open source!' }} # note the '', which is required to output It's
Operatorsο
Logical operators:
|| && ! != == <= < > >=Index:
[]Logical grouping:
()Property dereference:
.
GitHub uses loose equality comparisons: If types donβt match, variable is cast to a number.
NULL ->
0- Boolean
true ->
1false ->
0
- String
Empty string ->
0Parsed from any legal JSON format, otherwise
NaN
Array ->
NaNObject ->
NaN
Warning
A comparison of one NaN to another NaN doesnβt result in a true
Note
GitHub ignores case when comparing strings
Note
Objects and array are only considered equal when they are the same instance
Functionsο
GitHub provides some builtin functions. Some functions cast input to a string:
NULL ->
''Boolean ->
'true'or'false'Number -> decimal format
Array/Object: Not converted to a string
Functions:
contains( search, item): does the string contain the given slicestartsWith( search, item): does the string start with the given sliceendsWith( search, item): does the string end with given sliceformat( string, replaceValue0...): Formats a string, variables inserted in{N}`, where N is an integerjoin( array, opetionalSeperator): concatenates elements into a stringtoJSON( value): prints JSON representation of a valuefromJSON(value): returns JSON data type for valuehashFiles(path): returns the hash for one or multiple files given by pathsuccess(): checks none of the previous steps have been cancelled or failedalways(): always executes, even if step is cancelledcancelled(): true if workflow is cancelledfailure(): returnstrueif any previous step or ancestor job fails
Contextsο
Contexts are a way to access information about workflow runs, variables, runner environments, jobs, and steps. Each context is an object that contains properties, which can be strings or other objects.
Examples of some context types include: github, env, vars, job, steps, runner, secrets, needsβ¦
As part of an expression you can access context info in two ways:
github['sha']
github.sha
Note
Attempting to dereference a non-existent property evaluates to an empty string
You can print a context to the log if you want to see what is inside of them:
run: echo '${{ toJSON(github) }}'
run: echo '${{ toJSON(steps) }}'
Contexts have many attributes so it is best to look here for documentation.
Variablesο
Variables provide a way to reuse non-sensitive configuration information.
You can set environment variables on a workflow level, in the workflow YAML file, or across multiple workflows at the organisation, repository or environment level.
Single Workflow Variablesο
You can set workflow variables at three levels:
Workflow level
Job level
Step level
Note
You can list all variables available at a particular step by using run: env
Configuration Variables across multiple workflowsο
Warning
These are still in beta and subject to change
Configuration variables can be set at organisation, repository or environment level. Configuration variables are available in the vars context
If variables have the same name, the one with the lowest level takes precedence: organisation < repository < environment.
You can add configuration variables in the GitHub settings. This is same for secrets.
Example Commenting on PRο
on:
pull_request:
types: [opened, synchronize, reopened]
In the example, we see that this pull_request event is triggered when the PR is opened and re-opened. Synchronize triggers the event when the PR is updated with a push.
- name: Update PR
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'π Check the Sphinx build passed before merge!'
})
The step in the example uses the GitHub provided github-script action which is used for issues and PRs.
In GitHub, a PR is treated the same as an issue.
The step will only run if the triggering event is of type pull_request, which is extracted from the github
context.
Note
The nice thing about this event type is that it will actually block the PR from being merged until the workflow has completed successfully. This is done automatically.
GitHub Actions Securityο
Check out this link: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
Triggering Actions in another Repoο
You can use action files from another repo in your workflow.
If you are using your own action, it is recommended to have a separate repository for your action. However it is also possible to have your action within your repo with other things in it.
For example, an action file in actions/my_action/action.yml
To call an action from another repo, you have to alter your repo settings to allow calls from other repositories to access actions.
runs-on: ubuntu-latest
steps:
- name: "Test-action"
uses: <repository_owner>/<repository_name>/actions/my_action@<branch_name or reference>
using: "composite"
steps:
- run: |
python ${{ github.action_path }}/../my_script.py
shell: bash
Note
As seen in the example, you need to use the github.action_path to reference files
in relation to the actions file.
Doing it this way even allows you to call actions in the same repository you are in. The other method you can use, is to first checkout the repository containing the action you want to run, and then call the action locally:
- uses: actions/checkout@v4 # need to checkout repo first in this case
- uses: ./.github/<action_name>
In this case the action version is determined by the reference you use to checkout the repository containing the action.
Note
In theory you can have an action.yml anywhere in your repo, not just in the .github/actions. It makes more sense as a public action repo to have the action in the repo root.
Calling Reusable Workflowο
You can call a workflow both in the same repo or in another repo.
uses: <org>/<repo_name>/.github/workflows/my_workflow.yml@main
# OR
uses: ./.github/workflows/my_workflow.yml
Job flow controlο
Here is an example of some job control flow. You can use the needs keyword to
achieve this.
jobs:
job1:
...
job2:
needs: job1
# note not in ${{ }}
if: |
always() &&
( needs.job1.result != 'skipped' )
The use of always means that this job will still be evaluated even if the previous
dependent job is failed or cancelled or skipped.
Workflow inputsο
Undefined workflow inputs are treated as empty strings.
on:
workflow_dispatch:
workflow_call:
my_input:
description: ''
required: true
type: boolean
# Later on if you call inputs.my_input, if called from workflow_call it will be the input boolean,
# if from workflow_dispatch (i.e. undefined), it will be an empty string ''
If-Elseο
Github actions has a shorthand for doing if else type statements.
${{ inputs.use_local_image == 'true' && '--use_local_image' || '' }} \
${{ inputs.image_tag && format('--image_tag="{0}"', inputs.image_tag) || '' }} \
If the expression is evaluated to True, then the statement after the && will be used.
If the expression is evaluated to False, then the statement after the || will be used.
Setting Github Actions step outputο
To do this you simply write to the GITHUB_OUTPUT environment file.
To access the output from a step, you need to assign the step an id.
outputs:
number-of-days:
description: "Number of days since unix epoch - UTC"
value: ${{ steps.get-days.outputs.days }}
runs:
using: "composite"
steps:
- id: get-days
# -u gives UTC, +%s gives value in seconds, / 86400 converts seconds to days
run: echo "days=$(( $(date -u +%s) / 86400 ))" >> $GITHUB_OUTPUT
shell: bash
Writing to Github Actions Output from pythonο
Wrtiting to the Github Output like this will be the variable will be available in the step.output for the action step calling the python script.
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as var:
var.write(f"run_job_config={json.dumps(run_job_config)}\n")
Note
json.dumps from a dictionary is a good way to get data in a nice format for using in Github Actions.
Debugging in Github Actionsο
Sometimes it is nice to view a context in github actions for debugging. It is not possible to simply echo. This method can be used for any json based github actions data.
- name: "Debug Job Output"
run: |
echo "OUTPUTS:"
python -c 'import json; json_string=${{ toJson(steps.process-request.outputs.temporary_labels) }}; \
print(f"temporary_labels = {json.dumps(json.loads(json_string), indent=2)}")'
Evaluation of inputsο
Actions inputs are always strings!
If an input has a default of "" , then you can do something like what follows in an Actionβ¦
run: |
python my_script.py ${{ inputs.marks && format('--marks="{0}"', inputs.marks) || '' }} \
In a workflow, if you have an input as a boolean type, you can do the following in a step if
- if: inputs.use_test_image
# if using a ! you should use ${{ }} since ! is something in yaml syntax
you can also do this in a workflow step:
run: |
python my_script.py ${{ inputs.use_test_image && '--image_name=test_image' || '' }} \
Matrix Jobs:ο
Matrix jobs allow you to run a dynamic number of jobs in parallel.
test-session:
needs: [setup-test-session, set-temp-labels]
if: |
always() &&
((needs.setup-test-session.result == 'success') &&
((needs.set-temp-labels.result == 'success') ||
(needs.set-temp-labels.result == 'skipped')))
strategy:
fail-fast: false
matrix:
test-run: ${{ fromJson(needs.setup-test-session.outputs.run_job_config) }}
runs-on: [self-hosted, "${{ matrix.test-run.runner_label }}"]
Format for Json inputsο
with:
labels: '{"<runner_name>": ["<label1>"]}'
This is how you can do json as an input. Note the use of β and β.
This is because you should pass json as a string within github actions, and use
the toJson and fromJson to do the conversions between strings and json.
From the Github UI, use: {"<runner_name>": ["<label1>"]} without external β or β