← ALL POSTS
CUSTOM SOFTWARE STEP-BY-STEP GUIDE

Set up CI/CD with GitHub Actions

If deploying your app involves a person remembering steps, it will eventually be deployed wrong. CI/CD fixes that: every push runs your tests automatically (continuous integration), and code that passes on your main branch deploys itself (continuous deployment). GitHub Actions is the easiest place to start because it's built into GitHub, free for a generous amount of usage on public and private repos, and configured with one YAML file in your repo. In this guide you'll set up a workflow that tests every push and pull request for a Node project, then add a deploy job that only runs on main.

Prerequisites

Step 1: Understand the moving parts

Three terms and you know the whole system:

Step 2: Create the CI workflow

In your repo, create .github/workflows/ci.yml:

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: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

What each piece does:

Commit the file and push. Go to the Actions tab in your repo and you'll see the run in progress, with live logs for every step.

Step 3: Add a deploy job gated on main

Now extend the same file with a second job that deploys, but only after tests pass and only on the main branch:

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm ci

      - name: Deploy
        run: npm run deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

The two lines that make this safe:

Replace npm run deploy with your actual deploy command. For a Cloudflare Workers site that's npx wrangler deploy; for a server it might be a script that rsyncs files and restarts a service. Whatever you type by hand today goes here.

Step 4: Store credentials as secrets

Your deploy command almost certainly needs a token or key. Do not put it in the YAML. In your repo, go to Settings, then Secrets and variables, then Actions, and add a repository secret (for example DEPLOY_TOKEN). The ${{ secrets.DEPLOY_TOKEN }} syntax in the workflow injects it as an environment variable at runtime. Secrets are encrypted, masked in logs, and not exposed to workflows triggered from forks.

Step 5: Protect the main branch

The pipeline only helps if people can't route around it. In Settings, then Branches, add a branch protection rule (or ruleset) for main that requires the test check to pass before merging. Now a PR with failing tests physically cannot merge, and since deploys only happen from main, nothing broken reaches production.

Verify it

Run the full loop once, on purpose:

  1. Open a branch, break a test, and push a PR. The check should go red and the merge button should lock.
  2. Fix the test, push again, watch it go green, and merge.
  3. Watch the Actions tab: the test job runs on main, then the deploy job runs, then your change is live.

If all three happen without you touching a server, you have CI/CD. From here you can add more later (linting, multiple Node versions, staging environments), but resist the urge to build a cathedral. A pipeline that tests every change and deploys main automatically already puts you ahead of most small shops we walk into.

Stuck on this, or want it done for you? That's the job.

Email us →
RELATED READING
Python: the duct tape of business automation Explainer
Workflow automation: five real examples from real businesses Explainer
Testing that fits a small app budget Explainer
Where to host your app: VPS, PaaS, or serverless Explainer
Why we build in TypeScript Explainer
NO FORMS. JUST EMAIL.
mason@hurbs.io
or (832) 457-4317, LA and Houston