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

The pipeline, written down

A pipeline stops being a thing someone configured in a portal and becomes a file in your repository — reviewed, branched, and diffed like the code it ships.

§1

The pipeline is a file in the repository

The first part taught what a pipeline is: trigger, build, test, deploy, one shape on any platform. This part teaches where it lives. In Azure DevOps a pipeline is not a thing you click together in a portal and hope to remember — it is a file, azure-pipelines.yml, committed to the root of the repository it builds, sitting beside the code it ships. That single decision changes everything downstream. A file goes through pull request review, so a change to how you deploy gets the same second pair of eyes as a change to what you deploy. A file lives on a branch, so you can try a risky pipeline change on a feature branch and merge it only when it works. And a file has a history: git blame tells you who changed the retry count and when, and git diff shows you exactly what moved between the release that worked and the one that did not.

This is the same argument Class Twenty made about infrastructure — a resource you provisioned by hand is a resource nobody can reproduce — turned on the delivery machine itself. A pipeline clicked together in a portal is knowledge that lives in one person's memory and one afternoon's clicking; a pipeline in the repository is knowledge the whole team can read, review, and roll back. You already met this shape from the other side: GitHub Actions, in Classes Twenty-Two through Twenty-Four, put the workflow in .github/workflows as YAML for exactly these reasons. Azure DevOps makes the same bet with different nouns, and this part is largely you learning the nouns — because the instinct, pipeline-as-code, you already have.

A pipeline you can't diff is folklore.

§2

Stages hold jobs, jobs hold steps

Open an azure-pipelines.yml and you are reading a three-level nesting, top to bottom. A stage is a major phase of delivery — Build, Test, Deploy-to-Staging, Deploy-to-Production — and stages run one after another, each a natural place to put a gate. Inside a stage are jobs, units of work that each run on one agent; jobs in a stage run in parallel unless you make one depend on another. Inside a job are steps, which run in order on that agent and are the only level where actual work happens. A step is one of two things and no third: a script — a shell command you write inline — or a task, a pre-packaged unit like AzureCLI@2 that someone else wrote and versioned so you do not have to. Learn to say it as a sentence and you can read any file: stages hold jobs, jobs hold steps, and a step is a script or a task.

Pipeline (YAML)
A pipeline defined declaratively in a YAML file — conventionally azure-pipelines.yml at the repository root — that Azure DevOps reads on each trigger to build a run. Because it is a file under version control, the pipeline is reviewed, branched, diffed, and rolled back exactly like application code, rather than configured by hand in a portal and remembered.

If this scaffolding feels familiar, it should: it is the GitHub Actions vocabulary from Class Twenty-Two with the labels swapped. You are not learning a new idea, you are re-nouning one you already hold. The table below is the whole translation on one page — the thing to pin above your desk the first week on a team that ships on Azure DevOps.

Table 1 — GitHub Actions, re-nouned for Azure Pipelines
GitHub ActionsAzure PipelinesWhat it is
WorkflowPipelineThe whole file that runs on a trigger — .github/workflows/x.yml becomes azure-pipelines.yml
JobJobA set of steps that run together on one machine; the one noun that does not change
Step / ActionStep / TaskA single unit of work; a reusable packaged step is an action there, a task here (e.g. AzureCLI@2)
RunnerAgent (in a pool)The machine that actually executes the job — the subject of the next part
Reusable workflowTemplateA YAML fragment factored out and referenced so many pipelines share one definition
EnvironmentsEnvironmentsA named deploy target that records history and can carry approvals; same word, same idea

Two mappings deserve a flag. Azure Pipelines has a level GitHub Actions does not foreground — the stage — which is where multi-phase delivery and the gates between phases live; you will lean on it the moment a deploy needs a staging step before production. And the @2 suffix on a task is its major version, pinned deliberately so a task author's breaking change cannot silently rewrite your deploy overnight — the same discipline you already apply to a package lockfile.

§3

What starts it, and the step you never write

