GitHub Actions: Build a CI/CD Pipeline

Stop deploying by dragging folders to a server. This guide builds a real GitHub Actions pipeline: automated tests on every push, safe secret handling, database services in CI, and SSH deploys to your VPS that only run when tests pass.

Written by Projectech6 min readPublished
For B.E./B.Tech Computer Science and IT students who deploy by zipping files or dragging folders to a server and want automated testing and deployment Topics: GitHub Actions, CI/CD, YAML, DevOps
Illustration of a GitHub Actions CI/CD pipeline showing code push flowing through build, test, and deploy stages automatically.
Illustration generated for this guide.
In this guide

Right now your deployment process is probably: make changes, hope you remember everything, copy files to the server, restart something, pray. It works until the day you forget a step at 11 PM before a demo — or a teammate's "small fix" breaks the build and nobody notices for a week.

CI/CD (Continuous Integration / Continuous Deployment) replaces hope with automation: every push runs your tests automatically, and passing code deploys itself. GitHub Actions gives you this for free on GitHub. This guide builds a real pipeline, step by step.

CI vs CD in one paragraph

  • Continuous Integration: every push to the repo automatically builds the code and runs the test suite. Broken code is caught in minutes, while the change is fresh in someone's mind.
  • Continuous Delivery/Deployment: every passing build is automatically released — to staging always, to production either automatically (deployment) or with one approval click (delivery).

The pipeline is: push → build → test → (deploy to staging) → (deploy to production). Each stage gates the next.

Your first workflow: test on every push

GitHub Actions workflows live in .github/workflows/ as YAML files. Here is a complete Node.js CI workflow:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm test
      - run: npm run lint

Reading it top to bottom: the workflow is named CI; it triggers on pushes to main and on pull requests; the test job runs on a fresh Ubuntu virtual machine; it checks out your code, sets up Node 22 with npm caching, installs dependencies deterministically, then runs tests and linting. If any step fails, the whole job fails and GitHub shows a red X on the commit.

Key concepts in that file:

  • Events (on) — what triggers the workflow: pushes, pull requests, schedules, manual dispatch.
  • Jobs — run in parallel by default on separate fresh VMs. Use needs: to sequence them.
  • Steps — sequential commands or pre-built actions (uses:) from the marketplace.
  • Runners — the VMs GitHub provides. ubuntu-latest is free for public repos with generous limits for private ones.

Note: Every job starts from a clean VM — nothing persists between runs unless you explicitly cache it or upload artifacts. This is a feature: your build cannot secretly depend on leftover state from a previous run.

Adding a database service

Real apps need a database in tests. Actions can spin up service containers alongside your job:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mongo:
        image: mongo:7
        ports:
          - 27017:27017
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test
        env:
          MONGO_URL: mongodb://localhost:27017/testdb

The services block starts MongoDB before your steps run; your tests reach it at localhost:27017. Postgres, Redis, and MySQL all work the same way.

Secrets: the right way

Pipelines need credentials — server SSH keys, API tokens, Docker Hub passwords. Rules:

  • Never hardcode secrets in workflow files. Use GitHub's encrypted secrets: repository Settings → Secrets and variables → Actions.
  • Reference them as [secrets.DEPLOY_KEY] — GitHub masks them in logs automatically.
  • Scope secrets narrowly: a deploy key should only access what deployment needs, not your whole cloud account.
  • Rotate any secret that was ever committed to git — assume it is compromised.

Deploying: the CD half

A common student-friendly pattern: deploy to your VPS over SSH when main passes tests.

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test          # only deploy if the CI workflow's tests pass
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1
        with:
          host: [secrets.VPS_HOST]
          username: [secrets.VPS_USER]
          key: [secrets.VPS_SSH_KEY]
          script: |
            cd /opt/myapp
            git pull origin main
            docker compose up --build -d

The needs: test line is the CI/CD contract: deployment only happens if tests pass. The SSH action connects to your VPS and runs a short update sequence — pull, rebuild, restart. Keep the remote script short; the heavy logic (what to build, how to test) lives in versioned workflow files, not in commands typed on the server.

Safer progression for team projects: deploy automatically to a staging environment on every push, but require a manual approval (GitHub Environments with required reviewers) before production. You get automation without 2 AM surprises.

Caching and speed

Pipelines get slow as projects grow. The standard fixes:

  • Dependency caching — cache: npm (or pip/gradle equivalents) avoids reinstalling everything every run.
  • Fail fast — run lint and unit tests before slow integration tests so quick failures surface in seconds.
  • Matrix builds — test across Node versions or OSes in parallel with a strategy.matrix.
  • Artifacts — actions/upload-artifact passes build outputs (compiled frontend, test reports) between jobs instead of rebuilding.

Environments and approvals

For team projects, the step between "CI passes" and "production deploys" deserves a gate. GitHub Environments provide it:

  • Define environments (e.g. staging, production) in repository settings.
  • Required reviewers: production deploys wait for a human approval — automation proposes, a teammate disposes.
  • Environment secrets: the production database password exists only in the production environment scope, invisible to staging workflows and pull-request runs.
  • Deployment history: GitHub records every deployment per environment — who approved, what commit, when.

Reference the environment in the deploy job:

jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    needs: test
    environment: production   # triggers the approval gate + scoped secrets
    steps:
      # ... deploy steps using secrets.PROD_VPS_HOST ...

The result: pushes auto-deploy to staging for everyone to see, and production updates only after tests pass and a human approves. Fast for developers, safe for users.

Common mistakes

  • Testing nothing in CI. A pipeline that only builds is decoration. The value is in the tests — see Testing JavaScript with Jest to have something worth running.
  • Deploying from a developer's laptop instead of the pipeline. If production can only be updated by one person's machine, you do not have CD — you have a script.
  • Secrets in workflow files or logs. Echoing a secret for debugging prints *** if you use the secrets context — but hardcoding it in YAML exposes it to everyone with repo access.
  • No branch protection. CI on pull requests is toothless if main still accepts direct pushes. Protect main: require PRs and passing checks before merge.
  • Deploying on every push without staging. For team projects, auto-deploy to staging on push, gate production behind approval.
  • Ignoring the free-tier minutes. Private repos get limited free minutes; cache aggressively and avoid redundant jobs so you do not burn the quota mid-semester.

Quick checklist

  • Workflow runs tests + lint on every push and pull request
  • main branch protected: requires PR + passing checks
  • Secrets stored in GitHub Secrets, referenced via secrets context, never in YAML
  • Deploy job gated on tests passing (needs:)
  • Staging auto-deploys; production needs approval (team projects)
  • Pipeline is the only path to production — no manual server edits

Where to go from here

More project guides

More in Web Development