NUS Hackers Wiki
NUS Hackers Wiki
  • NUS Hackers Wiki
  • Hackerschool
    • Virtual Machines and Linux
    • Beginners' Guide to the Terminal
      • Introduction to the Terminal
      • Modern Shell Tools
      • Shell Scripting
      • Real World Scripting
      • Resources
    • Self-Hosting: Three Easy Pieces
      • 1. Setting up your server
      • 2. Running Services
      • 3. Monitoring your server
    • Vim
    • Introduction to Zig
      • Language Basics
      • Error Handling
      • Memory Management
      • Working with C
      • Exploring comptime
    • CI/CD with Github Actions
      • Background
      • Basics of Github Actions
        • Target workflow
        • Running unit tests
        • Linting code
        • Deploying to Github Pages
      • Advanced use cases
        • Pollers
        • Github script
        • Executing third-party scripts
        • Reusable workflows
      • Cookbook
    • Lightning Git
      • Git Concepts
      • Getting Started with Git
      • Making your first commit
      • Branching
      • Merge Conflicts
      • Integrating remote repositories
      • Collaborative Workflows
      • Commit Manipulation and Reflog
      • Interactive rebasing
      • filter-repo
  • Orbital
    • JavaScript
      • Browser Developer Tools
      • Getting Started
      • Datatypes
      • Operators and Operations
      • Loops and Conditions
      • Functions
      • Strings
      • Arrays
      • HTML
        • Getting Started
        • Tag Attributes
        • HTML Forms
        • Browser Inspector
      • CSS
        • Selectors
        • Colors in CSS
        • Measurements in CSS
        • The Box Model
        • Adding Styles - Part 1
        • Adding Styles - Part 2
      • Working with the DOM
        • Querying the DOM - Selectors
        • Querying the DOM - Element Attributes
        • Querying the DOM - Element Styles
        • Events with JS and HTML
        • Exercise: Click Counter
        • Editing the DOM
        • Fetch Requests
        • Exercise: The NUSMods API
    • React
      • Setup
      • State
    • React Native
      • Setup
      • Intro to JSX
      • Basic Syntax
      • Handling UI
      • Props
      • State Management
    • Git
      • Setup
      • Command Glossary
      • Fundamental Concepts
        • Getting Started
        • Integrating Remote Repositories
        • Branching
        • Merge Conflicts
      • Collaborative Workflows
        • Fork and PR Workflow
        • Branch and PR Workflow
      • Advanced Concepts
        • Ignoring Files
        • Commit Message Conventions
        • Github Collaborators
        • CI/CD with Github Actions
        • Advanced Git Commands
      • FAQ
    • Telegram Bot
      • Creating a TeleBot
      • API Calls
      • Telebot Basics
      • Integrating API's
    • Relational Database
      • Database Overview
      • Database Design
      • Entity Relationship Diagram
      • SQL Basics & PostgreSQL
    • TypeScript
      • Types and Interfaces
      • Utility Types
      • Typing Component Props, Events, and Hooks
      • Why You Should Avoid Using any (and What to Do Instead)
      • TypeScript Tricks You’ll Use All the Time in React
Powered by GitBook
On this page
  • Constructing workflows
  • Designing the workflow
  • Breaking it down
  • Workflow name
  • Workflow triggers
  • Defining jobs
  • Fetching the current repository
  • Setting up Node.js
  • Installing dependencies
  • Running unit tests
  • Visualizing the workflow
  • Verifying the workflow
  • Manually running workflows
  • pull_request event
Edit on GitHub
Export as PDF
  1. Hackerschool
  2. CI/CD with Github Actions
  3. Basics of Github Actions

Running unit tests

This section will tackle our very first task: running unit tests.

Constructing workflows

The most fundamental questions that Github Actions workflows answers are:

  1. When will it run?

  2. What will it do?

"When will it run?" is answered by specifying the events that the workflow responds to. In our scenario, we want this workflow to run when a pull request is created on the repository.

"What will it do?" can be then broken down into several more guiding questions?

  1. Which OS should this run in?

    1. Are there key differences between OSes that should be accounted for?

  2. What programming language is this project written in?

  3. What steps should be run to achieve the given workflow?

  4. Can certain tasks be split into separate jobs and run in parallel?

  5. What are jobs that depend on others?

We will take a look at how we can answer questions (4) and (5) in a following section. Let's first tackle questions (1), (2), and (3) in our scenario.

  1. In the case of calculator.test.ts, the OS we run it on does/should not matter as there are no OS specific test cases. For simplicity, we will pick Ubuntu.

  2. The project is written in React, so it depends on having Node.js available.

  3. We first need to get the repository, install all necessary dependencies, and run the test script that is provided (see package.json)

Designing the workflow

Now that we have clearly outlined the key details of this workflow, let's get down to writing your very first workflow file. Remember, all Github Actions workflows must reside in .github/workflows, so create a new file called ci.yml!

# .github/workflows/ci.yml
name: CI/CD Pipeline
on: [pull_request, workflow_dispatch]
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch repository
        uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      - name: Install dependencies
        run: |
          yarn
      - name: Run unit tests
        run: |
          NODE_ENV=production yarn test

Breaking it down

Let's break down each section and explain what we're doing.

Workflow name

name: CI/CD Pipeline

We are giving the workflow a name that Github Actions will display. If the name is omitted, GitHub displays the workflow file path relative to the root of the repository.

Workflow triggers

on: [pull_request, workflow_dispatch]

In this case, we specify the list of events as an array in YAML. However, there are instances where you might want to specify additional conditions on when the workflow may run (such as only executing when the pull request's target branch is the main branch). In that case, you will need to use the YAML dictionary definition. We will cover this in a bit.

Defining jobs

jobs:
  unit-tests:

We then start to specify the jobs that comprise the workflow. We give the job a job ID of unit-tests.

    runs-on: ubuntu-latest

Fetching the current repository

    steps:
      - name: Fetch repository
        uses: actions/checkout@v4

We declare the steps of a job under the steps array as a list of dictionaries. As we are running the job in a virtual machine, we will basically have an empty machine at the start of the job, and it is our responsibility to start populating and interacting with this empty machine to create the intended workflow.

The very first step we need to do is fetch the current repository, so that the virtual machine runner has access to the project structure, and more importantly, the unit tests. We give the step a human readable name (that is also displayed on Github) using the name key. If there is no name provided, Github will display the script or action that is being used.

Setting up Node.js

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'

We can specify inputs for the action using the with key, providing the various inputs as a dictionary. In this case, we want to use Node.js version 20, and we want to use the yarn package manager — instead of npm — as that is what we have used for the example application.

Installing dependencies

      - name: Install dependencies
        run: |
          yarn

Before we can run the unit tests, we need to ensure that all of the necessary project dependencies are retrieved. This is where we can use scripts in steps.

So, we declare our script through the run string. The pipe operator after the run key indicates that we are specifying a multi-line string in YAML.

The script we will run this time is yarn, which effectively installs the project's dependencies. You can think of this as running yarn directly in an Ubuntu terminal, where the current folder ($GITHUB_WORKSPACE) is the root of the Node.js project.

Running unit tests

      - name: Run unit tests
        run: |
          NODE_ENV=production yarn test

Finally, we can start to execute the unit tests of the project. Again, we use a script, but this time, the script will be NODE_ENV=production yarn test which effectively sets the environment variable NODE_ENV to be of value production.

test is a script that we have provided in the example application, and it essentially runs vitest , executing calculator.test.ts.

Voilà! You have successfully written your very first Github Actions workflow! Simple isn't it? Let's recap what we did.

Visualizing the workflow

At a glance, this is the high-level overview of the new CI/CD pipeline you have written.

To properly visualize and understand how the filesystem of the virtual machine runner changes throughout the job, we have also created this visualization (bolded text are the changes between steps):

Verifying the workflow

Manually running workflows

Once you have added the workflow, you need to commit and push it!

git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push -u origin main

Then, we can start to verify that the Github Action works as intended! This is where the workflow_dispatch event comes in handy, where we are able to manually trigger Github Actions. If you navigate to your fork of the example repository, you can visit the Actions tab. You will see the following:

It lists the workflows available, and what has run/are running. We are interested in our new pipeline CI/CD Pipeline, so select it and you should see the following:

It looks almost the same, but this time, there is a banner that tells you that "This workflow has a workflow_dispatch event trigger". Then, there is a dropdown to "Run workflow", select it and stick with main and click the "Run workflow" button:

Refresh the page, and you will now see a new entry in the table:

Give it a few seconds and then click into the action. You will see that the unit tests have failed:

This is because we have intentionally made one of the unit tests to fail (divide ½ is not 0.4!). Let's try to fix this unit test while exploring the the pull_request event!

pull_request event

This guide is not a software engineering exercise, so we will tell you exactly where the error is and we will focus on examplifying the pull_request event.

test('divide 1 / 2 to equal 0.5', () => {
  expect(divide(1, 2)).toBe(0.4);
});

We have set the expected value to be 0.4 when it should clearly be 0.5 ! Let's fix this as a pull request to your own repository to see the pull_request event in action.

Create a new branch, called fix-unit-test:

git checkout -b fix-unit-test

Then, go to the file calculator.test.ts and fix line 13 to be the following:

  expect(divide(1, 2)).toBe(0.5);

Then, add the file and create a commit. The commit message can be anything you want:

git add src/calculator.test.ts && git commit -m "Fix unit test"

Finally, push the branch to your fork:

git push -u origin fix-unit-test

Then, go to your fork on Github and create a new pull request. Pull request > New pull request. Give the pull request any title and you can leave the description blank.

The base branch should be YOUR main branch, not the original repository's branch!

Create the pull request and wait a while. You will notice that the component towards the bottom changes to this:

This is how you know that your workflow is running and it was triggered by the pull_request!

Now, if you select the CI/CD pipeline running, you will be brought back to the same page as earlier, instead, this time, you will notice that the workflow passes!

In fact, you will even see the individual steps of the job unit-tests that you defined! Wonderful! You have successfully:

  1. Created a new Github Actions workflow

  2. Observed how a failing unit test might look like

  3. Fixed and verified that the pull_request event is working

Go ahead and merge the pull request into main and update your local repository to receive the latest changes:

git checkout main
git fetch origin main && git merge origin/main

Now that we have implemented the very first step, let's take a look at implementing step 2: linting!

PreviousTarget workflowNextLinting code

Last updated 2 months ago

You might be asking yourself, "What if I have unit tests that are specific to an OS? Or version of Node.js? Or Ubuntu version?" Github Actions supports something it calls , that run a given workflow across a matrix of variables. We will briefly dive into it in the Cookbook!

Github Actions uses a -based specification language to define its workflows. It might seem a little strange at first, but it becomes more intuitive as you work with it. For a crash course on YAML syntax, refer to

Then, we specify the times where this workflow runs, aka "when will it run?". They are specified with the on key and the values correspond to the .

In our case, we know that we want to run the workflow during a pull request, so we have included the pull_request event. We have also included the workflow_dispatch event, so that we are able to manually trigger this workflow from Github without requiring a pull request (more about it ). This is particularly useful if we want to verify that the workflow works without going through the hassle of creating a pull request.

Recall that we said that every job runs in its own runner, which is a virtual machine by default. Therefore, we need to specify the OS that our unit-tests job will execute in, which we have decided earlier to be Ubuntu. As we are not particular about the version of Ubuntu we will use, we can use the ubuntu-latest , which is one of the .

This means that we can think of every step of this job executing within an Ubuntu virtual machine (because they really do!). So we will be using bash commands, and we will have access to things like the that is available in Ubuntu.

Here, we use an action — which are reusable extensions that perform some set of operations — called actions/checkout@v4. You can read more about what the action is comprised of , but essentially, we are using it to perform a checkout of the current repository, retrieving all of its contents (from the latest commit) onto the virtual machine runner.

The virtual machine runner has a root directory defined as $GITHUB_WORKSPACE . Without any other inputs for the actions/checkout@v4 action, we default to fetching the contents of the repository directly into the $GITHUB_WORKSPACE directory. If you are wondering how you can verify this information, you can refer to the appendix's which covers how to do so!

Then, we need to setup Node.js on the virtual machine runner. We can use the action actions/setup-node@v4 to setup Node.js automatically for us. You can read more about the action .

Recall in Defining jobs, we mention that all steps in the job are effectively running on Ubuntu, so we will specify scripts that use Ubuntu's built-in shell: bash. You might want to specify a different shell for certain use cases, which .

Again, if you are wondering how you can verify this information, you can refer to the cookbook's recipe on which covers how to do so!

The following steps expects some level of understanding of Git. You can refer to our !

We had intentionally set one of the unit test assertions to be incorrect, specifically along :

matrix strategies
YAML
this guide.
list of events that trigger a workflow
here
many available Github-hosted runners
apt package manager
here
here
you can do so here
Git guide for more information
these lines
debugging Github Actions
debugging Github Actions