June 14, 2023 8 min read

Take action and learn GitHub Actions!

Programmers love to automate stuff. But automation is beneficial only when it takes less time than it returns. With GitHub Actions, we may achieve that profit.

Last updated: June 14, 2023
A worn, black-and-white movie clapper board
Photo by Harald Müller

"Never spend 6 minutes doing something by hand when you can spend 6 hours failing to automate it." - the old joke goes. Even though, as programmers, we tend to overcomplicate things, there are obvious benefits of automation.

How long can you work on making a routine task more efficient before you're spending more time than you save? - xkcd comic

Think about it - it's not like your whole day is about programming. Other mundane or organizational tasks also need our attention - labeling issues, updating packages, integrating and deploying code, etc. Oh, and we shouldn't forget about everyone's favorite - meetings.

Over the last decade, developers created many automation tools and platforms like CircleCI, Jenkins, or Travis (not Scott) to solve some of the mentioned issues. But we will focus on a relatively new player in the game - GitHub Actions.

Not another CI/CD tool?

Before we learn what GitHub Actions are, we should find out what they are not. "GitHub Actions is a CI/CD tool." While doing research, I repeatedly saw this misconception. And don't get me wrong - it is, but it doesn't have to be. The use case is broader. GitHub Actions is a platform to automate tasks with workflows. CI or CD is one of many possible workflows.

Why use GitHub Actions?

Ok, but why pick them instead of other battle-tested solutions? There are a couple of reasons.

  • It doesn't require third-party integrations. Looking at statistics, you most likely use GitHub already.
  • The setup is easy. You don't need to be a DevOps specialist to use it (don't be mad at me, guys - you're invaluable for more complex stuff).
  • It abstracts low-level terminal commands and other logic. Recall how many small steps are required to start a Node.js app from scratch. Have you done it? Good, now you can forget them again because they'll be abstracted.
  • There are many integrations with various tools and tech stacks. Java, .NET, Node.js? Go to the GitHub Marketplace and find the environment you need. Oh, and there is an integration with Go.
  • You don't need to start from scratch. Mentioned marketplace offers tons of ready-to-use apps and actions. You can also base your custom actions on templates.

What is a GitHub workflow?

First, let's familiarize ourselves with basic terminology. We'll learn top-down, starting from general concepts and moving on to details.

A workflow is an automated process that will run one or more jobs. You can configure it by adding the YAML file in the .github/workflows directory. We'll take a look at syntax in the following sections.

A job is a set of steps in a workflow. Steps can run commands, set up tasks, or run a GitHub action. By default, jobs run in parallel, but you can configure them to run sequentially by adding dependencies.

An action is a single step in a job. It's a custom app for the GitHub Actions platform that performs a frequently repeated task. You can create your custom action or find many ready-to-use in GitHub Marketplace.

A runner is a server that runs your workflow when they're triggered. Each job in a workflow runs in a new virtual environment. GitHub manages those servers and offers three major operating systems: Ubuntu, Windows, and macOS.

How do GitHub Actions work?

GitHub emits events when something happens in or to your repository, like opening a PR or issue. In response to those events, you can run GitHub Actions. Those actions are also repositories. Overall, it's similar to the event-driven nature of JavaScript. So, the general idea is straightforward:

  • Listen to an event.
  • Trigger the proper workflow.

Syntax of the GitHub workflow config file

As I mentioned, you need a YAML file to configure the GitHub workflow. Let's see how to structure such a file.

  • name - the name of the workflow. The repo action page displays it.
  • on - the name of the event that triggers the workflow, like push or pull_request. There is a list of all GitHub events.
  • jobs - it's a section where you put individual jobs.
  • job-name - a customizable name for a particular task. Under this key, you put a list of steps. Each step can:
    • Have a name.
    • use a predefined action.
    • run a terminal command.
    • Be executed with some parameters.

CI pipeline with GitHub Actions

People learn best with examples (at least I do), so let's look at an example CI config.

