Skip to content
CAMPUX Cloud Bootcamp Phase Four · Class Thirty-Nine
Phase Four — Operate, Secure & AI
4 drills · 2 situations · 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.

Phase Four · Operate, Secure, AI · Class 39 · Part B · 15 min · Assumes Class 39 Part A

After this you can write a GitHub Actions workflow with dependent jobs and deliberate branch triggers.

New to this? Read Class 39 Part A first. Already done it, or know the material? Carry on.

§1

The pipeline is a file in the repository

A GitHub Actions workflow is a file under .github/workflows/, committed beside the code it checks and ships. Workflow changes go through pull request review like application changes, live on branches for safe testing, and have a history: git blame shows who changed a step; git diff shows what moved between the release that worked and the one that did not.

This applies the infrastructure-as-code idea from Class Twenty to the delivery process: a workflow in the repository is visible, reviewable, and reversible. Classes Twenty-Two through Twenty-Four introduced GitHub Actions. Here we connect those pieces into a full, production-minded path to Azure.

A pipeline you can't diff is folklore.

Play

Play it through

Move the pipeline into the repository beside the code it builds, then see what a change to it looks like when it can be reviewed like anything else. It plays on its own and stops when it needs your hands.

Run one for real. The lab for this class is on its own page: Build a first workflow. It introduces workflow YAML, runs a check on a pull request, and shows how to diagnose a missing checkout step. Review the repository plan and runner availability before starting.

§2

Workflows contain jobs, jobs contain steps

Open a workflow in .github/workflows/. Its events decide when it runs. A job groups work on one runner; jobs can run in parallel or wait for another job with needs:. Each job has ordered steps: either a shell command under run: or a packaged action under uses:. Use this sentence to read a file: events start workflows, workflows contain jobs, and jobs run steps on runners.

Workflow (YAML)
A GitHub Actions automation described in a YAML file under .github/workflows/. GitHub reads its events and jobs to create each run. Since the file is version-controlled, the team can review, compare, and roll back changes alongside application code.

Classes Twenty-Two through Twenty-Four introduced these building blocks. This track connects them into a release you can explain and operate. The table names each part and its job.

Table 1 — The main parts of a GitHub Actions workflow
PartExampleWhat it does
Eventpush, pull_requestStarts a workflow when a matching change happens
Workflow.github/workflows/ci.ymlDescribes events and jobs in a YAML file
Jobbuild, deployGroups steps and runs on one runner; needs: sets job order
Runnerubuntu-latestMachine that executes a job
Step / actionrun: / uses:Command you write or packaged action you call
Artifactupload-artifactFile transferred from one job to another
Environmentwarehouse-productionDeployment target whose configured rules can pause a job

GitHub Actions has no required stage layer. Use jobs to represent phases, needs: to set their order, and a configured environment to protect a deployment. An action reference such as actions/checkout@v6 selects a major release line; teams that need tighter supply-chain control can pin a reviewed full commit SHA.

§3

What starts the workflow, and how source arrives

A workflow can listen for more than one event. push runs after matching commits are pushed; pull_request checks proposed changes before merge. Add branch filters so a workflow runs where you intend. Here, both events target main, but only the deploy job has the additional condition that limits it to a push to main.

GitHub Actions does not fetch your source code automatically. The job needs an explicit actions/checkout step before a command such as npm ci can read the repository. Each job may receive a separate runner; if a later job needs built files, upload and download an artifact rather than expecting files to appear on both machines.

push
Start a run after a commit is pushed to a branch selected by the event filters.
pull_request
Start checks for proposed changes. Keep deployment credentials unavailable to untrusted fork code.
checkout
An explicit action step that places repository files on the runner for later steps to use.
§4

A first working workflow, read end to end

This complete workflow checks pull requests and pushes, then deploys only a successful push to main. Read it top to bottom: events start the run, the build job tests and packages the site, and the deploy job waits for that build before logging into Azure through OIDC.

