Automation with CI/CD
Advanced · 19 min read · ▶ live playground · ✦ checkpoint
Code review gives a team human judgment. Automation gives the team repeatable checks. CI/CD is the habit of running those checks and deployments from the same branch and pull request workflow you already learned, so every change gets the same basic safety pass before it merges or ships.
CI is the shared "run the checks" machine
CI means continuous integration: automated work that runs when code is pushed or a pull request opens. For a JavaScript project, the first CI job usually runs the same scripts people already run locally:
{
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"build": "vite build"
}
}You met those pieces earlier:
- Section 19 put linters, formatters, npm scripts, and build tools behind project commands.
- Section 20 made
typecheckan explicit TypeScript command. - Section 21 made
testa repeatable behavior check. - Section 23 made pull requests the place where review and checks meet.
CI does not replace any of those ideas. It connects them. If a teammate forgets to run npm test locally, CI still runs it. If a reviewer misses a formatting or type error, CI still reports it. The point is not to make humans unnecessary; it is to stop wasting human attention on checks a machine can run exactly the same way every time.
A first GitHub Actions workflow
GitHub Actions workflows live in .github/workflows/. A first checks workflow can be small:
name: Checks
on:
pull_request:
push:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm testRead it top to bottom:
onsays when the workflow runs: for pull requests, and for pushes tomain.jobs.checksnames one job.runs-onchooses a Linux runner.checkoutdownloads the repository into the runner.setup-nodeinstalls Node and enables npm caching.npm ciinstalls exactly from the lockfile.- The final steps run the project contract: lint, typecheck, test.
The workflow is intentionally boring. Boring is good here. A new teammate can open it and see the gate: "Can this branch install, lint, typecheck, and test?"
The pull request then shows checks beside review:
Pull request #14
Title: Add refresh button to weather widget
Checks:
Checks / checks passed in 1m 42s
Required before merge:
- approval from 1 reviewer
- Checks / checksThat is Section 23's human workflow and the machine workflow reinforcing each other. Review asks, "Is this change understandable, maintainable, and worth merging?" CI asks, "Do the repeatable project commands still pass?"
What a failing check is telling you
A failing CI run is not a moral event. It is a saved terminal transcript from a clean machine.
Checks / checks
✓ npm ci
✓ npm run lint
✕ npm run typecheck
src/ui/refreshButton.ts:18:21 - error TS2345:
Argument of type 'WeatherData | undefined' is not assignable to parameter of type 'WeatherData'.That failure is useful because it happened before merge. It also happened somewhere neutral: not only on your laptop, not only in your editor, not only with your cached files.
The right response is ordinary:
$ npm run typecheck
$ git add src/ui/refreshButton.ts
$ git commit -m "Guard refresh before weather data loads"
$ git pushThe pull request updates, CI runs again, and reviewers can see both the fix and the new result.
Use CI for checks that should be true for every shared change. Keep the first gate focused:
npm run lintfor suspicious code patterns.npm run typecheckfor TypeScript promises.npm testfor automated behavior checks.npm run buildwhen build output is part of the deployable contract.
Do not put every slow experiment into the required gate. A twenty-minute required check teaches people to avoid the process. Fast checks create trust.
CD is shipping from the same pipeline
CD usually means one of two related ideas:
- Continuous delivery: every merged change is kept deployable, and production shipping is a deliberate button, approval, or controlled step.
- Continuous deployment: every change that passes the pipeline ships automatically.
Beginners do not need to win a terminology argument. The useful idea is: once checks pass, the same workflow can create deployable output and send it to an environment.
An environment is a place the app runs: preview, staging, production. Section 22 used that word for deployment configuration. CI/CD uses it for shipping flow too.
Preview deployments are especially useful with pull requests:
Pull request #14
Title: Add refresh button to weather widget
Preview deployment:
URL: https://weather-widget-pr-14.example.app
Commit: d0f6a22 Tighten refresh button label
Status: ready
Checks:
Checks / checks passedNow review can include the running app, not just the diff. A reviewer can click the preview URL, try the refresh button, and leave a comment tied to the exact branch.
A production shipping flow is stricter:
Production deployment
Source branch: main
Commit: 42b7c91 Merge pull request #14 from add-refresh-button
Checks: passed
Approval: required from @weather-team/release
Environment: productionThat shape keeps production special without making it mysterious. The branch rule says only main can deploy. The required checks say broken code cannot ship quietly. The approval says a human still decides when production changes happen.
A deployment workflow shape
The exact deploy command depends on where the app lives. The durable structure looks like this:
name: Deploy
on:
pull_request:
push:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build
preview:
needs: checks
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
environment: preview
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- run: npm run deploy:preview
production:
needs: checks
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- run: npm run deploy:productionThis is a shape, not a command to paste blindly. deploy:preview and deploy:production are project-specific scripts. A static site host, a serverless platform, and a container setup all deploy differently.
The important pattern is stable:
- Build and check first.
- Preview pull requests.
- Ship production only from
main. - Put production behind environment rules or approvals when the project needs them.
- Keep secrets in the CI/CD provider's secret store, not in source files.
What automation should not do
Automation can run commands. It cannot decide product taste, judge maintainability, understand every user risk, or notice that a requirement was misunderstood. That remains review work.
The healthy split looks like this:
Automation handles:
install
lint
typecheck
test
build
deploy mechanics
Humans handle:
intent
design tradeoffs
naming clarity
product behavior
rollout judgment
whether the tests are the right testsWhen CI fails, fix the code or the check. When review finds a risk CI cannot see, improve the code, add a test if the risk is repeatable, and let automation guard that lesson next time.
That closes the Git and collaboration section: snapshots, branches, pull requests, review, conventions, and automation. Next section adds AI to the developer workflow, but the rule stays the same: a tool can speed up work, while human review and automated checks keep the work accountable.
Checkpoint
Answer all three to mark this lesson complete