Tutorial - CI / CD using GitHub Actions
Easy CI/CD Setup with GitHub Actions: A Practical Tutorial
CI/CD
In software projects, CI/CD plays a crucial role in ensuring the quality and reliability of the codebase. Whether it’s an open-source or private project, CI/CD pipelines streamline the development process and help manage code changes effectively. With multiple contributors and teams working on the project, having a robust CI/CD pipeline is essential. Here are some key aspects of CI/CD that benefit all types of software projects:
1. Code Integration
CI/CD pipelines are triggered whenever new code changes are pushed to the repository. The system automatically integrates the code, ensuring that all changes smoothly merge with the existing codebase. This automatic integration reduces the chances of conflicts and streamlines the development process.
2. Automated Testing
Automated testing is a cornerstone of modern development workflows. In CI/CD pipelines, comprehensive tests are run with each code change. These tests include unit tests, integration tests, and end-to-end tests to ensure that the code is stable, functional, and ready for further development or deployment. The continuous feedback helps developers identify and fix bugs early, enhancing the quality of the code.
3. Workflows
CI/CD workflows define a set of tasks to be executed when changes are pushed to the repository. These workflows may include building the project, running tests, generating documentation, or deploying the code to different environments. With tools like GitHub Actions, and Jenkins, teams can automate these processes to reduce manual intervention and increase efficiency.
4. Code Quality Checks
Code quality checks are integrated into the CI/CD pipeline to ensure that the code adheres to predefined standards. These checks typically involve linting, static code analysis, and security scans to catch potential issues early on. By automating these processes, CI/CD pipelines help maintain high code quality, improve readability, and prevent technical debt from accumulating.
5. Deployment Automation
Continuous Deployment (CD) automates the process of deploying code changes to the target environments once all checks pass. This reduces the risk of human error, accelerates release cycles, and ensures that the deployment process is consistent and repeatable. Whether deploying to production, staging, or testing environments, CD pipelines make it easier to keep everything synchronized and up-to-date.
Creating a CI/CD Pipeline in GitHub
To create a CI/CD pipeline in GitHub, you can define your workflows in the .github/workflows directory of your repository. Each workflow is represented by a YAML file that specifies the triggers, jobs, and steps to be executed. This configuration allows you to automate various aspects of the development process, from integration and testing to deployment. Below are the key components for setting up a GitHub CI/CD pipeline:
1. Workflow Triggers
Workflows are triggered by specific events in the GitHub repository, such as code pushes, pull requests, or scheduled events. You can configure these triggers in the on section of the YAML file, specifying the conditions under which the pipeline should run.
2. Jobs
A workflow consists of one or more jobs. Jobs are executed in parallel by default, but you can define dependencies between them if needed. Each job contains a series of steps that are executed sequentially within the job.
3. Steps
Steps are individual commands or actions that run as part of a job. These steps can include installing dependencies, running tests, building the application, or deploying it to a server. You can use pre-built GitHub Actions or define custom commands within each step.
Example CI/CD Workflow
name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Deploy to server
run: ./deploy.sh
In this example:
• The pipeline is triggered by pushes and pull requests to the main branch.
• The build job installs dependencies and runs tests on an Ubuntu runner.
• The deploy job runs after the build job completes successfully, deploying the application to a server.
Creating a CI Pipeline
Our task here is ensure when someone creates a pull request (PR), the project is built successfully before allowing the PR to be merged. This helps catch any build errors early in the development process.
Steps :
Create a file named
.github/workflows/build.yml
in the root folder of the project.Add the configuration to the .yml file
name: Build on PR
on:
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install Dependencies
run: npm install
- name: Run Build
run: npm run build
What does this workflow do ?
- It is triggered on pull requests against the
main
branch.
- It runs on an Ubuntu-based runner.
- It checks out the repository code using
actions/checkout@v3
.
- It sets up Node.js version 20 using
actions/setup-node@v3
.
- It installs the project dependencies using
npm install
.
- It runs the build command using
npm run build
.
Now,
- Commit and push the changes to the
main
branch of your forked repository.
- Create a new branch with some minimal changes and create a PR from it to the
main
branch.
- You should see the workflow run automatically when the PR is created. The build status will be displayed on the PR page.
Now let’s break down each parts of the workflow
name
: Specifies the name of the workflow, in this case, "Build on PR".
on
: Defines the event that triggers the workflow. Here, it is triggered on pull requests to themain
branch.
jobs
: Contains the list of jobs to be executed in the workflow.build
: Defines a job named "build".runs-on
: Specifies the type of machine to run the job on, in this case, an Ubuntu-based runner.steps
: Lists the steps to be executed in the job.uses: actions/checkout@v3
: Checks out the repository code.uses: actions/setup-node@v3
: Sets up Node.js with the specified version.run: npm install
: Installs the project dependencies.run: npm run build
: Runs the build script defined in thepackage.json
file.
When a pull request is opened or updated against the main
branch, this workflow will be triggered. It will check out the code, set up Node.js, install dependencies, and run the build script.
Create a CD Pipeline
But before deploying we must dockerize our application
Creating a Dockerfile for User App Deployment
In this section, we’ll walk through creating a custom Dockerfile for your application, specifically for the user-app within a monorepo setup. Docker allows you to containerize the application, providing an isolated environment for running the application consistently across different platforms.
To begin, create a new file named docker/Dockerfile.user in the root of your repository. This Dockerfile will define the steps necessary to build a Docker image for your user application.
Here’s the content of the Dockerfile.user:
FROM node:20.12.0-alpine3.19
WORKDIR /usr/src/app
COPY package.json package-lock.json turbo.json tsconfig.json ./
COPY apps ./apps
COPY packages ./packages
# Install dependencies
RUN npm install
# Generate Prisma client
RUN npm run generate-prisma
# Build only the user-app
RUN npm run build --filter=user-app
CMD ["npm", "run", "start-user-app"]
Explanation of Each Section:
1. Base Image (FROM node:20.12.0-alpine3.19)
We are using a specific version of the Node.js image as our base. In this case, node:20.12.0-alpine3.19 is an Alpine-based image for a smaller footprint. Alpine Linux is lightweight, making it perfect for containerized applications.
2. Setting Working Directory (WORKDIR /usr/src/app)
The WORKDIR instruction sets the working directory for the application inside the container. All subsequent commands will be run from this directory.
3. Copying Configuration Files (COPY package.json package-lock.json turbo.json tsconfig.json ./)
This step copies the necessary configuration files into the container. These files include package.json, package-lock.json, turbo.json, and tsconfig.json—all of which are crucial for managing dependencies and project configurations.
4. Copying Application Files (COPY apps ./apps and COPY packages ./packages)
The COPY commands copy the apps and packages directories into the container, preserving the structure of the monorepo.
5. Install Dependencies (RUN npm install
)
After the necessary files are in place, we install the dependencies defined in package.json by running npm install.
6. Generate Prisma Client (RUN npm run generate-prisma
)
This command runs a custom script (generate-prisma), which likely generates the Prisma client for database access. This is essential for any database interaction.
7. Build Only the User App (RUN npm run build --filter=user-app
)
Here, we specify that only the user-app should be built, using the --filter=user-app option. This is useful in a monorepo setup where you may want to build and deploy individual apps instead of the entire project.
8. Start the User App (CMD ["npm", "run", "start-user-app"]
)
Finally, the CMD instruction defines the command to start the user-app once the container is running. This runs the npm run start-user-app command, which should start the application.
Deploying Your Docker Image to Docker Hub with GitHub Actions
Once you’ve created your Dockerfile and containerized your application, the next step is to deploy the Docker image to Docker Hub. Docker Hub is a cloud-based registry service where you can store and share Docker images. This section will guide you through setting up a Continuous Deployment (CD) pipeline in GitHub Actions to automatically build and push your Docker image to Docker Hub.
Steps to Prepare for Deployment
1. Sign In to Docker Hub
Sign in to your Docker Hub account or create a new one if you don’t have one already.
2. Create a New Repository on Docker Hub
Create a new repository where you will store your Docker image. You’ll need to specify a repository name for your project.
3. Set Up GitHub Action Secrets
To authenticate GitHub Actions with Docker Hub, you’ll need to securely store your Docker Hub credentials in GitHub as secrets. Follow these steps:
• Go to your GitHub repository’s Settings.
• Navigate to the Secrets section in the sidebar.
• Add the following secrets:
DOCKER_USERNAME: Your Docker Hub username.
DOCKER_PASSWORD: Your Docker Hub access token .
Creating the CD Pipeline
1. Create the deploy.yml
File
In the root of your GitHub repository, create a new file at .github/workflows/deploy.yml. This file will define the steps for your deployment pipeline.
2. Configure the Workflow
Add the following YAML content to deploy.yml
. This configuration will automate the process of building and deploying your Docker image to Docker Hub every time code is pushed to the main branch.
name: Build and Deploy to Docker Hub
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./docker/Dockerfile.user
push: true
tags: your-dockerhub-username/your-repo-name:latest
- name: Verify Pushed Image
run: docker pull your-dockerhub-username/your-repo-name:latest
Breakdown of the Workflow:
• Triggering the Workflow - The on section defines that the workflow will be triggered every time code is pushed to the main branch.
• Checkout Repository - The first step, Check Out Repo, uses the actions/checkout@v2 action to pull the latest code from your GitHub repository.
• Docker Hub Login - The Log in to Docker Hub step logs into Docker Hub using the docker/login-action@v1. It uses the DOCKER_USERNAME and DOCKER_PASSWORD secrets to authenticate.
• Build and Push Docker Image - In the Build and Push Docker Image step, the docker/build-push-action@v2 is used to build the Docker image using the Dockerfile.user located in the ./docker/ directory. The image is then tagged with your-dockerhub-username/your-repo-name:latest
and pushed to Docker Hub.
• Verify Pushed Image - Finally, the Verify Pushed Image step ensures that the Docker image has been successfully pushed by pulling the image from Docker Hub and verifying its presence.
- Customizing the Workflow - Replace
your-dockerhub-username
and your-repo-name with your actual Docker Hub username and repository name. For example, if your Docker Hub username is johnDoe and your repository is user-app.
Conclusion
In this tutorial, we’ve gone through the process of setting up a Continuous Integration and Continuous Deployment (CI/CD) pipeline using GitHub Actions and Docker. These steps, from configuring the Dockerfile to deploying the image to Docker Hub, have been incorporated into my personal project. If you’d like to explore the full implementation, feel free to check out the repository here:
Project
By automating the build, testing, and deployment process, these workflows ensure a consistent, reliable, and efficient way to manage updates to your project. Implementing these best practices will help streamline your development cycle and enhance collaboration, whether you’re working on a personal project or as part of a larger team.