23/03/2025
In this project, we will set up a basic CI/CD pipeline. To illustrate this, we will use a basic Node.js and Express app.Our goal is that each time we commit something to the main GitHub branch, we want to run our tests. If the tests pass, then we will deploy our app in production.For this project, the production environment is a VM running Debian.
Set up the app
npm init -y
Install Express, Mocha, and Chai (for the tests). Here, we install chai@4.3.10
as it handles CommonJS imports, while version 5 does not.
npm install express
npm install mocha chai@4.3.10 supertest --save-dev
Create the Express app
Create an app.js file:
// app.js
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/api", (req, res) => {
res.json({ message: "API is working" });
});
module.exports = app;
Create a file server.js and start the server:
// server.js
const app = require("./app");
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Let’s start the local server :
node server.js
Go to http://localhost:3000 and check if that « Hello World! » appears.
Add the tests
Create a test folder:
mkdir test
Create the Unit tests
Create the file test/unit.test.js:
// test/unit.test.js
const { expect } = require("chai");
describe("Basic Math", () => {
it("should add two numbers correctly", () => {
expect(2 + 2).to.equal(4);
});
it("should check if a string contains a word", () => {
const str = "Hello World";
expect(str).to.include("World");
});
});
Create the integration tests
Create the file test/integration.test.js:
// test/integration.test.js
const request = require("supertest");
const app = require("../app");
describe("Integration Tests", () => {
it("GET / should return Hello World", async () => {
const res = await request(app).get("/");
expect(res.status).to.equal(200);
expect(res.text).to.equal("Hello World!");
});
it("GET /api should return JSON response", async () => {
const res = await request(app).get("/api");
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal({ message: "API is working" });
});
});
Set up npm scripts
Modify your package.json:Modifie ton package.json :
{
"name": "nodejs-ci-cd",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node server.js",
"test": "mocha test/**/*.test.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.2"
},
"devDependencies": {
"mocha": "^11.1.0",
"chai": "^4.3.10",
"supertest": "^7.1.0"
}
}
Test locally
Run the tests:
npm test
You should get something like
> mocha test/**/*.test.js
Integration Tests
✔ GET / should return Hello World
✔ GET /api should return JSON response
✔ GET /api should return JSON response
Basic Math
✔ should add two numbers correctly
✔ should check if a string contains a word
5 passing (31ms)
Put the code on GitHub
Create a repository and add it to your project, follow the instructions from Github
Set up GitHub Actions
Create the .github/workflows directory:
mkdir -p .github/workflows
Add the .github/workflows/ci.yml file:
touch ci.yml
name: CI for Node.js App
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
Push the code on Github and check if the tests are passing
Check the Actions tab of your repo to ensure that the tests pass.
Set up automated deployment when tests pass
Be sure you have Node.js installed on your server, and Git.
Clone your project:
git clone <URL_DE_TON_REPO> ~/apps/nodejs-ci-cd
cd ~/apps/nodejs-ci-cd
npm install
Use PM2 to keep it running.
sudo npm install -g pm2
pm2 start server.js --name "nodejs-app"
pm2 save
Automate deployment
Create the deploy.sh script on your VM:
nano ~/apps/nodejs-ci-cd/deploy.sh
Paste this :
#!/bin/bash
cd ~/apps/nodejs-ci-cd
git pull origin main
npm install
pm2 restart nodejs-app
Make it executable
chmod +x ~/apps/nodejs-ci-cd/deploy.sh
Add deployment to the pipeline
Update your ci.yml:
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ vars.SERVERHOST }}
username: ${{ vars.SERVERUSER }}
port: ${{ vars.SERVERPORT }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: "bash /home/antoine/apps/nodejs-ci-cd/deploy.sh"
Add the infos to Github
As you may have noticed, we use environment variables from Github action.
We also need to setup ssh jeys to let Github Action run the commands for the deployement on our VM
Let’s run this command to generate the key :
ssh-keygen -t ed25519 -C "your_email@example.com
This command will generate two files
id_ed25519 (the private key)
id_ed25519.pub (the public key)
By default the keys will be created in ~/.ssh/
Set the permissions on the private key
chmod 600 ~/.ssh/id_ed25519
Now we need to put the public key in the list of keys that can be used to connect to our VM
Copy the contents of your public key:
cat ~/.ssh/id_ed25519.pub
Then append the public key to the authorized_keys file:
echo "your_public_key_here" >> ~/.ssh/authorized_keys
Ensure the file is in the right location (~/.ssh/authorized_keys) and has the correct permissions:
chmod 600 ~/.ssh/authorized_keys
Restart SSH on the server:
sudo systemctl restart ssh
Be sure the VM allows connecting with a key, to do so open :
nano /etc/ssh/sshd_config
Verify that the following lines are present and not commented out (no # at the beginning of the line):
PubkeyAuthentication yes
PasswordAuthentication no # you can keep this value to yes even if it's less secure
Restart ssh
systemctl restart ssh
To verify if you can acutally connect to the server with the key you cna test it like so :
ssh -i ./yourPrivate.key -p 22 user@<VM-ip>
Add the private key to Github
Go to the folder where you saved your SSH keys :
cd /root/.ssh
Get the private key :
cat id_ed25519
It should look like this :
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEZWQyNTUxOQAAACDfyqxZPq/jVxqg
1PM7J/v2D3gF41UbwGBj3G8W+M4uM79fQDRNdgkPm4F0dghhAAAAJQCn9Flfp/RZ
X6cAAAAg3kBIUgB0dI3AwIjk5GMQXoEDCnD2MyHHo7HvDWud4G/cDpK53M8ZdAHg
dW9V1bWoGR2cU6BAM+xPM4PiRwA==
-----END OPENSSH PRIVATE KEY-----
Take the key and store it in a secret variable in GitHub Actions as SSH_PRIVATE_KEY
.
Conclusion
Now you should have a basic working CI/CD pipeline for your Node.js app with GitHub Actions.
