Migrating from Jenkins to GitHub Actions for CI/CD

Introduction

Jenkins has been a go-to tool for CI/CD for many developers. However, it can be heavy and complex for smaller projects or when simplicity is desired. In this blog post, I will walk you through my experience migrating from Jenkins to GitHub Actions for CI/CD, highlighting the reasons for the switch and the steps taken to implement a new workflow.

Prerequisites

Before you start the migration, ensure you have the following:

  • A GitHub repository for your project.
  • Access to your VPS (Virtual Private Server) where the application will be deployed.
  • SSH keys generated and configured for your VPS.
  • GitHub secrets configured for storing sensitive information like SSH keys.

Why Migrate from Jenkins to GitHub Actions?

  • Complexity and Maintenance: Jenkins requires managing a separate server and handling its updates and maintenance, which can be time-consuming.
  • Performance: Jenkins can be resource-intensive, which might be overkill for smaller projects.
  • Integration: GitHub Actions provides seamless integration with GitHub repositories, making it easier to manage workflows within the same platform.
  • Simplicity: GitHub Actions offers a straightforward YAML configuration, which is easier to read and manage compared to Jenkins' XML-based configurations.

being small but absolute working workaround works as follows.

  • This workflow run on push on main branch and deploys in github and then only in vps.

In this guide, we'll be using appleboy/ssh-action, which lets you ssh into the server and run particular script.

For this workflow, we'll first need to have our ssh keys in our vps and the user must be able to login via ssh.

You'll want to generate ssh keys and then put your public keys in authorized_keys, which simply lets the current user ssh into the sever.

Setting Up the GitHub Actions Workflow

After generating ssh keys, put your:

  • PRIVATE_KEY, USERNAME, and HOST in your repository's secrets variables.

Here’s the GitHub Actions workflow that I set up to build and deploy my web application to a VPS:

name: Test the web app and deploy on vps

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Install Nix package manager
        uses: DeterminateSystems/nix-installer-action@main

      - name: Install Node.js and pnpm
        run: |
          nix profile install nixpkgs#nodejs_22
          nix profile install nixpkgs#nodePackages_latest.pnpm

      - name: Build website
        run: |
          pnpm install
          pnpm run build

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy over the vps
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: 22
          script: |
            cd ~/source_dir/
            bash ./.github/deployer.sh

A graph of this workflow

The deployer script

The following script is ran by github actions on github build success

#!/usr/bin/env bash

# Written by @pwnwriter
# This script builds the Nest Nepal website. If the build fails, it attempts to build from the previous commit.

### Variables

USER="ubuntu"
REPO_DIR="/home/${USER}/repo_name"
INFO_DIR="/home/${USER}/repo_name-log"
LOG_FILE="$INFO_DIR/build.log"
SERVER_LOG="$INFO_DIR/server.log"
PORT=3002

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# -------------------- Helper functions ---------

# Appends log messages
append_log() {
    printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG_FILE"
}

# Prints informational messages
print_info() {
    local message="${1}"
    printf "${YELLOW}[INFO]${NC} %s\n" "$message"
    append_log "[INFO] $message"
}

# Prints success messages
print_success() {
    local message="${1}"
    printf "${GREEN}[SUCCESS]${NC} %s\n" "$message"
    append_log "[SUCCESS] $message"
}

# Prints error messages
print_error() {
    local message="${1}"
    printf "${RED}[ERROR]${NC} %s\n" "$message"
    append_log "[ERROR] $message"
}

# Creates log directories
create_log_dirs() {
    print_info "Creating log directories"
    mkdir -p "$INFO_DIR" || {
        print_error "Failed to create log directory"
        exit 1
    }
}

# Kills the current running process $port
kill_existing() {
    local pid
    pid=$(ss -lptn "sport = :$PORT" | grep -oP '(?<=pid=)\d+')
    if [ -n "$pid" ]; then
        print_info "Killing existing process with PID $pid"
        kill -9 "$pid" || {
            print_error "Failed to kill process with PID $pid"
            exit 1
        }
    else
        print_info "No existing process found on port $PORT"
    fi
}

# Builds the website
website_build() {
    print_info "Installing required modules"
    pnpm install 2>&1 | tee -a "$LOG_FILE" || {
        print_error "Failed to install modules"
        return 1
    }

    print_info "Building website"
    pnpm run build 2>&1 | tee -a "$LOG_FILE"
    if [ $? -eq 0 ]; then
        print_success "Build successful, starting website"
        kill_existing
        nohup pnpm start >>"$SERVER_LOG" 2>&1 &
        print_success "Website is live and running"
    else
        print_error "Build failed"
        return 1
    fi
}

# Main function
main() {
    cd "$REPO_DIR" || {
        print_error "Unable to change directory to $REPO_DIR"
        exit 1
    }

    print_info "Pulling latest changes from the main branch"
    git pull origin main --rebase || {
        print_error "Failed to pull latest changes from main branch"
        exit 1
    }

    create_log_dirs
    website_build

    if [ $? -ne 0 ]; then
        print_info "Build failed, attempting to build from previous commit"
        print_info "Reverting to previous commit"
        git checkout HEAD~ || {
            print_error "Failed to revert to previous commit"
            exit 1
        }
        website_build || {
            print_error "Build from previous commit failed"
            exit 1
        }
    fi
}

# Execute
main