1. Courses
  2. Developer Foundations
  3. Ship a Project End to End
  4. Stop 2

40 minIntermediate

Your team's engineering playbook

A playbook is the short set of rules your team agrees on: how branches are named, how big a PR may be, who approves, what "done" means, when you release and how you hotfix. A product manager does not write the code but is often the best person to get these rules written down, agreed and kept, and every developer is the one who follows them. This lesson gives you a complete starter set and a way to measure whether it works.

By the end you can

  • Name the ten decisions every team playbook must answer, and a sensible default for each
  • Write a CONTRIBUTING.md, PR template and CODEOWNERS file for a real repo
  • Turn the rules that matter into a GitHub ruleset, so they are enforced instead of remembered
  • Explain the DORA metrics and measure lead time and deploy frequency from GitHub data with a script
  • Run a playbook review with the team without it becoming a blame session

The everyday picture

Every Mangrove store runs on the same standard operating procedures: how stock is received, how a goods received note (GRN) is posted, how the cash drawer is counted at shift change, what to do when the till goes offline. A cashier who moves to a store across town can work on day one because the rules are the same. Nobody argues every morning about how to count the drawer; that decision was made once, written down, and improved when something went wrong.

An engineering team without a playbook argues every day instead. Each PR is a new debate about naming, size, who should look at it and whether it is ready. A playbook moves those debates to one meeting a quarter, and leaves the daily work for actual work. Like a good SOP, it is short, specific, and changed by the people who follow it.

The ten decisions

Most playbooks answer the same questions. You do not need to invent them; you need the team to agree on an answer to each, and write it in one place. These defaults suit a team of three to ten people shipping a web API or app (they are the ones used in Build it):

DecisionA sensible defaultWhere it livesLearn more
BranchingGitHub flow: short branches off main, main always releasableCONTRIBUTING, rulesetBranches and releases
Branch namesfeat/123-short-name, fix/123-...CONTRIBUTINGBranches
Commit formatConventional Commits; PR title becomes the squash commitCONTRIBUTING, lint checkEveryday Git
PR size and contentUnder about 400 changed lines; template filled; issue linkedPR templatePull requests
ReviewFirst response within one working day; one code-owner approval, two for risky pathsCONTRIBUTING, CODEOWNERS, rulesetPull requests
Merge methodSquash only, so main has one commit per PRRulesetPull requests
Definition of doneA checklist: criteria met, tests, safe migration, docs, seen on stagingCONTRIBUTING, PR templateDebug and test
Release cadence and versionsWeekly tag vX.Y.0; semantic versioning; changelogCONTRIBUTING, release workflowBranches and releases, Node and npm
Hotfix procedureIncident issue, failing test, small PR, same CI, patch tag at once, note within 3 daysCONTRIBUTINGShip a project
OnboardingClone to first PR in one day, with written stepsCONTRIBUTING, READMERemotes

Two principles make the table work. First, enforce what you can, write down the rest. "No direct pushes to main" belongs in a ruleset, where nobody can forget it. "Respond to reviews within a day" cannot be enforced by a tool, so it goes in CONTRIBUTING.md and into the metrics below. Second, every rule needs a reason a new developer would accept. "Under 400 lines" is not arbitrary: review quality drops on very large PRs, and large PRs wait longest (you will see it in the data).

Quick check

Which of these rules can a GitHub ruleset enforce on its own?

Idea issue + acceptance ready? Branch feat/123-name < 400 lines? Pull request template filled green? CI lint, types, tests 1 approval Review code owner approves on main Squash merge title = commit Tuesday Weekly release tag vX.Y.0 deployed Production stores use it Ruleset on main no direct push checks required squash only Hotfix same gates, now tag vX.Y.Z
The agreed path from idea to production. Each arrow has a gate: is the issue ready, is the PR small, is CI green, has a code owner approved. The ruleset enforces the middle three on main. A hotfix skips the weekly wait but none of the gates.

Review expectations: the rule that matters most

In most teams the slowest part of shipping is not writing code; it is waiting for review. A PR that waits two days goes stale, collects merge conflicts (Merge conflicts without panic) and gets merged with a tired "LGTM". So make the expectations explicit:

  • Response time. First response within one working day. "Response" can be a full review or "I'll get to this after lunch". Count working days, and say which days they are: Monday to Friday in many countries, Sunday to Thursday in others, and teams across time zones may need both.
  • Who approves. routes each PR to the right people. The last matching line in the file wins, so put the general rule first and specific paths below it.
  • What a comment means. Agree that "Request changes" blocks and a plain comment is a suggestion. Some teams prefix suggestions with "nit:" so the author knows it is optional.
  • Who merges. The author merges after approval. They know when it is safe to ship and will watch for problems after.
  • Small first. Reviewers pick up small PRs before big ones. That rewards authors for splitting work.

A product manager can help here without reading code: in the sprint review, look at the slowest PRs of the last two weeks (the script below lists them) and ask why. The answer is almost never "the reviewer was lazy". It is usually "the PR was 2,000 lines" or "nobody knew who should review it", and both have playbook fixes.

Quick check

A reviewer leaves a plain comment that starts with "nit:". Under this playbook, what does it mean?

DORA metrics: is the playbook working?

Rules are only worth keeping if they make delivery faster and safer. research has shown, across many organisations, that a few delivery measures move together with better outcomes. The original four keys are:

  • Deployment frequency: how often you release to production.
  • Change lead time: how long a change takes from commit to running in production.
  • Change fail rate: what share of deployments need an immediate fix, such as a hotfix or rollback.
  • Time to restore: how long it takes to recover when a deployment fails. DORA renamed it "failed deployment recovery time" in 2023.

In 2024 DORA added a fifth, deployment rework rate (unplanned deployments caused by production incidents), and now groups them as throughput (how fast) and instability (how safely). The four keys are still the common starting point. The research finding that matters for you: speed and stability go together. Teams that release small changes often also break things less, because each change is small enough to review well and easy to roll back.

All four can be read from GitHub if your team follows the playbook: releases are tags, every change is a PR, and every incident is an issue with a label. That is a second reason to write the rules down. Without consistent tags and labels there is no data.

Quick check

A team made 10 releases last month, and 3 of them needed a hotfix or a rollback straight away. What is its change fail rate?

time First commit commit date PR opened PR created Merged to main PR merged Release v2.4.0 deploy 1 Incident #171 issue opened Hotfix v2.4.1 deploy 2, issue closed Release v2.5.0 deploy 3 Change lead time commit to live Recovery time incident to fix Deploy frequency: 3 deploys Change fail rate: 1 in 3
The four DORA keys on one timeline. Change lead time runs from the first commit to the release that ships it. Recovery time runs from the incident issue being opened to the fix being live. Deploy frequency counts the releases, and change fail rate is the share that needed a hotfix, here 1 of 3. Each needs a playbook habit: tag every release, merge every change through a PR, label every incident.

Build it

A complete playbook for partner-api, the service behind Mangrove's Partner app: four files you can copy, a ruleset that enforces the important parts, and a script that measures lead time and deploys. Replace your-org and team names with yours. The core of the lead-time calculation is also a Practice exercise you can run in your browser.

  1. Write CONTRIBUTING.md

    GitHub links to this file when someone opens an issue or PR, and shows it as a "Contributing" tab on the repo page. Put it in the repo root or .github/. Keep it to one screen of rules; link out for detail.

    Markdown
    # Contributing to partner-api
    
    This is how we work. If something here is wrong or slows you down, open a PR to change it.
    
    ## Your first day
    1. Get access: GitHub team `partner-team`, the staging VPN, and the team chat channel.
    2. `git clone git@github.com:your-org/partner-api.git && cd partner-api`
    3. `nvm use` (Node version from `.nvmrc`), then `npm ci`, then `cp .env.example .env`
    4. `npm run check` must pass. If it does not, tell us: that is a bug in this guide.
    5. Pick an issue labelled `good first issue` and open a draft PR before lunch.
    
    ## Branches
    - `main` is always releasable. Nobody pushes to it directly; the ruleset blocks it.
    - Branch from `main`: `feat/123-short-name`, `fix/123-short-name`, `chore/short-name`.
    - Delete the branch after merge (GitHub does it for you).
    
    ## Commits
    - Conventional Commits: `feat(api): add stock endpoint`, `fix(pos): round VAT in cents`.
    - Small and focused. The squash commit on `main` is the PR title, so make the title good.
    
    ## Pull requests
    - Aim for under 400 changed lines. Split bigger work behind a feature flag.
    - Fill in the template. Link the issue with `Closes #123`.
    - Open as draft early; mark ready when CI is green.
    
    ## Reviews
    - First response within one working day (Monday to Friday). Small PRs first.
    - One approval from a code owner. Two for anything under `src/payments/` or `prisma/`.
    - "Request changes" means it must change; "Comment" means a suggestion. Say which.
    - The author resolves threads and merges.
    
    ## Definition of done
    - [ ] Acceptance criteria in the issue are met
    - [ ] Tests added or updated; `npm run check` passes in CI
    - [ ] Database migration is backwards compatible (expand, then contract)
    - [ ] Logs and errors make sense to the on-call person
    - [ ] README, API docs or `.env.example` updated if behaviour or config changed
    - [ ] Product owner has seen it on staging (screenshots or a link in the PR)
    
    ## Releases and versions
    - Release every Tuesday after lunch: tag `vX.Y.0` on `main`. The release workflow deploys.
    - Semantic versioning: breaking API change = major, new feature = minor, fix = patch.
    - `CHANGELOG.md` is updated in the release PR from the merged PR titles.
    
    ## Hotfixes
    1. Open an issue labelled `incident` with what users see.
    2. Branch `fix/123-...` from `main`, write a failing test, fix, PR marked `hotfix`.
    3. One approval is enough; CI is still required.
    4. Tag `vX.Y.Z+1` straight after merge. Post the release link in the team channel.
    5. Within 3 working days: a short blameless note on the issue (cause, fix, prevention).
  2. Add the PR template

    Saved as .github/pull_request_template.md, it fills every new PR description (Pull requests and code review). The "Risk" boxes tell reviewers, and you, where to look hardest.

    Markdown
    ## What and why
    <!-- One or two sentences. Link the issue: Closes #123 -->
    
    ## How to test
    <!-- Steps a reviewer can follow on staging or locally -->
    
    ## Screenshots or API examples
    <!-- For UI changes: before and after. For API changes: a request and response -->
    
    ## Risk
    - [ ] Database migration
    - [ ] Changes money, stock or prices
    - [ ] Touches login, permissions or secrets
    - [ ] Needs a feature flag or a config change on deploy
    
    ## Checklist
    - [ ] Tests added or updated
    - [ ] Under 400 changed lines, or explained why not
    - [ ] Docs or `.env.example` updated if needed
  3. Add CODEOWNERS

    Save as .github/CODEOWNERS. Owners must be people or teams with write access. Because the last match wins, a change under src/payments/ goes to the leads team, and everything else to the whole team. The /.github/ line protects the workflows, the rules and this file itself.

    Output
    # .github/CODEOWNERS: who is asked to review what. The LAST matching line wins.
    *                     @your-org/partner-team
    /src/payments/        @your-org/partner-leads
    /prisma/              @your-org/partner-leads
    /.github/             @your-org/partner-leads
    /docs/                @your-org/partner-team @your-org/product
  4. Enforce the rules with rulesets

    Keep the ruleset JSON in the repo, under .github/rulesets/, so changes to the rules are reviewed like code. The branch ruleset requires a PR, one approval from a code owner, approval of the latest push by someone other than its author, resolved threads, squash merges and green CI. A second ruleset stops release tags being moved or deleted, so v2.4.1 always means the same code. Both files validated against GitHub's published REST API schema.

    JSON
    {
      "name": "main: reviewed, green, squashed",
      "target": "branch",
      "enforcement": "active",
      "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
      "rules": [
        { "type": "deletion" },
        { "type": "non_fast_forward" },
        { "type": "pull_request", "parameters": {
            "required_approving_review_count": 1,
            "require_code_owner_review": true,
            "dismiss_stale_reviews_on_push": true,
            "require_last_push_approval": true,
            "required_review_thread_resolution": true,
            "allowed_merge_methods": ["squash"] } },
        { "type": "required_status_checks", "parameters": {
            "strict_required_status_checks_policy": true,
            "required_status_checks": [ { "context": "check (22)" }, { "context": "check (24)" } ] } }
      ]
    }
    Terminal
    gh api repos/{owner}/{repo}/rulesets --method POST --input .github/rulesets/main.json
    gh api repos/{owner}/{repo}/rulesets --method POST --input .github/rulesets/release-tags.json
    gh api repos/{owner}/{repo}/rulesets --jq '.[] | "\(.id)  \(.name)  \(.enforcement)"'

    On GitHub (we could not run this from here): each POST returns the created ruleset with its id, and the last command lists both. To change a rule later, edit the file in a PR and apply it with gh api repos/{owner}/{repo}/rulesets/ID --method PUT --input .... The two status check names must match your CI job names exactly; with a matrix they are check (22) and check (24). If a hotfix ever needs someone to bypass the rules, add that person or team under bypass_actors deliberately, and review the bypass log.

  5. Measure lead time and deploys with a script

    This script uses gh, so it runs with your existing login and permissions. It finds PRs merged into main in the last N days with one GraphQL search (paginated, 100 at a time), lists the releases in the same window, and works out three lead times. "First commit to released" is the DORA change lead time; the other two show where the time goes.

    JavaScript
    // scripts/lead-times.mjs: lead time and deploys for the last N days, from GitHub data.
    // Usage, inside the repo with gh logged in:  node scripts/lead-times.mjs 30
    import { execFileSync } from 'node:child_process';
    
    const days = Number(process.argv[2] ?? 30);
    const since = new Date(Date.now() - days * 864e5).toISOString().slice(0, 10);
    const gh = (...args) => JSON.parse(execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 64e6 }));
    
    const repo = gh('repo', 'view', '--json', 'nameWithOwner').nameWithOwner;
    const query = `query($q: String!, $endCursor: String) {
      search(query: $q, type: ISSUE, first: 100, after: $endCursor) {
        nodes { ... on PullRequest { number createdAt mergedAt additions deletions
          commits(first: 1) { nodes { commit { authoredDate } } } } }
        pageInfo { hasNextPage endCursor } } }`;
    const prs = gh('api', 'graphql', '--paginate', '--slurp', '-f', `query=${query}`,
      '-f', `q=repo:${repo} is:pr is:merged base:main merged:>=${since}`).flatMap((p) => p.data.search.nodes);
    const releases = gh('release', 'list', '--limit', '200', '--json', 'tagName,publishedAt')
      .filter((r) => r.publishedAt >= since).toSorted((a, b) => a.publishedAt.localeCompare(b.publishedAt));
    
    const hours = (a, b) => (new Date(b) - new Date(a)) / 36e5;
    const rows = prs.map((pr) => {
      const firstCommit = pr.commits.nodes[0].commit.authoredDate;
      const shipped = releases.find((r) => r.publishedAt >= pr.mergedAt);    // first release after the merge
      return { number: pr.number, size: pr.additions + pr.deletions, review: hours(pr.createdAt, pr.mergedAt),
        toMerge: hours(firstCommit, pr.mergedAt), toProd: shipped ? hours(firstCommit, shipped.publishedAt) : null };
    });
    
    const pct = (xs, p) => { const s = xs.toSorted((a, b) => a - b); return s[Math.min(s.length - 1, Math.floor(p * s.length))]; };
    const fmt = (h) => (h < 48 ? `${h.toFixed(1)} h` : `${(h / 24).toFixed(1)} days`);
    const line = (label, xs) => console.log(`${label.padEnd(24)} median ${fmt(pct(xs, 0.5)).padEnd(9)} p90 ${fmt(pct(xs, 0.9))}`);
    
    console.log(`${repo}: ${rows.length} PRs merged into main since ${since}`);
    line('PR opened -> merged', rows.map((r) => r.review));
    line('First commit -> merged', rows.map((r) => r.toMerge));
    line('First commit -> released', rows.filter((r) => r.toProd !== null).map((r) => r.toProd));
    const patches = releases.filter((r) => !r.tagName.endsWith('.0'));
    console.log(`Releases: ${releases.length} in ${days} days (${patches.length} patch/hotfix), ${rows.filter((r) => r.toProd === null).length} PRs not released yet`);
    const slow = rows.toSorted((a, b) => b.review - a.review).slice(0, 3);
    console.log('Slowest reviews:', slow.map((r) => `#${r.number} ${fmt(r.review)}, ${r.size} lines`).join(' | '));

    Run it inside the repo:

    Terminal
    node scripts/lead-times.mjs 30

    The script was not run against GitHub's live API for this lesson, so we checked it two ways. The GraphQL query validated against GitHub's published schema (a misspelt field such as mergedAtt is rejected with "Cannot query field"), and we ran the script unchanged against a stand-in gh that returned 22 PRs and 7 releases shaped exactly like GitHub's responses. Your numbers will differ; the output looks like this:

    Output
    your-org/partner-api: 22 PRs merged into main since 2026-09-07
    PR opened -> merged      median 9.0 h     p90 2.1 days
    First commit -> merged   median 21.0 h    p90 2.6 days
    First commit -> released median 3.6 days  p90 6.2 days
    Releases: 7 in 30 days (2 patch/hotfix), 0 PRs not released yet
    Slowest reviews: #148 4.1 days, 2000 lines | #159 2.9 days, 1500 lines | #143 2.1 days, 1125 lines

    Read it like a product manager. A PR waits a median of 9 hours for review, but the slowest three were 1,000 to 2,000 lines, which supports the size rule. Lead time to production is about three and a half days, mostly the wait for the weekly release; moving to two releases a week would cut it more than any coding speed-up. Two of seven releases were patches, a change fail rate of roughly 2 in 7, worth watching. Note the script's two simplifications: it treats every release as a deploy, and every non-.0 tag as a fix.

  6. Measure recovery time from incident issues

    If every production incident gets an issue labelled incident, opened when it starts and closed when users are fine again, then recovery time is the time between the two. gh has jq built in, so one command gives the hours:

    Terminal
    gh issue list --label incident --state closed --search "created:>=2026-09-07" \
      --json number,createdAt,closedAt \
      --jq 'map((.closedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) | map(. / 3600)
            | {incidents: length, hoursToRestore: map(. * 10 | round / 10)}'

    On two sample incidents (opened 08:10 and closed 13:05 UTC, opened 10:40 and closed 15:20 UTC), the same jq expression printed:

    Output
    {"incidents":2,"hoursToRestore":[4.9,4.7]}
  7. Review the playbook with the team every quarter

    Book 45 minutes. Bring the four numbers for the last three months and the five slowest PRs. Ask three questions: which rule did we break most, and why? Which rule slowed us down without catching anything? What one change do we try next quarter? Record the decision as a PR to CONTRIBUTING.md, so the history of the playbook lives in Git like everything else.

