CI/CD doesn't need to be complex to be professional. A well-crafted pipeline with GitHub Actions can run lint, tests, build, and deploy in a single YAML configuration that any engineer on the team can understand and maintain.
This article builds a real pipeline from scratch, explains every decision, and shows the patterns that avoid the most common production problems.
Basic workflow structure
A GitHub Actions workflow is a YAML file in .github/workflows/. The structure is simple: when to run, in what environment, with what steps.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test --coverage
- run: pnpm build
That's the minimum pipeline. Simple, but already catches most problems before reaching production.
Dependency cache: the detail that changes speed
Without cache, each run installs all dependencies from scratch. That can be 2-3 minutes. With cache, installation of unchanged dependencies is skipped.
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm' # ← lockfile hash as cache key
For npm, use cache: 'npm'. For yarn, cache: 'yarn'. The cache uses the lockfile hash as a key. When the lockfile changes, the cache is invalidated.
Separating CI from CD: the deploy job
CI (continuous integration) runs on every PR. CD (continuous delivery) runs only when code enters production. Separate into distinct jobs with explicit dependency:
jobs:
ci:
runs-on: ubuntu-latest
steps:
# ... lint, test, build
deploy:
needs: ci # only runs if CI passes
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # only on main
environment: production # requires manual approval if configured
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
# your deploy script here
Secrets: how to do it right
Never put credentials in the YAML. Use GitHub Secrets, available in Settings > Secrets and variables > Actions of the repository.
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: ./scripts/deploy.sh
For environments (production, staging), use Environment Secrets. They allow configuring different secrets per environment and adding required reviewers before deployment.
Matrix builds: testing across multiple versions
For libraries or APIs supporting multiple Node versions, matrix builds run CI in parallel across all combinations:
jobs:
ci:
strategy:
matrix:
node-version: [18, 20, 22]
runs-on: ubuntu-latest
name: Test on Node ${{ matrix.node-version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
Artifacts: preserving outputs between jobs
If you want to use the build output in the deploy job, or preserve coverage reports, use artifacts:
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
The complete pipeline
name: CI/CD
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: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test --coverage
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: ci
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./scripts/deploy.sh
88 lines. Complete pipeline, secure, with cache, CI/CD separation, and conditional deploy. That's all most projects need.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk