CI Build & Test On PRs: A Step-by-Step Guide

by Ahmed Latif 45 views

Introduction

Hey guys! Today, we're diving into setting up Continuous Integration (CI) for our projects. Specifically, we're going to focus on building and testing our code whenever a Pull Request (PR) is made to the main branch. This is super important because it helps us catch any issues early on, ensuring that our main branch stays healthy and stable. We'll be using GitHub Actions, which is a fantastic tool for automating our workflows directly within our GitHub repositories. So, let's get started!

Why CI on PRs is Crucial

First off, let's talk about why running builds and tests on PRs is so crucial. Imagine you're working on a big project with a team of developers. Everyone's making changes, and without a proper CI system, it's like driving on a highway without traffic lights. Changes can collide, and you might end up with a merge conflict or, even worse, broken code in your main branch.

By implementing CI, we're essentially putting up those traffic lights. Every time someone opens a PR, our CI system kicks in, building the code and running tests. This automated process gives us immediate feedback on whether the changes introduce any issues. Think of it as a safety net – it catches problems before they make their way into the main codebase. This not only saves us time in the long run but also keeps our development process smooth and efficient.

Furthermore, CI helps us maintain code quality. When tests are run automatically, we can ensure that new features or bug fixes don't break existing functionality. It also encourages developers to write tests, which is always a good practice. In short, CI on PRs is a cornerstone of modern software development, enabling us to deliver high-quality software with confidence. We'll walk through setting this up step by step, so you can implement it in your own projects.

Setting Up the Workflow

Okay, so how do we actually set this up? The first thing we need to do is create a workflow file in our GitHub repository. This file tells GitHub Actions what to do when certain events occur, like a PR being opened. Let's break down the steps:

  1. Create the Workflow File:

    • In your repository, navigate to the .github/workflows directory. If these directories don't exist, you'll need to create them.
    • Create a new file, for example, ci.yml. This is where we'll define our workflow.
  2. Define the Workflow:

    • Open the ci.yml file in a text editor.
    • We'll start by giving our workflow a name and specifying the trigger events. Here’s what the basic structure looks like:
    name: CI
    
    on:
      pull_request:
        branches: [ main ]
    
    • In this snippet, name: CI simply names our workflow "CI." The on section specifies when this workflow should run. We're using pull_request, which means the workflow will run on any pull request. The branches: [ main ] line further specifies that it should only run when a PR is made against the main branch. This ensures that we're only running CI on PRs that are intended to be merged into our main codebase.
  3. Define the Jobs:

    • Next, we need to define the jobs that our workflow will execute. Jobs are essentially sets of steps that run in a specific environment. For our CI setup, we'll have jobs for building the frontend and backend, as well as running tests.
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Set up Node.js
            uses: actions/setup-node@v2
            with:
              node-version: '16'
          - name: Install dependencies
            run: npm install
          - name: Build
            run: npm run build
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Set up Node.js
            uses: actions/setup-node@v2
            with:
              node-version: '16'
          - name: Install dependencies
            run: npm install
          - name: Run tests
            run: npm run test
    
    • Let’s break this down:
      • jobs: is the main section where we define our jobs.
      • We have two jobs: build and test.
      • runs-on: ubuntu-latest specifies that these jobs should run on the latest version of Ubuntu, which is a common choice for CI environments.
      • steps: lists the individual steps that will be executed in each job.
      • uses: actions/checkout@v2 is a predefined action that checks out our repository code. This is a crucial first step in any workflow.
      • name: Set up Node.js and the corresponding uses line use another predefined action to set up Node.js. We're specifying version 16 in this example, but you should use the version that matches your project's requirements.
      • name: Install dependencies and run: npm install install the project dependencies using npm (Node Package Manager). If you’re using Yarn, you would use yarn install instead.
      • name: Build and run: npm run build execute the build command defined in our package.json file. This typically involves compiling our frontend code.
      • Similarly, name: Run tests and run: npm run test execute the test command, running our project’s tests.
  4. Commit and Push:

    • Once you've created and configured your ci.yml file, commit the changes and push them to your GitHub repository.
  5. See it in Action:

    • Now, whenever you open a PR against the main branch, GitHub Actions will automatically trigger this workflow. You can see the progress and results in the "Actions" tab of your repository. This is where the magic happens – you’ll get real-time feedback on your code changes!

Expanding the Workflow

The basic workflow we've set up is a great starting point, but there's so much more we can do to enhance our CI pipeline. Here are a few ideas to consider:

Adding More Tests

Our current workflow runs the tests defined in our package.json, but we can add more specific tests to ensure our code is rock-solid. For example, we might want to include linting, which checks our code for style issues, or end-to-end tests, which simulate user interactions with our application. By expanding our test suite, we can catch a wider range of potential issues.

Parallelizing Jobs

If our build and test processes take a long time, we can speed things up by running jobs in parallel. GitHub Actions allows us to define multiple jobs that run concurrently. For example, we could run our frontend tests and backend tests at the same time. This can significantly reduce the overall CI time, especially for larger projects.

Integrating with Code Coverage Tools

Code coverage tools help us measure how much of our code is being tested. By integrating these tools into our CI pipeline, we can track our coverage over time and identify areas that need more testing. This can help us improve the overall quality of our codebase.

Adding Status Badges