# .github/workflows/warehouse.yml — working example
name: Build and deploy warehouse

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test
      - run: npm run build
      - name: Package the built site
        run: |
          cd dist
          zip -r ../warehouse.zip .
      - uses: actions/upload-artifact@v7
        with:
          name: warehouse
          path: warehouse.zip
          if-no-files-found: error

  deploy:
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: warehouse-production
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/download-artifact@v7
        with:
          name: warehouse
          path: warehouse
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - uses: Azure/webapps-deploy@v3
        with:
          app-name: basecamp-warehouse
          package: warehouse/warehouse.zip

This example builds and tests on both pull requests and pushes to main. Only a push to main can start the deploy job, which uses the protected environment and OIDC identity from Class Twenty-Three. Configure the Azure federated credential and a least-privilege role assignment, then add the three Azure IDs as repository secrets. Create warehouse-production and its protection rules in repository settings; writing its name in YAML alone does not create an approval gate.

Build and deploy use separate jobs and may run on different temporary machines. The build job packages its output as a ZIP and uploads it. The deploy job downloads that same ZIP instead of rebuilding, so the tested build is the one that reaches Azure.

The same workflow appears below with beginner comments. Lines beginning with # are notes for people reading the file; YAML ignores them.

# .github/workflows/warehouse.yml — the same workflow, explained
name: Build and deploy warehouse  # Name shown on the Actions page

on:  # Events that may start this workflow
  push:  # Run after a commit is pushed
    branches: [main]  # Only pushes to main match
  pull_request:  # Also test proposed changes before merge
    branches: [main]  # Only pull requests targeting main match

permissions:  # Default token access for jobs in this workflow
  contents: read  # Allow reading this repository's source

jobs:  # Jobs are the larger pieces of work
  build:  # This job installs, tests, builds, and packages the app
    runs-on: ubuntu-latest  # Give the job a temporary Ubuntu runner
    steps:  # Steps run one after another on this runner
      - uses: actions/checkout@v6  # Put repository files on the runner
      - uses: actions/setup-node@v4  # Install Node.js for the npm commands
        with:  # Settings passed to setup-node
          node-version: 22  # Use Node.js version 22
      - run: npm ci  # Install exact versions from the lockfile
      - run: npm test  # Stop if a test fails
      - run: npm run build  # Create production files in dist
      - name: Package the built site  # Label the next shell command
        run: |  # The bar starts a multi-line shell script
          cd dist  # Enter the folder made by the build
          zip -r ../warehouse.zip .  # Put its files in one ZIP

      - uses: actions/upload-artifact@v7  # Save a file for another job
        with:
          name: warehouse  # Call the saved build warehouse
          path: warehouse.zip  # Save the ZIP made above
          if-no-files-found: error  # Fail clearly if the ZIP is missing

  deploy:  # This separate job sends the built app to Azure
    needs: build  # Wait until the build job succeeds
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'  # Never deploy a pull request
    runs-on: ubuntu-latest  # This job also gets its own temporary runner
    environment: warehouse-production  # Use rules configured for this environment
    permissions:  # Give only the deploy job its extra permission
      contents: read  # Permit source access if the job needs it
      id-token: write  # Allow a short-lived GitHub identity token for Azure OIDC
    steps:
      - uses: actions/download-artifact@v7  # Fetch the build made above
        with:
          name: warehouse  # Choose the artifact with this name
          path: warehouse  # Put its ZIP in this folder
      - uses: azure/login@v2  # Exchange GitHub identity for temporary Azure access
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}  # Entra application ID
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}  # Entra tenant ID
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}  # Azure subscription ID
      - uses: Azure/webapps-deploy@v3  # Deploy the downloaded package to App Service
        with:
          app-name: basecamp-warehouse  # Existing Azure Web App name
          package: warehouse/warehouse.zip  # Deploy the exact ZIP from the build job

There are four important connections here. First, needs: build prevents deployment if the build job fails. Second, the uploaded ZIP passes the exact tested files to the next job. Third, the deploy job requests an OIDC token only because it alone has id-token: write. Fourth, the named GitHub environment applies whatever protection rules administrators configured. The name alone does not create an approval, and a workflow file must never contain a client secret.

Case File · Campux Retail

The Friday-night file-copy becomes a committed pipeline

Basecamp's warehouse deploy gets its first .github/workflows/warehouse.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 deploy 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 OIDC identity, 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.

