DEV Community

Cover image for Automating Your Project Deployment with GitHub Actions: A Step-by-Step Guide
Gina Beki
Gina Beki

Posted on

Automating Your Project Deployment with GitHub Actions: A Step-by-Step Guide

As developers, we often spend hours repeatedly performing the same deployment tasks. Have you ever asked yourself how much time you could save if you could automate this process? In this article, I’ll walk you through implementing a CI/CD pipeline using GitHub Actions, demonstrated with a practical example project.

Understanding CI/CD in Modern Development

Think of CI/CD as your personal development assistant: First, it checks your code for any issues through automated tests and quality checks (that’s the CI part). Once everything looks good, it handles the deployment process automatically (that’s the CD part) — from security checks to the final deployment on Vercel or whatever deployment platform of your choice. It’s like having a code reviewer who ensures every code change is perfect before it goes live! 🚀

Let’s break down what each part does:

Continuous Integration (CI)

  • Automatically runs tests when you push code
  • Checks your code quality and style
  • Makes sure your code works well with existing features
  • Helps catch problems early before they reach production

    Continuous Deployment (CD)

  • Automatically deploys your tested code

  • Ensures your app is always ready to go live

  • Handles all the deployment steps for you

  • Gets your changes to users quickly and safely

Implementation Guide

Prerequisites

Before we begin, make sure you have the following installed on your machine:-

  • Node.js(v18 or higher)
  • npm(comes with Node.js)
  • Git
  • GitHub account
  • Vercel account (free tier is fine)
  • Your favorite code editor (I’m using VS Code) Project Setup

1. Install Dependencies and setting up package.json

Before we start implementing, let’s understand how our project is organized. I will guide you in creating a project that demonstrates GitHub Actions automation:

First, let’s create a repo on GitHub called automation-demo-project (or name it anything of your preference). Clone it to your local machine and cd into the project.

git clone [your-repo-url]
cd automation-demo-project
Enter fullscreen mode Exit fullscreen mode

Initialize the project using npm by running this command:-

npm init -y
Enter fullscreen mode Exit fullscreen mode

This command will generate a package.json file:-

{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {}
}
Enter fullscreen mode Exit fullscreen mode

Then run this command to install the dev dependencies that we will be using:-

npm install --save-dev eslint jest nodemon supertest

Enter fullscreen mode Exit fullscreen mode
  • eslint: Helps maintain code quality by checking our code for potential errors and enforcing consistent coding styles.
  • jest: A JavaScript testing framework that makes it easy to write and run tests.
  • nodemon: Automatically restarts our application when file changes are detected.
  • **supertest: **Provides a high-level abstraction for testing HTTP requests, perfect for testing our Express.js API Then, install Express by running this command:-
npm install express

Enter fullscreen mode Exit fullscreen mode
  • **express: **a Node.js web framework that helps to create server and API endpoints easily

After installing the dependencies, let’s create a .gitignore file in the root of our project to tell Git which files and directories (like node_modules) shouldn’t be tracked or uploaded to our repository — this helps keep our repository clean and prevents unnecessary files from being committed. Add the following:

node_modules/
.secrets
.vercel
Enter fullscreen mode Exit fullscreen mode

This is what our package.json looks like so far:-

{
  "name": "automation-demo-project",
  "version": "1.0.0",
  "description": "Demo project for GitHub Actions",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.17.1"
  },
  "devDependencies": {
    "eslint": "^8.42.0",
    "jest": "^29.7.0",
    "nodemon": "^3.0.0",
    "supertest": "^6.3.4"
  }
}

Enter fullscreen mode Exit fullscreen mode

Let’s add scripts to our package.json to manage various tasks in our project. Update the scripts section as follows:-

"scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
Enter fullscreen mode Exit fullscreen mode

These scripts allow us to:

  • npm start: Launch the server
  • npm run dev: Run in development mode with auto-reload
  • npm test: Run tests
  • npm run test:coverage: Generate test coverage report
  • npm run lint: Check code quality
  • npm run lint:fix: Automatically fix code style issues

Your complete package.json should look like this:

{
  "name": "automation-demo-project",
  "version": "1.0.0",
  "description": "Demo project for GitHub Actions",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.17.1"
  },
  "devDependencies": {
    "eslint": "^8.42.0",
    "jest": "^29.7.0",
    "nodemon": "^3.0.0",
    "supertest": "^6.3.4"
  }
}

Enter fullscreen mode Exit fullscreen mode

This should be the initial structure after installing all the above dependencies:

automation-demo-project/
├── package.json
├── package-lock.json
├── .gitignore
└── node_modules/
Enter fullscreen mode Exit fullscreen mode

Now let’s create a .eslintrc.js file in the root and add the following code:-

module.exports = {
    env: {
      node: true,
      es2021: true,
      jest: true,
      commonjs: true
    },
    extends: ['eslint:recommended'],
    parserOptions: {
      ecmaVersion: 2021
    },
    globals: {
      process: true
    },
    rules: {
      'indent': ['error', 2],
      'linebreak-style': ['error', 'unix'],
      'quotes': ['error', 'single'],
      'semi': ['error', 'always'],
      'no-unused-vars': 'warn',
      'no-console': 'warn'
    }
  };

Enter fullscreen mode Exit fullscreen mode

Then, create eslint.config.mjs, and add the following code:

import globals from 'globals';
import pluginJs from '@eslint/js';

/** @type {import('eslint').Linter.Config[]} */
export default [
  {
    files: ['**/*.js'],
    languageOptions: {
      sourceType: 'commonjs',
      globals: globals.node,
    },
  },
  {
    files: ['**/*.test.js'],
    languageOptions: {
      globals: globals.jest,
    },
  },
  pluginJs.configs.recommended,
];

Enter fullscreen mode Exit fullscreen mode

Up to this point, this is the folder structure:

automation-demo-project/
├── .gitignore
├──eslint.config.mjs
├── package.json
├── package-lock.json
├── .eslintrc.js
└── node_modules/

Enter fullscreen mode Exit fullscreen mode

Create a folder named test in the root. Create index.test.js file in the test folder, and add the following code that will be used to test the code in index.js that we will be creating next:

const request = require('supertest');
const app = require('../index');

describe('API Tests', () => {
  test('GET / should return hello message', async () => {
    const response = await request(app).get('/');
    expect(response.body.message).toBe('Hello Github Actions!');
    expect(response.statusCode).toBe(200);
  });
});

Enter fullscreen mode Exit fullscreen mode

Next, let’s create a file in the root called index.js, which will have a simple Express server that returns a “Hello” message — we’ll use it to demonstrate how GitHub Actions can automatically test and deploy our code whenever we make changes

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello Github Actions!' });
});

module.exports = app;

Enter fullscreen mode Exit fullscreen mode

After creating index.js, this is our updated folder structure:

automation-demo-project/
├.gitignore
├test/
       └── index.test
├package.json
├package-lock.json
├.eslintrc.js
├index.js
├eslint.config.mjs
├node_modules/
Enter fullscreen mode Exit fullscreen mode

2. Implementing CI Pipeline

automation-demo-project/
├.github/
           └── workflows/
                   └── main.yml  
├test/
       └── index.test
├package.json
├package-lock.json
├.eslintrc.js
├index.js
├.gitignore
├eslint.config.mjs
├node_modules/

Enter fullscreen mode Exit fullscreen mode

The Continuous Integration (CI) phase automatically validates our code through testing, linting, and quality checks, then builds our application by installing dependencies and optimizing assets. Once CI passes, the Continuous Deployment (CD) phase takes over. Inside main.yml, add the code below:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE
      - uses: actions/checkout@v4

      # Setup Node.js environment
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm" # Caches npm dependencies

      # Install dependencies
      - name: Install Dependencies
        run: npm ci # Uses clean install, preferred in CI environments

      # Check for lint errors
      - name: Run ESLint
        run: npm run lint

      # Run tests
      - name: Run Tests
        run: npm test

      # Optional: Add test coverage reporting
      - name: Generate Test Coverage
        run: npm run test -- --coverage
Enter fullscreen mode Exit fullscreen mode

3. Setting Up CD with Vercel

Next, we’ll add a deploy.yml file alongside our CI configuration in the .github/workflows directory. This file handles our automated deployment process to Vercel. Our complete workflow structure looks like this:

automation-demo-project/
├.github/
           └── workflows/
                   └── main.yml  
                   └── deploy.yml  
├test/
       └── index.test
├package.json
├package-lock.json
├.eslintrc.js
├index.js
├.gitignore
├eslint.config.mjs
├node_modules/
Enter fullscreen mode Exit fullscreen mode

Continuous Deployment (CD) automates our deployment process to Vercel. Here’s how our workflow handles it inside deploy.yml:

name: Deploy to Vercel

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # Check only required secrets for personal account
      - name: Check Required Secrets
        run: |
          if [ -z "${{ secrets.VERCEL_TOKEN }}" ]; then
            echo "Error: VERCEL_TOKEN is not set"
            exit 1
          fi

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          cache: "npm"

      - name: Install Dependencies
        run: npm ci

      - name: Build
        run: npm run build --if-present

      # Simplified Vercel deployment for personal accounts
      - name: Deploy to Vercel
        run: |
          npx vercel --token ${{ secrets.VERCEL_TOKEN }} --prod --yes

