Skip to content
CAMPUX Cloud Bootcamp Phase Four · Class Thirty-Nine
Phase Four — Operate, Secure & AI
Reading 15 min · Drills 4 · Part VIII of IX
Class Thirty-Nine · Pipelines
Class Thirty-Nine · Part VIII

Make the machine say no

A bug caught by a test on the developer's own branch costs minutes; the same bug caught by a customer at checkout costs the customer, the reputation, and the afternoon — so this part turns the pipeline into the thing that refuses to ship code the tests do not vouch for.

§1

The cost of a defect climbs the later you meet it

A defect is cheapest at the moment it is written and most expensive at the moment a customer finds it, and everything between those two points is a slope you are either sliding down or climbing back up. The developer who catches a broken calculation in a unit test on their own laptop pays a minute and a shrug. The same broken calculation caught in code review costs an hour of two people's attention. Caught by QA a week later, it costs a bug ticket, a context-switch back into code nobody remembers, and a re-test. Caught by a customer in production, it costs all of that plus the money the wrong number moved, the trust it spent, and an incident bridge convened at an hour nobody chose.1 The defect did not get worse. The distance you had to reach back to fix it did.

Shift-left is the whole discipline named after that slope. If the cost of a defect rises the further right it travels — right being later, toward release and the customer — then the move that pays is to drag detection as far left as it will go, toward the commit that introduced it. The pipeline you built across this track is where that move becomes automatic: tests run on every change, before the artifact is ever published, so a defect meets a machine that was watching for it on the same afternoon it was born, not a customer three weeks later. This is not about testing more for its own sake. It is about buying the cheap catch instead of the expensive one, every time, without relying on anyone remembering to.

Shift-left
Moving defect detection earlier in the delivery flow — leftward, toward the commit — so faults are caught by automated checks in continuous integration rather than by humans downstream. The name treats the pipeline as a timeline that runs left to right: build, test, publish, deploy. Every step you can make a test fail at is a step the defect never reaches.

Fail in the pipeline, not in production.

§2

Tests as steps, and the results the pipeline keeps

A test in your repository does nothing for you until the pipeline runs it on every change, so the first move is mechanical: add a step that executes the test suite. In an Azure DevOps job that is usually a single script line — dotnet test, npm test, pytest — and it carries a property you get for free that matters more than it looks. A test runner exits non-zero when a test fails, and a pipeline step that exits non-zero fails the job. So the mere act of running the tests, with no extra configuration, already makes the pipeline refuse to continue when they fail. That default is the entire fail-fast behaviour, and §4 is mostly about not throwing it away by accident.

Running the tests and recording what they said are two separate things. The test runner writes its verdict to a results file — in a standard format like JUnit, xUnit, NUnit, or the VSTest .trx — and by itself that file is an artifact nobody reads. The PublishTestResults task takes that file and hands it to Azure DevOps, which parses it into the run's Tests tab: how many passed, which failed, how long each took, and the failure message and stack for every red one, laid out where the whole team looks rather than buried in a log. Publishing is why a failed run tells you which test broke and why, at a glance, instead of making you scroll a wall of console output to find the one line that matters. It also builds history — flaky tests and slow tests become visible over runs, not anecdotes.

The tests themselves come in tiers, and a healthy pipeline runs more than one. The distinction an interviewer wants you to hold is speed-and-scope against confidence.

Table 1 — Two test tiers a pipeline runs, and what each is for
TierWhat it exercisesSpeed & where it runsWhat a failure tells you
Unit One function or class in isolation, with its collaborators faked, asserting the logic does what it claims for given inputs. Milliseconds each; thousands run on the build agent with no external dependency. First tests in the pipeline. A precise, local fault: this function computes the wrong thing. The stack points near the line that broke.
Integration Several components together across a real boundary — a database, a queue, an HTTP call — checking the seams hold, not the logic within them. Seconds to minutes; needs services stood up (a container, a test database), so it runs after units pass. A wiring fault: the pieces are each fine but disagree at the join — a schema, a serialization, a contract mismatch.

