Ship It: CI, Bots, and Releases
"The robots handle the vigilance so you can spend your attention on judgment."
Everything so far happened between you, your agent, and your repository. But open any active
project on GitHub and you'll see an entourage you didn't create: green checkmarks appearing on
every pull request, PRs opened by accounts named dependabot and release-please, security scans running on a schedule, version numbers bumping
themselves. None of it is magic, and none of it is optional knowledge anymore — this machinery
is how modern software actually ships, and every piece of it is built from things you already
know: branches, commits, PRs, and tags.
One lens makes the whole chapter click: everything here is either a safety net (it catches mistakes before users see them) or a memory (it records what happened and why, for the future maintainer — usually you). CI checks are safety nets. Changelogs and releases are memories. The bots just run both without being asked.
8.1 CI — Where the Green Checkmark Comes From
CI — Continuous Integration — is the fix, and the idea is almost embarrassingly simple: every time anyone pushes, a robot builds the project from scratch and runs every check against it. Not "when someone remembers." Every push, every PR, every time. On GitHub the robot service is called GitHub Actions: each run spins up a runner — a fresh, disposable Linux machine in the cloud — that clones your repo, installs dependencies from the lockfile, and works through the checklist. If everything passes, your PR gets the green checkmark. If anything fails, you get a red X and a log pointing at the failure — minutes after pushing, not weeks later in production.
The checklist itself is just a YAML file committed to your repo, which means it's versioned, reviewed, and branch-protected like everything else. A real, minimal one:
name: CI
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: 22
- run: npm ci # install EXACTLY the lockfile versions
- run: npm run lint # style + suspicious patterns
- run: npm test # the whole suite, every time
- run: npm run build # prove a production build worksRead it top to bottom: on these triggers, run these steps. That's the entire
programming model. The steps are the same commands you (or the hooks you wrote in Automating with Hooks) run locally — the difference is where and always. A pre-commit hook is a seatbelt you buckle on your own machine, and --no-verify unbuckles it. CI runs on a machine nobody can sweet-talk. Hooks are
the seatbelt; CI is the law.
Branch protection (the rulesets from Pull Requests) is what wires the checkmark to the merge button: no green, no merge — for you, your teammates, and every agent equally.
The sibling acronym, CD — Continuous
Delivery or Deployment — is what happens after green: code that reaches main ships to users automatically. Many projects (including this site) deploy
on every merge — a second workflow builds the app and publishes it the moment a PR lands. That
immediacy is exactly why the gate in front of main matters: when merging is shipping, "we'll fix it before the release" is not a sentence that exists.
"Add a GitHub Actions workflow to my repo that runs lint, tests, and a build on every pull request"
"My tests pass locally but fail in CI — walk me through the usual suspects (lockfile, node version, environment)"
8.2 The Robot Coworkers: Dependabot, CodeQL & Friends
Once CI guards the gate, something clever becomes possible: you can let robots propose changes, because no proposal — human, agent, or bot — gets through without passing the same checks. A bot's PR is a completely ordinary PR: a branch, a diff, a conversation, a checkmark. You already know how to review one. Meet the three coworkers you'll see most.
Dependabot — the dependency gardener
Your project stands on dozens of open-source packages, and dependencies age like food,
not wine: every one has its own release stream and, occasionally, its own security
holes. Dependabot watches all of them and opens PRs on your behalf — branches named dependabot/npm_and_yarn/... with commits like chore(deps): bump lodash from 4.17.20 to 4.17.21. The elegant part: its
PR triggers your CI. The robot proposes, your test suite disposes. If the update
breaks the build, you find out in the PR — not in production.
CodeQL — the code detective
GitHub's code-scanning engine treats your codebase like a database and runs queries written by security researchers against it — hundreds of known-dangerous shapes, like user input flowing unsanitized into HTML (cross-site scripting) or into file paths. It runs on PRs and on a schedule, so when a new attack pattern is discovered, your old code gets re-checked too. Think of it as a security specialist who re-reads your entire repo every week and only speaks up on a match.
Secret scanning — the leak alarm
Remember the staged .env drama from What NOT to Commit? GitHub runs a last line of defense: it recognizes the
formats of API keys and tokens and — with push protection on — refuses the push
outright. A blocked push is a gift. Treat any key that reaches a public commit as
burned, and rotate it.
Dependabot is configured with — you guessed it — a YAML file in the repo. The one setting worth knowing on day one is grouping, which turns thirty tiny weekly PRs into one digestible one:
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
groups:
minor-and-patch:
update-types: ["minor", "patch"]
# major bumps stay separate — those need real reviewTwo quieter pieces complete the safety net. The lockfile (package-lock.json) pins the exact version of every package and every package's packages, so your laptop, CI, and production all install
byte-identical dependencies — it's why the lockfile belongs in Git even though you never
edit it by hand. And supply-chain pinning: careful repos
reference third-party Actions by full commit hash instead of a friendly tag. You know from Tags & Releases that a tag is just a movable label — and if an attacker compromises an Action's repo, they can
quietly move v4 to malicious code. A commit hash can't be moved. Same Git concept, now a security
boundary.
Here's what a healthy history looks like with the robots at work — one of these commits was written by a human:
git log --oneline -n 4
e4f5a6b feat: add csv export
b7c8d9e chore(deps): bump the npm group with 3 updates
a1b2c3d chore(main): release 1.4.2 (#118)
9f8e7d6 fix: handle empty header rowTry It: Review the Robot's PR
git diff, merge it, and clean up the branch — the same moves the "Merge" button does for you on
GitHub.Loading playground...
"Set up Dependabot for my repo with weekly, grouped minor/patch updates"
"Dependabot opened a major-version bump PR — read the changelog of the dependency and tell me what could break"
8.3 Releases on Autopilot: SemVer, Conventional Commits, release-please
A green merge says the code is good. A release answers two different questions: what do we call this state, and what changed since the last one? In Tags & Releases you did this by hand — decided a version number, wrote an annotated tag. This lesson is about the grammar behind those version numbers, and the robot that does the paperwork.
Semantic Versioning: the number is a promise
A version like 2.4.1 reads as major.minor.patch, and each position
carries a promise to whoever upgrades: a patch bump (2.4.1 → 2.4.2) means "bug
fixes only, upgrade blind"; a minor bump (2.4 → 2.5) means "new features, nothing
you rely on changed"; a major bump (2 → 3) means "something breaks — read the notes before
touching it." That's why Dependabot's major-version PRs deserve your full attention while patch
bumps barely need a glance: the versioning scheme is literally telling you how scared to be.
Conventional Commits: the payoff
Since Committing you've been writing feat: and fix: prefixes, and in Automating with Hooks a hook started enforcing them. Here's the payoff: those prefixes map straight onto SemVer. fix: means the next release is at least a patch. feat: promotes it to a minor. A feat!: or a BREAKING CHANGE: footer forces a major. Your commit history stopped being prose and became data — which means a
machine can read it.
release-please: the release accountant
release-please (Google's oddly polite
release bot) watches main and keeps a running draft of the next release. It reads every
conventional commit since the last tag, computes the right version bump, and opens — a pull
request. The PR contains exactly two things: an updated CHANGELOG.md grouping your commits into Features and Bug Fixes, and the version bump. As more commits land
on main, the bot quietly amends its own PR. When you decide it's release time, you merge the PR
like any other — and the bot tags the commit and publishes a GitHub Release. If you've ever wondered
what a commit like chore(main): release 1.1.0 (#42) is: that's someone merging the accountant's paperwork.
The changelog it maintains is the memory half of this chapter at its purest — the human-readable answer to "what changed since 1.0.0?", assembled from messages you were already writing:
## 1.1.0 (2026-07-17)
### Features
* add csv export (#31)
### Bug Fixes
* handle empty header row (#33)Try It: Be release-please for a Day
v1.0.0. Do the accountant's job
by hand exactly once — read the log, write the changelog, commit the paperwork, cut the tag
— and you'll never wonder what the bot does again.Loading playground...
"Set up release-please for my repository and explain what its first PR will contain"
"Read my commits since the last tag and tell me the next version number — and why"
Loading challenge...