Continuous Integration means every change gets tested automatically; Continuous Deployment means a passing change ships automatically too. Together, they remove "did anyone actually run the tests before merging?" as a question that needs a human.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
on defines what triggers the workflow — here, any push to main and any PR targeting it. npm ci (not npm install) installs exact versions from the lockfile, which is what you want for reproducible builds.
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- name: Deploy
run: ./scripts/deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
needs: test means this job only runs if the test job succeeded — a broken build never reaches the deploy step. The if condition further restricts deploys to pushes on main, so a PR from a branch never triggers one.
${{ secrets.DEPLOY_KEY }} pulls from GitHub's encrypted secrets store, configured in the repo settings — never commit an API key or deploy credential directly into a workflow file, even a private repo's.
The Actions tab shows each job's steps with individual pass/fail status and full logs per step. A failing test job blocks the PR merge button if you've set branch protection to require it — which is the actual enforcement mechanism, not just a visual warning.
From here, common additions are a linting step, a matrix build testing multiple Node versions, caching node_modules between runs to speed things up, and a staging deploy that runs on every PR before the production deploy on merge to main.
Comments