Bring it to your team

Use these to start the first playbook conversation, then keep them for the quarterly review.

  • If a new developer joins on Monday, what exactly do they do to open their first PR by Monday evening? Is it written down?
  • Which of our rules are enforced by GitHub, and which do we only remember?
  • Who reviews billing and payment code, and what happens when that person is away?
  • How long did our five slowest PRs last month wait for review, and how big were they?
  • What is our hotfix path, and how long did the last one take from report to live?
  • Can we see deploy frequency and lead time for the last three months? What would make them better?

Red flags

  • The rules exist only in one senior developer's head.
  • Admins bypass the ruleset "only for urgent things", most weeks.
  • CODEOWNERS lists a single person for everything.
  • Metrics shown per developer, or used in performance reviews.
  • A playbook nobody has changed in a year, or one longer than two screens.

Practice

Calculate change lead time for a PR

Write two functions like the heart of the lead-time script. leadTimeHours(pr, releases) returns the hours from pr.firstCommitAt to the first release published at or after pr.mergedAt, or null if no release has shipped it yet. The releases may come in any order. medianHours(values) returns the median of the numbers, ignoring null values; for an even count, take the average of the two middle values. All dates are ISO strings in UTC.

JavaScript exerciseRuns in your browser with tests
function leadTimeHours(pr, releases) {
  // Uses the first release in the list. Is that the one that shipped this PR?
  const shipped = releases[0];
  return (new Date(shipped.publishedAt) - new Date(pr.firstCommitAt)) / 36e5;  // ms to hours
}

