2026-07-28 · DevOps

I Automated My Whole Deploy Pipeline with GitHub Actions (Took 2 Days)

I used to deploy manually. SSH into the server, pull the latest code, run the build script, cross my fingers, and hope nothing broke. It worked — until the day I pushed a typo to production at 11 PM on a Friday and didn't notice until Monday morning.

That was the last straw. I spent the next weekend building a proper CI/CD pipeline with GitHub Actions. Two days of setup. It's paid for itself a hundred times over.

Why GitHub Actions Instead of Jenkins?

I've used Jenkins before. It's powerful but you need to maintain a server, manage plugins, deal with Java issues. For a solo developer, that overhead kills productivity. GitHub Actions wins on simplicity — the runners are managed by GitHub, the YAML is straightforward, and the marketplace has pre-built actions for everything.

The free plan gives 2,000 minutes/month. My pipeline runs in ~4 minutes, so 500 runs/month before hitting limits.

The Pipeline Structure

I split it into three workflows:

  • CI: Runs on every PR. Lint, test, build (~90s).
  • CD: Runs when merged to main. Docker build, deploy.
  • Nightly: 2 AM. Security scans, dependency updates.

The CI Workflow

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Run tests
        run: npm test
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Key decisions: use npm ci not npm install — it's faster and uses exact lockfile versions. Enable caching with cache: 'npm' — first run 60s, subsequent runs 5s.

The CD Workflow

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test
      - name: Build
        run: npm run build
      - name: Deploy to Cloudflare Workers
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}

The wrangler-action does the heavy lifting. Pass your API token as a secret, and it handles auth and deployment.

Slack Notifications

I added a Slack step so the team knows when deployments happen:

      - name: Notify Slack
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: '{"channel":"#deployments","text":"🚀 Deployed ${{ github.repository }}"}'

Comes in handy when something breaks and you need to trace which change caused it.

What I'd Do Differently

1. Start with caching earlier. First few months no caching meant 3+ min builds. Adding cache dropped it to 90s.

2. Add a staging environment. Everything goes straight to prod right now. Planning to add a manual approval gate.

3. Pin action versions. Using @v4 tags is risky — maintainers can push breaking changes. Pin to specific SHAs.

4. Matrix testing for Node versions. If your app supports multiple versions, test them all in parallel:

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

The Cost Calculation

  • GitHub Actions: Free (public) or $4/month (Team, 3,000 min)
  • Cloudflare Workers: $0 (free tier) or $5/month
  • Time saved: 30 min/deploy × 3-4/week = 6 hours/month

No more Friday night deployment anxiety. That alone is worth the setup.

Start Small

Don't copy-paste my whole pipeline. Start with 10 lines of YAML that just runs tests. Add complexity as you go. I almost gave up trying to build the perfect pipeline on day one.

What does your CI/CD pipeline look like? I'm always looking for ways to improve mine.

← Back to Home