Order them by that table. Units are fast and cheap, so run them first and let them fail the build in seconds; integration tests are slower and need scaffolding, so run them after, once the cheap checks have already caught the obvious. This is shift-left applied inside the pipeline itself: the fastest, most localised catch goes leftmost, so most bad changes die before the expensive tests ever start.

§3

Coverage: an honest number people misread

Once tests run in the pipeline, the next question a manager asks is "how much of the code do they cover?" — and code coverage answers it, with a caveat you must state in the same breath. Your test runner, given a coverage flag, records which lines and branches of the product code were actually executed while the tests ran, and writes it in a standard format — Cobertura or JaCoCo. The PublishCodeCoverageResults task then surfaces it in the run's Code Coverage tab as a percentage and a line-by-line map of what was and was not touched.

Here is the caveat, and saying it out loud is how you sound like an engineer instead of a dashboard. Coverage measures execution, not assertion. A test can call a function, cover every line inside it, and check nothing about the result — the coverage number climbs and the code is no safer. So eighty per cent coverage does not mean eighty per cent of your logic is verified; it means eighty per cent of your lines were run by something during the tests. Coverage is a useful floor and a dishonest ceiling: a low number reliably tells you a region is untested and dangerous, while a high number tells you almost nothing about whether the tests would notice if the behaviour broke. Read it as "what is definitely untested," never as "what is definitely safe."2

Which is exactly why coverage belongs in a pipeline but not as a target to be chased. Track it so a pull request that drops coverage on a critical module gets a second look; publish it so the trend is visible. But an engineer who treats the percentage as the goal will get the percentage — teams reliably write assertion-free tests to hit a mandated number, and the pipeline goes green over code no one has actually checked. The number is a smoke detector, not a certificate.

§4

The quality gate: where the pipeline refuses

Everything so far produces signals — pass or fail, covered or not. A quality gate is the point in the pipeline where those signals are turned into a verdict that can stop the line. It is not a feature you switch on; it is a policy you assemble from the fail-fast behaviours you already have, placed deliberately before the deploy so that nothing which fails it can reach an environment.

Quality gate
A pass/fail checkpoint in the pipeline that must be satisfied before the flow may proceed to the next stage — typically before deployment. It aggregates conditions such as "all tests passed" and "coverage did not fall below the threshold," and when any condition fails, the pipeline stops rather than shipping. A gate is a machine saying no so a tired human does not have to.

The simplest gate you already own: because a failing test exits non-zero and fails the job, and because a failed job halts the stage, an unbroken test suite is a de facto gate on every run. The discipline is not to defeat it — which is precisely what a stray continueOnError: true on a test step does, quietly turning "the tests must pass" into "the tests may fail and we ship anyway." Half the failures of this section are not missing gates; they are gates disarmed by one hopeful line.

Gating on coverage is where people expect a switch and there isn't one. The publish task shows coverage; it does not, on its own, fail a build when coverage drops below a number. You enforce a coverage threshold one of three ways: a marketplace task built for it (the widely used Build Quality Checks extension), an external analysis service whose result is literally called a quality gate — SonarQube or SonarCloud, which fail the pipeline when your defined conditions are not met — or, at the crudest, a script step that reads the coverage file and exits non-zero below the line.3 Knowing that the threshold is something you add, not a checkbox Azure DevOps ships, is the detail that separates having wired a real gate from having assumed one exists.

The last piece places the gate where it cannot be skipped. A gate that only runs when someone remembers to run the pipeline is optional, and optional gates are decoration. You make it mandatory with a branch policy — build validation on the main branch — so the pipeline, tests and all, must pass before a pull request can merge at all. Now the gate is not a step in a flow a developer might route around; it is a condition on the branch itself. The tests do not ask to run. They must.

Fig. 1 · The gate sits before the deploy, and only green passes
A pipeline where a quality gate of tests and coverage sits between build and deploy; passing runs proceed to production while failing runs stop before any environment is touched. commit a change build quality gate tests · coverage pass → deploy · prod the artifact ships fail ↓ build fails nothing ships caught here, never an incident there
Draw this once by hand. The one idea is the position of the red box: the gate sits between build and deploy, so a change that fails its tests or drops its coverage stops at "build fails, nothing ships" and never reaches production. Move the gate to after the deploy and it is not a gate — it is a postmortem.
§5