Status badges are a great way to visually represent the status of our CI pipeline. We can add a badge to our repository's README file that shows whether the latest build and tests passed or failed. This provides a quick and easy way for anyone to see the health of our project.

Deployment

For many projects, CI is just the first step in a larger continuous delivery (CD) pipeline. We can extend our workflow to automatically deploy our application to staging or production environments after the tests pass. This allows us to automate the entire software release process.

Frontend and Backend Builds

Now, let's dive deeper into building both the frontend and backend. Often, modern applications have distinct frontend and backend components, each with its own build process and dependencies. We need to ensure that our CI workflow handles both of these effectively.

Frontend Build

The frontend typically involves compiling JavaScript, CSS, and other assets into a production-ready bundle. This often involves tools like Webpack, Parcel, or Rollup. Our CI workflow needs to replicate this process.

  1. Install Frontend Dependencies: We've already covered this in our basic workflow, but it's worth reiterating. We use npm install (or yarn install) to install the necessary frontend dependencies.
  2. Run the Frontend Build Command: This is usually defined in our package.json file under the scripts section. Common commands include npm run build or yarn build. Our workflow should execute this command to build the frontend.
  3. Cache Dependencies: To speed up our CI process, we can cache the frontend dependencies. This means that we don't have to download them every time we run the workflow. GitHub Actions provides a caching mechanism that we can use for this purpose.

Backend Build

The backend build process can vary depending on the language and framework we're using. For example, if we're using Node.js, the build process might involve transpiling TypeScript or bundling our code. If we're using Java, it might involve compiling the code and packaging it into a JAR file. If you are using Python, it could involve setting up the environment and installing the dependencies.

  1. Set Up the Backend Environment: This might involve installing a specific version of Node.js, Java, Python, or another runtime. GitHub Actions provides actions for setting up various environments.
  2. Install Backend Dependencies: Just like with the frontend, we need to install the backend dependencies. This might involve using npm install, mvn install (for Maven), pip install (for Python), or another package manager.
  3. Run the Backend Build Command: This command depends on our backend framework. For example, it might be npm run build, mvn compile (for Maven), or a custom script that packages our application.
  4. Run Backend Tests: Running backend tests is crucial for ensuring that our APIs and other backend components are working correctly. This involves defining test scripts in our package.json or using a testing framework specific to our language (e.g., Jest for Node.js, JUnit for Java, Pytest for Python).

Example: Frontend and Backend Jobs

Here's an example of how we might define separate jobs for building the frontend and backend in our ci.yml file:

jobs:
  frontend-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install frontend dependencies
        run: npm install
      - name: Build frontend
        run: npm run build
  backend-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install backend dependencies
        run: npm install
      - name: Build backend
        run: npm run backend:build

In this example, we have two separate jobs: frontend-build and backend-build. Each job has its own set of steps for setting up the environment, installing dependencies, and running the build command. This allows us to build the frontend and backend independently, which can speed up our CI process.

Running Existing Frontend Tests

Now that we're building our frontend, it's crucial to run our existing frontend tests as part of the CI process. This ensures that any new changes don't break existing functionality.

Test Frameworks

There are several popular testing frameworks for frontend applications, such as Jest, Mocha, and Cypress. The specific steps for running tests will depend on the framework we're using.

  1. Jest: Jest is a widely used testing framework developed by Facebook. It's known for its simplicity and ease of use. To run Jest tests, we typically use the npm test command, which is defined in our package.json file.
  2. Mocha: Mocha is another popular testing framework that provides a flexible and extensible testing environment. It's often used in conjunction with assertion libraries like Chai and mocking libraries like Sinon.
  3. Cypress: Cypress is a more comprehensive testing framework that's designed for end-to-end testing. It allows us to simulate user interactions with our application and verify that everything is working as expected.

Configuring the Workflow

To run our frontend tests in the CI workflow, we need to add a step to our ci.yml file that executes the test command. This is typically the same command we use to run tests locally (e.g., npm test or yarn test).

Here's an example of how we might add a test step to our workflow:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test

In this example, we've added a test job that runs on the latest version of Ubuntu. The steps include checking out our code, setting up Node.js, installing dependencies, and running the tests using npm test. This simple addition ensures that our tests are run automatically whenever we open a PR, giving us valuable feedback on the quality of our code.

Test Reports and Artifacts

It's often useful to generate test reports as part of our CI process. These reports can provide detailed information about test results, including which tests passed, which failed, and any error messages. We can also configure our workflow to upload test reports as artifacts, which can be downloaded and reviewed later.

GitHub Actions provides several actions for generating and uploading test reports. For example, we can use the actions/upload-artifact action to upload test reports as artifacts. We can also use third-party tools like Codecov or SonarCloud to track code coverage and quality metrics.

Conclusion

Alright, folks! We've covered a lot in this article. We've seen why setting up CI on PRs is a game-changer for software development, ensuring code quality and stability. We walked through the step-by-step process of creating a GitHub Actions workflow, from setting up the basic structure to building frontend and backend components and running tests.

By implementing these practices, you're not just automating tasks; you're building a robust safety net for your codebase. This means fewer surprises, smoother merges, and ultimately, more reliable software. Remember, CI is an investment in your project's future. It might seem like extra work upfront, but the payoff in terms of reduced bugs and increased development speed is well worth it. So, go ahead, set up CI for your projects, and watch your development process transform! Keep coding, and I’ll catch you in the next one!