function medianHours(values) {
  // The middle value, ignoring nulls (PRs not released yet)
}
Ctrl + Enter runs
Show a solution
JavaScript
function leadTimeHours(pr, releases) {
  const shipped = releases
    .filter((r) => r.publishedAt >= pr.mergedAt)                 // ISO strings in UTC sort as text
    .toSorted((a, b) => a.publishedAt.localeCompare(b.publishedAt))[0];
  if (!shipped) return null;                                     // merged, not released yet
  return (new Date(shipped.publishedAt) - new Date(pr.firstCommitAt)) / 36e5;
}

function medianHours(values) {
  const xs = values.filter((v) => v !== null).toSorted((a, b) => a - b);
  const mid = Math.floor(xs.length / 2);
  return xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2;
}

The script in Build it uses a slightly simpler percentile for its p90 line. For a team's monthly review, either is fine; what matters is using the same method every month.

Pass the exercise to finish this task.

Read another team's numbers and propose one change

Here is the script's output for the store-app team (sample numbers). Write four or five sentences for the team's quarterly review: where does the time go, what else stands out, and which one playbook change would you try first, with how you would know it worked?

Output
your-org/store-app: 31 PRs merged into main since 2026-09-07
PR opened -> merged      median 1.9 days  p90 6.4 days
First commit -> merged   median 2.8 days  p90 7.9 days
First commit -> released median 4.1 days  p90 9.0 days
Releases: 9 in 30 days (5 patch/hotfix), 2 PRs not released yet
Slowest reviews: #412 6.9 days, 240 lines | #398 6.1 days, 180 lines | #405 5.8 days, 310 lines
  • You named waiting for review as the biggest part of lead time (about 1.9 of the 4.1 days).
  • You noticed that the slowest PRs are small, so PR size is not the cause here; routing or reviewer availability is more likely.
  • You worked out a change fail rate of about 5 in 9 releases and flagged it as high.
  • You proposed one concrete change, such as a first-response rule or a second code owner, with a number to watch next quarter.
  • Nothing in your note blames or ranks a person.