GitHub Actions CI configYAML
1name: CI
2on:
3 pull_request:
4 branches:
5 - main
6 paths:
7 - '**.js'
8 - '**.jsx'
9 - '**.json'
10 - '**.yml'
11
12concurrency:
13 group: ${{ github.ref }}
14 cancel-in-progress: true
15
16jobs:
17 install-cache:
18 runs-on: ubuntu-latest
19 strategy:
20 matrix:
21 node: [16]
22 steps:
23 - name: Checkout commit
24 uses: actions/checkout@v3
25 - name: Use Node ${{ matrix.node }}
26 uses: actions/setup-node@v3
27 with:
28 node-version: ${{ matrix.node }}
29 cache: npm
30 - name: Install dependencies
31 run: npm ci --legacy-peer-deps
32 - name: Cache Cypress binary
33 uses: actions/cache@v3
34 id: cache-cypress
35 with:
36 path: ~/.cache/Cypress
37 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}
38 restore-keys: |
39 cypress-binary-
40 unit-test:
41 runs-on: ubuntu-latest
42 strategy:
43 matrix:
44 node: [16]
45 needs: install-cache
46 steps:
47 - name: Checkout commit
48 uses: actions/checkout@v3
49 - name: Use Node ${{ matrix.node }}
50 uses: actions/setup-node@v3
51 with:
52 node-version: ${{ matrix.node }}
53 cache: npm
54 - name: Install dependencies
55 run: npm ci --legacy-peer-deps
56 - name: Run unit tests
57 run: npm test
58 build:
59 runs-on: ubuntu-latest
60 strategy:
61 matrix:
62 node: [16]
63 needs: unit-test
64 steps:
65 - name: Checkout commit
66 uses: actions/checkout@v3
67 - name: Use Node ${{ matrix.node }}
68 uses: actions/setup-node@v3
69 with:
70 node-version: ${{ matrix.node }}
71 cache: npm
72 - name: Install dependencies
73 run: npm ci --legacy-peer-deps
74 - name: Run build
75 run: npm run build
76 - name: Upload build artifacts
77 uses: actions/upload-artifact@v3
78 with:
79 name: build-output
80 path: |
81 .cache
82 public
83 retention-days: 1
84 e2e-test-chrome:
85 runs-on: ubuntu-latest
86 strategy:
87 matrix:
88 node: [16]
89 needs: build
90 steps:
91 - name: Checkout commit
92 uses: actions/checkout@v3
93 - name: Use Node ${{ matrix.node }}
94 uses: actions/setup-node@v3
95 with:
96 node-version: ${{ matrix.node }}
97 cache: npm
98 - name: Install dependencies
99 run: npm ci --legacy-peer-deps
100 - name: Restore Cypress binary
101 uses: actions/cache@v3
102 id: cache-cypress
103 with:
104 path: ~/.cache/Cypress
105 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}
106 restore-keys: |
107 cypress-binary-
108 - name: Download build artifacts
109 uses: actions/download-artifact@v3
110 with:
111 name: build-output
112 - name: Run Cypress
113 uses: cypress-io/github-action@v4
114 with:
115 start: npm run serve
116 wait-on: 'http://localhost:8000'
117 browser: chrome
118 install: false
119 e2e-tests-firefox:
120 runs-on: ubuntu-latest
121 strategy:
122 matrix:
123 node: [16]
124 needs: build
125 steps:
126 - name: Checkout commit
127 uses: actions/checkout@v3
128 - name: Use Node ${{ matrix.node }}
129 uses: actions/setup-node@v3
130 with:
131 node-version: ${{ matrix.node }}
132 cache: npm
133 - name: Install dependencies
134 run: npm ci --legacy-peer-deps
135 - name: Restore Cypress binary
136 uses: actions/cache@v3
137 id: cache-cypress
138 with:
139 path: ~/.cache/Cypress
140 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}
141 restore-keys: |
142 cypress-binary-
143 - name: Download build artifacts
144 uses: actions/download-artifact@v3
145 with:
146 name: build-output
147 - name: Run Cypress
148 uses: cypress-io/github-action@v4
149 with:
150 start: npm run serve
151 wait-on: 'http://localhost:8000'
152 browser: firefox
153 headed: true
154 install: false

Oh boy, it's a long file. But don't be intimidated - we'll try to decipher it. Our workflow is called "CI." It will run when someone makes a pull request to the main branch. Also, changes must include files with listed extensions. Why? Because changing files like Markdown or MDX most likely won't break anything, so there is no need to run the workflow.

The concurrency section allows canceling jobs in progress when they are in the same group. For example, don't continue building a project after failing tests - it doesn't make sense.

The install-cache job starts by accessing the repository. There is a predefined action for that called just checkout. Then we need to set up the node. In our example, we'll use node 16 by accessing the number from the matrix. With a matrix strategy, you can automatically create multiple job runs based on variables.

Having a node, we can install dependencies. The npm ci command works like npm install, but developers designed it for automated environments. Finally, we're checking for cached Cypress binaries. We'll find out why later.

The unit-test job - without surprise - runs unit tests. It starts similarly to the previous job - by checking if there are cached files. We don't need to install dependencies every time we run the workflow. The last step actually runs tests.

The following job builds the project. It's an example of a sequential job. It has a dependency - the previous step. After the build process, the upload-artifact action caches the build output for one day.

The last two jobs are very similar - they both run end-to-end tests. One runs them on Chrome and one on Firefox. Those jobs must run after the build process. They try to access cached files for the project and Cypress to be efficient. After all of the jobs executed successfully, the whole workflow is finished.

GitHub Actions page with CI schema

I hope this post introduced you to GitHub Actions. After reading it, you should know why and how to use them. Maybe you'll even manage to automate some mundane tasks in under 6 hours now. We may continue this automation journey and write custom GitHub Action. But, for now, the best I can do is to offer you some helpful links.

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Front-end, web development, and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I am not a Nigerian prince to offer you opportunities. I do not send spam. Unsubscribe anytime.

Stay curious. Read more

Half of a record on white backgroundSeptember 1, 20227 min read

Accessible animations in React

Or how not to spin your users round (like a record). Some animations can make users sick. We'll take care of them and make non-essential animations optional.

Read post
List of CSS variables in Visual Studio Code.September 14, 20228 min read

Converting design tokens to CSS variables with Node.js

Converting design tokens is an error-prone process - I found about it the hard way. So, I made a simple Node.js script that will help me with that task.

Read post
Five metal gears on a black brackgroundSeptember 23, 202211 min read

Gatsby with Netlify CMS

In this post, we will look closely at a Netlify CMS. It is an example of a new type of CMS that is git-based. We will integrate it with a Gatsby example project.

Read post