A pipeline file begins by saying when it should run, and Azure DevOps gives you two triggers that answer two different questions. The CI trigger, written trigger:, fires when commits land on branches you name — this is the "someone merged to main, ship it" path. The PR trigger, written pr:, fires when a pull request targeting those branches is opened or updated, so the pipeline runs against the proposed change before it merges and reports back on the PR. The distinction is the difference between validating a change and acting on one: pr: tells you whether the code is safe to merge; trigger: does something because it now has.1

Then there is the step you do not write. In an ordinary job, the first thing a pipeline needs is your source code, and Azure Pipelines checks it out implicitly — a checkout: self is injected as the first step of every regular job, so the repo is already there when your script runs. You only write checkout explicitly when you want to change its behaviour or turn it off. There is one sharp exception worth carrying, because it catches people in exactly the place this part is heading: a deployment job does not clone the repo automatically. If your deploy steps need files from source, you must add checkout: self yourself.2 A deploy that only calls an already-built artifact or the Azure CLI usually does not — and the pipeline in the next section is built so it does not have to.

trigger:
The CI trigger. Runs the pipeline when commits are pushed to the listed branches. Omit it and the default is to build on every branch; set it explicitly so main-line ships are deliberate, not accidental.
pr:
The PR trigger. Runs the pipeline against a pull request before it merges, and reports status back on the PR. This is your automated gate on the change, distinct from acting on the merge.
implicit checkout
In a normal job, checkout: self runs first without being written. In a deployment job it does not — add it by hand if the deploy needs source.
§4

A first working pipeline, read end to end

Here is a real, correct azure-pipelines.yml — small, but every line of it does a job on a live system. It triggers on main, builds a Node app in one stage, and deploys the result in a second stage through a gated environment, authenticating to Azure by a service connection named on the page with no secret anywhere in the file. Read it against the sentence from §2: stages hold jobs, jobs hold steps, a step is a script or a task.

# azure-pipelines.yml — committed at the repository root
trigger:
  branches:
    include: [ main ]      # CI: ship when main moves

pr:
  branches:
    include: [ main ]      # validate PRs targeting main

stages:
  - stage: Build
    jobs:
      - job: build
        pool:
          vmImage: ubuntu-latest
        steps:
          # checkout: self is implicit here — source is already present
          - script: |
              npm ci
              npm run build
            displayName: Install and build
          - publish: dist            # hand the build output forward
            artifact: warehouse

  - stage: Deploy
    dependsOn: Build
    jobs:
      - deployment: deployWarehouse
        pool:
          vmImage: ubuntu-latest
        environment: warehouse-production   # the gate lives here
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  displayName: Deploy build to the warehouse app
                  inputs:
                    azureSubscription: azure-prod   # a service connection, by name
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az webapp deploy \
                        --name basecamp-warehouse \
                        --resource-group basecamp-prod \
                        --src-path $(Pipeline.Workspace)/warehouse

Five things earn their place. The two stages run in sequence because Deploy declares dependsOn: Build, so nothing deploys unless the build succeeded. Each stage runs on a fresh agent, so the build output does not simply carry over — the build job publishes it as an artifact (publish: dist, named warehouse), and the deployment job's deploy hook downloads it automatically into $(Pipeline.Workspace)/warehouse, which is exactly the path the deploy command reads. Build once, ship that same built thing — never rebuild in the deploy stage. The deploy is a deployment job, not an ordinary one — that is what binds it to the environment named warehouse-production, which records a deployment history and is the object an approval will later hang on (that is 39f; here it is just the gated shape). The work is done by the AzureCLI@2 task, and its azureSubscription input names a service connection — a credential stored and scoped in Azure DevOps, referenced only by name. There is no password in this file, which is the entire point of the last paragraph of §1: because the file is diffable and reviewable, the thing you least want to leak must never be in it. The credential behind that name is exactly the case file's problem, and the last part of the track fixes it properly.

Case File · Campux Retail

The Friday-night file-copy becomes a committed pipeline

Basecamp's warehouse deploy gets its first azure-pipelines.yml

