WritingHow to Set Up a GitHub Actions CI/CD Pipeline from Scratch — Clixo
4 min readci-cd, github-actions, deployment, automation

How to Set Up a GitHub Actions CI/CD Pipeline from Scratch

Step-by-step guide to building a production-ready GitHub Actions CI/CD pipeline: install, test, build, and deploy automatically on every push.

Most teams manually deploy code far longer than they should. The process is slow, error-prone, and blocks everyone every time something goes wrong. GitHub Actions gives you a straightforward path to full automation — tests run on every push, builds are reproducible, and deployments happen without anyone touching a server.

This guide walks you through setting up a complete CI/CD pipeline with GitHub Actions from an empty workflow file to a production-grade release process.

Setting Up a GitHub Actions CI/CD Pipeline: The Foundation

GitHub Actions organizes work into workflows — YAML files stored in .github/workflows/. Each workflow runs on a trigger (a push, a pull request, a schedule) and contains one or more jobs that execute on virtual runners.

Create your first workflow file at .github/workflows/ci.yml.

Step 1: Define Your Trigger

name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

This runs the pipeline whenever code is pushed to main or a pull request targets main. Pull requests get CI feedback before merge; pushes to main trigger the full deploy path.

Step 2: Run Tests in a Dedicated Job

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm test

Always use npm ci instead of npm install in CI — it installs exactly what the lockfile specifies and fails if the lockfile is out of sync. Cache dependencies using the cache key on the setup action; this alone can cut 60-90 seconds off a typical Node.js job.

Step 3: Build and Validate

Add a build job that depends on test passing:

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

Upload the build artifact so the deploy job can download it instead of rebuilding. This keeps your artifacts reproducible — the exact same bytes that passed tests are the bytes that ship.

Step 4: Deploy Only on Main

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

The if condition is critical. Pull requests run test and build but never trigger deploy. Only a direct push to main reaches production. Secrets are stored in GitHub's encrypted secrets store and injected as environment variables at runtime — never hardcode tokens in workflow files.

Managing Environment Variables Safely

Store all sensitive values in Settings > Secrets and variables > Actions. Reference them as ${{ secrets.MY_SECRET }}. For non-sensitive configuration that differs per environment (API base URLs, feature flags), use Variables rather than Secrets so you can inspect them.

Speeding Up the Pipeline

  • Cache aggressively. Node modules, pip packages, Go module caches — cache all of them.
  • Parallelize independent jobs. Tests and lint checks do not need to run sequentially.
  • Limit the trigger scope. Use paths filters so a docs-only commit does not trigger a full deploy.
  • Set timeouts. Add timeout-minutes: 10 to jobs so a stuck test does not burn runner minutes indefinitely.

What a Mature Pipeline Looks Like

A production pipeline typically has five stages:

  1. Lint — enforce code style, fail fast
  2. Test — unit and integration tests in parallel
  3. Security scan — dependency audit, static analysis
  4. Build — produce a reproducible artifact
  5. Deploy — push the artifact, run smoke tests, notify on failure

Start with the middle three and add the outer two once the core flow is stable.

Common First-Time Mistakes

  • Using npm install instead of npm ci in CI
  • Storing secrets in the repository or workflow file
  • Deploying from feature branches instead of main
  • Not caching dependencies, making every run slow
  • Skipping a smoke test after deploy — you find out production is broken from users

Setting up a solid pipeline is a one-time investment that pays back on every subsequent release. If you want a team that can ship continuously without incidents, the pipeline is the foundation you build on.

Talk to Clixo about building your deployment infrastructure