The previous five lessons were labs: every step was written down, every file was handed to you and every failure was anticipated. This one is not. This one is the brief. Here you have a company context, a set of constraints, a budget and a list of requirements with their acceptance criteria, and from there you decide: which tool, which deployment strategy, which tests to write first, which scans to run on every PR and which only on main, where to put the gates, what to leave out and —hardest of all— how to justify each of those decisions to somebody who was not there when you took them.
You can do it on a project of your own (better, if you have one) or on Mini-Reservalia. What you cannot do is copy the pipeline from 07-05 as it stands: the brief's context has constraints that force you to deviate from it in at least three places, and finding them is part of the exercise.
Contents
- The brief
- The context: Citas Norte, S.L.
- Mandatory requirements and acceptance criteria
- Deliverables
- Self-assessment rubric
- A work plan in five sessions
- Optional challenges
- Typical mistakes in the final project
- Common Mistakes and Tips
- Exercises
- Conclusion and closing the course in practice
- The brief
You join a small company as delivery lead. It has a product in production and no pipeline at all. They deploy by hand, they never deploy on Fridays, and the last time something went wrong it took them two hours to go back because nobody remembered which version had been running before.
Your brief: take that product from its current state to a complete end-to-end pipeline, with quality gates, an immutable artifact, automated deployment with rollback, and metrics that prove the situation has improved.
The condition of the brief: everything you build has to be verifiable by an external reviewer who has never spoken to you, in less than an hour, looking only at the repository.
That last condition is what turns the exercise into something close to real work. A pipeline that only works when you are standing there to explain it is not a deliverable: it is a dependency.
- The context: Citas Norte, S.L.
Constraints are what force decisions to be justified. If everything were possible, there would be nothing to decide.
The company. Citas Norte, S.L. sells booking software for small businesses. It has:
| Fact | Value |
|---|---|
| Paying customers | 180 businesses |
| Volume | ~4,000 appointments a month |
| Technical team | 3 people: a tech lead, a backend developer, and one person splitting their time between frontend and support |
| Product | A Node.js + PostgreSQL monolith, and an SPA |
| Current environments | Just one: production |
| Current deployment | git pull over SSH and pm2 restart, by hand, by the tech lead |
| Frequency | Once every two weeks, Tuesday or Wednesday morning |
| Tests | 40 unit tests that "almost always" pass; nobody runs them before deploying |
| Latest incident | 2 h 20 min of downtime from a deployment that broke the login |
The constraints, which are non-negotiable:
- Tooling budget: €0/month. The company is approving no new spending until the next financial year. Everything has to fit into free plans.
- Nobody can be dedicated to operating the pipeline. The team is three people and all three are 100% on product. A self-hosted Jenkins is ruled out because of this, not for technical reasons.
- The current maintenance window is sacred to the business: on Saturday mornings, between 9:00 and 13:00, 40% of the weekend's bookings are concentrated. Downtime there costs customers.
- There is personal data (name, phone number and email of the end customers of those 180 businesses). Any leak is a reportable incident.
- The tech lead goes on holiday for three weeks in two months' time. During that period, the other two people have to be able to deploy and roll back without her.
- The team distrusts automation. The backend developer, specifically, has said in so many words: "every minute we spend on this is a minute we are not spending on what we get paid for". Your proposal has to pay off quickly and you have to be able to prove it with numbers.
Three consequences of reading the constraints properly (check them against your decisions when you finish):
- Constraints 2 and 6 together rule out any solution requiring ongoing maintenance. Fewer pieces and more standard beats more powerful.
- Constraint 5 means the rollback has to be executable by somebody who did not write it, with one button and without remembering commands. Documenting is not optional here: it is a functional requirement.
- Constraint 3 does not mean "deployment windows". It means that time to restore matters more than frequency. If you can revert in 3 minutes, Saturday stops being frightening; if you take 2 hours, no window will save you.
The baseline you have to measure before touching anything (it is the most frequently skipped requirement in the whole project):
| DORA metric | Estimated current value | How you would measure it today |
|---|---|---|
| Deployment frequency | 0.5 / week | Counting the production git log entries |
| Lead time (commit → production) | ~9 days | From the commit to the deployment it went out in |
| Change failure rate | ~20% (1 in 5) | Post-deployment incidents ÷ deployments |
| Time to restore | 140 min | The last incident |
- Mandatory requirements and acceptance criteria
Every requirement comes with its verifiable criterion. The formula "an external reviewer must be able to check that…" is deliberate: if it cannot be checked by looking at the repository and its runs, it does not count.
Group A — Continuous integration (module 2)
| # | Requirement | Acceptance criterion |
|---|---|---|
| A1 | Reproducible build | A reviewer must be able to run the same command CI runs, on their own machine, and get the same result. Lockfile committed, runtime version pinned explicitly, no dependency on prior runner state |
| A2 | Three layers of tests | There are unit tests, integration tests (against a real dependency: a database or an API) and at least one end-to-end test against the running system. The reviewer can tell which file belongs to which layer without asking |
| A3 | Quality gate | A PR with a lint failure, a broken test or coverage below the threshold cannot be merged. Verifiable by opening a test PR: the merge button is disabled |
| A4 | Immutable artifact identified by digest | The pipeline produces an artifact (a container image or equivalent) published to a registry, referenced by digest in every deployment. The reviewer can see the digest in the run summary and check that it is the same one that was deployed |
| A5 | Branch protection | main accepts no direct pushes. There is at least one required check. Verifiable by attempting a direct push: it is rejected |
| A6 | Fast feedback | The time from push to the first meaningful result is measured and documented, and is under 10 minutes |
Group B — Continuous deployment (module 3)
| # | Requirement | Acceptance criterion |
|---|---|---|
| B1 | CI/CD separation | They are separate workflows with different permissions. The CD one does not run if the CI one failed. Verifiable in the YAML and in the run history |
| B2 | At least one environment with approval | There is an environment whose deployment requires an explicit human action. The reviewer can see a run in the history that sat in the Waiting state |
| B3 | Idempotent deployment | Running the deployment twice with the same artifact changes nothing the second time. Verifiable by re-running the workflow and reading the log |
| B4 | Post-deployment smoke test | There is an automatic check that fails if the deployment was bad. It must be demonstrated: a historical run where the smoke test blocked a deployment |
| B5 | Promotion without rebuilding | The artifact that reaches the last environment is byte for byte the same one validated in the first. There is an explicit digest check in the log |
| B6 | Timed rollback < 10 min | There is a rollback procedure executable with one action, and a historical run with the time measured in the summary |
| B7 | Rollback executable by somebody else | Constraint 5 of the context. Documented in a runbook with the exact command and with no tacit knowledge |
Group C — Advanced practices (module 4)
| # | Requirement | Acceptance criterion |
|---|---|---|
| C1 | Cache and parallelisation with measured times | The PIPELINE.md documents the time before and after applying caching and parallelisation, with screenshots or links to the specific runs |
| C2 | Logic in scripts, thin YAML | The steps containing logic live in scripts/, are runnable locally and the YAML invokes them. Verifiable: the reviewer can run ./scripts/deploy.sh on their own machine |
| C3 | At least one reusable piece | A composite action, a reusable workflow or a script shared between at least two workflows, with its documentation |
| C4 | Automated dependency management | Dependabot or equivalent configured, with grouping and a policy on what is auto-merged and what is not |
| C5 | No "false green" | No ` |
Group D — Security (module 4 and lesson 07-05)
| # | Requirement | Acceptance criterion |
|---|---|---|
| D1 | Least privilege | Explicit permissions: in every workflow. No write-all. The repository's default permission is read |
| D2 | Actions pinned by SHA | All third-party actions by 40-character SHA with a version comment. Verifiable with a grep |
| D3 | All five scans | Secrets, SCA, SAST, image and configuration. All five report somewhere you can look them up |
| D4 | Severity policy | There is a document saying what breaks the build and what does not, and the pipeline implements it. Exceptions have a justification and an expiry date |
| D5 | No long-lived secrets on the critical path | OIDC or ephemeral tokens are used. If there is a persistent secret, it lives in a protected environment and is justified |
| D6 | Personal data protected | Constraint 4 of the context: no test uses real data, no log prints personal data, no database dump leaves the production environment |
Group E — Observability (module 3 and lesson 07-04)
| # | Requirement | Acceptance criterion |
|---|---|---|
| E1 | Metrics exposed | The application exposes queryable metrics covering at least three of the four golden signals |
| E2 | An SLO with an error budget | Documented with its four elements (indicator, threshold, objective, window) and the budget arithmetic written out |
| E3 | A symptom-based alert | Defined with a for:, with a linked runbook, and demonstrated: there is evidence of having watched it go to firing |
| E4 | The four DORA metrics calculated automatically | A scheduled job calculates and publishes them. The reviewer can see at least two runs to compare |
| E5 | The improvement demonstrated | The PIPELINE.md compares the baseline from section 2 with the current metrics |
Total: 29 requirements. Nobody expects all of them to be immaculate; what is expected is that all of them have been considered and that the absences are documented decisions, not oversights.
- Deliverables
4.1 The repository
Public (or private with access for the reviewer), with a real history: branches, PRs, checks, successful runs and failed runs. A repository with a single giant commit does not demonstrate a pipeline; it demonstrates a file.
Suggested structure:
project/ ├── .github/ │ ├── workflows/ ci.yml, cd.yml, rollback.yml, security.yml, dora.yml │ ├── actions/ your own composite actions │ └── dependabot.yml ├── scripts/ all the logic: deploy, smoke, watch, dora... ├── src/ test/ ├── security/ POLICY.md, exceptions.json, .trivyignore ├── observability/ compose, prometheus.yml, alerts.yml, SLO.md, RUNBOOK.md ├── Dockerfile .dockerignore ├── PIPELINE.md ← deliverable 4.2 ├── POSTMORTEM.md ← deliverable 4.3 └── README.md what it is, how to run it, badges
4.2 PIPELINE.md — decisions and trade-offs
This is the deliverable that shows the most and that is done the worst. It is not documentation of what the pipeline does —that is readable in the YAML—: it is the record of why it is the way it is and what was sacrificed.
Minimum template:
# Pipeline decisions
## Context and constraints
A summary of the brief's constraints and how they shape what follows.
## Baseline (before)
| Metric | Value | How it was measured |
## Decisions
### D1 — CI/CD tool: GitHub Actions
**Alternatives considered:** GitLab CI, CircleCI, self-hosted Jenkins.
**Decision:** GitHub Actions.
**Reason:** the code is already on GitHub (identity constraint, 06-07);
the free plan is sufficient for the volume (constraint 1); zero operations
(constraint 2).
**Trade-off accepted:** vendor coupling. Mitigated by putting all the
logic in `scripts/` (C2): a migration would cost ~5 days, not 4 weeks.
**When to revisit this decision:** if the minutes consumed exceed the free
plan, or if the company changes repository provider.
### D2 — Deployment strategy: rolling, not canary
...
### D3 — What we have NOT done and why
- **No ephemeral per-PR environments.** High maintenance cost for a team
of three (constraint 2). To be revisited when the team grows.
- **No load testing in the pipeline.** The current volume (4,000
appointments/month) does not justify it.
...
## Measurements
| Change | Before | After | Reference run |
| Dependency cache | 3 min 40 s | 1 min 15 s | #128 vs #131 |
## After
| DORA metric | Before | Now | Source |The "What we have NOT done and why" section is what separates professional work from an exercise. A pipeline with no explicit limits is a pipeline that will grow uncontrollably until somebody abandons it.
4.3 POSTMORTEM.md — a failure caused on purpose
Cause a real failure, let it travel as far as your pipeline stops it, and write the post-mortem. Suggested failures, in order of difficulty:
| Failure | Where it should be stopped | What it demonstrates |
|---|---|---|
| A broken test | In CI, in the first job | The basics |
| A regression the tests do not cover | In the staging smoke test | That the layers complement each other |
| A backwards-incompatible DB migration | In the deployment, or in the rollback | What you learned in 04-06 |
| A version that starts fine and degrades after 5 minutes | In the post-deployment watch, with automatic rollback | The complete loop closed |
The structure of the post-mortem, blameless:
# Post-mortem: <descriptive title of the impact, not of the cause>
**Date:** · **Impact duration:** · **Severity:** · **Author:**
## Impact
What stopped working, for whom and for how long. In user terms,
not infrastructure terms: "businesses could not see the day's slots",
not "the container was unhealthy".
## Timeline
| Time | Event | Who / what detected it |
|---|---|---|
| 10:32 | PR #47 merged | — |
| 10:38 | Deployment to staging | pipeline |
| 10:39 | Smoke test fails | pipeline (automatic) |
| 10:39 | Deployment to production blocked | pipeline |
## Root cause
The five whys technique, until you reach something systemic.
If the final answer is "a developer made a mistake", you have not got to the end:
the next question is "why did the system let that mistake get there?".
## What worked
Just as important as what failed. This is where the investment in the pipeline is justified.
## What did not work
## Corrective actions
| # | Action | Owner | Date | Status |
With an owner and a date, or it is not an action: it is a wish.
## Lessons4.4 Dashboard or metrics summary
A versioned Grafana dashboard, or —if you do not set up Prometheus— a $GITHUB_STEP_SUMMARY from a scheduled job with the four DORA metrics, the SLO status and the error budget consumption. What is being assessed is that there is a surface where somebody who is not you can see how things are going without having to ask you for anything.
- Self-assessment rubric
Apply it to yourself honestly. The "Inadequate" column describes what most people deliver on their first attempt.
| Criterion | Inadequate | Solid | Excellent |
|---|---|---|---|
| Reproducible build | The pipeline "works" but cannot be reproduced locally | Lockfile, pinned runtime, the same command locally and in CI | Plus, verified periodically by a job that runs the build on a clean machine |
| Tests | Unit tests only, or coverage with no assertions | Three identifiable layers, a coverage threshold that breaks | Plus, a contract test between implementations, documented edge cases and flaky detection |
| Quality gate | The checks exist but do not block | main protected with a required check, verified from the negative side |
An aggregator job that decouples the protection from the shape of the pipeline |
| Artifact | Rebuilt in each environment | Image published, deployed by digest | Signed, with an SBOM, and the signature verified before deploying |
| CI/CD separation | A single workflow does everything | Separate workflows with different permissions | Plus, the CD can be launched by hand with an arbitrary digest for retries |
| Approval gate | There is none | Environment with a required reviewer | Conditional approval: automatic if the metrics are healthy, human if not |
| Idempotency | Redeploying restarts the service needlessly | The script detects it is already deployed and does nothing | Plus, the script rejects mutable references and validates before touching anything |
| Smoke test | Only checks that it returns 200 | Checks health, functionality and the deployed version | Plus, it checks that errors still return errors, and there is a run where it blocked |
| Rollback | It exists as a document | An executable workflow, timed, < 10 min | < 3 min, automatic from metrics, and successfully executed by somebody else |
| Performance | Not measured | Caching and parallelisation applied, times documented | Selective execution by changes, and the cost in minutes measured as well |
| Reuse | Everything copied and pasted between workflows | One shared, documented piece | A versioned reusable workflow, with inputs, outputs and its own test |
| Least privilege | No permissions |
Explicit at workflow level and widened per job | Verified by provoking the failure, and with what each job needs documented |
| Scans | None, or with ` | true` | |
| Secrets | In the repository or in global variables | Environment secrets, minimum scope | OIDC / ephemeral tokens; no long-lived secret on the critical path |
| Observability | Logs only | Metrics exposed and a dashboard | An SLO with a budget, a demonstrated alert and deployments annotated on the dashboard |
| DORA | Not calculated | Calculated by hand once | A scheduled job, history and a trend compared with the baseline |
PIPELINE.md |
Describes what the YAML does | Explains why, with alternatives | Includes trade-offs, explicit limits and when to revisit each decision |
POSTMORTEM.md |
Narrates the failure | Timeline, root cause, actions | Blameless, with "what worked", and the actions have an owner and a date |
| Reproducibility by a third party | Only you know how to run it | A README with the steps | An external reviewer verifies the whole thing in an hour without asking you anything |
How to score yourself: count how many rows fall into each column.
- Mostly in Inadequate: you have a pipeline that works but is not defensible. Prioritise group A and B4/B6.
- Mostly in Solid: this is a professional, deliverable pipeline. It is the realistic objective of the course.
- Several in Excellent: you are above what many teams in production have. Pick two or three to take to excellent rather than raising them all at once.
- A work plan in five sessions
Five sessions of 2-4 hours. The order is not arbitrary: each session leaves something working and measurable, and none of them depends on finishing the next one to deliver value. It is the same principle as the seven increments in 05-04.
Session 1 — Measure and make the project verifiable (without touching the pipeline)
- Measure the baseline. The four DORA metrics with whatever data you have, even if approximate. Write them into
PIPELINE.mdtoday, because in three weeks you will not be able to reconstruct them. - Get the project verified with a single command:
npm ci && npm run lint && npm test && npm run build. If that does not work on a clean machine, fix it before writing a line of YAML. - Write the missing test for the most recent bug you had.
- Commit the lockfile if it is not there.
Session deliverable: one command that verifies the project, and the baseline written down.
Session 2 — CI and the first gate
ci.ymlincrementally, as in 07-01: first get it to run, then get it to install and test, then jobs in parallel, then caching. Measure the times at each step and note them down.- The three layers of tests (07-02). Start with the one you are most afraid of breaking.
- Coverage with a threshold 2-3 points below the current value.
- Protect
mainwith an aggregator job as the only required check. - Break something on purpose and check that it blocks.
Deliverable: A1-A6 met, with the before/after times for the cache.
Session 3 — Artifact and deployment
- Multi-stage Dockerfile, non-root,
HEALTHCHECK(07-03). - Publication to the registry by digest, with the digest visible in the summary.
scripts/deploy.sh, idempotent and runnable locally. Write it before the YAML: if you start with the YAML, you will end up with logic inside the YAML.scripts/smoke.shchecking health, functionality and version.cd.ymlwith two environments and approval on the second.rollback.yml, timed. Run it and note the time.- Cause a bad deployment and check that the smoke test stops it.
Deliverable: B1-B7 met, with the timed rollback run and the blocked deployment run.
Session 4 — Security
- An audit of what you have built so far, using the list from 07-05.
- Least-privilege
permissions, provoking a failure to understand it. - Actions by SHA + Dependabot.
- The five scans, one at a time, checking that each one detects something real before moving on to the next.
- A written severity policy and the exception expiry job.
- SBOM, signature and verification before deploying.
Deliverable: D1-D6, with evidence that each scan has detected something.
Session 5 — Observability, closing and documentation
/metricswith the golden signals (07-04).- An SLO with the budget arithmetic written out.
- A symptom-based alert, fired on purpose and with a screenshot.
- A scheduled DORA job. Run it twice a few days apart so you have a trend.
- Cause the post-mortem failure and write it up.
- A complete
PIPELINE.md, including the section on what you have not done. - Take the rubric.
Deliverable: E1-E5, PIPELINE.md, POSTMORTEM.md and the self-assessment.
A word on pacing. If you are short of time, sacrifice group E before group B. A pipeline that deploys and reverts but is not measured is incomplete; one that is measured but cannot revert is dangerous. And if you have to pick a single requirement out of the 29, make it B6: a timed rollback. It is the one that turns fear into a number.
- Optional challenges
For once the basics are closed off. Each one stands on its own.
| Challenge | What it adds | Difficulty | Reference |
|---|---|---|---|
| A real canary with an automatic decision | Deployment by weight + measurement + automatic promotion or withdrawal | High | 03-04, 07-03 ex. 2 |
| Ephemeral per-PR environments | One environment per pull request, destroyed when it closes | High | 02-07, 06-02 |
| A matrix of two tools | The same pipeline in GitHub Actions and in GitLab CI, comparing the real effort | Medium | The whole of module 6 |
| IaC for the target environment | Terraform that creates the host, the registry and the network, with plan on the PR and apply after approval |
High | 03-03 |
| DB migrations in the pipeline | Expand and contract, a versioned migration, and a rollback that loses no data | Very high | 04-06 |
| An external admission policy | A script that validates signature, SBOM, scans and provenance, consulted by the CD | Medium | 07-05 challenge |
| Shard distribution by timing | Real balancing of the suite using history | Medium | 07-02 challenge |
The most instructive of all, if you want to close the course with something that changes your perspective, is the matrix of two tools: reimplementing the pipeline in GitLab CI forces you to separate what is your process from what is GitHub syntax, and that separation is exactly what 06-07 was arguing for. It takes less time than it looks —the real work is already in the scripts— and the result is an equivalence table you wrote yourself.
- Typical mistakes in the final project
These three are, by a distance, the most repeated. All three have the same shape: doing the visible part before the part that holds it up.
Mistake 1: starting with the YAML instead of the scripts
How it shows up. You open ci.yml and start typing. Three days later there are 200 lines of YAML with twenty-line run: | blocks each, and the only way to test a change is to push and wait four minutes.
Why it matters. The debugging cycle goes from seconds to minutes, and multiplied by every attempt. A problem that locally takes five ten-second iterations turns into five four-minute iterations, with commits saying "fix CI", "fix CI 2", "now for real". On top of that, that logic is not testable, not reviewable, and cannot be run in an emergency if CI is down.
The right order:
1. The command works in your terminal 2. The command lives in a script with arguments and defaults 3. The script is in the repository and has its documentation 4. The YAML CALLS the script
The acid test: can you deploy without GitHub Actions? If the answer is no, the logic is in the wrong place.
Mistake 2: automating before you have a test worth having
How it shows up. A beautiful pipeline that deploys in ninety seconds... broken code. The tests exist, they always pass, and they cover none of what actually breaks.
Why it matters. Automating deployment multiplies the speed at which changes reach production. If the verification is weak, what you have multiplied is the speed at which bugs reach production. A fast pipeline on top of bad tests is objectively worse than the manual deployment you started with, because the manual one at least involved a person looking.
The right order: a test that catches the last real bug you had, before automating the deployment. If you cannot write it, do not automate yet: automate only as far as staging and leave production manual until the safety net exists.
The warning sign: if your suite has never gone red because of a real bug —only because of syntax errors— it is not testing anything.
Mistake 3: not measuring the baseline and ending up unable to prove the improvement
How it shows up. Three weeks of work, an excellent pipeline, and then in the meeting with management: "so what has this improved?" — "Well... everything is much better".
Why it matters. It is the mistake that stops the next investment being approved. Remember constraint 6 of the context: the backend developer already thinks this is time stolen from the product. Without numbers, he is right, because his scepticism is based on something concrete (the hours invested) and your defence is not.
And there is a technical aggravating factor: the baseline cannot be reconstructed after the fact. Once the pipeline is in place, the "before" no longer exists. It is information destroyed by doing the work.
The right order: measure in session 1, before touching anything, even with rough estimates. Four numbers and the date. Ten minutes of work that pay for the next three weeks.
How you present it afterwards:
| Metric | Before (12/03) | Now (28/04) | Change |
|---|---|---|---|
| Deployment frequency | 0.5 / week | 6 / week | ×12 |
| Lead time | 9 days | 4 h | −98% |
| Change failure rate | 20% | 6% | −70% |
| Time to restore | 140 min | 3 min | −98% |
| Tech lead's time spent deploying | 45 min/deployment | 0 | −6 h/month |
That last row is the one that ends the discussion, and that is why it is worth having: it translates the work into person-hours, which is the unit whoever approves budgets thinks in. It is exactly the TCO argument from 06-07, applied to your own project.
- Common Mistakes and Tips
Symptom: the project gets stuck in session 3 and never reaches production. Cause: trying to set up the "real" target environment (a VPS, Kubernetes, AWS) before having the pipeline. Fix: use a simulated target like the one from 07-03 and complete the whole circuit. A pipeline that deploys to a local container but covers the seven pieces of B1-B7 is worth infinitely more than half a pipeline pointing at real infrastructure. Change the target at the end; it is the easiest part to change.
Symptom: 29 requirements look unmanageable and you never start. Cause: trying to do them in parallel. Fix: the five-session plan is ordered by dependency. Finish A completely before touching B. One finished group is worth more than five half-done ones, because incomplete groups cannot be verified.
Symptom: the PIPELINE.md ends up being a description of the YAML.
Cause: documenting while writing the code, when there is still no perspective.
Fix: write each decision down at the moment you discard an alternative, in one line: "discarded X because Y". By the end, those lines are the document. If you never discarded anything, you did not make decisions: you copied.
Symptom: the rubric comes out mostly in "Solid" and you do not know what to improve. Tip: do not push everything to "Excellent". Pick two or three deliberately: the ones that most reduce risk in your specific context. With Citas Norte's constraints, the three obvious candidates are the rollback (constraint 5), the signature verification (constraint 4, personal data) and DORA with a trend (constraint 6, somebody has to be convinced).
Tip — do not do it all yourself. If you have somebody available, ask them to verify your repository following the acceptance criteria, without you explaining anything. The points where they have to ask you something are exactly the ones you still need to document. It is the cheapest and most revealing test in the project.
Tip — the pipeline is never finished. When you close all 29 requirements, the pipeline will be good for today's context. With twice the team and ten times the traffic, three of your decisions will be wrong. That is why every decision in the PIPELINE.md carries a "when to revisit this decision": it is not bureaucracy, it is acknowledging that a pipeline is a living organism and that the alternative to revisiting it is rewriting it entirely two years from now.
- Exercises
These three are not lab variations: they are extensions of the final project itself, designed to be done after delivering it.
Exercise 1: the peer review
Swap repositories with somebody else who has done the project (or audit a real public repository that uses GitHub Actions). Apply the 29 acceptance criteria without talking to anyone and write an audit report with the findings prioritised.
Exercise 2: the holiday test
Constraint 5 of the context: the tech lead is away for three weeks. Prove the system survives.
Exercise 3: the same pipeline in half the time
Halve your pipeline's time without removing a single check. Document every change with its measurement.
Solutions
Solution 1. Audit report template:
# Pipeline audit of <repository>
**Auditor:** · **Date:** · **Time spent:** · **Commit audited:**
## Executive summary
Three sentences: overall state, the most serious risk, the improvement with
the best value/effort ratio.
## Criteria verification
| # | Requirement | Meets it | Evidence | Notes |
|---|---|---|---|---|
| A1 | Reproducible build | ✅ | I ran `npm ci && npm test` from clean: OK | — |
| A4 | Artifact by digest | ⚠️ | Run #88: publishes by digest but the CD uses `:main` | **Promotion does not guarantee the same artifact** |
| B4 | Smoke test | ❌ | I found no run where it blocked anything | It may never have been tested |
## Prioritised findings
### F1 (High) — The CD deploys by tag, not by digest
**Evidence:** `cd.yml:47` uses `ghcr.io/...:main`.
**Risk:** between validation in staging and deployment to production, another
merge can move the tag. Production would run unvalidated code.
**Fix:** resolve the digest once in a `prepare` job and propagate it through
`outputs`. Cost: ~30 min.
## What is done well
(Mandatory. An audit that only lists problems does not get read to the end and
does not get acted on.)
## Questions I had to ask the author
Every one of these is missing documentation.What this exercise teaches: auditing somebody else's work is the fastest way to see the holes in your own. Almost everybody finds two or three things in the other repository that their own is missing too and that they had not spotted in weeks.
Solution 2. The protocol for the test:
# Continuity test ("the holiday")
## The rule
During the exercise, whoever wrote the pipeline **does not speak, does not touch
the keyboard and does not answer questions**. They only observe and take notes.
## Scenario 1 — Routine deployment (target: < 15 min)
The other person must, using only the repository:
1. Work out how a deployment is done.
2. Launch it.
3. Approve the production gate.
4. Check that the new version is serving traffic.
## Scenario 2 — Rollback under pressure (target: < 10 min)
A bad version is deployed (it breaks `/health`). The other person must:
1. Notice that something is wrong (did the alert arrive? to whom?).
2. Find which version to go back to.
3. Run the rollback.
4. Confirm the recovery.
## Scenario 3 — Pipeline failure (target: deploy without CI)
GitHub Actions is down (simulate it by disabling the workflow) and an urgent
patch has to be deployed. Is there a documented manual path?
## Record
| Scenario | Time | Did they manage it? | Where they got stuck | Documentation that was missing |
## Corrective actions
Every sticking point is a failure of the SYSTEM, not of the person. It turns into:
- A line in the runbook, or
- A better default, or
- A clearer error message.The three failures that show up almost every time in this exercise, in case you want to get ahead of them:
- Finding the previous digest. Nobody knows where to look. Fix: print it in the summary of every deployment and accept an empty input in the rollback (07-03 exercise 1).
- The alert reached nobody. It was configured in Prometheus but had no recipient. Fix: Alertmanager with a real channel, and proof that it arrives.
- The manual path does not exist. All the knowledge is inside the YAML. Fix:
scripts/deploy.shrunnable locally (requirement C2), documented in the runbook with the literal command.
If your project passes scenario 3, you have met the most demanding criterion in the whole course: the pipeline is a convenience, not a dependency.
Solution 3. The strategy, in order of return:
# Reducing the pipeline time
## Initial measurement (run #142)
| Job | Duration | Critical path? |
|---|---|---|
| quality | 1m 10s | No |
| test (×4) | 3m 40s | **Yes** |
| coverage | 2m 50s | No |
| build | 2m 20s | Yes |
| publish | 4m 10s | **Yes** |
| **Total (wall clock)** | **10m 30s** | |
Rule to apply first: **only the critical path counts**. Optimising a job that runs
in parallel with a slower one saves not a single second of wall-clock time.
## Changes applied
### C1 — Dependency cache (−1m 50s)
`cache: 'npm'` in the 5 jobs. From the second run onwards.
Before: `npm ci` 22s/job · After: 4s/job.
### C2 — Docker layer cache (−2m 30s)
`cache-from: type=gha` / `cache-to: type=gha,mode=max`.
The biggest single saving: the native module stopped being recompiled.
### C3 — Reordering the Dockerfile (−40s)
`COPY package*.json` and `npm ci` BEFORE `COPY . .`.
Without this, any code change invalidates the dependency layer
and the C2 cache is useless. **C3 is what makes C2 work.**
### C4 — Merging `coverage` into the test job (−0s wall clock, −2m machine)
It was not on the critical path: it saves no wall-clock time, but it does save cost.
It is documented because cost is a metric too.
### C5 — Selective execution (−variable)
The heavy jobs only if the relevant files change, with an aggregator job
that always reports (07-01 exercise 2). On documentation-only PRs:
10m 30s → 45s.
### C6 — Discarded: larger runners
Would cut ~30% but costs money. Constraint 1 of the brief.
**The rejection is documented so nobody has to evaluate it again.**
## Result (run #171)
| Job | Before | After |
|---|---|---|
| test (×4) | 3m 40s | 1m 30s |
| publish | 4m 10s | 1m 20s |
| **Total (wall clock)** | **10m 30s** | **4m 15s** (−60%) |
| Machine minutes | 14m 10s | 7m 30s (−47%) |
## Checks NOT removed
None. The same 5 scans, the same coverage, the same matrix.
`git diff` of the workflows: only caching, ordering and conditions.The lesson of this exercise is C3: the Docker cache does not work if the Dockerfile is badly ordered, and plenty of people add C2, see no improvement and conclude that "caching is useless". Layer ordering is 80% of the result, and it is free.
- Conclusion and closing the course in practice
If you have got this far with the project delivered, you have something in your repository that many teams in production do not have: a pipeline that integrates, verifies in three layers, builds an immutable artifact identified by its content, scans it in five different ways, signs it, deploys it behind an explicit gate, checks that the deployment worked, promotes the very same artifact without rebuilding it, reverts in minutes —on its own, if the metrics get worse—, and measures itself so it can prove that all of that is worth something.
But the deliverable that will last longest is not the YAML. It is the two documents.
The PIPELINE.md, because it contains the one thing you cannot deduce by reading the code: why it is the way it is and what was sacrificed in return. A year from now, when somebody —perhaps you— wonders why there is no canary or why the actions are pinned to those strange SHAs, the answer will be written down, with its date and with the condition that would force a review. A pipeline without that document gets rewritten from scratch every two years because nobody dares touch what they do not understand.
And the POSTMORTEM.md, because it documents the only proof that really matters: that the system failed and held up. Anybody can show you a pipeline in green. Showing one that went red for the right reason, in the right place, and recovered in three minutes, is showing that it works.
There is one last thing this module has made you do without saying so explicitly, and it is worth naming now. In every lab you have broken something on purpose: a test, a deployment, a secret, a SQL query, a slow endpoint. That was not pedagogical decoration. It is the professional practice that separates whoever has a pipeline from whoever trusts their pipeline: verifying from the negative side. Anybody can check that a control lets the good stuff through; checking that it stops the bad stuff requires manufacturing the bad stuff, and almost nobody does it. If you take a single habit away from this course, let it be this one: every time you add a gate, cause the failure it is supposed to stop, and do not take the gate as good until you have watched it happen.
That closes the practical part and, with it, the taught material of the course. You have covered the principles (modules 1 to 4), four real contexts (module 5), the machinery (module 6) and end-to-end construction with your own hands (module 7). What remains is no longer material: it is where to go next.
Module 8 is the map of that continuation. The recommended reading —Continuous Delivery, Accelerate, The DevOps Handbook, the SRE Workbook— that provides the grounding and the data behind almost everything you have applied here. The communities and forums where these problems are discussed once they outgrow what fits in a course. The additional tools and plugins that were left out for space and that solve specific problems you will now be able to recognise: secret management with Vault, progressive delivery with Flagger or Argo Rollouts, internal developer platforms, policy as code. And the learning path and certifications, with a sensible order for going deeper depending on where you want to move —platform, SRE, supply chain security— and which certifications are genuinely worth it in each case.
Start with 08-01: Recommended Reading. And when you do, read it with the pipeline in front of you: you now have a real system to try every idea on, which is exactly what was missing the first time you opened this course.
CI/CD Course: Continuous Integration and Deployment
Module 1: Introduction to CI/CD
- Basic CI/CD Concepts
- Benefits of CI/CD
- Popular CI/CD Tools
- The Course Project: the Application We Are Going to Automate
- DORA Metrics: How Software Delivery Is Measured
Module 2: Continuous Integration (CI)
- Introduction to Continuous Integration
- Setting Up a CI Environment
- Build Automation
- Automated Testing
- Code Quality and Static Analysis
- Artifacts, Versioning and Promotion
- Integration with Version Control
Module 3: Continuous Deployment (CD)
- Introduction to Continuous Deployment
- Deployment Automation
- Infrastructure as Code and Reproducible Environments
- Deployment Strategies
- Feature Flags, Rollback and Failure Recovery
- Monitoring and Feedback
Module 4: Advanced CI/CD Practices
- CI/CD Pipelines
- Dependency Management
- Security in CI/CD
- Scalability and Performance
- Pipeline as Code: Templates, Reuse and Testing the Pipeline
- Databases in the Pipeline: Safe Migrations
Module 5: Implementing CI/CD in Real Projects
- Case Study: Web Project
- Case Study: Mobile Application
- Case Study: Microservices
- Case Study: Modernising a Legacy Project
Module 6: Tools and Technologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker and Kubernetes
- GitHub Actions in Depth
- Comparison and Criteria for Choosing a Tool
Module 7: Practical Exercises
- Exercise 1: Setting Up a Basic Pipeline
- Exercise 2: Integrating Automated Tests
- Exercise 3: Deploying to a Production Environment
- Exercise 4: Monitoring and Feedback
- Exercise 5: Hardening the Pipeline with Security and Secrets
- Final Project: A Complete End-to-End Pipeline