Basecamp Outfitters — nine stores, one warehouse system the whole business now leans on — arrived in the acquisition with thirty-one pipelines and one deploy that was not a pipeline at all: every Friday night an engineer built the warehouse app on their laptop and copied the output to the production server by hand. It is the exact release-is-a-person pattern the first part opened with, and it is where the reader starts, because it is both the most dangerous and the most winnable. The fix is the file above, near enough: a Build stage that runs the same npm build the engineer used to run by hand, and a Deploy stage whose deployment job targets a warehouse-production environment — the committed, reviewable shape replacing the Friday ritual nobody could diff.

The reader does not solve everything here, and knowing what to leave is part of the craft. The deploy still authenticates through azure-prod — the single subscription-scoped service connection, stored secret and all, that expires in five weeks and every pipeline in the org leans on. That is a real liability, but it is a credential problem, and the reader files it for 39i rather than fixing it half-way now; the approval that will make warehouse-production a true gate is 39f's job. What this part delivers is the thing that has to exist before either can: a warehouse deploy that is a file, in the repo, that the Basecamp engineers can read on the first morning and change on a branch — no longer folklore living in one person's Friday night.

§5

You can now read a real pipeline file

That is the whole ambition of this part, and it is not small. Hand the file in §4 to someone who has only ever clicked a deploy together and it is a wall of punctuation; hand it to you now and you can narrate it — this triggers on main, this stage builds, this deployment job deploys through a gated environment, this task authenticates by a named connection with no secret in the file. Every real Azure DevOps pipeline you meet is an elaboration of that same skeleton: more stages, more tasks, variables and templates factored out, but the same stages-jobs-steps spine holding it up. The rest of the track thickens the skeleton; it does not replace it.

And the skill is portable in the direction that pays. You did not learn "Azure DevOps YAML" as a closed dialect — you learned to map a delivery machine onto a file, which is the same move GitHub Actions asked of you with different nouns. An engineer who can walk into a repo, open the pipeline file, and read it aloud on the first morning is worth conspicuously more than one who needs the portal and the person who built it. That reading fluency is what this part hands you; the next one settles where all this actually runs.

On the job

The pull request that changed the deploy

You · Cloud Engineer · reviewing a pipeline change

A teammate opens a pull request that edits azure-pipelines.yml, and because the pipeline is a file, the change lands in your review queue like any other. The diff is three lines: a new task added to the deploy stage. You can see exactly what moved, ask why in a comment, and approve or block it before it ever touches production — the same gate you apply to application code, now applied to how the application ships. That is the quiet payoff of pipeline-as-code: the most dangerous change in the building, a change to the deploy itself, becomes an ordinary, reviewable, revertible diff instead of an untraceable afternoon in a portal.

Class 39b

Examination

Four drills, then two situations. Write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored.

Drill 01Recall
In an Azure Pipelines YAML file, what is the nesting from top to bottom, and what are the only two things a step can be?
Marked

B — stages hold jobs, jobs hold steps, and a step is a script or a task. That one sentence lets you read any Azure DevOps pipeline: stages are the sequential phases where gates live, jobs are the units that each run on one agent, and steps are the only level where work actually happens — either an inline script you write or a packaged task like AzureCLI@2. A inverts the two outer levels, which matters because it is stages, not jobs, that carry the deploy-after-build ordering. C swaps in GitHub and generic nouns and mis-nests them. D throws away the very structure that makes the file legible — get the hierarchy wrong and every pipeline you open is a wall of punctuation instead of a sentence you can narrate.

Drill 02Recall
Your job's first step runs npm ci and expects the source code to be present, but you never wrote a checkout step. In an ordinary Azure Pipelines job, what happens?
Marked

