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"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.
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, likepush
orpull_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.
- Have a
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: CI2on:3 pull_request:4 branches:5 - main6 paths:7 - '**.js'8 - '**.jsx'9 - '**.json'10 - '**.yml'1112concurrency:13 group: ${{ github.ref }}14 cancel-in-progress: true1516jobs:17 install-cache:18 runs-on: ubuntu-latest19 strategy:20 matrix:21 node: [16]22 steps:23 - name: Checkout commit24 uses: actions/checkout@v325 - name: Use Node ${{ matrix.node }}26 uses: actions/setup-node@v327 with:28 node-version: ${{ matrix.node }}29 cache: npm30 - name: Install dependencies31 run: npm ci --legacy-peer-deps32 - name: Cache Cypress binary33 uses: actions/cache@v334 id: cache-cypress35 with:36 path: ~/.cache/Cypress37 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}38 restore-keys: |39 cypress-binary-40 unit-test:41 runs-on: ubuntu-latest42 strategy:43 matrix:44 node: [16]45 needs: install-cache46 steps:47 - name: Checkout commit48 uses: actions/checkout@v349 - name: Use Node ${{ matrix.node }}50 uses: actions/setup-node@v351 with:52 node-version: ${{ matrix.node }}53 cache: npm54 - name: Install dependencies55 run: npm ci --legacy-peer-deps56 - name: Run unit tests57 run: npm test58 build:59 runs-on: ubuntu-latest60 strategy:61 matrix:62 node: [16]63 needs: unit-test64 steps:65 - name: Checkout commit66 uses: actions/checkout@v367 - name: Use Node ${{ matrix.node }}68 uses: actions/setup-node@v369 with:70 node-version: ${{ matrix.node }}71 cache: npm72 - name: Install dependencies73 run: npm ci --legacy-peer-deps74 - name: Run build75 run: npm run build76 - name: Upload build artifacts77 uses: actions/upload-artifact@v378 with:79 name: build-output80 path: |81 .cache82 public83 retention-days: 184 e2e-test-chrome:85 runs-on: ubuntu-latest86 strategy:87 matrix:88 node: [16]89 needs: build90 steps:91 - name: Checkout commit92 uses: actions/checkout@v393 - name: Use Node ${{ matrix.node }}94 uses: actions/setup-node@v395 with:96 node-version: ${{ matrix.node }}97 cache: npm98 - name: Install dependencies99 run: npm ci --legacy-peer-deps100 - name: Restore Cypress binary101 uses: actions/cache@v3102 id: cache-cypress103 with:104 path: ~/.cache/Cypress105 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}106 restore-keys: |107 cypress-binary-108 - name: Download build artifacts109 uses: actions/download-artifact@v3110 with:111 name: build-output112 - name: Run Cypress113 uses: cypress-io/github-action@v4114 with:115 start: npm run serve116 wait-on: 'http://localhost:8000'117 browser: chrome118 install: false119 e2e-tests-firefox:120 runs-on: ubuntu-latest121 strategy:122 matrix:123 node: [16]124 needs: build125 steps:126 - name: Checkout commit127 uses: actions/checkout@v3128 - name: Use Node ${{ matrix.node }}129 uses: actions/setup-node@v3130 with:131 node-version: ${{ matrix.node }}132 cache: npm133 - name: Install dependencies134 run: npm ci --legacy-peer-deps135 - name: Restore Cypress binary136 uses: actions/cache@v3137 id: cache-cypress138 with:139 path: ~/.cache/Cypress140 key: cypress-binary-${{ hashFiles('**/package-lock.json') }}141 restore-keys: |142 cypress-binary-143 - name: Download build artifacts144 uses: actions/download-artifact@v3145 with:146 name: build-output147 - name: Run Cypress148 uses: cypress-io/github-action@v4149 with:150 start: npm run serve151 wait-on: 'http://localhost:8000'152 browser: firefox153 headed: true154 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.
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.