CI/CD From Zero to Production in 30 Minutes With GitHub Actions
A no-fluff guide to shipping a real CI/CD pipeline that lints, tests, builds, and deploys automatically, without the enterprise boilerplate.
A CI/CD pipeline doesn’t need to be complex. Here’s a real workflow that runs on every push, catches bugs before merge, and deploys automatically.
The goal
On every pull request: lint, test, build. On merge to main: deploy. Under 30 minutes to set up. Copy and adapt.
Step 1: Create the workflow file
.github/workflows/ci.yml
Step 2: The CI pipeline
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Test
run: npm test -- --coverage
- name: Build
run: npm run build
That’s it for CI. Every PR now runs all four steps in parallel jobs if you split them, or sequentially here for simplicity.
Step 3: Deploy on merge to main
Add a deploy job that depends on ci:
deploy:
needs: ci
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
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 to production
run: npx your-deploy-cli deploy ./dist
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Add DEPLOY_TOKEN to GitHub → Settings → Secrets.
Caching dependencies
The cache: 'npm' in setup-node caches ~/.npm between runs. On a warm cache, npm ci goes from 90s to 8s on a typical project.
Useful additions
Matrix testing across Node versions:
strategy:
matrix:
node-version: ['20', '22']
Fail fast if coverage drops:
- name: Test with coverage threshold
run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'
Notify on failure (Slack):
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: '{"text":"❌ Build failed on ${{ github.ref }}"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Common mistakes
- Not pinning action versions. Use
@v4not@main. Unpinned actions can silently break. - Storing secrets in the workflow file. Always use
${{ secrets.NAME }}. - Running everything in one job. Split lint/test/build into parallel jobs to cut wall-clock time.
- Not caching. The cache step pays for itself on the first warm run.
The result
Every PR gets a green check or a red X before anyone reviews it. Merging to main ships to production automatically. The whole thing costs $0 on GitHub’s free tier for public repos, and ~$0.008 per minute for private repos.
That’s the baseline every project should have.