A — the checkout is implicit in a regular job. Azure Pipelines injects checkout: self as the first step of an ordinary job, so your source is already on the agent when npm ci runs; you only write checkout when you want to change or disable that behaviour. B is the trap that comes from over-generalising the one real exception: a deployment job does not clone the repo automatically, and there you must add checkout: self by hand if the deploy needs source. Knowing which kind of job you are in is the whole point — assume the deployment-job rule everywhere and you write noise; assume the regular-job rule everywhere and your deploy job silently has no files. C and D invent behaviours that do not exist; the trigger type has nothing to do with whether source is fetched.

Drill 03Select three
You are translating your GitHub Actions fluency to Azure Pipelines and reasoning about the azure-pipelines.yml file. Which three statements are true?
Marked

Workflow→pipeline and runner→agent, the file is reviewed like code, and @2 pins the task version. Those three are the load-bearing facts of this part: the noun map lets you read an Azure DevOps file on the strength of your GitHub Actions habits; version-controlled means the deploy gets the same review and rollback as the code; and the pinned major version is the same lockfile discipline you already trust. The two rejects are the ones that cost you. A secret in the file is the exact mistake §1 and §4 exist to prevent — the file is diffable and reviewable, so anything in it is effectively visible to everyone with repo access and permanent in git history; that is why the pipeline references a service connection by name and holds no secret. And the reusable workflow does have an equivalent — the template — which is precisely how thirty-one near-identical pipelines get factored down to one definition in 39d.

Drill 04Spot the error
This warehouse-deploy pipeline is about to be committed. One line is genuinely dangerous the moment it lands in the repository. Which?
# azure-pipelines.yml — warehouse deploy
1.  trigger:
      branches:
        include: [ main ]
2.  stages:
      - stage: Deploy
        jobs:
          - deployment: deployWarehouse
            environment: warehouse-production
3.            pool:
                vmImage: ubuntu-latest
4.            strategy:
                runOnce:
                  deploy:
                    steps:
                      - task: AzureCLI@2
                        inputs:
                          azureSubscription: azure-prod
                          scriptType: bash
                          scriptLocation: inlineScript
                          inlineScript: |
                            export CLIENT_SECRET="p@ss-prod-9f3a2b"
                            az webapp deploy --name basecamp-warehouse ...
Marked

D — the secret is in the file. The whole reason to reference Azure by a service connection named azure-prod is so the credential lives in Azure DevOps, scoped and revocable, and never touches the repo. The moment export CLIENT_SECRET="…" is committed, the production secret is visible to everyone with repo read access and — worse — is permanent in git history, so removing the line later does not remove the secret; you must rotate it. This is exactly the mistake §1 warns about: because the file is diffable and reviewable, anything in it is effectively published. The distractors are all correct-and-safe: triggering a deploy pipeline on main is normal (A); a deployment job is precisely the job type that binds to an environment (B); and pool: vmImage: ubuntu-latest is the standard way to select a Microsoft-hosted agent (C). The reflex to build: scan every committed pipeline for a literal secret before anything else.

Situation 01Write before you reveal
A Basecamp engineer, watching you replace the Friday-night file-copy with an azure-pipelines.yml, asks: "We already deploy fine by hand every week. What does putting it in a file actually buy us — isn't it just extra ceremony?" How do you answer?
Concede that the manual deploy works today. Then name what a file gives you that a person's Friday routine cannot — and price it in the moment it fails.
Reasoning

Concede the manual deploy works, because it does. Do not open by telling a decade-tenured engineer their Friday routine is wrong; it has shipped reliably for years. Open by agreeing — it works, on the day, when they do it. The case for the file is not that the manual deploy fails often; it is what happens on the rare day it does, and who is left holding it. That framing keeps the engineer on side instead of defensive, which is the whole game when you are the newcomer touching someone else's system.

Name the three things a file gives you that a person cannot. First, a record: git log tells you exactly what the deploy did and when it changed, so an outage is a diff to read, not a memory to interrogate. Second, review: because it is a file, a change to how you deploy goes through a pull request — a second pair of eyes on the most dangerous change in the building. Third, repeatability that outlives the person: the file deploys identically whether it is run by them, by you, or by someone hired next year, so the knowledge stops living in one person's Friday night and one person's head. The manual deploy is a single point of failure wearing a routine.

