GitHub actions in action

GitHub actions in action

5 min read
Edit this post
Read on DevTo

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 Tests
2
3on:
4 push
5
6jobs:
7 build:
8 runs-on: ubuntu-latest
9
10 steps:
11 - name: Checkout code
12 uses: actions/checkout@v2
13
14 - name: Install dependencies
15 run: npm install
16
17 - name: Run tests
18 run: npm test
  • push - workflow is triggered by any git 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 runner
  • run - in the next two steps we run npm 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 variables
2
3on:
4 push
5
6env:
7 MY_KEY: R@ndomKey2023
8
9jobs:
10 env-vars:
11 runs-on: ubuntu-latest
12 env:
13 API_KEY: RandomAPIKey
14 steps:
15 - name: Print Global Environment Variables
16 run: echo $MY_KEY
17 - name: Print Job Environment Variables
18 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.

  1. Interpolation ${{ env.API_KEY }} - typically used when you want to explicitly indicate that you are accessing an environment variable defined within the workflow.
  2. 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 job
2
3on:
4 schedule:
5 - cron: "0 * * * *" # Run every hour
6
7jobs:
8 run:
9 name: Run Python script
10 runs-on: ubuntu-latest
11 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 branch
2
3on:
4 push
5
6jobs:
7 run-if-main:
8 if: ${{ github.ref_name == 'main' }}
9 runs-on: ubuntu-latest
10 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-demo
2on:
3 push
4
5jobs:
6 job1:
7 runs-on: ubuntu-latest
8 outputs:
9 nodev: ${{steps.step1.outputs.nodev}}
10 steps:
11 - name: Checkout code
12 uses: actions/checkout@v2
13
14 - id: step1
15 name: Read .nvmrc file
16 run: echo "nodev=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
17
18 job2:
19 needs: job1
20 runs-on: ubuntu-latest
21 steps:
22 - name: Get node version
23 uses: actions/setup-node@v3
24 with:
25 node-version: ${{needs.job1.outputs.nodev}}
26
27 - name: Print node version
28 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 id step1 and specifying that result should be stored in the nodev.
  • run: echo "nodev=$(cat .nvmrc)" >> "$GITHUB_OUTPUT" - in this step data from .nvmrc file is printed using cat and stored in the nodev variable
    • $GITHUB_OUTPUT - env variable is used so the GitHub 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 from job1 that is stored in the nodev 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-workflow
2
3on:
4 workflow_call:
5 inputs:
6 message:
7 required: true
8 type: string
9
10# Jobs from the previous section
11jobs:
12 job1:
13 ...
14
15 job2:
16 ...
17
18 - name: Print message
19 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 workflows
2
3on: push
4
5jobs:
6 reusable:
7 uses: github-username/repository/.github/workflows/outputs-reusable-workflow.yaml@main
8 with:
9 message: "Hello, from reusable workflow"
  • uses - inside the jobs we define reusable job where inside the uses we can specify which workflow we want to use. We specify GitHub username and repository name with the full path to the workflow file
  • with - is used to specify parameters that will be passed down to the workflow

Bonus Tips

  1. 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
  1. 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 using workflow-dispatch
1on:
2 workflow_dispatch:

This will add Run workflow button in the GitHub UI

Github actions workflow

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.

Comments