Enter fullscreen mode Exit fullscreen mode

Setting up Vercel Tokens and GitHub Secrets

Commit and push all your changes to GitHub, and deploy your project on Vercel, here’s a detailed guide on how to deploy a project on Vercel.

If you check your GitHub Actions tab now, you’ll notice red ❌ failing checks — this is expected! The deployment is failing because we haven’t set up our Vercel token yet. We’ll fix this in the next step when we configure our GitHub secrets.

Now let’s create this file vercel.json in the root and add the following code:

{
    "version": 2,
    "builds": [
      {
        "src": "index.js",
        "use": "@vercel/node"
      }
    ],
    "routes": [
      {
        "src": "/(.*)",
        "dest": "index.js"
      }
    ]
  }

Enter fullscreen mode Exit fullscreen mode

The vercel.json file configures how our application runs on Vercel: it specifies version 2 of Vercel’s build system, tells Vercel to use Node.js runtime for our index.js file, and sets up routing so that all incoming requests are properly directed to our Express application — think of it as a roadmap that guides Vercel on how to serve our API.

Before our deployment workflow can run successfully, we need to configure our Vercel tokens and add them to GitHub secrets. Here’s how:

1. Get Your Vercel Token:

  • Go to vercel token
  • Click “Create Token” — Give it a name (e.g “Vercel_Token”), scope(choose the project under where your project is running), and expiration( e.g I chose 90 days, pick one that works for your project)
  • Copy the token (you won’t see it again!)
  • In the root of your project create a .secrets file and paste the token you’ve created as shown below:-
VERCEL_TOKEN=paste your token here
Enter fullscreen mode Exit fullscreen mode

The file structure should reflect this:

automation-demo-project/
├.github/
           └── workflows/
                   └── main.yml  
                   └── deploy.yml  
├test/
       └── index.test
├package.json
├package-lock.json
├.eslintrc.js
├index.js
├.gitignore
├.secrets
├eslint.config.mjs
├vercel.json
├node_modules/

Enter fullscreen mode Exit fullscreen mode

2. Add Token to GitHub Secrets:

  • Go to your GitHub repository
  • Click Settings → Secrets and Variables → Actions
  • Click “New repository secret”
  • Name: VERCEL_TOKEN
  • Value: Paste your Vercel token

Let’s understand each part of our deployment workflow:

Workflow Triggers

  • name: Deploy to Vercel — Identifies our workflow in the GitHub Actions dashboard
  • on: push: branches: [main] — Runs when code is pushed to the main branch
  • workflow_dispatch — Allows manual workflow trigger from GitHub UI

Job Configuration

  • runs-on: ubuntu-latest — Uses Ubuntu for running our deployment
  • steps — List of tasks our workflow will perform

Key Steps Explained

  • actions/checkout@v4 — Gets our repository code
  • Check Required Secrets — Verifies Vercel token is set
  • Setup Node.js — Prepares Node.js environment
  • Install Dependencies — Get project dependencies
  • Build — Build project if needed
  • Deploy to Vercel — Deploys to Vercel using our token

This workflow ensures our code is properly prepared and securely deployed whenever we push changes.

Successful Implementation

Let’s verify our implementation step by step:

1. Local Testing

First, let’s run our tests locally to ensure everything works as expected:

npm run test && npm run lint

Enter fullscreen mode Exit fullscreen mode

If the run is successful, this should be the output:

Image description

2. GitHub Actions Dashboard

Commit all your changes and push to the main branch to see it in action

Image description

Upon a successful run, you should see a green checkmark with the name of your last commit ✅ Each workflow took only seconds to complete, demonstrating the efficiency of our automated pipeline.

This workflow automatically deploys our project whenever we push code to our main branch. It checks out our code, sets up Node.js, and deploys to Vercel — all without us having to lift a finger! 🚀 The best part? Our sensitive deployment tokens are safely stored in GitHub secrets, keeping our application secure.

Conclusion

By implementing this automation pipeline, what used to take me 30 minutes of manual deployment now happens in just 2 minutes. I’d love to hear how you’re using GitHub Actions in your projects! Drop a comment below, check out my repository for the complete implementation, or reach out if you need help setting up your own automation pipeline.

I hope you found this article helpful, and that you feel confident building your automation pipeline with GitHub Actions.

You can clone this project here Automation demo repo

Top comments (0)