Close on the engineer's own interest, not the company's. The honest sell is personal: once the deploy is a file, they are no longer the person who has to be there every Friday, or the person paged when it breaks because only they know the steps. The file is not ceremony taking their expertise away — it is their expertise written down so it can be shared, reviewed, and not owned solely by them at 9pm on a Friday. That is the sentence that lands: we're not replacing your judgement; we're making it something the team can read.

Situation 02Write before you reveal
An interviewer slides a printed azure-pipelines.yml across the table: two stages, a build job, a deployment job with an environment and an AzureCLI@2 task. "Read this to me — what does it do, and is there anything you'd flag?" What do you say?
Narrate the file top to bottom using the spine from §2. Then check the two things this part told you to check: where the credential lives, and whether a deployment job that needs source has a checkout.
Reasoning

Narrate the spine first, so they hear that you read structure, not keywords. "It triggers on a branch; the first stage has a build job whose steps install and build; the second stage depends on the first and has a deployment job bound to an environment, so it deploys through a gated, history-tracked target. The work is an AzureCLI@2 task authenticating by a service connection named in the file." That is stages-hold-jobs-hold-steps said out loud, and it tells the interviewer in fifteen seconds that you can walk into their repo and read it on the first morning.

Then run the two checks this part drilled into you. One: where does the credential live? If the azureSubscription names a service connection and no secret appears in any script or inlineScript, that is right; if there is a literal password or token anywhere in the file, flag it hard — it is committed and permanent in history. Two: does the deployment job need source, and if so does it have a checkout: self? Because a deployment job does not clone the repo automatically, a deploy that reads files but omits the checkout is a real bug hiding in valid-looking YAML.

Close by naming what you would not change. Seniority is also restraint: say what is already correct — the stage ordering via dependsOn, the pinned @2 task version, the environment as the place a future approval will attach — so you are not just hunting for faults. That mix, fluent reading plus two sharp checks plus the discipline to leave good code alone, is exactly the judgement the question is testing. It is the same reading you now bring to any pipeline, on either platform, which is the portable skill this part was built to hand you.

Examination record · first attempt
0/4
Class 39b · Complete
Retain this much

Five things worth carrying out of this part

  1. The pipeline is a file — azure-pipelines.yml at the repo root — so it is reviewed in a pull request, branched, diffed, and rolled back exactly like the code it ships. A pipeline you can't diff is folklore.
  2. The spine is three levels: stages hold jobs, jobs hold steps, and a step is a script (a command you write) or a task (a packaged unit like AzureCLI@2). Say the sentence and you can read any file.
  3. Two triggers, two questions: trigger: acts when commits land on named branches; pr: validates a pull request before it merges. Checkout is implicit in a regular job — but a deployment job does not auto-clone, so add checkout: self if it needs source.
  4. A deploy authenticates by naming a service connection, never by holding a secret in the file. Anything committed is diffable, visible to repo readers, and permanent in git history — so the credential lives in Azure DevOps, referenced only by name.
  5. The nouns map cleanly from GitHub Actions: workflow→pipeline, runner→agent, action→task, reusable workflow→template. You are re-nouning a skill you already hold, which is why you can read an Azure DevOps file on the first morning.
Notes
  1. The trigger: and pr: keys have a shorthand list form and a longer form with branches, paths, and tags filters, and the defaults have shifted over time — notably, pr: triggers behave differently for Azure Repos versus GitHub-hosted repositories, and PR triggers configured in YAML can be overridden by branch-policy build validation. Confirm the current trigger defaults and the repo-host differences on Microsoft Learn before you rely on a pipeline firing — or not firing — on a given event.
  2. The implicit-checkout rule is the one people most often trip on: an ordinary job gets checkout: self injected as its first step, but a deployment job does not clone the repo automatically. This is current Microsoft Learn guidance at time of writing; treat "regular job checks out, deployment job does not" as the load-bearing distinction and verify against the deployment-jobs documentation if a deploy step mysteriously cannot find your source.