Show a model answer

"Almost half of our lead time is waiting for review: a median of 1.9 days from PR opened to merged, out of 4.1 days from first commit to release. Our slowest reviews were all small PRs (180 to 310 lines), so size is not the problem; they probably waited for the one person who owns that code. Separately, 5 of our 9 releases were patches, a change fail rate of roughly 55%, which deserves its own look at what the hotfixes fixed. For next quarter, we propose a one-working-day first-response rule and a second code owner for each area in CODEOWNERS. We will call it a success if the median review time drops below one day without the patch share going up."

Check yourself

The team agrees "no direct pushes to main". Where should that rule live?

The lead-time script shows PRs merge within a day, but change lead time to production is about four days. What is the most likely fix?

In this CODEOWNERS file, who is requested to review a change to src/payments/vat.ts?
* @your-org/partner-team
/src/payments/ @your-org/partner-leads

Flashcards

Open my review

Key takeaways

  • A playbook answers about ten decisions once, so the team stops re-arguing them in every PR. Keep it to two screens and give every rule a reason.
  • Enforce what tools can enforce (rulesets, CODEOWNERS, required checks); write down the rest in CONTRIBUTING.md and the PR template.
  • Review waiting time is usually the biggest delay. Set a response time, route reviews with CODEOWNERS, and keep PRs small.
  • DORA's keys (deploy frequency, lead time, change fail rate, recovery time) show whether the rules help. Read them from tags, PRs and incident issues, per team, as a trend.
  • Review the playbook every quarter and change it through a PR, like any other code.