Keeping the gate honest — and fast

A gate has one failure mode that guarantees it will be removed: it becomes slow enough that people resent it. If the full test suite takes forty minutes, developers stop running the pipeline on small changes, start batching, and eventually campaign to make the gate optional — and a slow gate that gets switched off protects nothing. So the second half of this discipline is keeping the gate quick enough that no one wants to escape it. The lever is parallelism. A test stage can fan its suite across several agents at once — unit tests split into slices that run side by side, integration suites sharded so ten minutes of tests finish in two on five agents. Azure DevOps supports this directly, from a job's parallel strategy to slicing test runs across multiple agents; the point is that wall-clock time, not total test count, is what decides whether the gate survives contact with a deadline.

The other honesty is ordering, which you have already met: cheap and fast on the left, slow and broad on the right, so most bad changes fail in the first fast slice and never pay for the expensive one. A gate arranged this way gives its verdict on the common case — a genuine bug — in seconds, and reserves the minutes only for changes that earned a deeper look by surviving the quick checks. That is what lets a gate be both strict and tolerable, and a gate that is both is a gate that stays. Campux is about to install exactly this in front of the one release it cannot afford to get wrong.

Case File · Campux Retail

The rewritten checkout gets a gate before it earns the right to ship dark

November is close; the new payment path may not reach production untested

Last part, the reader shipped Campux's rewritten checkout dark — deployed to production weeks before November, behind a feature flag switched off for everyone, waiting to be released on a dial. But shipping code dark only removes the risk of it being reached; it does nothing about the risk of it being wrong. A broken checkout deployed dark is still a broken checkout the moment the flag opens. So before that artifact was allowed anywhere near production, it had to pass through a gate the reader built into the pipeline: the unit suite covering the new pricing and tax math runs first and fails the build in seconds if a number is off; an integration suite then stands up a test database and a stub payment provider and checks the seams hold; results publish to the Tests tab so a failure names the exact broken case; and a branch policy on main makes the whole thing mandatory — no change reaches the checkout without passing it.

The payoff arrives on the calm afternoon a week before the peak, when the flag opens the new checkout to its first five per cent. The reader is not hoping the code is correct; the code cleared a gate that would have refused to deploy it otherwise. The strategy from Part G controls who sees the new path; this part is why the path was trustworthy enough to expose at all. Shipping dark bought the reader control over the release. The gate is what made the thing being released worth releasing — and it did so on the one estate, in the one month, where finding a payment bug in front of a customer would have cost Campux its year.

That is the sentence this part is built to earn in an interview: "I don't test to raise a number; I put a gate before the deploy so a defect fails in the pipeline on the afternoon it's written, not in front of a customer three weeks later." An engineer who can say that — and name where the gate sits, what disarms it, and why coverage is a floor and not a certificate — is describing the difference between a pipeline that ships confidence and one that ships hope.

On the job

The person who put the gate before the deploy

You · Cloud Engineer · owns what the pipeline refuses to ship

When a release goes wrong, the first question is always "how did this reach production?" — and the engineer who wired the gate has the answer ready: it couldn't have, if it failed a test. You make the tests run on every change, publish the results where the team reads them, put a mandatory gate before the deploy, and keep it fast enough that no one wants it gone. That is not a task you completed once; it is the standing reason the team trusts the pipeline with its busiest day. The gap was never that you couldn't test — it was proving the code is safe before it ships, and now that proof is a machine, not a promise.

Class 39h

Examination

Four drills, then two situations. These test whether you can tell a signal from a gate, and a floor from a certificate — the two distinctions this part turns on. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored.

Drill 01Recall
Your pipeline already runs the test suite with a dotnet test step. Why still add a PublishTestResults step after it?
Marked

B — the test step runs the tests; the publish step makes their verdict legible. Execution and recording are different jobs. The runner already ran everything and already failed the job on a red test by exiting non-zero — so C is wrong, and dangerously so: believing the publish step is what enforces failure is how someone deletes it to "speed things up" and quietly loses the fail-fast they thought they had. A is backwards; the tests ran at the dotnet test line. D invents auto-retry, which is not what publishing does and would hide exactly the flakiness you want visible. Publishing buys you the Tests tab — which test, what message, how long, and the trend across runs — instead of a wall of console output to scroll.

