Deployment, Reproducibility, and the Handoff
Who inherits what you built?
Chapter 14 — Deployment, Reproducibility, and the Handoff
“It works on my machine.” — the most expensive sentence in software; old enough that no one can claim it, common enough that everyone has said it
“I hated all my toil in which I toil under the sun, seeing that I must leave it to the man who will come after me.” — Ecclesiastes 2:18 (ESV)
Why This Matters
Last week you wrote the documentation. This week you find out whether any of it was true.
That is not a joke. Week 13 produced a README.md, an architecture overview, a runbook, and a decision-record index, and you ran the clean-machine test against them. Week 14 takes the same honesty and points it at the system instead of the prose. Can this thing exist somewhere that is not your laptop? Can somebody else start it, configure it, deploy it, break it, and put it back? Can they take it over after you graduate, move away, and stop answering email?
You are wearing the release engineer’s hat, with the operator’s hat in your other hand. In a real company these are people whose entire job is the distance between “the code is finished” and “the software is running” — packaging, configuration, secrets, environments, deploys, rollbacks, versioning, licensing, on-call. Solo, that distance is yours to close, and the SDLC calls this phase transition: the deliberate movement of a system from the builder to the operator, and from the operator to whoever comes next. It is the phase students skip, and it is the phase employers hire for. Nobody has ever been fired for shipping slowly. People get fired for shipping something nobody else can run.
Here is the hard test, and I want you to hold it in front of you for fifteen hours: a capstone that only runs on your laptop is a capstone that dies with your laptop. Not metaphorically. Your school account expires, your machine gets reimaged, the free tier you never wrote down lapses, and six months later the project you spent 240 hours on cannot be started by anybody, including you. Everything this week is aimed at that outcome, and everything this week is graded on evidence rather than claim: not “it should deploy” but here is the URL, here is the transcript, here is the timestamp, here is the rollback I rehearsed.
The AI thread runs both directions and it runs hard this week. As a tool, an assistant is genuinely good at the shape of a Dockerfile, a setup script, a CI workflow, a systemd unit — it has seen a million of them. It is also confidently, silently wrong about the details that matter: base-image tags that do not exist, packages named almost correctly, flags removed two releases ago, an apt-get line for a distribution you are not running. Generated infrastructure code fails in a specific way — it looks exactly right and does not work. As a workload, if your project calls a model, you are now handing your successor a dependency you do not control: a version string that can be deprecated, a key that costs money to use, a bill somebody has to own, and behavior that may quietly change under them. That has to be written down, by name, this week.
And underneath it all sits the week’s question, which is older and heavier than any of this. Ecclesiastes 2 is a man looking at everything he built and realizing he has to leave it to someone who may be a fool, and hating it. Who inherits what you built? You are about to spend a week making your work easy to take away from you. That is a strange thing to spend a week on. It is worth asking why we do it.
14.1 — “It Runs on My Machine” Is Not a Deliverable
Your development machine is the most heavily customized computer your project will ever touch. Over fourteen weeks you have installed things you have forgotten, set environment variables in a shell profile you have not opened since Week 2, granted permissions you clicked past, and left a database running with data you seeded by hand in Week 9. Your project runs there. It runs there because of the machine, and you cannot tell which parts.
The professional name for this is environment drift, and the discipline that fights it has one rule: everything the system needs must be either in the repository or written down as a prerequisite. Nothing may live only in your head or only in your shell.
Start with a hostile inventory. Ask what your project needs that is not in the repo:
| Category | The question | Where it hides |
|---|---|---|
| Tools | What must be installed, at what version? | your machine, from months ago |
| Services | What must be running or reachable? Database, cache, queue, third-party API, model endpoint | localhost in your config; assumed |
| Config | What values differ per environment? | hard-coded constants |
| Secrets | What credentials does it need? | your shell profile, or worse, a committed file |
| Data | What must exist before it works? Schema, seed rows, fixtures | a database you populated by hand |
| Accounts | What must somebody own? Repo, host, API keys, domain | your student email |
Run the search that finds the lies. Absolute paths and localhost are the two loudest tells:
# paths that only exist on your machine, and services you assumed were running
grep -rInE '(/Users/|/home/[a-z]|C:\\\\|localhost|127\.0\.0\.1)' . | grep -v node_modules
Every hit is a line item for this week. The goal is not zero hits — a test fixture may legitimately point at localhost. The goal is that every hit is either configurable or documented, and none of them are surprises.
Coach’s Note — The clean-machine test in Week 13 proved your documentation was runnable. This week proves your system is portable. They are different failures. Documentation fails when a step is missing; portability fails when a step is impossible because you never noticed you had something a stranger does not.
14.2 — Configuration and Secrets, Separated From Code
There is one architectural rule this week, and it is old, well-argued, and non-negotiable: configuration lives in the environment, not in the code. The clearest statement of it is Factor III of the Twelve-Factor App methodology (https://12factor.net/config), and the test it proposes is the one to remember — could you open-source this repository right now, this minute, without leaking a single credential? If the answer is no, your config and your code are tangled.
Configuration is anything that differs between where you develop and where it runs: database URLs, ports, hostnames, feature flags, log levels, API keys, model names. Not “things that might change” — things that differ by environment.
The artifact that carries this is .env.example, committed to the repo, and it is a contract, not a leftover. Here is the version most students ship:
# .env.example (bad)
DB_URL=
API_KEY=
SECRET=
PORT=
Four lines that answer no question a stranger has. What is SECRET for? What breaks if I change it? Where do I get API_KEY? Which of these can I leave blank? Now the version that does the job — this is PantryPilot’s, our running example; yours will name different things:
# PantryPilot — configuration contract.
# Copy to .env and fill in. Every variable here is read at startup, and the app
# refuses to boot if a REQUIRED one is missing. .env is gitignored; never commit it.
# REQUIRED — Postgres connection string.
# Local default after `docker compose up db`:
# postgres://pantry:pantry@localhost:5432/pantry
DATABASE_URL=
# REQUIRED — signs session cookies. Generate a fresh value PER ENVIRONMENT:
# openssl rand -base64 32
# Changing this logs everyone out. That is intended, not a bug.
SESSION_SECRET=
# REQUIRED — barcode/product lookup. Create a key in the vendor console (see
# docs/handoff.md, "Accounts and keys"). Free tier is rate-limited; the failure
# mode is a 429, handled in src/lookup/client.
PRODUCT_API_KEY=
# OPTIONAL — recipe suggestions. If unset, the AI feature is hidden and the rest
# of the app works normally. Usage is billed to whoever owns this key. Pin the
# version deliberately; never float to a "latest" alias — see docs/adr/0009.
RECIPE_MODEL_API_KEY=
RECIPE_MODEL=<provider>:<exact-model-version-string>
Every variable answers four questions: what it does, required or optional, where to get it, and what a safe local value is. That is the difference between a file and a contract.
Enforce it in code. A twelve-line startup check that reads .env.example, compares it to the actual environment, and exits with a readable message beats an hour of your successor’s confusion. Use code/check_config.py as a stack-agnostic version you can run today, then port the idea into your app’s own startup path.
On secrets, three rules and no exceptions.
- A secret that reached the repository is burned. Not “probably fine because the repo is private.” Rotate it. Public repositories are scraped within minutes, and private ones become public by accident. Rotation is the fix; rewriting history (
git filter-repo, or the BFG Repo-Cleaner — both are real tools with real docs) is optional cleanup you do after rotating. - Scan before you tag. Grep is a start; dedicated scanners such as
gitleaksandtrufflehogare better, and GitHub’s own secret scanning with push protection is worth turning on if it is available to your repository — availability and terms have changed over time, so check the current documentation rather than trusting a blog post. - Production secrets live in the host’s secret store. Not in a file you copy up, not in the CI log, not in the Dockerfile. Environment variables injected by the platform, or a managed secret manager.
Coach’s Note — The single most common way a student capstone leaks a key is not carelessness. It is a
.envcommitted in Week 3, before there was a.gitignore, and forgotten. Go look at your first ten commits. Today. Before you tag v1.0.0 on top of them.
14.3 — Scripted Setup or Containers: Pick One and Prove It
You need one documented path from “fresh clone” to “running.” Exactly one. Two half-built paths is worse than one that works, because your successor will pick the broken one.
The choice is real, and both answers are defensible:
Scripted setup (script/setup or equivalent) | Containers (Dockerfile / compose.yaml) | |
|---|---|---|
| What it promises | ”Run this on a supported machine and it will work" | "Run this anywhere the runtime exists and it will work” |
| Handles OS differences | Poorly — you will write branches | Well, by construction |
| Handles services (DB, cache) | You document installing them | You declare them and they start |
| Cost to your successor | They need your OS and your tool versions | They need the container runtime, and that is all |
| Failure mode | Silent drift; works for you, not for them | Image size, build times, and “why is my file not showing up” |
Recommendation, and I will commit to it: if your project needs a database or any second service, containerize it. Declaring Postgres in a compose file is fifteen lines and removes an entire category of your successor’s day one. If your project is a single-process tool with no services — TraceLens, our command-line log parser, is exactly this — a scripted setup plus a pinned lock file is honest, faster to build, and better matched to how the tool is actually installed. Do not containerize a CLI tool to look sophisticated. Do not hand-wave a five-service web app with “install Postgres.”
A bad setup script and a good one, side by side. Bad:
#!/bin/bash
npm install
npm start
That is not a setup script; it is two commands you already knew. It does not check anything, tells you nothing when it fails, and starts the server instead of setting up — so it can never be run twice. Good:
#!/usr/bin/env bash
set -euo pipefail # fail on error, on unset variable, and inside pipes
need() { command -v "$1" >/dev/null || { echo "MISSING: $1 — see README.md prerequisites"; exit 1; }; }
need node; need npm; need docker
node_major=$(node -p 'process.versions.node.split(".")[0]')
[ "$node_major" -ge 20 ] || { echo "Node 20+ required; found $(node -v)"; exit 1; }
[ -f .env ] || { cp .env.example .env; echo "created .env — fill it in, then re-run"; exit 1; }
docker compose up -d db # idempotent: safe to run twice
npm ci # exact versions from the lock file, not 'npm install'
npm run db:migrate && npm run db:seed -- --demo
echo "Setup complete. Start with 'npm run dev', open http://localhost:8080,"
echo "and log in as demo@example.com / demo1234"
Four properties separate them: it fails fast with a readable message, it checks its prerequisites instead of assuming them, it is idempotent (running it twice does no harm), and it ends by telling you the next command and what you should see. Note that the good version is a hybrid — a script that is the single documented entry point, delegating the services to a compose file. That is the common professional shape and it counts as one path, because there is still exactly one command a stranger runs. The version numbers are illustrative; pin whatever your project actually requires, and make sure the number in the script matches the number in your README.md.
Then prove it. Claiming reproducibility is worth nothing; the deliverable is evidence. Three tiers, in increasing order of what they prove:
- Fresh clone on your own machine. Cheapest. Catches “I never committed that file.” Rehearse with
code/preflight.sh, which clones into a throwaway directory, runs your documented setup, runs your smoke check, and times the whole thing. - A clean environment that is not your machine. A fresh cloud development environment, a new container from a bare base image, a classmate’s laptop, a re-imaged lab machine. This is the real test, and it is the one the milestone requires. Catches “that tool has been on my laptop since freshman year.”
- A stranger, unaided, on a timer. Hand a classmate the URL and say nothing for thirty minutes. Write down every question they had to ask. Each one is a documentation defect with your name on it.
Coach’s Note — Time the setup and write the number down. “About fifteen minutes” is not a number. A successor’s first day has roughly six usable hours in it; if setup eats three, you have taken half their first day, and they have not read a line of your code yet.
14.4 — Deploying Somewhere Real, Cheaply
“Deployed” does not mean the same thing for every project, and pretending it does is how students waste a week hosting something that did not need hosting.
| Shape of project | What “deployed” honestly means | Cheapest thing that works | The thing that bites |
|---|---|---|---|
| Static site or client-only app | A public URL serving your built files | Static hosting wired to your repo — GitHub Pages is free for public repositories, though terms change; verify | The build must run in CI, not on your laptop |
| Web app + database | A URL a stranger can hit, with a managed database behind it | A small platform-as-a-service hobby plan, or one small virtual machine | Free tiers sleep, expire, or change; the database is the part that costs money |
| CLI tool or library (TraceLens) | A published, installable artifact and install instructions somebody verified | A tagged release with the built artifact attached, or a package registry | ”Deployment” here is packaging — do not host a CLI to look impressive |
| Scheduled or batch job | A scheduler that runs it on time without you | A scheduled CI workflow, or cron on a small machine | Silent failure; you need an alert, not a log |
| Anything calling a model API | Same as your app; the model is one more outbound call | Same as above | Cost, rate limits, and who owns the key |
Three rules that survive any hosting choice.
Deploy from the tag, never from your working copy. If the thing running in production came from a directory on your laptop, nobody — including you — can say what is in it. Deployment reads from the repository at a named version. That is the entire reason tags exist.
Do not fabricate free-tier arithmetic. Providers change limits, sleep policies, and prices constantly, and this book will not tell you a number that will be wrong by the time you read it. Do the arithmetic yourself this week, write it in docs/handoff.md, and date it: what is free, what is not, what happens when the free thing ends, and whose credit card is on file. Appendix A walks the cheapest paths that work, including the no-admin ones.
If you genuinely cannot deploy, say so and compensate. Some capstones cannot go on the public internet — a project handling sensitive data, a system requiring hardware, an institutional rule. That is a legitimate constraint, not an excuse, and it is graded as a constraint if you argue it in writing. The compensating deliverable is: a container image or scripted setup that a grader can run in one command, a recorded demo, and a docs/deployment.md that states plainly what would be required to host it and what it would cost. What is not acceptable is silence. Either way, write the deployment down as you did it, not as you planned it — docs/deployment.md is a short document naming the target, exactly how a deploy is performed, how to verify it worked, how to see the logs, and how to undo it.
Rollback is a deliverable. Not a paragraph — a rehearsal. Deploy v1.0.0. Then deploy something broken on purpose. Then get back to v1.0.0 using only the numbered steps in your runbook, with a timer running. Write the wall-clock number down. And answer the one question everyone forgets: if a database migration ran, does rolling back the code leave the schema ahead of it? If the answer is yes, that is fine — but it has to be in the runbook, in advance, in daylight.
14.5 — Versioning, Tagging, and the Release You Can Point At
Until this week your project has been a moving branch. A branch is not something you can point at. This week it becomes v1.0.0 — a fixed, named, immutable thing that a grader, a successor, or an employer can check out and run.
Use semantic versioning (https://semver.org/): MAJOR.MINOR.PATCH. MAJOR when you break compatibility, MINOR when you add functionality compatibly, PATCH when you fix things. For a capstone, 1.0.0 is the honest label for “the scope I committed to in the specification, delivered.” If you knowingly shipped less than that, 0.9.0 is a more honest number and costs you nothing — the Week-16 rubric rewards the accurate claim, not the impressive one.
Create the tag annotated, not lightweight, so it carries an author, a date, and a message:
git tag -a v1.0.0 -m "v1.0.0 — pantry tracking, expiry dashboard, barcode add"
git push origin v1.0.0
Then publish a release on that tag, with notes. Your platform can draft notes from merged pull requests; treat that draft as a first pass, not the deliverable, because it lists commits and your reader wants changes.
CHANGELOG.md is where the human-readable history lives. The Keep a Changelog convention (https://keepachangelog.com/) is the standard shape, and this is the version most students write:
## v1.0.0
- lots of bug fixes
- final version
- cleanup
That entry tells the reader nothing and, worse, tells the grader nothing, since it cannot be checked against anything. Here is the same release, written by someone who has been keeping requirement and defect identifiers since Week 3:
## [1.0.0] — 2026-04-24
### Added
- Expiry dashboard, sorted by days-to-expiry, with a 3-day warning band (FR-14, FR-15).
- Add an item by barcode scan in a single step (FR-07).
- Optional recipe suggestions; hidden when RECIPE_MODEL_API_KEY is unset (FR-21, ADR-0009).
### Changed
- Pantry list paginates at 50 items to hold NFR-P2 (p95 page load under 1.5 s with
500 items; measured 1.1 s — see docs/test-results.md).
- Session lifetime shortened from 30 days to 7 (NFR-S3).
### Fixed
- Duplicate items created when the lookup endpoint timed out and the client retried (DEF-031).
- Expiry dates off by one for items added after 8 p.m. local time (DEF-037).
### Known issues
- Recipe suggestions fail silently instead of surfacing an error when the model API
returns 429 (DEF-044, deferred to v1.1 — see docs/handoff.md §5).
### Upgrading
- Requires migration 0011; run `npm run db:migrate` before starting v1.0.0.
Every line ties to something a reader can look up. That is the payoff for fourteen weeks of identifiers, and it is why the traceability work in Chapters 3, 4, and 11 was not bureaucracy.
Coach’s Note — A “Known issues” section makes a release look more professional, not less. Every real release has them. What looks amateur is a changelog implying nothing is wrong, followed by a demo where something is obviously wrong.
14.6 — Licensing Your Repository and Honoring Everyone Else’s
Two obligations, opposite directions, and most students think about neither.
Outward: what may others do with your code? A repository with no license file is not “open.” Under default copyright, you retain all rights, and nobody may legally copy, modify, or redistribute it off the platform. Know the one exception precisely, because it is the detail people get wrong in interviews: by making a repository public on GitHub you accept GitHub’s terms, which grant every other GitHub user the right to view your repository and to fork it within GitHub. That is the entire grant. So the recruiter who admired your project can fork it — and still cannot use a line of it in anything, and the successor who inherits it is in an awkward position. “Public on GitHub” is a visibility setting, not a permission grant, and a fork button is not one either.
| Option | Others may | You should know | Reasonable when |
|---|---|---|---|
No LICENSE | Look — and, on GitHub, fork within GitHub. Nothing else. | Default copyright, all rights reserved; the fork right comes from GitHub’s terms, not from you | You have an institutional or commercial reason — write it down |
| MIT | Use, modify, redistribute, including commercially | Very short; they must keep your notice | You want maximum reuse with minimum friction — the common capstone default |
| Apache-2.0 | Same as MIT, roughly | Longer; includes an explicit patent grant and a NOTICE convention | The project touches anything patent-adjacent, or you want the extra clarity |
| GPL-3.0 | Use and modify | Copyleft: distributing derived works triggers source obligations | You want downstream work kept open — understand the obligation before choosing it |
Use SPDX identifiers — the standard short names (MIT, Apache-2.0, GPL-3.0-only) whose canonical list lives at https://spdx.org/licenses/. Put the identifier in your README.md and the full text in LICENSE at the repository root. https://choosealicense.com/ is a decent orientation tool.
Two warnings, and I am giving you the honest version rather than the confident one. First: this is orientation, not legal advice. Second, and more practical: your institution may have a policy about who owns coursework and sponsored-project code. Some universities claim rights in work produced with substantial institutional resources; many do not; industry-sponsored capstones almost always have an agreement. Ask your program, in writing, before you publish, and keep the answer. Ten minutes now, versus a real problem later.
Inward: what do you owe the code you used? Every dependency arrived with a license and most of them impose at least one obligation — commonly, ship the copyright notice and license text with any distribution. Attribution licenses on assets (icon sets, fonts, photos, datasets) usually require visible credit. Copyleft licenses can impose obligations on your source if you distribute a derived work.
So build an inventory. Most ecosystems have a license-listing tool for their package manager; find the one for yours, run it, and put the output in THIRD-PARTY-NOTICES.md. The shape you want — illustrative rows, since your dependencies will differ:
| Dependency | Version | License (SPDX) | Obligation you must actually meet |
|---|---|---|---|
<your web framework> | 4.19.2 | MIT | Ship the copyright notice and license text |
<your icon set> | 6.5.1 | CC-BY-4.0 | Visible attribution in the UI or an about page |
<a GPL-licensed binary you shell out to> | 3.1 | GPL-3.0-only | Distribution triggers source obligations — check before bundling |
<a dataset> | 2025-06 | see terms | Data licenses are often not software licenses. Read them. |
Do not take a model’s word for what a package is licensed under. Assistants are wrong about this specific fact often, and the failure is invisible until it matters. Read the license file in the dependency itself.
14.7 — The Handoff Package: What a Successor Needs on Day One
Here is the mental model that makes this week concrete. Imagine you are hit by a bus tonight. (The industry really does call it that. The gentler version is “wins the lottery and moves to Portugal,” which is the same problem with better outcomes.) Tomorrow morning, a competent engineer who has never seen your project is told to take it over. They cannot call you. What do they need?
Not “the code is well commented.” That sentence, and its cousin “everything is in the README,” is the single most common handoff document written by students, and it is worth zero:
# Handoff (bad)
The code is well commented and mostly self-explanatory.
Everything you need to know is in the README.
Email me if you have questions.
Three failures in three lines. It asserts quality rather than demonstrating it, it points at a document instead of adding to it, and its escape hatch is the one thing a handoff assumes is unavailable — you.
The good version is a specific, four-part package. Start from code/handoff-template.md and land it in your repo as docs/handoff.md.
1. The day-one path. A numbered table: step, exact command, expected result, what to do when it fails. Target: running in under thirty minutes. This overlaps your README.md on purpose — the successor should never have to reconstruct a sequence from two documents.
2. The week-one ramp, ending in a merged change. Read the architecture overview. Trace one request end to end, with the files named in order. Then fix a specific, small, real issue that you deliberately left open and labeled good-first-issue, with acceptance criteria already written. A successor who ships something in week one has joined the project; a successor still reading in week three has not.
3. The ownership table. Every account, key, host, and recurring cost: where it lives, who owns it today, what it costs, and the exact steps to transfer or rotate it. Every row with your name in it is a liability, and the handoff is not finished until those names change.
4. The landmines. At least five things that look like bugs and are not, or look fine and are not. Each one: the symptom, the actual cause, what to do. This is the section that saves the most hours, because it is knowledge that exists nowhere else — not in code, not in tests, not in the specification. Only in you.
Backlog hygiene belongs here too, and it is cheap points people leave on the table. An issue tracker full of items titled “fix the thing” and “search is weird” is not a backlog; it is a pile. Groom the top five: a clear title, the requirement or defect ID, acceptance criteria, and the files most likely involved. Compare —
- Bad: “Search is broken”
- Good: “DEF-052 — Search ignores expiry filter when the query is empty. Repro: clear the search box with ‘Expiring soon’ active; all items return. Expected (FR-12): the filter applies regardless of query text. Likely in
src/search/query.ts, the early-return at the top ofbuildFilter. Done when: the added test case intests/search.spec.tspasses and manual repro no longer reproduces.”
The second one can be picked up by a stranger on a Tuesday morning. The first one cannot be picked up by you in a month.
14.8 — The Bus Factor and the Knowledge Only You Have
Bus factor: the number of people who would have to disappear before a project cannot continue. Your capstone’s bus factor is one. It cannot be more than one — you are the only engineer. But the severity of that one is entirely in your control, and it is measured by how much of the project lives only in your memory.
Spend an hour hunting it. The question that surfaces it is: what have I had to remember instead of read? Sit with your project and force yourself to write down the things you know that are nowhere in the repository. Most students find between five and fifteen. A sample of the shape, drawn from PantryPilot:
| Knowledge only you have | How it bites your successor | Cost to write down | Where it goes |
|---|---|---|---|
| The seed script must run before the first login, or the roles table is empty and every request 500s | Hours of debugging a “broken” auth system that is fine | 10 min | docs/runbook.md, plus a guard in the script |
| The product API returns HTTP 200 with an empty body for unknown barcodes | They “fix” a bug that is the vendor’s behavior | 15 min | docs/architecture.md and a regression test |
| The staging key is on a free tier with a daily call cap, so it fails every afternoon | A phantom performance bug | 5 min | docs/handoff.md ownership table |
| Migration 0007 assumes 0006 already ran in production; running them out of order drops a column | Data loss | 20 min | An ADR, plus a note in CHANGELOG.md |
Look at the right-hand columns. The whole register costs under an hour, and the consequences run from a wasted week to destroyed data. That is the best hour you will spend this semester.
Then close each item three ways, in this order of preference: make it impossible (add the guard, add the test, make the tool fail loudly), make it visible (write it into the runbook or the architecture doc), or make it findable (put it in the handoff landmines list). Writing it down is the floor, not the ceiling. A comment in a runbook is better than nothing; a script that refuses to run in the wrong order is better than a comment.
Coach’s Note — The reflex here is embarrassment: “I should have written this down in Week 9.” Skip the embarrassment; it costs time and buys nothing. Every professional engineer discovers this same list on the way out the door. The difference between a professional and an amateur is only that the professional writes it down anyway, at the end, instead of leaving with it.
14.9 — AI in the Handoff: Both Directions
As a tool. Ask an assistant for a Dockerfile, a compose file, a setup script, or a CI workflow and you will get something structurally correct in seconds. Take it — and then treat every concrete detail as unverified. Generated infrastructure fails in a distinctive way: the shape is right and the specifics are stale or invented. Base-image tags that never existed. A package name off by a hyphen. A flag removed two major versions ago. Install commands for a distribution you are not running. There is also a documented supply-chain risk in the neighborhood: assistants sometimes suggest package names that do not exist, and attackers have registered such names hoping someone installs them without looking. Verify that every package you are told to install is the package you meant, from the registry you meant.
So the loop is: generate, then run it somewhere clean, then believe it. In that order, every time. Never commit generated infrastructure that you have not executed end to end in an environment that is not your development machine. And when it works, read it — you will present this in Week 15 and be asked what a line does, and “the assistant wrote it” is not an answer that survives a committee.
As a workload. If your system calls a model, you are handing your successor a dependency you do not control, and it needs its own section in docs/handoff.md:
- The pinned version string, exactly, in configuration — never a floating alias. Aliases move; your behavior moves with them and your tests start failing for reasons that are not in your diff. Record why you pinned it (an ADR is the right home).
- Key ownership and rotation. Whose account, whose billing, what the rotation procedure is, and what breaks during the swap.
- Cost ownership. What it costs per unit of usage as of today’s date, what a plausible month looks like, where the alarm is set, and — bluntly — who pays when you graduate. If the answer is “nobody,” then the honest design is a system that degrades gracefully with the feature turned off, and you should verify that it does.
- Deprecation. Providers retire models. Assume your pinned version will be sunset while your successor owns it. Write the fallback: what the system does when that call fails, and what it takes to move to a different model.
- The evaluation to re-run. A small fixture set and a command, so a successor changing models can tell whether the change made things worse — and a plain statement of which outputs are non-deterministic. Without it, “we upgraded the model” is a change nobody can assess.
Log the assistant’s role this week in docs/ai-usage.md, the same as every other week: what you asked, what you accepted, what you changed, and what you verified. The spine rule does not bend at deployment. The assistant can write the container definition. Only you can be accountable for what is running.
Coach’s Note — Here is the sharpest version of the spine rule I can give you for this week. Generated code that you have executed on a clean machine and read is yours. Generated code you pasted and shipped is a liability with your name on it — and deployment is precisely where that liability turns into an outage somebody else has to fix.
14.10 — Interactive Lab: The Handoff Readiness Auditor
On this chapter’s page you will find The Handoff Readiness Auditor. Use it twice: once at the start of the week, before you fix anything, and once at the end.
It runs the takeover test as a scored interview across five areas — environment reproducibility, configuration and secrets, documentation completeness, issue and backlog hygiene, and knowledge that exists only in your head. Each question wants evidence, not intent: not “is setup documented?” but “when did somebody who is not you last run it, and how long did it take?” Answer honestly; the tool is useless if you grade yourself on what you plan to do this weekend.
Three things come back. A readiness score, which is only useful as a before-and-after. A rendered projected first week for your successor — what they get stuck on, in what order, and for how long — which is the output that actually changes behavior, because seeing “day 1: blocked 4 hours on an undocumented database step” lands differently than reading “setup docs incomplete.” And a prioritized remediation list, ordered by hours-saved-per-hour-spent, which is your work queue for the rest of the week.
The bus-factor panel is the part to sit with. It asks you to name each thing only you know and estimate what writing it down would cost. Most students are surprised twice: first by how many items there are, and second by how cheap most of them are to close. Take that output straight into §7 of your handoff guide. Do the lab before you start Milestone 14 — it will tell you which of the fifteen hours matter most in your project, and that is not the same answer for everyone.
14.11 — Who Inherits What You Built?
“I hated all my toil in which I toil under the sun, seeing that I must leave it to the man who will come after me, and who knows whether he will be wise or a fool? Yet he will be master of all for which I toiled and used my wisdom under the sun. This also is vanity.” (Ecclesiastes 2:18–19, ESV)
That is not a pious thought. It is a man in a bad mood, and the Preacher means it: he built, he was skilled, he was wise about it — and then he did the arithmetic on who gets it afterward and hated everything. Verse 21 puts the sting in it: someone “who has toiled with wisdom and knowledge and skill must leave everything to be enjoyed by someone who did not toil for it” (ESV). Anyone who has watched a good system inherited by someone who did not care about it knows exactly what that sentence tastes like.
I am not going to talk you out of that feeling. Scripture does not. It sits there in the middle of the book, unresolved for a while, because it is true: you will leave your work to someone. Every line you wrote this semester will eventually be maintained by someone who did not write it, or deleted by someone who never read it. The capstone makes this unusually vivid. You are about to spend fifteen hours specifically on making your work easy to take away from you, and next semester somebody may fork it, rename it, and misunderstand your best decision.
But notice what the Preacher’s despair actually rests on: the assumption that the point of the work was that he keeps it. Ownership. Control. The permanence of the builder’s grip. Take that assumption out and the same facts read differently. A few chapters later he lands somewhere else — that there is nothing better than to find enjoyment in one’s toil, and that this too is from the hand of God (Ecclesiastes 2:24; 3:13). The work was never a possession to be defended into eternity. It was a gift, given to be done, and given to be handed on.
Which is why Christians have never treated succession as the end of meaning. Paul tells Timothy to take what he heard “in the presence of many witnesses” and entrust it “to faithful men, who will be able to teach others also” (2 Timothy 2:2, ESV) — the verse over last week’s chapter, and the reason those two chapters sit next to each other. Entrusting is not loss. It is the design. And Paul, writing about a different kind of building, says it as plainly as an engineer could want: “like a skilled master builder I laid a foundation, and someone else is building upon it. Let each one take care how he builds upon it” (1 Corinthians 3:10, ESV). Someone else is building on it. That is stated as fact, not tragedy. The only question left to the builder is how carefully he laid it for them.
This is vocation in the ordinary Lutheran sense, and it is the frame this whole course has been running on. Your work is not primarily a monument to you; it is service rendered to a neighbor, through the station you happen to occupy. Chapter 4 asked who your neighbor is when you write software, and answered: the user you will never meet. This week the answer extends by one person. Your neighbor is also your successor — the engineer who opens this repository at nine in the morning a year from now with a bug report in one hand and no way to reach you. Every undocumented landmine you leave costs that person a day of their life. Every hour you spend this week buys them back a day. That is not paperwork. That is love of neighbor performed in Markdown, and it is the most concrete form of it this course will ever ask of you.
So there is something genuinely humbling about this week, and I want you to feel it rather than skip past it: you are making yourself unnecessary on purpose. Every instinct in an insecure engineer runs the other way — to be the only one who understands it, to be indispensable, to hold the knowledge that makes you hard to replace. That instinct is job security purchased with somebody else’s time, and it is a small, ordinary form of pride. The mature move, the Christian move, is the opposite: write it all down, hand it over completely, and let the work stand without you. Ecclesiastes is right that you cannot keep it; it never tells you not to build it well. Build it well because you cannot keep it, and hand it over as one who was always a steward and never an owner. Psalm 90 gives you the prayer for exactly this hour, and it is worth praying honestly over a repository: “establish the work of our hands upon us; yes, establish the work of our hands!” (Psalm 90:17, ESV). Not preserve me. Establish the work. That is a request only someone who has let go can make.
14.12 — Common Pitfalls
Pitfall: Configuration that “works” because it defaults to your machine.
Example: const DB = process.env.DATABASE_URL || "postgres://localhost:5432/pantry" — so a missing variable silently starts against a database that does not exist on any other computer, and the failure appears three screens later as a timeout.
Fix: Required config has no default. Fail at startup with the variable name and a pointer to .env.example. Only genuinely optional values get defaults, and those defaults must be safe everywhere.
Pitfall: Rotating a leaked secret is “on the list” instead of done.
Example: A .env was committed in Week 3. The student plans to rewrite history before submitting, and never does — and even if they had, the key was exposed for eleven weeks.
Fix: Rotate first, today. History rewriting is optional cleanup afterward. Then add .env to .gitignore and run a scanner over the full history before you tag.
Pitfall: Two half-finished setup paths, and a README.md that mentions both without saying which is current.
Example: A Dockerfile from Week 9 that no longer builds sits beside a script/setup that works. The successor picks the container path, loses two hours to a build error, and concludes the project is abandoned.
Fix: Pick one, make it work, and delete the other. Say in the README, in one sentence, which path is supported. A broken alternative is a trap you left behind.
Pitfall: Deploying from the laptop.
Example: Dragging a build folder up to a host, or running the deploy command from a working copy with three uncommitted changes. Nobody can ever say what is actually running.
Fix: Deploy from a tag. If the deploy is manual, the runbook’s first step is git checkout v1.0.0 in a clean clone. Verify with a version string the running system reports.
Pitfall: A rollback that exists only as a sentence.
Example: docs/runbook.md says “to roll back, redeploy the previous version.” Nobody has ever tried it, and the previous version’s migration is not reversible.
Fix: Rehearse it once, on a timer, this week. Numbered commands. Include the honest answer about database state, even when that answer is unpleasant.
Pitfall: Shipping generated infrastructure you never ran on a clean environment.
Example: An assistant’s Dockerfile with a base-image tag that does not exist and an install line for the wrong package manager. It builds on your machine because a cached layer is hiding the error.
Fix: Build it from scratch with no cache, in a clean environment, before it is committed. Then read every line and be able to explain it in Week 15.
14.13 — Where Your Hours Went This Week
A realistic shape for the ~15 hours. Your project will move the weights around — a CLI tool spends less on hosting and more on packaging; a data-heavy app spends more on migrations and backups.
| Work | Hours |
|---|---|
Hostile inventory; extracting config; writing the real .env.example | 2.0 |
| Building the one setup path (script or container) until it actually works | 3.5 |
| Proving it on an environment that is not your machine; fixing what that reveals | 1.5 |
| Deploying, verifying, and rehearsing rollback with a timer | 3.0 |
Version, tag, release notes, CHANGELOG.md, license, third-party inventory | 2.5 |
| Handoff guide, backlog grooming, bus-factor register | 2.0 |
| Reading, the auditor lab, and this week’s quiz | 0.5 |
| Total | 15.0 |
If deployment eats six hours, stop and switch strategies rather than pushing through — the fallback in §14.4 (container plus recorded demo plus an honest docs/deployment.md) is worth far more than six lost hours chasing a platform. Log what actually happened, not what you meant to happen. Week 16 asks for the honest log, and an estimate that was wrong is data; an estimate that was edited is not.
14.14 — Reps
The reps are in the exercises, and this week they are the milestone, done in pieces. Preview:
- Rep 1 — the hostile inventory: grep your repository for absolute paths and localhost, and turn every hit into a decision.
- Rep 3 — rewrite
.env.exampleas a real contract, then check it withcode/check_config.py. - Rep 5 — the amnesia test: run
code/preflight.shagainst a fresh clone and time it. - Rep 7 — the secret hunt, through the history and not just the working tree.
- Rep 10 — tag a release candidate, write the notes yourself, then rehearse the rollback on a timer.
- Rep 11 — the bus-factor sweep: thirty minutes on what you have had to remember instead of read.
Do this week’s quiz on the chapter page when you finish reading. It is part of the 15% and, more usefully, it is the early-warning system — if the deployment vocabulary is not in your hands yet, better to find out on a Tuesday than in front of a committee.
14.15 — This Week’s Milestone
Milestone 14 — Deployable Release v1.0 & Handoff Package: a configuration contract with no secrets in the repository or its history, one reproducible setup path proven somewhere that is not your machine, a real deployment (or an argued equivalent), a rehearsed rollback, an annotated v1.0.0 tag with release notes and a changelog entry, a license and a third-party inventory, and the handoff package itself. As always: the milestone is graded twice. It carries its own points, and it is the final deliverable, produced a week at a time. Every artifact above is a line on the Week-16 rubric — the release tag, the runbook, the handoff guide, the license, the change log. Skipping this week does not cost you a small penalty now; it forfeits the milestone and moves the same artifact points into Week 16, when there is no time left to earn them. See Appendix C for the full contract, and Appendix B for the document kit.
14.16 — Coach’s Final Word
Fourteen weeks ago you had an idea. Today you have something with a version number, a URL or an installable artifact, a license, and a document that lets a stranger take it over. That last one is the part that makes you an engineer rather than a student who codes. Anybody can build something they understand. Building something somebody else can run, operate, and continue is a different discipline, and it is the one every team you ever join will be quietly measuring you on from your first week.
Do the unglamorous parts. Rehearse the rollback even though nothing is broken. Write the five landmines even though you remember them perfectly. Rotate the key even though the repo is private. Nobody applauds any of it, and all of it is the difference between a project that survives you and a project that ends when your laptop does.
Then hand it over with an open hand. You cannot keep it. You were never going to. Build it well anyway, and lay the foundation carefully for whoever builds on top of it — because someone will, and that was always the plan. Two weeks left. Next week you learn to tell the truth about all of this in thirty minutes.
See you on Monday.
Up next: the exercises turns this week into twelve reps · Milestone 14 is Milestone 14, the deployable release and the handoff package · then Chapter 15 — building the thirty-minute case for everything you just shipped. Previous: Chapter 13. Reference: Appendix A (workbench and the cheapest paths that work), Appendix B (document kit), Appendix C (grading contract), Appendix E (glossary).
Week 14 Knowledge Check
const DB = process.env.DATABASE_URL || "postgres://localhost:5432/pantry"; .env.example. Only genuinely optional values get defaults, and those defaults must be safe everywhere. This is the same discipline as the .env.example contract: every variable answers what it does, whether it is required, where to get it, and what a safe local value is. .env committed in Week 3, before there was a .gitignore. The repository has been private the whole time. What comes first?docs/handoff.md, along with what you scanned with and what you found. Go look at your first ten commits before you tag on top of them. docs/deployment.md: "Tested the setup on a clean machine and it worked fine." That scores near zero. What earns the points?Clean-environment run — 2026-04-22, 14:05-14:31
Environment: fresh cloud dev container, blank image, no local tooling
Operator: me, following README.md literally, no improvising
Attempt 1: FAILED at step 3. `npm ci` errored - package-lock.json was
gitignored since Week 4. Committed it (commit a91c4f2).
Attempt 2: FAILED at step 5. Seed script silently did nothing because
SEED_DEMO defaulted to false. Made it explicit in .env.example.
Attempt 3: PASSED. Clone to running: 9 min 40 s. Smoke test: 34 tests, 12 s. Dockerfile from Week 9 that no longer builds, sitting beside a script/setup that works. What does Chapter 14 require?Dockerfile and it builds successfully on your laptop. What is the chapter's rule for generated infrastructure?