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 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.
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.
| Part | Example | What it does |
|---|---|---|
| Event | push, pull_request | Starts a workflow when a matching change happens |
| Workflow | .github/workflows/ci.yml | Describes events and jobs in a YAML file |
| Job | build, deploy | Groups steps and runs on one runner; needs: sets job order |
| Runner | ubuntu-latest | Machine that executes a job |
| Step / action | run: / uses: | Command you write or packaged action you call |
| Artifact | upload-artifact | File transferred from one job to another |
| Environment | warehouse-production | Deployment 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.
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.
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.
The Friday-night file-copy becomes a committed pipeline
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.
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.
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.
The pull request that changed the deploy
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.
Examination
Four drills, then two situations. Write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored.
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.
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.
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.
env: AZURE_CLIENT_SECRET: "p@ss-prod-9f3a2b"
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.
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.
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.
Five things worth carrying out of this part
- 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.
- Events start workflows, workflows contain jobs, and jobs run ordered steps on runners. Use needs: when one job must wait for another.
- 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.
- 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.
- 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.
- 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. ↩
Write a GitHub Actions workflow with dependent jobs and deliberate branch triggers.
Play the lesson above to stamp this class.