Drill 02Recall
A teammate reports the module hit 85% code coverage and calls it safe to ship. What is the most accurate correction?
Marked

B — coverage is execution, not assertion. A test can run every line of a function and check nothing about the result; the number climbs and the code is no safer. So 85% means 85% of the lines were touched by some test, not that 85% of the behaviour is verified — C's equation of the two is the exact misread that gets bad code shipped behind a green dashboard. A treats 100% as a mandate, which reliably produces assertion-free tests written to hit the target. D overcorrects into throwing away a genuinely useful signal: a low number reliably flags untested, dangerous regions. Read coverage as a floor that tells you what is definitely untested — never as a ceiling that certifies what is safe.

Drill 03Select three
You are building a quality gate in front of Campux's checkout deploy. Which three actions match the shift-left, fail-fast posture this part teaches?
Marked

Fast tests first, gate made mandatory by branch policy, stage parallelised to stay fast. Those three are the whole posture: catch cheaply and early, make the catch un-skippable, and keep it quick enough to survive a deadline. The two rejects are the section's named failure modes. Putting the gate after the deploy is not a gate at all — production already ran the code, so the check became a postmortem; the entire point of Figure 1 is that the red box sits before the deploy. And continueOnError on the test step is the one line that disarms the fail-fast you got for free — it turns "the tests must pass" into "the tests may fail and we ship anyway," which is a gate in name and a hole in fact.

Drill 04Spot the error
A new engineer sketches the test stage for the checkout pipeline. One line quietly defeats the gate it sits in. Which?
# checkout pipeline — test stage before deploy
1.  Run the unit suite first; it fails the build in
    seconds if the pricing math is wrong.
2.  Run the integration suite next, against a test
    database and a stub payment provider.
3.  Set continueOnError: true on the test step so a
    red build never blocks the team's afternoon.
4.  Publish the results and coverage, and require the
    pipeline to pass via a branch policy on main.
Marked

Line three — and the tell is that it undoes the one thing you got for nothing. A test runner exits non-zero on failure and a failing step fails the job; that default is the gate. continueOnError: true tells the pipeline to treat the step's failure as a pass, so a red test suite still lets the stage proceed to deploy — the checkout ships broken and the run shows green. Every other line is the plan at its best: fast unit tests first is correct shift-left ordering (A inverts it), a stubbed payment provider is exactly how you integration-test a payment path without charging a real card (B is wrong), and a branch policy makes the gate mandatory, not optional (D inverts its purpose). The dangerous line is the one that looks like a small convenience and is actually the off switch.

Situation 01Write before you reveal
Two days before the November freeze, a lead asks you to lower the coverage threshold from 70% to 40% "just for this release" so a late change can merge, and to add continueOnError to the flaky integration test that keeps going red. What do you say?
One request lowers a floor; the other disarms the gate entirely. They are not the same ask, and they do not get the same answer. What does the flaky test actually need?
Reasoning

Separate the two asks, because one is negotiable and one is not. Lowering a coverage threshold is a floor moving, not a gate opening — it means new code may be less tested than your standard, which is a real cost but a bounded one, and on the eve of a freeze it may be a defensible trade for a small, well-understood change. So you can discuss the threshold: agree it drops only for this pull request, only for the specific files, and snaps back the day after. That is a floor bent, in writing, with an expiry.

Refuse the second ask, because it is not the same kind of thing. continueOnError on the integration test does not lower a floor; it removes the gate — the pipeline goes green whether or not that test passes, on this release and every release after, until someone remembers to take it back out, which no one will. Two days before the busiest date of the year, on the payment path, is the exact worst moment to make the checkout's integration test advisory. The flaky test is a real problem, but the fix is to fix the flake — quarantine it into a non-blocking lane you actually triage, or stabilise it — not to blind the pipeline to it.

Give them the sentence for the room. "I'll drop the coverage floor for this one PR with an expiry — that's a trade we can price. I won't make the checkout's tests advisory two days before peak; a flaky test gets quarantined and triaged, not switched to always-pass, because 'ignore this test for now' is how the one bug we can't afford ships green." That distinguishes the engineer who protects the gate under pressure from the one who quietly dismantles it to make a deadline.

