Setting Up CI/CD for a Side Project with GitHub Actions
- #setup
- #tooling
- #tutorial
A few minutes of GitHub Actions configuration buys you a real safety net: nothing broken merges to main, and deploys happen without you running commands by hand.
The minimum useful workflow
Start with a single workflow file that runs on every pull request and push to main. Workflows live in .github/workflows/ as YAML, and each one describes jobs made of steps that run in order. For a typical Node/TypeScript project:
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
That’s the baseline: install, build, test, on every PR. --frozen-lockfile catches a lockfile that’s drifted from package.json, a surprisingly common source of “works on my machine,” since without it a stale lockfile silently installs different versions than what’s committed.
Type checking and linting get their own steps
If you have separate typecheck and lint scripts, don’t bury them inside build:
- run: pnpm typecheck
- run: pnpm lint
Splitting these out means GitHub shows each as its own checkmark or X in the PR UI. You know at a glance whether it was a type error, a lint rule, or a failing test, no digging through logs required.
Caching, mostly automatic
actions/setup-node’s cache: pnpm option handles dependency caching once you point it at your lockfile. No separate cache step needed. Larger projects can add framework-specific caches (Next.js’s .next/cache) with actions/cache, keyed on a hash of the lockfile and source files, but for most side projects the dependency cache alone turns a two-minute install into a few seconds, and that’s the whole win most people are looking for.
Secrets, when a step needs one
Once a step needs something sensitive, an API key for a test suite that hits a real service, a deploy token, don’t put it in the workflow file. Add it under the repo’s Settings → Secrets and variables → Actions, then reference it:
- run: pnpm test
env:
STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
GitHub redacts secret values from logs automatically, so a step that accidentally echoes one won’t leak it in the Actions UI. Worth setting up early, since retrofitting secrets into a workflow that’s been hardcoding a value in a YAML file (even a test-only one) means rotating that value afterward, not just deleting a line.
Deploy automation, only where you actually need it
If you’re on Vercel or Netlify, skip a deploy step in Actions entirely. Connecting the repo directly gives you git-push deploys and preview URLs with zero YAML. Save a dedicated deploy workflow for platforms without native GitHub integration, a VPS, a Docker registry push, Fly.io via flyctl. There, add a job that only runs after tests pass:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: flyctl deploy --remote-only
Testing against a real database
If your test suite needs Postgres rather than a mock, spin one up as a service container right in the workflow instead of pointing tests at a shared dev database:
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
GitHub tears the container down when the job finishes, so every run starts from a clean database with no leftover state from the last one poisoning a test that depends on an empty table.
The setting that makes it actually matter
Turn on “Require status checks to pass before merging” in branch protection for main. Without it, a red CI check is a suggestion. With it, a broken build can’t merge, which is the entire point when you’re often the only reviewer of your own PRs. Two minutes, and it turns the workflow file from documentation into an actual gate.
Resist building a 200-line workflow on day one. Install, build, test first. Add typecheck and lint once those scripts exist. Add deploy automation only if the platform doesn’t already do it. If a CI step starts needing its own debugging sessions, it’s doing more than a side project needs.