CI/CD Skeleton
Stand up a GitHub Actions pipeline that gates every PR on build + test + lint.
Why
A CI skeleton that runs on every PR is the cheapest bug prevention you will ever install. It makes 'it works on my machine' irrelevant because the machine is now a clean runner. Without it, broken code merges silently and the first person to discover the breakage is the one deploying to prod. The skeleton also creates the deployment runway: once build+test is green in CI, adding a deploy step is a 10-line job, not a project. Teams without this skeleton end up deploying from laptops with rsync, which is how prod goes down on a Friday.
When
Right after project-init, before the first feature PR. Takes 30-45 min. Skip only if the repo will never have >1 contributor.
How
1. Create .github/workflows/ci.yml triggered on PR + push to main - jobs: lint, test, build — run in parallel, all must pass 2. Use a setup-node action with cache: 'pnpm' to skip reinstall - actions/setup-node@v4 with node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile 3. Make the jobs fail fast on lint, slow on test - lint: 30s timeout, fails on any warning - test: 10min timeout, runs vitest --coverage - build: produces artifact, uploads for deploy job 4. Add a deploy job gated on branch == main && CI passed - uses: needs: [lint, test, build] - if: github.ref == 'refs/heads/main' - Deploy to staging; prod is a manual approval (environment: prod) 5. Protect the main branch in repo settings - Require status checks: ci/lint, ci/test, ci/build - Require 1 review + require branches up-to-date before merge
Example
# .github/workflows/ci.yml
name: ci
on: { pull_request: {}, push: { branches: [main] } }
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version-file: '.nvmrc', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version-file: '.nvmrc', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm test --coverageCommon Problems
- ⚠Not using --frozen-lockfile in CI, so a teammate's dirty lockfile silently changes versions on everyone.
- ⚠Caching node_modules instead of the pnpm store — the cache key never invalidates correctly and you get stale deps.
- ⚠Running all jobs in one giant job instead of parallel — PR feedback takes 12 min instead of 3.
- ⚠Forgetting branch protection, so someone can push straight to main and bypass CI entirely.
Best Practices
- ✓Pin every action to a major version (@v4) and use Dependabot to bump them — unpinned actions break silently when upstream ships a bad release.
- ✓Set a timeout-minutes on every job so a hung process fails the PR instead of running for 6 hours.
- ✓Use concurrency groups to cancel superseded runs on the same branch — saves 50%+ of CI minutes.
- ✓Put secrets in GitHub Environments (not repo-level), so staging secrets can't leak into a PR fork.