Situation 02Write before you reveal
An interviewer says: "Tell me how your pipeline stops a broken change from reaching production — walk me through it." You have two minutes. What do you say?
They want to know if you have a structure or will improvise. Walk the boxes of Figure 1, name where the gate sits, and say the one thing that could disarm it out loud.
Reasoning

Name the principle before the mechanics, so they know you understand the why. "The whole idea is shift-left: I want a defect to fail in the pipeline on the afternoon it's written, not in front of a customer three weeks later, because the later you catch it the more it costs." Then walk the shape: a change triggers a build; the fast unit suite runs first and fails the build in seconds if the logic is wrong; the slower integration tests run next against a test database and stubbed dependencies; results and coverage publish so a failure names the exact broken case.

Then put the gate where it belongs and say what would break it. "The gate sits before the deploy — a failing test fails the job, so nothing that fails reaches an environment — and I make it mandatory with a branch policy on main, so tests must pass before the code can even merge. The one thing that would quietly defeat all of that is a continueOnError on the test step, so I watch for exactly that. And I keep the stage parallelised, because a gate that gets slow gets switched off." Naming the failure mode unprompted is what signals you have actually run one of these, not just read about it.

Close on the honest limit, because it is the most senior thing you can say. "I track coverage but I don't chase it — it tells me what's definitely untested, not what's definitely safe, since it measures execution and not assertion. So the gate proves the tests I have all pass; it doesn't prove I've written the right tests. That's a judgement, not a number, and it's the part of the job the pipeline can't do for me." That answer moves you from "person who set up some tests" to "engineer who owns what ships" — and it survives the follow-up because every clause of it is true.

Examination record · first attempt
0/4
Class Thirty-Nine · Complete
Retain this much

Five things worth carrying out of this part

  1. A defect gets no worse over time — the distance you must reach back to fix it does. Shift-left is dragging detection toward the commit, so the pipeline catches faults on the afternoon they are born instead of in production three weeks later.
  2. Running tests and publishing their results are separate acts. The test step already fails the build on a red test by exiting non-zero; PublishTestResults makes the verdict legible in the Tests tab. Deleting the publish step does not remove the fail-fast — but it removes your ability to see what failed.
  3. Coverage measures execution, not assertion. A low number reliably flags what is untested; a high number certifies almost nothing. Read it as a floor, never a ceiling, and never make the percentage the goal.
  4. A quality gate is a pass/fail checkpoint placed before the deploy. The test suite is a de facto gate for free; a coverage threshold is something you add (Build Quality Checks, SonarQube, or a script), and a branch policy is what makes the gate mandatory rather than optional.
  5. A gate survives only if it stays fast. Order cheap fast tests left, slow broad tests right, and parallelise the stage — because a slow gate gets resented, and a resented gate gets switched off, and a gate that is off protects nothing.
Notes
  1. The claim that a defect costs dramatically more the later it is caught is an old one in software, often cited with confident multipliers — ten times more in test, a hundred times more in production. Treat the specific numbers with suspicion; the original studies are dated and their methodology is argued about. The direction, though, is settled and matches anyone's lived experience: a fault is cheaper to fix the closer you are to having written it, and the whole value of a pipeline gate is buying the cheap catch instead of the expensive one.
  2. This is not a reason to skip coverage — it is a reason to read it correctly. Mutation testing exists precisely to measure what coverage cannot: it changes your code deliberately and checks whether a test notices, which is closer to "are these tests any good." It is worth knowing the concept exists even if your pipeline does not run it, because it names the exact gap between a line being executed and a behaviour being verified.
  3. Azure DevOps has no native "fail the build if coverage falls below N" switch on the publish task, which surprises people who expect a checkbox. The three routes named — the Build Quality Checks marketplace extension, a SonarQube or SonarCloud quality gate, or a script step that reads the coverage file and exits non-zero — are the common ways teams enforce a threshold. Check the current marketplace and docs before wiring one, as extension names and task versions change; the principle that you must add the enforcement yourself does not.