CI Build & Test On PRs: A Step-by-Step Guide
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:
-
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.
- In your repository, navigate to the
-
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." Theon
section specifies when this workflow should run. We're usingpull_request
, which means the workflow will run on any pull request. Thebranches: [ main ]
line further specifies that it should only run when a PR is made against themain
branch. This ensures that we're only running CI on PRs that are intended to be merged into our main codebase.
- Open the
-
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
andtest
. 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 correspondinguses
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
andrun: npm install
install the project dependencies usingnpm
(Node Package Manager). If you’re using Yarn, you would useyarn install
instead.name: Build
andrun: npm run build
execute the build command defined in ourpackage.json
file. This typically involves compiling our frontend code.- Similarly,
name: Run tests
andrun: npm run test
execute the test command, running our project’s tests.
-
Commit and Push:
- Once you've created and configured your
ci.yml
file, commit the changes and push them to your GitHub repository.
- Once you've created and configured your
-
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!
- Now, whenever you open a PR against the
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.
- Install Frontend Dependencies: We've already covered this in our basic workflow, but it's worth reiterating. We use
npm install
(oryarn install
) to install the necessary frontend dependencies. - Run the Frontend Build Command: This is usually defined in our
package.json
file under thescripts
section. Common commands includenpm run build
oryarn build
. Our workflow should execute this command to build the frontend. - 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.
- 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.
- 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. - 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. - 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.
- 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 ourpackage.json
file. - 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.
- 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!