GitHub actions in action
GitHub Actions is a continuous integration and deployment tool offered by GitHub. With GitHub actions, you can automate workflows and tasks directly within your GitHub repository.
GitHub Actions are made up of one or more jobs, which can be triggered by a specific event, such as a push to a branch or a pull request being opened. Each job is made up of one or more steps, which define a specific action to be taken. These actions can include running a script, deploying code, sending notifications, and more.
GitHub actions provide a wide range of pre-built actions that can be used out of the box, as well as the ability to define your own custom actions. Actions are defined in the YAML file, which can be stored in your repository inside the .github/workflows
directory.
GitHub has a section of a large variety of pre-made actions that can be added to your project.
Creating first action
Let's create an action from scratch. In .github/workflows
directory create a file with the .yaml
or .yml
extension. Here's an example of a simple GitHub Actions workflow that installs NPM dependencies and runs tests:
1name: Node Tests2
3on:4 push5
6jobs:7 build:8 runs-on: ubuntu-latest9
10 steps:11 - name: Checkout code12 uses: actions/checkout@v213
14 - name: Install dependencies15 run: npm install16
17 - name: Run tests18 run: npm test
push
- workflow is triggered by anygit push
event and runs a series of steps on an Ubuntu-based runner.jobs
- defines a list of jobs we want to be executed.checkout
- is considered a third-party action. The checkout step clones your repository into the runner environment, so further any files from your repository can be accessed by the runnerrun
- in the next two steps we runnpm install
to install all dependencies, and run tests.
Note. If any of the steps fail, the entire job will fail, and the workflow will stop.
Environment variables
Environment variables can be defined globally or scoped to specific jobs. Global variables are available to all jobs. Here is an example of environment variable configuration
1name: ENV variables2
3on:4 push5
6env:7 MY_KEY: R@ndomKey20238
9jobs:10 env-vars:11 runs-on: ubuntu-latest12 env:13 API_KEY: RandomAPIKey14 steps:15 - name: Print Global Environment Variables16 run: echo $MY_KEY17 - name: Print Job Environment Variables18 run: echo ${{ env.API_KEY }}
We defined two steps, the first one for accessing the global environment variable and the second for accessing the local environment variable that is defined under the jobs
.
Notice that we are using different syntax for accessing env variables, although it's not necessary which syntax you will use, although there is a difference between them.
- Interpolation
${{ env.API_KEY }}
- typically used when you want to explicitly indicate that you are accessing an environment variable defined within the workflow. - Variable reference
$API_KEY
- looks for environment variable in the shell instance
Schedule job
GitHub actions support events that allow to run some jobs periodically using crontab syntax. Here a is simple example of how we can run a Python script every hour.
1name: Periodic job2
3on:4 schedule:5 - cron: "0 * * * *" # Run every hour6
7jobs:8 run:9 name: Run Python script10 runs-on: ubuntu-latest11 steps:12 run: my_script.py
NOTE: Crontab.guru allows to use of crontab syntax with ease of use.
Conditions
Conditions allow to use the if
statement if you need to skip some job or step. For example, we would like to run a job only when changes are being pushed or merged into main
branch.
1name: Run on the main branch2
3on:4 push5
6jobs:7 run-if-main:8 if: ${{ github.ref_name == 'main' }}9 runs-on: ubuntu-latest10 steps:11 - run: echo Job successfully executed
Please note that you must use single quotes and not double quotes in order to execute the job.
Persist data between jobs
By default job doesn't persist any data, since each job runs within its own isolated runner. To persist data between runners and jobs we will use outputs. The outputs of a job will be accessible to all other jobs that have a dependency on it.
Let's see an example of how to read .nvmrc
file and use its output to install a specific Node version.
1name: outputs-demo2on:3 push4
5jobs:6 job1:7 runs-on: ubuntu-latest8 outputs:9 nodev: ${{steps.step1.outputs.nodev}}10 steps:11 - name: Checkout code12 uses: actions/checkout@v213
14 - id: step115 name: Read .nvmrc file16 run: echo "nodev=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"17
18 job2:19 needs: job120 runs-on: ubuntu-latest21 steps:22 - name: Get node version23 uses: actions/setup-node@v324 with:25 node-version: ${{needs.job1.outputs.nodev}}26
27 - name: Print node version28 env:29 VERSION: ${{needs.job1.outputs.nodev}}30 run: echo "$VERSION"
outputs
- map an output to a job output,nodev
will be like a global variable that can be accessed at any step.${{steps.step1.outputs.nodev}}
- here we're accessing a specific step with idstep1
and specifying that result should be stored in thenodev
.run: echo "nodev=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
- in this step data from.nvmrc
file is printed usingcat
and stored in thenodev
variable$GITHUB_OUTPUT
- env variable is used so theGitHub
can recognize output variables
needs: job1
-needs
property specifies that this job should run sequentially to avoid race conditions when executing jobs.node-version: ${{needs.job1.outputs.nodev}}
- finally we are accessing data output fromjob1
that is stored in thenodev
variable.
There is other way how you can persist data using actions/cache or using strategy.matrix
Creating Reusable Workflows
Reusable workflows allow to reference entire workflow inside another workflow. Let's take our previous example and include this workflow into another workflow file, but before we will slightly modify it
1name: output-reusable-workflow2
3on:4 workflow_call:5 inputs:6 message:7 required: true8 type: string9
10# Jobs from the previous section11jobs:12 job1:13 ...14
15 job2:16 ...17
18 - name: Print message19 run: echo ${{ inputs.message }}
workflow_call
- event that helps us define some parameters that can be passed to workflow with the name message
Now let's define another workflow in a separate file where we call this reusable workflow.
1name: Reusable workflows2
3on: push4
5jobs:6 reusable:7 uses: github-username/repository/.github/workflows/outputs-reusable-workflow.yaml@main8 with:9 message: "Hello, from reusable workflow"
uses
- inside the jobs we definereusable
job where inside theuses
we can specify which workflow we want to use. We specify GitHub username and repository name with the full path to the workflow filewith
- is used to specify parameters that will be passed down to the workflow
Bonus Tips
- To test GitHub actions locally instead of triggering action by pushing changes to your repo you can use actpackage its written in a Go lang and can be installed with
brew install act
Act requires Docker in order to run workflow inside container. Here is how you can run workflow
1act -W .github/workflows/demo.yaml
- By default job is triggered when certain event is called, like
push
, but sometimes we want to call job manually and this can be achieved usingworkflow-dispatch
1on:2 workflow_dispatch:
This will add Run workflow
button in the GitHub UI
Conclusion
We've explored some essential functionality of the GitHub actions. There is a lot of thins to explore and you definitely should visit they're documentation. All examples that we explored in this article can be found in this repository.