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
- Run on every PR.
- Block merge if tests fail.
- Deploy automatically on merge to main.
- 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.