CI/CD on GitHub Actions for boutique studios — under 50 lines

Most boutique studio CI/CD is overengineered: hundreds of YAML lines, complex caching, custom runners, monthly debugging of pipeline issues. A simpler setup under 50 lines handles 90% of needs reliably.

Many boutique studios inherit CI/CD pipelines that look like enterprise refugees: 300 lines of YAML, custom Docker images, matrix builds across 5 OS versions, caching strategies more complex than the application itself.

A small team building typical web apps needs maybe 50 lines. The rest is performance theater.

What CI/CD must do

  1. Run on every PR.
  2. Block merge if tests fail.
  3. Deploy automatically on merge to main.
  4. Provide rollback when deploy goes wrong.

That's it. Everything else is optional.

Minimal GitHub Actions workflow

name: CI/CD

on:
  pull_request:
  push:
    branches: [main]

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

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - name: Deploy
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: ./scripts/deploy.sh

40 lines. Works.

What this gives you

  • Every PR runs lint, tests, build.
  • Failing tests block merge (via branch protection rules).
  • Main branch auto-deploys on merge.
  • Built-in caching for node_modules.
  • Deploy uses standard scripts.

Common additions that are usually wrong

Matrix builds across Node versions

Useful for libraries. Pointless for an internal application that runs on one Node version in production. Skip.

Test sharding

Useful when tests take >15 minutes. Premature optimization for most teams.

Coverage thresholds in CI

Tempting but counterproductive — incentivizes test theater over meaningful tests.

Custom runners

Self-hosted runners add operational burden. Use only when bottlenecked by GitHub-hosted runner limits.

Build matrix for Docker images

Usually one image per service. Don't optimize for parallel builds you'll never run.

What's worth adding

Concurrency limits

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

Prevents two simultaneous deploys racing.

Required status checks

In branch protection rules, require the test job to pass before merge. Without this, the CI workflow is advisory.

Slack notification on deploy

- name: Notify deploy
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: 'C123ABC'
    slack-message: "Deployed ${{ github.sha }} to production"
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }}

Pre-commit hook integration

Run linter locally before push. Saves CI cycles.

Secrets management

  • Secrets in repo settings (Settings → Secrets).
  • One per environment (DEV, STAGING, PROD).
  • Don't echo in workflows (GitHub masks them but be careful).
  • Rotate quarterly.

Caching

Default cache: 'npm' in actions/setup-node handles most cases. Don't write custom caching unless profiling shows specific need.

Deployment patterns

Rsync to VPS

- name: Deploy via rsync
  run: |
    rsync -avz --delete \
      ./dist/ \
      ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/app/

Docker push

- uses: docker/build-push-action@v5
  with:
    push: true
    tags: registry.example.com/app:${{ github.sha }}

Vercel/Cloudflare/Netlify

Their GitHub integration handles deploy without explicit CI step. Even simpler.

Rollback

Best rollback is forward fix. But sometimes you need to revert quickly:

  • Tag last known good release.
  • Have a workflow to deploy a specific tag.
  • Maintain N previous builds as artifacts.

Cost

GitHub Actions free tier: 2000 minutes/month for private repos. For a 5-engineer team with reasonable test discipline, this is plenty. Public repos get unlimited.

Anti-patterns

  • Tests in CI that pass locally and fail in CI without explanation. Flaky tests destroy trust. Fix or quarantine.
  • 10-minute test runs. Investigate. Parallelize. Pre-build images.
  • Deploys touched by hand. Manual deploys defeat the purpose.
  • Single workflow file 500 lines long. Split into multiple, simpler ones.

Verdict

CI/CD for boutique studios should be 40-80 lines of YAML. Run tests, deploy main, notify Slack. Skip matrix builds, custom runners, complex caching, coverage thresholds until you have specific reasons. Simple CI ships more reliably than complex CI everyone is afraid to touch.

Learn more about our competence
Web development, AI, automation — what we build and how.