CI/CD and Continuous Deployment
Week 11 · From a pull request to production, on Project Pulse
Week 11 · From a pull request to production, on Project Pulse
Same people, same project. Which team would you rather be on?
"Instead of heading for a big integration breakdown, I decided to have a small breakdown every Tuesday evening."
Adapted from Graham Parke, No Hope for Gomez!
Integration should be a routine, not a drama.
An agent multiplies the pull requests your team produces. It doesn't multiply the hours anyone has to review them.
The pipeline is the one reviewer that checks every pull request the same way.
Actions configured and three-week branches is not CI.
"It works on my machine." "Fine, then we'll ship your machine." That's a container.
Image
A read-only template. Like a class.
Container
A running instance. Like an object.
The image is the artifact the pipeline ships.
classDiagram
direction LR
Repository "1" --> "0..*" Workflow : holds
Event "1..*" --> "0..*" Workflow : triggers
Workflow "1" *-- "1..*" Job
Job "1" *-- "1..*" Step
Job "1" --> "1" Runner : runs onon:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a... # v4.4.0
- uses: actions/setup-java@cf277c60... # v4.9.1
with: { java-version: "21", distribution: "temurin", cache: maven }
- run: mvn --batch-mode verify
working-directory: ./backend
frontend: # npm ci, ESLint, type check, Prettier
dockerfile: # HadolintPoint at the event, a job, a step that uses an action, and a step that runs a command.
Red in CI and green in your IDE means CI found something real.
flowchart LR
pr["Pull request"] --> ci["maven-build.yml<br/>build · frontend · dockerfile"]
ci -->|"all green, up to date"| main["Merge to main"]
main --> build["azure-webapps-deploy.yml<br/>image tagged with the SHA"]
build --> deploy["staging slot"]
deploy -->|"a person swaps"| prod["production slot"]Project Pulse, until 3 November 2026.

| Rule | What it stops |
|---|---|
| Restrict deletions | Deleting main |
| Block force pushes | Rewriting history; a released SHA always exists |
| Require a pull request | Pushing around the checks |
| Require status checks: build, frontend, dockerfile | Merging red or still running |
| Require branches up to date | Merging what was tested against an old main |
A gate the maintainer can walk around is not a gate.
"I'll just bypass it this once." Famous last words, and a famous first incident report.
Require branches to be up to date: the checks rerun on what will actually merge.
- name: ESLint (report only)
continue-on-error: true
run: npm run lint:checkRequired "frontend" blocks a broken npm ci, and nothing else yet.
"It passed on the third try" is not a test result. It's a slot machine.
| What's automatic | Who decides production | |
|---|---|---|
| Continuous integration | Build and test every change | Not about production |
| Continuous delivery | Every green merge becomes a release that could ship | A person presses the button |
| Continuous deployment | Every green merge ships | The pipeline |
To staging
Every merge to main, automatically.
Continuous deployment.
To production
When a person swaps the slots.
Continuous delivery.
Which would you want for your client's MVP, and why?
tags: ghcr.io/${{ env.REPO }}:${{ github.sha }}A rebuild is a new artifact nobody tested.
The deploy workflow skips the tests. When is that OK?
Only when the tests passed on exactly the code being merged: required, and up to date.
"Hardening ingestion of Cloudflare-generated configuration files in the same way we would for user-generated input."
Code
Swap back. Seconds. No rebuild.
Data
Migrations run forward only. The old build meets the new schema.
A schema change the previous build can't live with is a release you can't roll back.
V5: safe
Code stopped writing nulls first. Columns checked: zero nulls in staging and prod. Then NOT NULL.
V6: not
Pull request #99: category stored as a name, not a number. Code and column changed together. The old build expects a number and finds a name.
| Secret | What it can do |
|---|---|
| AZURE_WEBAPP_PUBLISH_PROFILE | Deploy anything to staging |
| CLAUDE_CODE_OAUTH_TOKEN | Run Claude Code on my account |
| GITHUB_TOKEN | Fresh per job; push the image |
Least privilege: maven-build.yml asks for contents: read and nothing else.
Never "fix" a fork's workflow with pull_request_target: it runs with your secrets, and a stranger's code can run with them.
Publish profile
A password that works until someone regenerates it.
OIDC
Azure trusts GitHub's token for this workflow and returns a credential that lasts minutes.
Project Pulse's deploy workflow holds the publish profile. Until #108, it named its Actions by tag.
claude.yml runs Claude Code when someone writes @claude in an issue or a comment. It holds a secret, reads text anyone can write, and can reach the network.
An agent in CI never acts on text from someone outside the team.
npm ci
Installs exactly what package-lock.json says. Fails if it disagrees.
npm install
May update the lockfile within package.json's ranges.
Project Pulse, until #108: the pull request workflow ran npm ci; the deploy workflow ran npm install.
v1.0.0-final-FINAL-really-final is not Semantic Versioning.
git tag -a v0.1.0 -m "Checkpoint 2: proving slice"
git push origin v0.1.0
Then: Releases, Draft a new release, choose the tag, Generate release notes.
v0.1.0 at Checkpoint 2, v0.2.0 at Checkpoint 3, v1.0.0 for the MVP.
Every flag gets an owner and a removal date. A retired flag is deleted, never repurposed.
A cloud account bills by the hour. Set a cost alert the day you create it.
Developers
Write features. Rewarded for change. Hand the release over the wall.
Operations
Keep production up. Rewarded for stability. Catch what comes over.
Every release is a negotiation, every outage a search for someone to blame. What would you change?
"You build it, you run it."
Werner Vogels, Amazon
One team owns a change from commit to production.
| Means | You've done it | |
|---|---|---|
| Culture | One team, commit to production | You deploy what you build |
| Automation | Done twice by hand? Pipeline step | Today's workflows |
| Lean | Small batches, flowing | Team 2 |
| Measurement | Numbers, not opinions | DORA; observability, week 12 |
| Sharing | Dashboards, knowledge, blameless postmortems | Reliability |
Speed
Deployment frequency. Change lead time, commit to production. Recovery time after a failed deploy.
Stability
Change fail rate. Deployment rework rate.
DORA: "speed and stability are not tradeoffs."
class SRE implements DevOps { }Google's own summary, from The Site Reliability Workbook: "class SRE implements interface DevOps."
Asked to fix a failing build, an agent can add @Disabled, set continue-on-error, grow a baseline, or regenerate the lockfile. Each one turns the check green.
Ask of every such diff: does this make red green without fixing anything?
Delegate
Draft a workflow. Explain a failed run's log. Draft release notes. Split a migration into expand and contract. Spot unpinned Actions.
Keep
What's required on main, and who may bypass it. When production changes. Which secrets exist. Whether a schema change can roll back.
Checked on every pull request, blocked when red, deployed from what was checked, undoable when it goes wrong.