CI/CD That Ships Itself: Our GitHub Actions Blueprint

CI/CD That Ships Itself: Our GitHub Actions Blueprint

Backer posted Originally published at pocketportfolio.app 5 min read

CI/CD That Ships Itself: Our GitHub Actions Blueprint

(Pocket Portfolio — 12 Weeks of Shipping)

Fast releases are only useful if they are consistent and safe.
This post breaks down Pocket Portfolio’s CI/CD pipeline on GitHub Actions: how we lint, type-check, test against Firebase emulators, run Playwright smoke tests, and then canary-deploy to the Edge.

The goal is simple: every commit proves it can ship itself.


Pipeline Overview

Stages executed per pull request and on main:

1) Lint and TypeCheck
2) Build with reproducible caching
3) Firebase emulator tests (unit and integration)
4) Playwright UI smoke tests
5) Artifact capture (build, coverage, screenshots)
6) Canary deploy to next.pocketportfolio.app with manual approval
7) Post-deploy health checks and auto-rollback

We use:

  • GitHub Actions matrix to cover Node versions and Next.js runtime modes
  • Project-level concurrency to avoid clobbering environments
  • Required checks enforced by branch protection rules

Repository Structure (relevant parts)


.github/
workflows/
ci.yml
deploy-canary.yml
apps/
web/                  # Next.js 14 app
packages/
ui/                   # shared components
config/               # ESLint, TS config
tests/
e2e/                  # Playwright specs
seeds/                # Firebase test data

````

---

## Workflow 1: CI (PR and main)

`./github/workflows/ci.yml`

```yaml
name: CI

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

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  setup:
    name: Setup
    runs-on: ubuntu-latest
    outputs:
      node: ${{ steps.matrix.outputs.node }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - name: Install
        run: pnpm install --frozen-lockfile

  lint_typecheck:
    name: Lint and TypeCheck
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - run: pnpm eslint .
      - run: pnpm tsc -p tsconfig.json --noEmit

  build:
    name: Build
    needs: [setup, lint_typecheck]
    runs-on: ubuntu-latest
    env:
      NEXT_TELEMETRY_DISABLED: 1
      NODE_ENV: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - name: Build web
        run: pnpm --filter ./apps/web build
      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: web-build
          path: apps/web/.next

  test_emulators:
    name: Firebase Emulator Tests
    needs: build
    runs-on: ubuntu-latest
    services:
      firestore:
        image: docker.io/bitnami/firebase-emulator:latest
        ports:
          - 4000:4000
          - 8080:8080
          - 9099:9099
          - 9199:9199
    env:
      FIREBASE_AUTH_EMULATOR_HOST: localhost:9099
      FIRESTORE_EMULATOR_HOST: localhost:8080
      GCLOUD_PROJECT: test-project
      NODE_ENV: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - name: Seed test data
        run: pnpm tsx tests/seeds/seed.ts
      - name: Unit and integration tests
        run: pnpm test -- --reporter=junit --coverage
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage

  e2e_smoke:
    name: Playwright Smoke (headless)
    needs: test_emulators
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - name: Install browsers
        run: pnpm exec playwright install --with-deps
      - name: Run smoke tests
        env:
          BASE_URL: http://localhost:3000
        run: |
          pnpm --filter ./apps/web dev & sleep 8
          pnpm exec playwright test tests/e2e/smoke.spec.ts --reporter=line
      - name: Upload Playwright artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-artifacts
          path: |
            playwright-report
            test-results
````

What this does:

* Cancels superseded runs automatically
* Builds once and reuses artifacts
* Spins Firebase emulators for realistic tests
* Runs Playwright in headless mode against a local dev server

---

## Workflow 2: Canary Deploy to Edge

`./github/workflows/deploy-canary.yml`

```yaml
name: Deploy Canary

on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [ main ]

permissions:
  contents: read
  deployments: write
  id-token: write

concurrency:
  group: deploy-canary
  cancel-in-progress: false

jobs:
  gate:
    name: Require green CI
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - run: echo "CI passed, proceeding to deploy."

  deploy:
    name: Deploy to next.pocketportfolio.app
    needs: gate
    runs-on: ubuntu-latest
    environment:
      name: canary
      url: https://next.pocketportfolio.app
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - name: Build
        run: pnpm --filter ./apps/web build
      - name: Vercel Deploy (Edge)
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          working-directory: ./apps/web
          scope: ${{ secrets.VERCEL_ORG_ID }}
          prod: false
      - name: Health check
        run: |
          curl -fsSL https://next.pocketportfolio.app/api/health || exit 1
          curl -fsSL https://next.pocketportfolio.app/api/health-price || exit 1

Notes:

  • workflow_run ensures deploy runs only after CI succeeds on main.
  • Environment protection rules can require manual approval inside GitHub before the deploy job starts.
  • We target Edge by configuring Vercel project functions to run on the Edge runtime.

Health Checks and Auto-Rollback

We keep deploys safe with a simple pattern:

  1. After deployment, curl health and quote endpoints
  2. If any return non-2xx, mark the job failed
  3. A failing canary keeps production untouched

Optional rollback step (if you use Vercel Protected Branches):

      - name: Roll back canary to previous deployment
        if: failure()
        run: |
          echo "Rolling back..."
          # Example: pin to previous successful alias or commit sha via Vercel CLI

Speed and Stability Techniques

  • Caching: actions/setup-node with cache: pnpm cuts install time.
  • Concurrency: single deploy job at a time prevents environment drift.
  • Artifacts: store build output and Playwright results for debugging.
  • Separate concerns: CI proves code is safe; deploy workflow just ships.
  • Emulators: Firebase emulator suite catches integration issues locally and in CI.

Failure Modes We Handle

  • Stuck third-party provider: Playwright tests stub provider responses for critical flows.
  • Flaky UI timing: Playwright uses test id selectors and realistic waits.
  • Race conditions: matrix and concurrency settings keep builds deterministic.

Minimal Smoke Spec Example

tests/e2e/smoke.spec.ts

import { test, expect } from '@playwright/test';

test('dashboard loads and shows health badge', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByTestId('health-badge')).toHaveText(/healthy|degraded/);
});

test('price endpoint responds', async ({ request }) => {
  const res = await request.get('/api/health-price');
  expect(res.ok()).toBeTruthy();
});

What To Measure

  • CI total duration (target: under 8 minutes for PRs)
  • Flake rate (target: below 1 percent over 30 days)
  • Canary failure rate and mean time to recovery
  • Playwright runtime and retries per suite

Assets

  • Workflow diagram (Setup -> Lint -> Build -> Emulators -> Playwright -> Canary Deploy)
  • Screenshots of GitHub Checks and Environment approvals
  • Example Playwright report


TL;DR

Every commit runs the same path production will run: build, test, verify, deploy, and prove it is healthy.
If any step fails, the release stops itself. That is CI/CD that ships itself.

2 Comments

2 votes
0
2 votes
1

More Posts

Automate your open-source project's builds & deployments with CI/CD using GitHub Actions!

Ayush Thakur - Oct 2, 2025

From Code Push to Docker Hub: CI/CD with GitHub Actions

Imthadh Ahamed - Oct 14, 2025

Sovereign Intelligence: The Complete 25,000 Word Blueprint (Download)

Pocket Portfolioverified - Apr 1

CI/CD Security Mistake: Are You Giving Your Build Container Root Access to Your Server?

Waffeu Rayn - Nov 1, 2025

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

3 comments
2 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!