/ CI/CD, GITHUB ACTIONS, DEVOPS, AUTOMATION, WORKFLOW

How to set up a CI/CD pipeline with GitHub Actions

Shipping code confidently means knowing it has been built, tested, and deployed automatically every time you push. That’s the promise of a CI/CD pipeline — and GitHub Actions makes it easy to set one up without ever leaving your repository.

In this post you’ll go from zero to a working pipeline: you’ll create a repository, write a workflow file, and have automated tests running on every push and pull request.


What is CI/CD and why GitHub Actions?

CI (Continuous Integration) is the practice of automatically building and testing code every time a change is pushed to a shared branch. CD (Continuous Delivery / Deployment) extends that by automatically delivering a verified build to a staging or production environment.

GitHub Actions is GitHub’s built-in automation platform. Workflows live inside your repository as YAML files, run on GitHub-hosted virtual machines (called runners), and are free for public repositories and within generous limits for private ones.

Key concepts you’ll encounter:

  • Workflow — an automated process defined in a .yml file under .github/workflows/
  • Event — a trigger that starts a workflow (e.g. push, pull_request, schedule)
  • Job — a set of steps that run on the same runner machine
  • Step — a single shell command or a pre-built action
  • Action — a reusable unit of automation (e.g. actions/checkout, actions/setup-node)

Step 1: Create a GitHub repository

Start by creating a new repository on GitHub — or use an existing one. If you’re starting from scratch:

# Initialise a local project and push it to GitHub
git init my-project
cd my-project
git remote add origin https://github.com/<your-username>/my-project.git

Make sure your repository has a default branch (usually main) before adding any workflow, since the workflow trigger below listens to pushes on that branch.

💡 Tip: If your project already has tests (e.g. npm test, pytest, go test) you’ll see the most value from a CI pipeline immediately.


Step 2: Create the workflow file

GitHub Actions picks up any .yml file inside the .github/workflows/ directory automatically. Create that directory and add your first workflow:

mkdir -p .github/workflows
touch .github/workflows/ci.yml

Open ci.yml and add the following pipeline definition. This example targets a Node.js project, but the structure is the same for any language — only the setup action and run commands change.

# .github/workflows/ci.yml
name: CI/CD Pipeline

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

jobs:
  build-and-test:
    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"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

Here’s what each block does:

  • on — defines when this workflow runs. The pipeline activates on every push to main and on every pull request targeting main.
  • jobs.build-and-test — a single job that runs on a fresh Ubuntu virtual machine provided by GitHub.
  • actions/checkout@v4 — clones your repository into the runner so subsequent steps can access your code.
  • actions/setup-node@v4 — installs the specified Node.js version and makes npm available.
  • npm ci — installs dependencies from package-lock.json for a clean, reproducible build.
  • npm test / npm run build — your project’s test and build commands. Swap these for pytest, go build, mvn test, etc. as needed.

Step 3: Commit and trigger the pipeline

Push your new workflow file and watch the pipeline run:

git add .github/workflows/ci.yml
git commit -m "ci: add GitHub Actions CI/CD pipeline"
git push origin main

Navigate to your repository on GitHub and click the Actions tab. You’ll see the workflow appear and begin running within seconds. Each step updates in real time, and the overall job goes green (or red) once it finishes.

💡 Tip: A red check on a pull request will block merging when branch protection rules are configured — a great way to enforce quality gates without manual review.


Step 4: Extend your pipeline

Once the basic pipeline is green, here are common next steps:

Add a deploy stage

To deploy only after all tests pass, add a second job that depends on the first:

deploy:
  needs: build-and-test
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main'

  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Deploy to production
      run: ./scripts/deploy.sh
      env:
        DEPLOY_TOKEN: $

The needs keyword enforces ordering — deploy only starts when build-and-test succeeds. Sensitive credentials are stored in GitHub Secrets (Settings → Secrets and variables → Actions) and injected at runtime via $, keeping them out of your source code.

Cache dependencies for faster runs

Repeated npm ci calls download the same packages every time. The cache action skips that:

- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: $-node-$
    restore-keys: |
      $-node-

Add this step before the install step to cut build times significantly on large projects.

Run against multiple Node.js versions

Use a matrix strategy to test compatibility across versions:

strategy:
  matrix:
    node-version: ["18", "20", "22"]

GitHub Actions will spin up a parallel job for each matrix entry automatically.


Conclusion

You now have a fully automated CI/CD pipeline that builds and tests your code on every push and pull request. The workflow file is version-controlled alongside your code, reviewable in pull requests, and free to iterate on.

From here you can add linting, security scanning, Docker image builds, or deployment steps — all using the same YAML structure. Explore the GitHub Actions Marketplace for thousands of pre-built actions to slot into your pipeline.


References: