How to Build a CI/CD Pipeline with GitHub Actions

Introduction to Automated CI/CD Pipelines

Continuous Integration and Continuous Deployment (CI/CD) pipelines are fundamental components of modern software delivery. By automating the validation, testing, and deployment of code changes, engineering teams can release features quickly while maintaining stability. Manually running tests and deploying assets from local machines introduces risks, such as configuration discrepancies, security vulnerabilities, and human error.

GitHub Actions provides a built-in CI/CD framework that runs workflows directly inside your GitHub repository. It integrates with your code history, enabling you to trigger automated pipelines on events like pull requests, branch merges, or version tags. This guide explores the details of building a production-grade CI/CD pipeline, optimizing build speeds through dependency caching, and securing deployment keys using OpenID Connect (OIDC).

To design a robust CI/CD workflow, we must understand the core components of GitHub Actions. The system uses YAML files stored in the '.github/workflows' directory of the repository to define pipelines. A workflow consists of one or more jobs that run in parallel or sequentially.

Each job executes on a runner—a virtual machine or container hosted by GitHub or self-hosted in your cloud environment. Jobs contain a series of steps that run shell commands or reference pre-built Actions from the GitHub Marketplace. By separating your pipeline into distinct jobs (such as linting, testing, and deploying), you can isolate failures and optimize performance, ensuring that compilation only occurs if static code analysis passes.

Creating a Code Validation Workflow

Let's create a validation workflow named 'ci.yml' that runs on every pull request. This workflow will check code styling, verify TypeScript types, and execute unit tests, preventing broken code from merging into the main branch:

name: Continuous Integration

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run lint check
        run: npm run lint

      - name: Verify types
        run: npm run typecheck

      - name: Run unit tests
        run: npm run test

In this workflow, we use 'actions/checkout' to download the repository files and 'actions/setup-node' to install the runtime environment. Using 'npm ci' instead of 'npm install' ensures a clean, reproducible installation of dependencies based strictly on the project's lockfile, preventing version drift in the runner environment.

  • Trigger Filters: Restrict workflow runs to pull requests targetting specific branches.
  • Static Analysis: Execute lint checks to maintain layout and formatting integrity.
  • Isolated Runners: Spin up fresh virtual machines for each execution pipeline.

Optimizing Pipelines using Dependency Caching

A common bottleneck in CI pipelines is the time spent installing dependencies on every run. To speed up execution, we can implement dependency caching using GitHub's caching action. In a Node.js project, caching the global npm cache directory can reduce build times by 50% or more.

The setup-node action includes built-in support for caching package managers. At Bramsley Digital Studio, we configure it by adding the 'cache: 'npm'' property:

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

When the workflow runs, the runner checks for a matching cache key based on the 'package-lock.json' file. If a match is found, the dependencies are restored instantly.

If the lockfile has changed, the runner installs the dependencies and updates the cache for future workflow runs. This caching pattern should be applied to bundler outputs, static assets, and Docker layers to optimize pipeline efficiency.

Orchestrating Continuous Deployment Jobs

Once code validation is complete, the Continuous Deployment (CD) job handles releasing the application. Deploying applications to cloud environments requires authentication credentials, such as API tokens or service account keys. Hardcoding these secrets in your repository is a security risk.

GitHub Actions provides encrypted secrets, allowing you to store credentials securely in the repository settings and access them as environment variables during execution. For cloud providers, using OpenID Connect (OIDC) is recommended. OIDC allows the GitHub Actions runner to request short-lived access tokens directly from the cloud provider, eliminating the need to store long-lived credentials in GitHub entirely.

Let's look at a CD job config that deploys a web application to Cloudflare Pages after a merge. This job runs only when code is pushed to the main branch and requires the validation job to pass:

  deploy:
    needs: validate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build assets
        run: |
          npm ci
          npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name="my-react-app"

This deployment job uses the 'cloudflare/wrangler-action' to upload the compiled 'dist' directory directly to Cloudflare's network, ensuring the new build goes live immediately across all edge nodes globally. Using the 'needs: validate' constraint ensures that broken builds are never deployed to production.

Automated Release Pipelines and Edge Deployments with Bramsley

Designing a fast, secure, and resilient CI/CD pipeline requires careful planning around task coordination, secrets management, and caching strategies. Misconfigured pipelines can lead to slow deployments, security vulnerabilities, or broken releases. At Bramsley, we design and implement enterprise-grade CI/CD pipelines that optimize your software delivery lifecycle.

At Bramsley, our engineering team specializes in setting up secure deployments, optimizing build caches, and deploying edge-native architectures to accelerate feature delivery. Partner with us to build automated pipelines that scale with your team.

Bramsley Digital Studio

Enterprise Digital Architecture

We engineer digital infrastructure that drives measurable B2B growth. Experts in Legacy System Migration and High-Performance Frontends.

Architecture Specs & Case Studies

Scale Your Operations

  • Legacy System Migration
  • Scalable Infrastructure
  • High-Performance Frontends
  • Global Edge Deployment