On your own · one-line change

Predict the run before you push

In the workflow from the lab, add one step at the end of the job: - run: test -f README.md. Before you push, predict whether the run goes green or red, and why. Then rename README.md on a branch, push, and predict again.

Check your prediction

With README.md present, test -f exits 0 and the step passes. After the rename it exits 1, the step fails, and the job turns red. Any command's exit code is a check; that is how every test step in Part H works.

§5

You can now read a real pipeline file

Read a workflow from the outside in: events start a run, jobs divide the work, and steps perform it on runners. Use needs: to order jobs, artifacts to pass built files, and explicit environment rules to protect deployment.

This part builds directly on the GitHub Actions foundations in Classes Twenty-Two through Twenty-Four. The next lesson looks at the runners that execute each job.

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 .github/workflows/warehouse.yml. The diff adds a new action step to the deploy job. 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 01Read the structure
Which statement correctly describes a GitHub Actions workflow?
Marked

B. An event such as push starts the workflow. A job groups steps and runs on a runner. Other jobs may use separate runners, so files move between them only when you deliberately pass them, for example with an artifact.

Drill 02Find the missing step
A job runs npm ci, but its steps do not include actions/checkout. What is the likely problem?
Marked

B. GitHub-hosted runners are temporary machines. Add an explicit checkout action before commands that read repository files. Each job has its own steps; source appearing in one job does not place it on another job's runner.

Drill 03Select three
Which three statements about the example workflow are correct?
Marked

Correct: one, three, and five. Job ordering prevents deployment after a failed build. Uploading and downloading an artifact makes the tested package available to the deploy job. The narrowly scoped OIDC permission belongs only on the job that signs in to Azure. The workflow's event condition keeps deployments off pull requests. An environment name uses its protection rules only after repository settings configure them.

Drill 04Spot the error
A teammate adds this line to the workflow. What must you do immediately?
env:
  AZURE_CLIENT_SECRET: "p@ss-prod-9f3a2b"
Marked

C. A committed credential must be treated as exposed even after deleting the line. Revoke or rotate it, then store required secrets in the appropriate secret manager. The example workflow uses OIDC for Azure sign-in, so it does not need a client secret at all.

Situation 01Write before you reveal
A Basecamp engineer, watching you replace the Friday-night file-copy with an .github/workflows/warehouse.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 asks you to explain the workflow from the event through the Azure deployment. Where do you start, and what are the two most important release safeguards?
Walk through the build and artifact first. Then explain what prevents an unsafe or unapproved deployment.
Reasoning

Start with the event. A push or pull request targeting main starts the workflow. The build job checks out the source, installs dependencies, runs tests, builds the site, packages it, and uploads the artifact. The deploy job waits for a successful build, downloads that exact package, and signs in to Azure through OIDC.

Then name the safeguards. The deploy condition excludes pull requests, and the GitHub environment can pause the job only if protection rules were configured in repository settings. OIDC avoids a stored Azure client secret; Azure RBAC still limits what the identity may change. Do not claim the environment name alone creates approval.

Follow one package to its destination. A teammate reviewing the pull request should see the build run without a production deployment. After the approved change reaches main, the deploy job should consume the package produced by that run, rather than rebuilding a potentially different version. If the build fails, deploy must remain skipped. If production approval is required, verify the configured environment actually pauses the job before treating the workflow as guarded.

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 — .github/workflows/warehouse.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. Events start workflows, workflows contain jobs, and jobs run ordered steps on runners. Use needs: when one job must wait for another.
  3. push runs after matching commits are pushed; pull_request checks proposed changes. GitHub Actions requires an explicit checkout step when a job needs repository files.
  4. Azure sign-in uses GitHub OIDC and a federated credential. Grant id-token: write only to the deploy job; use Azure RBAC to limit that identity.
  5. Separate jobs do not share files automatically. Upload the build as an artifact, then download it in the deploy job so the tested package is the one you release.
Notes
  1. GitHub documents event filters for push and pull_request workflows. Pull requests from forks receive restricted token permissions by default; keep privileged deployment credentials out of untrusted pull_request code. ↩