Setup basic CI/CD with Github Action and Node.js /
DevOps Fundamentals

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.