Skip to main content

CI/CD Pipeline with GitHub Actions

This tutorial guides you through setting up a complete CI/CD pipeline using GitHub Actions, helping you automate your software deployment process. You'll learn key concepts, practical steps, and best practices while building a simple Node.js application, enabling you to streamline your development workflow effectively.

Difficulty
Beginner
Duration
45 minutes
Steps
5

Tutorial: CI/CD Pipeline with GitHub Actions

Learning Objectives and Outcomes

In this tutorial, you will learn how to:

  • Understand the fundamentals of Continuous Integration and Continuous Deployment (CI/CD).
  • Set up a CI/CD pipeline using GitHub Actions to automate your software deployment process.
  • Write and configure GitHub Actions workflows using YAML.
  • Build and push Docker images as part of your CI/CD pipeline.
  • Deploy your application automatically using GitHub Actions.
  • Manage secrets and environments securely in GitHub Actions.

By the end of this tutorial, you will have a working CI/CD pipeline integrated with your GitHub repository, enabling you to streamline your development process.

Prerequisites and Setup

Before diving into the tutorial, ensure you have:

  • Basic knowledge of Git: You should understand how to clone repositories, commit changes, and push to GitHub.
  • Basic knowledge of YAML: Familiarity with YAML syntax is essential for writing GitHub Actions workflows.
  • Docker installed locally: Required for building and testing container images.
  • A container registry account: Such as Docker Hub or GitHub Container Registry, for pushing images.

Setting Up Your Environment

  1. Create a GitHub Account: If you don't have one, sign up at GitHub.
  2. Create a Repository: Create a new repository where you will set up your CI/CD pipeline.
  3. Clone the repository locally and create a simple Node.js application to use throughout this tutorial.

Step-by-Step Instructions with Examples

Step 1: Workflow Basics

GitHub Actions workflows are YAML files stored in .github/workflows/ in your repository. Each workflow defines when to run and what to do.

  1. Create the workflows directory:
    mkdir -p .github/workflows
    
  2. Create .github/workflows/ci-cd.yml with a basic structure:
    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@v4
    
  3. Key concepts in this file:
    • on: Defines the trigger events — here, pushes and pull requests to main.
    • jobs: Each job runs in its own environment. You can have multiple jobs.
    • runs-on: The operating system for the job runner (e.g., ubuntu-latest).
    • steps: Sequential tasks within a job, using either run (shell commands) or uses (pre-built actions).
  4. Commit and push this file. GitHub automatically detects it and displays it under the Actions tab.

Step 2: Build and Test

Extend your workflow to install dependencies, run tests, and produce a build artefact.

  1. Update the build job in your workflow file:
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Set up Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
    
          - name: Install dependencies
            run: npm ci
    
          - name: Run tests
            run: npm test
    
          - name: Build application
            run: npm run build
    
  2. The cache: 'npm' option caches your node_modules between workflow runs, speeding up subsequent builds.
  3. Use npm ci instead of npm install in CI pipelines — it installs exact versions from package-lock.json for reproducible builds.
  4. Push your changes. In the Actions tab, watch each step run in real time. If a test fails, the workflow stops and later jobs will not execute.

Step 3: Docker Build

