ProgrUmar Logo
Module 10: Deployment & DevOps

CI/CD with GitHub Actions

Duration: 16 mins

GitHub Actions Pipeline

Create a .github/workflows/ci.yml file that installs dependencies, runs tsc --noEmit, eslint, and your test suite on every push and pull request. Block merges until checks pass.

CI/CD with GitHub Actions

As development teams scale, manually running tests, verifying types, and deploying new releases is a recipe for user-facing regressions. Continuous Integration (CI) and Continuous Deployment (CD) automate these quality checks. By building pipelines that run on every Git pull request, you verify that new code compiles cleanly, conforms to style rules, and passes tests before merging. GitHub Actions is the standard workflow engine for Next.js. In this lesson, we will cover how to configure a build pipeline, cache package dependencies to speed up execution runs, enforce type validation checks, and set up automated branch protection gates.


1. Defining Your GitHub Actions Workflow Location

GitHub Actions reads configuration files written in YAML format. To define a new automated pipeline, create a folder structure in your project root at .github/workflows/ and add a file named ci.yml:

my-app/
└── .github/
    └── workflows/
        └── ci.yml  ← Write your pipeline steps here

2. Writing a Robust CI Pipeline Configuration

Here is a complete, production-grade testing pipeline configuration that triggers on every pull request or push to your main branch:

# .github/workflows/ci.yml
name: Continuous Integration

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

jobs:
  test-and-build:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Checkout repository files
      - name: Checkout Code
        uses: actions/checkout@v4

      # Step 2: Set up Node.js environment
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          # Enable automatic dependency caching
          cache: 'npm'

      # Step 3: Clean install dependencies
      - name: Install Dependencies
        run: npm ci

      # Step 4: Run ESLint verification checks
      - name: Run Linter
        run: npm run lint

      # Step 5: Verify TypeScript compilations
      - name: Type Check
        run: npx tsc --noEmit

      # Step 6: Execute unit and component tests
      - name: Run Tests
        run: npx vitest run

      # Step 7: Build Next.js project to verify compilation
      - name: Build Project
        run: npm run build
        env:
          # Inject mock database keys required during build compile checks
          DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres"
          NEXT_PUBLIC_API_URL: "https://api.progrumar.com"

3. Optimizing CI Run Speeds with Package Caching

By default, virtual runner machines spawn with a clean operating system. Running npm install on every single run downloads hundreds of megabytes of package dependencies, consuming minutes of pipeline execution time.

By using the cache: 'npm' parameter inside the actions/setup-node action, GitHub caches your global package cache folder. If your package-lock.json file did not modify since the last run, the runner restores your libraries instantly, reducing pipeline run times from minutes to seconds.


4. Enforcing Type Checks via tsc --noEmit

While Next.js's compiler runs type assertions during the build process, compiling the entire project before checking types is computationally heavy.

Running npx tsc --noEmit executes TypeScript's compiler engine in validation mode, checking for type misalignments (such as passing a string to an integer prop) and outputting errors without writing compiled files to disk. Adding this step early in your pipeline catch bugs before heavy bundling scripts run.


5. Securing Merges with Branch Protection Rules

Creating a testing workflow is only half the solution; you must also prevent developers from bypassing checks to push broken features to production directly.

Configure Branch Protection Gates on GitHub:

  1. Navigate to your GitHub repository settings, click Branches.
  2. Add a protection rule targeting your main branch (e.g. main).
  3. Enable **Require status checks to pass before merging**.
  4. Search for your target workflow job name (e.g. test-and-build) and select it as a required check.
  5. Click **Save**. Now, the **Merge Pull Request** button on GitHub remains locked until all pipeline scripts complete successfully.

6. Continuous Deployment (CD): Automating Vercel Deploys

While Vercel handles deployments via its Git integration automatically, complex architectures require deploying only after custom integration test runs complete.

You can disable Vercel's default Git integration and trigger deployments programmatically via the Vercel CLI inside your GitHub Actions pipeline:

# Example CD deployment job snippet:
deploy:
  needs: test-and-build # Only run if test suites pass
  runs-on: ubuntu-latest
  steps:
    - name: Checkout Code
      uses: actions/checkout@v4
    - name: Install Vercel CLI
      run: npm install --global vercel
    - name: Deploy to Vercel
      run: vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }} --yes

7. Common Gotchas

  • Build Failures from Missing Build Environment Variables: If your Next.js pages query API parameters during compilation (such as static metadata tags), running npm run build inside pipelines will crash unless you configure mock environment variables in the YAML file settings.
  • Using npm install instead of npm ci: Calling npm install in pipelines updates package lock files and modifies dependencies dynamically, causing different build environments across machines. Always use npm ci (Clean Install) inside CI scripts.

Key Takeaways

  • Place GitHub Actions pipelines in .github/workflows/ files using YAML configurations.
  • Configure package caching using cache: 'npm' inside node setups to speed up build runs.
  • Run ESLint and npx tsc --noEmit to catch syntax and type errors early.
  • Enforce branch protection rules to block merging pull requests that fail testing runs.
  • Use npm ci rather than npm install inside CI scripts to ensure reproducible builds.

Automating tests with GitHub Actions ensures code health at compile time. However, building and deploying applications safely requires passing sensitive keys across environments without exposing them. In the next lesson, we cover Environment Variables & Secrets Management, learning how to partition public settings from secure backend credentials.

Chat with us