Building a CI (Continuous Integration) pipeline using GitHub Actions

·

8 min read

Introduction to CI/CD

CI/CD stands for Continuous Integration and Continuous Delivery. Somewhere CD is also Continuous Deployment.

Continuous Integration

It is a software developer best practice. We will continuously take a new code that we write and we integrate it to the main code that is being shared to every one.

Consider many developers are working on a code and integrating it. To make sure that code in source repository works for every system, we need a server.

The server will detect when the code has been updated in the source repository, will build it (if required), run the automated tests (if required) and will take the result and report back to the development team.

We can add more checks to our CI process.

CI became popular with the rise of AGILE methodology and what we call as extreme programming so teams can work together.

Extreme programming means releasing our software many times a day instead of once a month.

There have been many tools that are created to make the CI pipelines. Some of them are

  1. CircleCI

  2. Travis CI

  3. GitHub Actions

  4. Jenkins

Continuous Delivery

Continuous Delivery builds on CI. CD gives confidence that our code is in production. It adds an extra guarantee into our process that each time our we add code in our main branch it is ready to be delivered.

We don't need to release our code every time but we need to be sure that our code is ready to go in production anytime. This lets us schedule our product to be released for beta testers so we can get early feedback.

Continuous Deployment

This takes the continuous delivery and automates it. It is not for every product. Continuous Deployment is a process we use to release our software.

Some products need to go through heavy testing and documentation and other manual processes, in such a case automatic deployment is not a wise decision.

Consider some underwater software that detects the depth, the error in such a case will cost a human life or any costly equipment. So manual look at the working of code and testing is required before the software used in the machine is set for release.

Pipelines

Pipeline is the set of steps needed to complete the CI/CD processes.

GitHub actions

As per the official documentation, GitHub actions is a CI/CD tool to automate our software workflows. It helps to build, test and deploy our code right from the GitHub.

When actions detects that some new code is ready, it runs through the 'workflow'. It will go through the different steps of this workflow to automate the different actions that we (might) want to perform in our CI pipeline.

CI pipeline can include actions like running unit tests or even deploying our code in different environments.

GitHub actions can work on any platform and can work with any programming language.

If some tests or actions of the CI process fails, the pipeline will break and it will notify the developer.

Getting familiar with terms

.yml or YAML is the file format in which we script our workflows.

There is a ci.yml file which is ** workflow configuration file ** . It contains the steps that are going to be performed by our CI server.

It contains a list of indented structure that specifies the steps needed to be taken by our CI pipeline.

This is the sample YAML file available on GitHub Actions

on: push
jobs:
  test:
    strategy:
      matrix:
        platform: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.platform }}
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: 16
    - run: npm install-ci-test
    - uses:

  publish:
    needs: [test]
    steps:
    - uses: actions/checkout@v3

It contains a list of ** jobs ** that will be done on ** push ** on code.

One of the the jobs this file will do is ** test **

Whatever we are putting in our matrix value, which is under the ** strategy ** here, whatever value we put *** platform: [ubuntu-latest, macos-latest, windows-latest] *** include all the different ways that our job will run. *** In this case it will run once on each platform. ***

We add different values beside ** platform ** . For example we can add different versions of the node.js runtime on which our code needs to run on.

We define the actions that need to be taken on ** push ** action under the ** steps ** property. We only need one line that our job is going to use in this ie actions/checkout@v3 The checkout actions has already been built for us so anyone can share their code and check it in the CI environment.

The shared actions that others have built can be used by just putting in the name of the action. Those actions can be found moving horizontally on the github actions page .

Setting up GitHub actions

We will make our own CI pipeline for our project. Find the code here

Go the ** actions ** tab in the repository. You will find the different starter workflows there.

We will skip all of it and will create our own workflow for this project.

We will see GitHub providing us starter workflows. But we will skip all of this and will write our GitHub actions workflow in our VS Code editor.

GitHub reads the YAML file for CI pipeline.

Defining the workflow

To define the workflow we we create a folder at the top the repository. We will create a folder named ** .github ** which will contain our GitHub configuration file.

.github will contain a folder named ** workflows ** .Workflows will contain the yaml configuration file that we are aiming to make in this section.

Create a file named *** node.yml *** as we are creating a backend project here.

Creating a build pipeline

We will start small. We will just build our frontend and backend using yaml command. We are going to run this pipeline whenever there is some new code is added to the repository.

We will start by naming our pipeline as

name: NASA Project CI

Now we have to write the action for push in our repository

on: 
  push:
    branches: [master]
  pull_request:
    branches: [master]
jobs:
  build:
  deploy: 
    needs: build

In the above portion we are doing the following things:

1 . We are specifying the 'on' keyword that takes two scenarios

2 . One scenario is 'push' action and other scenario is 'pull_request' x.

3 . In these push and pull_request actions we are mentioning the branch that will be affected. In this case it will be [master] branch

4 . We specified the name and on parameters, now we will specify the 'jobs' our yml file will perform.

5 . The 'needs' parameters ensures that the 'deploy' action will only perform when 'build' action has been completed.

Understanding the complete YAML code

name: NASA Project CI
on:
  push: 
    branches: [master]
  pull_request:
    branches: [master]
jobs: 
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js version 16
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build --prefix client

In previous section we have seen what is the meaning 'jobs' keyword. We have added a bunch of new keywords. Let's understand the meaning of these words now

runs-on tells the operating system on which the server will run. Here we will be using the ubuntu-latest which is linux based

uses specifies the steps what will be used for our workflow. It takes the name of the actions. GitHub provides some actions our we can create our own. These are scripts that we can reuse so it is wise to specify the version number of the script. So ** actions/checkout@v2 ** @v2 specifies the version.

with specifies what has to be done along with uses parameter. We want to specify the more specific option with our actions like here we want to run our node at version 16. We passed '16' as the string of our node-version.

name is used to make a user more familiar with what our job is doing in any step. Her we have specified as Use Node.js version 16

run allows us to run our own command. It will use our OS to run the command we passes. We used the npm install and npm run build --prefix client. The will be run for our server and client.

We will now push this file in our repository and since our pipeline will start on push event the workflow should start.

Finding existing actions on GitHub

We used the existing actions to launch our Node workflow, so we need to know where these actions are available on Github for our future use.

We need to go to the Marketplace tab of the GitHub.

We will need to search for any particular action, say set up Node and it will give us a bunch of options along with instructions on how to use them.

We will find tons of different actions already been made for us to use which were built by other users.

Testing our code in many different environments

We will add a new command which tests both our client and server.

- run : npm test

In our client side code, our npm test is running in watch mode which means it cannot stop on it's own. We will specify that we are running a CI workflow and hence GitHub actions will disable the watch mode

env:
      CI: true

We can also specify the PORT number on which we want to specify the app to run.

We can run our tests in different environments using 'strategy' option. We will set different variables inside 'matrix' option.

Consider we want to make sure our code runs on two different versions of the Node. We can set property name 'node-version- inside matrix

strategy:
      matrix: 
        node-version: [14.x, 16.x]

We now need to specify the node-version from the matrix we specified. It will be done like this

with:
          node-version: ${{matrix.node-version}}

The GitHub workflow file will look something like this after following all the steps


name: NASA Project CI
on:
  push: 
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs: 
  build:
    env:
      CI: true
    strategy:
      matrix: 
        node-version: [14.x, 16.x]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js version ${{matrix.node-version}}
        uses: actions/setup-node@v3
        with:
          node-version: ${{matrix.node-version}}
      - run: npm install
      - run: npm run build --prefix client