After a successful build and test phase, build a Docker image and push it to a container registry.

  1. Create a Dockerfile in your repository root:
    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    EXPOSE 3000
    CMD ["node", "index.js"]
    
  2. Add a docker job that depends on the build job:
      docker:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Log in to Docker Hub
            uses: docker/login-action@v3
            with:
              username: ${{ secrets.DOCKER_USERNAME }}
              password: ${{ secrets.DOCKER_PASSWORD }}
    
          - name: Build and push Docker image
            uses: docker/build-push-action@v5
            with:
              context: .
              push: true
              tags: ${{ secrets.DOCKER_USERNAME }}/my-app:latest,${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
    
  3. The needs: build directive ensures the Docker job only runs after the build and test job succeeds.
  4. Tagging with ${{ github.sha }} in addition to latest gives you an immutable image reference for each commit.

Step 4: Deployment

With your Docker image pushed to a registry, automatically deploy it to your server.

  1. Add a deploy job that depends on the docker job:
      deploy:
        needs: docker
        runs-on: ubuntu-latest
        environment: production
        steps:
          - name: Deploy to server
            uses: appleboy/ssh-action@v1
            with:
              host: ${{ secrets.DEPLOY_HOST }}
              username: ${{ secrets.DEPLOY_USER }}
              key: ${{ secrets.DEPLOY_SSH_KEY }}
              script: |
                docker pull ${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
                docker stop my-app || true
                docker rm my-app || true
                docker run -d \
                  --name my-app \
                  --restart unless-stopped \
                  -p 3000:3000 \
                  ${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
    
  2. The environment: production declaration links this job to a GitHub Environment, enabling protection rules such as required reviewers before deployment proceeds.
  3. Adapt the deployment script to your hosting target — for Kubernetes, use kubectl set image; for AWS ECS, use the AWS CLI or a dedicated action.

Step 5: Secrets and Environments

Secrets keep sensitive values out of your source code. GitHub Environments add approval gates and environment-scoped overrides.

  1. Add repository secrets:
    • Go to Settings → Secrets and variables → Actions → New repository secret.
    • Create secrets for: DOCKER_USERNAME, DOCKER_PASSWORD, DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY.
  2. Create a production Environment:
    • Go to Settings → Environments → New environment and name it production.
    • Enable Required reviewers to enforce a manual approval step before every production deployment.
    • Add environment-specific secrets here if they differ from repository-level secrets.
  3. Reference secrets in your workflow using ${{ secrets.SECRET_NAME }}. GitHub automatically masks them in job logs.
  4. Use environment variables for non-sensitive configuration shared across jobs:
    env:
      NODE_ENV: production
      PORT: 3000
    
  5. Never hardcode credentials, tokens, or connection strings in workflow files — always use secrets.

Key Concepts Explained Along the Way

  • Continuous Integration (CI): The practice of automatically testing and integrating code changes into a shared repository.
  • Continuous Deployment (CD): The process of automatically deploying code changes to production after passing tests.
  • Workflows: A YAML file that defines the automated processes in GitHub Actions.
  • Jobs: A collection of steps that run in the same environment. Jobs can run in parallel or be sequenced using needs.
  • Steps: Individual tasks executed within a job, using either run commands or reusable uses actions.
  • Secrets: Encrypted values stored in GitHub, injected into workflows at runtime and masked in logs.
  • Environments: Named deployment targets with optional protection rules and environment-specific secrets.

Common Mistakes and How to Avoid Them

  • Using npm install in CI: Use npm ci instead for reproducible, deterministic installs.
  • Not pinning action versions: Use actions/checkout@v4 (not @main) to avoid unexpected breaking changes.
  • Forgetting needs: Without needs, jobs run in parallel — always chain dependent jobs explicitly.
  • Incorrect branch name: Verify your workflow triggers on the correct branch (e.g., main not master).
  • Exposing secrets in logs: Never echo a secret value directly. GitHub only masks values referenced via ${{ secrets.NAME }}.

Exercises and Practice Suggestions

  • Add a matrix build to test against multiple Node.js versions simultaneously using strategy.matrix.
  • Configure a scheduled workflow (on: schedule) to run nightly integration tests.
  • Add a Slack or email notification step that fires when a deployment succeeds or fails.
  • Experiment with GitHub Environments by adding a required reviewer before the production deploy proceeds.

Next Steps and Further Learning

  • Explore advanced GitHub Actions features such as reusable workflows and composite actions.
  • Learn about deploying to managed cloud services (AWS ECS, Google Cloud Run, Azure Container Apps).
  • Add security scanning steps — use trivy for container image scanning and snyk for dependency auditing.

With these tools and knowledge, you can create powerful and efficient CI/CD pipelines using GitHub Actions, streamlining your development workflow and increasing your team's productivity.

Prerequisites

  • git basics
  • yaml basics

Steps

  • 1
    Workflow Basics
  • 2
    Build and Test
  • 3
    Docker Build
  • 4
    Deployment
  • 5
    Secrets and Environments

Category

DevOps
Vibgrate CLI

See a real scan run

A replay of the actual CLI running against our test repositories — live progress, real findings, a genuine DriftScore. Nothing executes in your browser.

Replay
demo@vibgrate — bash
npx @vibgrate/cli scan
 
╭──────────────────────────────────────────╮
Vibgrate Drift Report
╰──────────────────────────────────────────╯
 
── node-turborepo (node) .
Runtime: >=18.0.0 (6 majors behind)
Frameworks:
Turbo: 1.13.4 → 2.10.12 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 1 1-behind 3 2+ behind 1 unknown
 
── @repo/admin (node) apps/admin
Frameworks:
TanStack Query: 5.102.5 → 5.102.5 (current)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vite: 5.4.21 → 8.2.2 (3 behind)
Dependencies:
3 current 9 1-behind 3 2+ behind 4 unknown
 
── @repo/api (node) apps/api
Frameworks:
Express: 4.22.2 → 5.2.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
7 current 5 1-behind 3 2+ behind 4 unknown
 
── @repo/web (node) apps/web
Frameworks:
Next.js: 14.2.35 → 16.3.3 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 6 1-behind 3 2+ behind 5 unknown
 
── @repo/config (node) packages/config
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 2 1-behind 5 2+ behind 0 unknown
 
── @repo/database (node) packages/database
Frameworks:
Prisma: 5.22.0 → 7.10.0 (2 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 0 1-behind 3 2+ behind 1 unknown
 
── @repo/types (node) packages/types
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
0 current 0 1-behind 1 2+ behind 1 unknown
 
── @repo/ui (node) packages/ui
Frameworks:
React: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
Dependencies:
1 current 4 1-behind 1 2+ behind 1 unknown
 
── @repo/utils (node) packages/utils
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
0 current 1 1-behind 2 2+ behind 1 unknown
 
Tech Stack
Frontend: React, React DOM
Meta-frameworks: Next.js
Bundlers: tsx, Turbo, Vite
CSS / UI: Autoprefixer, PostCSS, Tailwind CSS
Backend: Express
ORM / Database: Prisma, Prisma Client
Testing: Vitest
Lint & Format: ESLint, ESLint Prettier, ESLint React, Prettier, typescript-eslint
 
Services & Integrations
Auth: JWT 9.0.3
Databases: Prisma 5.22.0
 
TypeScript
v5.3.3 · strict ✔ · MIXED · target: ES2022
 
Build & Deploy
Package Managers: pnpm
Monorepo: npm-workspaces, pnpm-workspaces, turbo
 
Product Purpose Signals
Frameworks: react, nextjs
Evidence: 177
Top Signals:
- [heading] Dashboard (apps/admin/src/pages/Dashboard.tsx)
- [title] Revenue Overview (apps/admin/src/pages/Dashboard.tsx)
- [copy] workspace:* (packages/ui/package.json)
- [copy] ./dist (packages/ui/tsconfig.json)
- [copy] ./src/index.ts (packages/ui/package.json)
- [copy] @repo/config/tsconfig-base.json (packages/ui/tsconfig.json)
- [copy] @repo/ui (packages/ui/package.json)
- [copy] #3b82f6 (apps/admin/src/pages/Dashboard.tsx)
Unknowns:
- No pricing or billing evidence found.
- No integrations/connectors evidence found.
- No route structure evidence found.
 
Security Posture
Lockfile ✖ · .env ✔ · node_modules ✔
 
Platform
Native modules: turbo
 
Code Quality
Files: 36 · Functions: 183 · Avg complexity: 2.62 · Avg length: 21.13 lines
Max nesting: 2 · Circular deps: 0 · Dead code: 0%
God files: apps/admin/src/pages/Products (448 lines)
 
Database Schema
postgresql · 8 models · 1 enum
Models: Address, CartItem, Category, Order, OrderItem (+3 more)
 
Findings (16 errors, 11 warnings)
Node.js runtime ">=18.0.0" reached end-of-life on 2025-04-30 (latest: 24.0.0).
vibgrate/runtime-eol in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in .
60% of dependencies are 2+ major versions behind in node-turborepo.
vibgrate/dependency-rot in .
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/admin
Vite is 3 major versions behind (current: 5.4.21, latest: 8.2.2).
vibgrate/framework-major-lag in apps/admin
vite is 3 major versions behind (spec: ^5.0.12, latest: 8.2.2).
vibgrate/dependency-major-lag in apps/admin
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/api
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in apps/api
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in apps/api
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in apps/api
Next.js is 2 major versions behind (current: 14.2.35, latest: 16.3.3).
vibgrate/framework-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/web
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/config
56% of dependencies are 2+ major versions behind in @repo/config.
vibgrate/dependency-rot in packages/config
eslint-plugin-react-hooks is 3 major versions behind (spec: ^4.6.0, latest: 7.1.1).
vibgrate/dependency-major-lag in packages/config
Prisma is 2 major versions behind (current: 5.22.0, latest: 7.10.0).
vibgrate/framework-major-lag in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/database
75% of dependencies are 2+ major versions behind in @repo/database.
vibgrate/dependency-rot in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/types
100% of dependencies are 2+ major versions behind in @repo/types.
vibgrate/dependency-rot in packages/types
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/ui
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/utils
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in packages/utils
67% of dependencies are 2+ major versions behind in @repo/utils.
vibgrate/dependency-rot in packages/utils
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in packages/utils
 
╭──────────────────────────────────────────╮
Top Priority Actions
╰──────────────────────────────────────────╯
 
1. Upgrade EOL runtime in node-turborepo
End-of-life runtimes no longer receive security patches and block ecosystem upgrades.
./.
>=18.0.0 → 24.0.0 (6 majors behind)
Impact: −10 drift points (runtime & EOL)
 
2. Fix security posture: no lockfile found
Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.
./
Missing: package-lock.json, pnpm-lock.yaml, or yarn.lock
 
3. Upgrade Vite 5.4.21 → 8.2.2 in @repo/admin (+2 more)
3 major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.
./apps/admin
Vite: 5.4.21 → 8.2.2 (3 majors behind)
./apps/api
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
./packages/utils
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
Impact: −5–15 drift points
 
4. Reduce dependency rot in @repo/types (100% severely outdated)
1 of 1 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/types
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
5. Reduce dependency rot in @repo/database (75% severely outdated)
3 of 4 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/database
@prisma/client: 5.22.0 → 7.10.0 (2 majors behind)
prisma: 5.22.0 → 7.10.0 (2 majors behind)
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
╭──────────────────────────────────────────╮
Architecture Layers
╰──────────────────────────────────────────╯
 
Archetype: nextjs (80% confidence)
Files classified: 24 (11 unclassified)
Folders classified: 8
apps/admin/src presentation 100% 4 files
apps/admin/src/pages presentation 100% 2 files
apps/api/src/middleware middleware 100% 2 files
apps/api/src/routes routing 100% 2 files
apps/web/src/app presentation 100% 4 files
apps/web/src/app/products presentation 100% 2 files
apps/web/src/app/products/[id] presentation 100% 1 file
packages/ui/src presentation 100% 6 files
Unclassified source (sample): 11
 
presentation 15 files drift ████████████████████ 100 risk high
routing 4 files drift ████████████████████ 100 risk high
middleware 2 files drift ███████▍░░░░░░░░░░░░ 37 risk moderate
config 2 files drift ░░░░░░░░░░░░░░░░░░░░ 0 risk none
shared 1 file drift ████████████████████ 100 risk high
 
╭──────────────────────────────────────────╮
DriftScore Summary
╰──────────────────────────────────────────╯
 
DriftScore: 66/100
Risk Level: HIGH
Projects: 9
Classified: 8 nano · 1 micro · 0 small · 0 standard
Billable: 0.42 · 9 detected → 0.42 billable projects (micro-project pricing)
0.1 micro · 0.32 nano
These fractions add up across repositories, then round down to whole billable projects.
 
Score Breakdown
Runtime: ████████████████████ 100
Frameworks: █████████▏░░░░░░░░░░ 46
Dependencies: ██████▏░░░░░░░░░░░░░ 31
EOL Risk: ████████████████████ 100
 
Scanned at 2026-08-26T09:08:28.481Z · 7.1s · 286 files scanned · 56 workspace files · 27 dirs
Press Run to start.