Episode transcriptRules your team only argues once · about 10 minutes · tap any line to play from there
  1. NadiaImran, Ravi, who owns one of our partner corner shops, reports wrong VAT on his invoices. The fix is ready by noon. When does it reach Ravi?
  2. ImranOn my team? It depends who's around. The billing person is at a distribution centre for a stock count, so maybe tomorrow evening. Then it waits for the next deploy, which nobody has scheduled.
  3. NadiaSo Ravi and hundreds of other shops get wrong invoices for two more days, and nobody did anything wrong. There was just no agreed path. That's what a team playbook fixes, and it's a job a PM is very well placed to do.
  4. ImranI'm not writing the code, though. Why me?
  5. NadiaBecause the playbook isn't code. It's agreement. Who reviews what, how fast, when we release, what done means. You run the meetings where those get decided, and you see the delays from the outside.
  6. NadiaThink of a store's standard operating procedures. How stock is received, how a goods received note is posted, how the cash drawer is counted at shift change. A cashier who moves to a store across town can work on day one, because the rules are the same.
  7. ImranAnd nobody argues every morning about how to count the drawer. That was decided once and written down.
  8. NadiaExactly. A team without a playbook argues every day instead. Every pull request is a new debate about naming, size, and who should look at it. A playbook moves those debates to one meeting a quarter.
  9. ImranSo what goes in it?
  10. NadiaAbout ten decisions. How you branch, and here the default is short branches off main, with main always releasable. Branch names with the issue number. Conventional commit messages. A size limit for pull requests, roughly four hundred changed lines. Review rules. Squash merges only.
  11. NadiaThen a definition of done, a release rhythm with version numbers, a hotfix procedure, and onboarding: a new developer goes from clone to first pull request in one day.
  12. ImranThat's a lot of rules. Won't people just forget them?
  13. NadiaThat's the first principle. Enforce what a tool can enforce, and write down the rest. No direct pushes to main goes in a GitHub ruleset, where nobody can forget it, even during an incident. Respond to reviews within a day can't be enforced, so it goes in the contributing guide, and into the numbers.
  14. NadiaThe second principle is that every rule needs a reason a new developer would accept. Four hundred lines isn't magic. Big pull requests get worse reviews and wait the longest.
  15. NadiaIf you can see your screen, the first diagram shows the path from idea to production, top to bottom. If not, picture eight boxes: idea, branch, pull request, CI, review, squash merge, weekly release, production. Each arrow has a small question on it, like is it ready, is it small, is it green.
  16. NadiaOn the right, a box says ruleset on main, covering the pull request, CI and review steps. And lower down, a hotfix box joins at the release step. Same gates, it just doesn't wait for Tuesday.
  17. ImranWhich rule matters most, in your experience?
  18. NadiaReview expectations. In most teams the slowest part of shipping isn't writing code. It's waiting for review. A pull request that waits two days goes stale, collects merge conflicts, and gets a tired approval.
  19. NadiaSo write down a first response time, say one working day. And say which days count: Monday to Friday in many countries, Sunday to Thursday in others. Use a code owners file so the right people are asked automatically.
  20. ImranI've seen code owners files. The first matching line decides who reviews, right?
  21. NadiaThe opposite, Imran, and it's a common mix-up. The last matching line wins, like in a gitignore file. So you put the catch-all rule for the whole team first, and specific paths, like payments, below it.
  22. ImranGot it. General first, specific last.
  23. NadiaAnd agree what a comment means. Request changes blocks the merge. A plain comment is a suggestion. The author merges after approval, because they know when it's safe to ship.
  24. ImranHow do I know if the playbook is actually helping? I don't want rules for the sake of rules.
  25. NadiaThat's where DORA comes in. DORA is a long-running research programme on software delivery. Their original four keys are deployment frequency, change lead time, change fail rate, and time to restore.
  26. ImranI've seen a fifth one somewhere.
  27. NadiaYes. In twenty twenty-three they renamed time to restore to failed deployment recovery time, and in twenty twenty-four they added a fifth, rework rate. The four keys are still the usual starting point.
  28. NadiaThe finding that matters most for you: speed and stability go together. Teams that ship small changes often also break things less, because each change is easy to review and easy to roll back.
  29. NadiaThe second diagram puts all four on one timeline. If you're looking, a vertical line with dots and squares. If not, picture one change. First commit, pull request opened, merged, released. That span is change lead time.
  30. NadiaFurther down, an incident issue opens, and a hotfix release closes it. That span is recovery time. Count the releases and you have deploy frequency. In the picture, one of three releases needed a hotfix. That's change fail rate.
  31. ImranAnd all of this comes out of GitHub?
  32. NadiaIf you follow the playbook, yes. Releases are tags, every change is a pull request, every incident is an issue with a label. That's a second reason to write the rules down. Without consistent habits, there's no data.
  33. ImranSo it's tags, pull requests and labelled issues. Habits we should have anyway.
  34. NadiaOne warning, though. Never put these numbers next to people's names. The moment you rank developers, people game it: tiny meaningless pull requests, rushed approvals, incidents not filed. Show them per team, as a trend over months.
  35. NadiaNow the Build it. You write a contributing guide that fits on one screen: first day steps, branches, commits, pull requests, reviews, a definition of done checklist, release rhythm, and the hotfix steps.
  36. NadiaThen a pull request template with what and why, how to test, screenshots, and risk boxes, like database migration or touches money.
  37. NadiaThen a code owners file, with two leads on payments and on the database schema. Then two rulesets kept as files in the repo. One protects main. The other stops anyone moving or deleting a release tag, so a version number always means the same code.
  38. ImranAnd the measuring part?
  39. NadiaA short script that uses the GitHub command line tool. It finds the pull requests merged into main in the last thirty days, and the releases in the same window, and works out three times: opened to merged, first commit to merged, and first commit to released.
  40. NadiaThe script wasn't run against GitHub's live API for the lesson, so it was checked two ways. The query validated against GitHub's published schema, and the script ran unchanged against sample data shaped like GitHub's responses.
  41. ImranWhat did the sample say?
  42. NadiaReview took a median of nine hours, but the three slowest were between one and two thousand lines. Lead time to production was about three and a half days, mostly waiting for the weekly release. And two of seven releases were patches.
  43. ImranSo the cheapest speed up isn't coding faster. It's releasing twice a week.
  44. NadiaExactly the right reading. And the last step is a forty-five minute review every quarter. Bring the numbers and the five slowest pull requests. Ask which rule we broke most, which rule slowed us down without catching anything, and what one change we try next. The change goes in as a pull request to the guide.
  45. ImranQuestions I'll take to my team. If a new developer joins on Monday, what exactly do they do to open a pull request by evening? Which of our rules does GitHub enforce, and which do we only remember? And who reviews billing code when that person is away?
  46. NadiaThat last one would have saved Ravi two days.
  47. ImranCan I practise reading these numbers before I bring them to a review?
  48. NadiaYes. The first practice task runs in your browser: you write the heart of the script, the lead time for one pull request and a median. The second gives you another team's numbers and asks for one change you'd propose, without blaming anyone.
  49. NadiaHere's one to think about. Your script shows pull requests merge within a day, but lead time to production is four days. A manager says, tell the developers to code faster. Is that right? Take a second.
  50. ImranNo. The time isn't in the coding or the review. It's the wait between merge and release. The fix is releasing more often, once rollback is easy.
  51. NadiaRight. The numbers point at the process, not the people.
  52. ImranMy takeaways. One, a playbook answers about ten decisions once, so we stop re-arguing them in every pull request. Two, enforce what tools can enforce, and write down the rest with a reason for each rule. Three, measure with the four DORA keys, per team, as a trend.
  53. NadiaOne nuance. Keep the playbook short, two screens at most, and change it through pull requests. A playbook nobody has changed in a year is usually one nobody reads.
  54. NadiaNext time, a new kind of teammate: AI coding assistants. Fast, tireless, and in need of clear instructions and careful review.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Pipeline template

Build with a dependency cache, unit and end-to-end tests, then staging and production gated on the tests.

Open in the DevOps & TechOps Lab
Next stopWorking with AI coding assistants
Was this lesson helpful?
Questions and discussion

Loading the discussion…