CAMPUX Cloud Bootcamp Phase Three · Class Twenty-Two
Phase Three — DevOps Core
Reading 40 min · Drills 6 · 2 Labs
Aligned to AZ-400
Class Twenty-Two

GitHub Actions fundamentals

Everything this phase has built — the branch, the pull request, the Bicep, the Terraform plan — still depends on a human remembering to run the right command; this class hires the machine that never forgets.

§1

The anatomy of a workflow

Five classes ago your infrastructure became text in a repository. That bought review, history, and repeatability — but somebody still has to run the linter, run the what-if, and run the deploy, and humans forget, skip steps at 5pm on a Friday, and go on holiday. GitHub Actions is GitHub's built-in automation: you describe, in a YAML file that lives in the repository at .github/workflows/, what should run and when — and from then on it runs every time, identically, whether it is the first pull request of the morning or the fortieth.

Workflow
An automated process defined in a YAML file in .github/workflows/, started by an event, made of one or more jobs, each a sequence of steps run on a fresh machine called a runner.

One small, real workflow contains every concept in this class. This is Campux's first — it validates the Bicep from Class Twenty on every pull request:

# .github/workflows/validate.yml
name: Validate infrastructure

on:
  pull_request:
    branches: [ main ]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Bicep (fails on errors)
        run: az bicep build --file infra/main.bicep
on — the trigger
The event that starts the workflow: a push, a pull_request, a schedule (cron), or workflow_dispatch — a button a human presses.
job
A named unit of work. Multiple jobs run in parallel by default, each on its own machine; needs: chains them in order.
runner
The machine a job runs on. ubuntu-latest is a fresh virtual machine GitHub provides, used once and destroyed.1
step
One command (run:) or one reusable action (uses:). Steps in a job run in order and share the runner's filesystem.
action
A packaged, reusable step from the marketplace — actions/checkout@v4 clones your repository so later steps can see the code. You will use ten of these before you ever write one.

Read the flow once, top to bottom: a pull request against main fires the trigger; GitHub boots a fresh Ubuntu machine; the machine checks out the branch and compiles the Bicep; and the result — green tick or red cross — lands on the pull request itself. Wire that into the Class Eighteen branch protection rule that requires status checks, and broken infrastructure code physically cannot merge. That is the whole trick, and every pipeline you will ever build is this pattern with more steps.

§2

Contexts and expressions

Workflows need to know things about the run they are in — who opened the pull request, which branch, what secret to use. That information arrives through contexts, read with the ${{ … }} expression syntax. You need surprisingly few of them:

# the contexts you will actually use
steps:
  - name: Show the facts of the run
    run: |
      echo "Actor:  ${{ github.actor }}"
      echo "Branch: ${{ github.ref_name }}"
      echo "Event:  ${{ github.event_name }}"

  - name: Deploy — but only from main
    if: github.ref_name == 'main'
    run: ./deploy.sh
  # secrets.NAME and vars.NAME — covered next section

Three ideas cover most working uses. The github context holds the facts of the event — actor, branch, event name — so a single workflow can behave differently per situation. The if: key gates a step or a whole job on an expression, which is how "run the deploy only on main" is written. And secrets and vars are contexts too — pipes through which configuration reaches the run without living in the file. When a workflow misbehaves, the first debugging move is almost always printing the github context values: half of all "why did this not run?" mysteries are an event name or branch ref that is not what you assumed.

§3

Secrets, variables, environments

The moment a workflow does anything real — deploys, publishes, talks to Azure — it needs configuration, and some of that configuration is sensitive. GitHub gives you three containers for it, and choosing the wrong one is a security decision made by accident.

Table 1 — Three containers, three jobs
ContainerWhat goes in itBehaviour
SecretsCredentials, keys, tokens — anything that grants accessEncrypted at rest, never shown again after saving, masked as *** if they appear in logs. Read via secrets.NAME.
VariablesPlain configuration — region names, resource group names, flagsVisible to anyone who can read settings; fine, because nothing in them grants access. Read via vars.NAME.
EnvironmentsA named target — nonprod, production — with its own secrets, variables, and protection rulesA job that declares environment: production gets production's values — and must first pass its rules, including required human approval.

Environments are the one of the three that beginners skip and professionals lean on. The same workflow file can deploy to nonprod on every merge and to production only after a named person clicks approve — not because the YAML branches cleverly, but because each environment carries its own credentials and its own gate. The production secret does not exist for a job that has not passed production's protection rules. That is the same design instinct as Class Eight's RBAC: do not trust the process to behave; scope what it can reach.

One warning that will save you an incident: secret masking is a courtesy, not a guarantee. GitHub scrubs exact matches of secret values from logs, but a secret that gets transformed — encoded, split, interpolated into a URL — walks straight past the filter.2 The rule is not "masking will catch it"; the rule is that no step should ever print a secret, and a reviewer should treat an echo anywhere near secrets. as a finding.

§4

Matrix builds and reuse

Workflows accumulate duplication fast: the same steps against three environments, the same validation in four repositories. Actions has two answers. A matrix runs one job definition many times with different inputs, in parallel:

# one job definition, three parallel runs
jobs:
  validate:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [ dev, test, prod ]
    steps:
      - uses: actions/checkout@v4
      - name: What-if against ${{ matrix.environment }}
        run: |
          az deployment group what-if \
            --resource-group rg-campux-${{ matrix.environment }} \
            --template-file infra/main.bicep

And for duplication across repositories, a reusable workflow — a workflow whose trigger is workflow_call — lets other workflows invoke it like a function, inputs and secrets passed explicitly. The judgement call mirrors Class Twenty's modules exactly: extract when the same steps live in three places, not before. A matrix that multiplies a broken job produces three failures instead of one insight, and a reusable workflow extracted too early becomes a dependency every team is afraid to touch. Compose small, tested pieces — the lesson has not changed since Bicep, only the file format has.

§5

The workflow file is production code

Here is the fact that separates teams that take CI seriously from teams that have incidents about it: the workflow file can read your secrets and deploy to your production. That sentence has an implication — whatever reviews production changes must review this file with the same care. A one-line edit to a workflow can widen a trigger, print a credential, skip a validation step, or point a deploy at the wrong subscription, and it will look like harmless YAML in a diff full of real code.

The pipeline is production code.

Review a workflow diff with four questions. Trigger: did on: change — does anything new start this workflow, and could an outsider's pull request start it? Permissions: what can the job now reach — new secrets, a new environment, a wider token? Steps: is anything echoed, uploaded, or sent somewhere that was not before? Gates: did a needs:, an if:, or an environment protection quietly disappear? Four questions, thirty seconds each — and they catch the class of bug that a thousand green runs never will, because the run only proves the workflow did what it says, not that it says the right thing.

Case File · Campux Retail

The robot joins the team

campux-platform gets its first workflow

Until this week, the Class Twenty Bicep was validated by whoever remembered to run az bicep build before pushing — which, under deadline, was not everyone. This class the §1 workflow lands in .github/workflows/validate.yml: every pull request against main now compiles the Bicep and posts a what-if against the non-production resource group, and the Class Eighteen ruleset is updated to require that check before merging. The reviewer who used to eyeball templates for typos now reads a machine-produced list of exactly what would change.

The first week pays for itself: a pull request renaming a storage parameter goes red in ninety seconds because a module still referenced the old name — caught by the robot before a human reviewer had even opened the diff. Nothing deployed yet; the workflow only validates. Deployment needs credentials, and Campux is not putting a subscription key in a secret when Class Nine promised something better. That promise — a pipeline that deploys with no stored secret at all — is next class.3

Watch · Microsoft Learn

The official module, and a CAMPUX overview

Work the module first; the docs page is the reference you will keep returning to
Microsoft Learn · Module

Introduction to GitHub Actions
learn.microsoft.com/training/modules/introduction-to-github-actions/

GitHub Docs — Understanding GitHub Actions
docs.github.com/actions/get-started/understanding-github-actions

CAMPUX overview video

A walkthrough of a workflow file's anatomy, a live run in the Actions tab going from yellow to green, and a secret being masked in the log output will live here. Video to be added.

Lab 1 · Your first workflow

Make the robot do the checking

~15 minutes · a GitHub repository from Class 18 · browser or terminal

Add a workflow to a repository you own, trip it with a pull request, and watch a run happen. Use the hello-github repository from Class Eighteen, or any repository you can push to.

  1. On a new branch, create the workflow file at exactly this path — the folder names matter:

    git switch -c add-workflow
    mkdir -p .github/workflows
    What to notice: workflows only run from .github/workflows/. A YAML file anywhere else in the repository is just a file.
  2. Create .github/workflows/hello.yml — a trigger, a job, two steps:

    name: Hello CI
    on:
      pull_request:
      workflow_dispatch:
    jobs:
      greet:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Report the facts
            run: |
              echo "Actor:  ${{ github.actor }}"
              echo "Event:  ${{ github.event_name }}"
              ls -la
    What to notice: two triggers — pull_request for the machine, workflow_dispatch so you can also fire it by hand from the Actions tab.
  3. Commit, push, and open a pull request:

    git add .github/workflows/hello.yml
    git commit -m "Add first workflow"
    git push -u origin add-workflow
    On screen: open the PR on github.com — within seconds a check named Hello CI / greet appears on it, yellow while running. Click through to the Actions tab and watch the steps execute line by line.
  4. Read the finished run, then trigger it the second way:

    # in the run log, find:
    Actor:  <your-username>
    Event:  pull_request
    The lesson: the ls -la output proves the runner checked out your branch's code on a fresh machine. Now go to Actions → Hello CI → "Run workflow" and fire it manually — same file, different trigger, and the log's Event line now reads workflow_dispatch. Merge the PR when you are done.
Lab 2 · Secrets & masking

Watch a secret get scrubbed — and sneak past

~10 minutes · same repository · Settings access required

Store a secret, see the log mask it, then see why masking is not enough. The "secret" here is a harmless string — never use a real credential for this exercise.

  1. In the repository: Settings → Secrets and variables → Actions → New repository secret. Name it LAB_SECRET, value campux-rosebud-2026.

    What to notice: after saving, GitHub will show you the name but never the value again — even you cannot read it back. Update or delete are the only options.
  2. On a branch, add a workflow .github/workflows/mask.yml that uses it — including one deliberately bad step:

    name: Masking demo
    on: workflow_dispatch
    jobs:
      demo:
        runs-on: ubuntu-latest
        steps:
          - name: Echo the secret (bad practice, on purpose)
            run: echo "The secret is ${{ secrets.LAB_SECRET }}"
          - name: Transform it (worse)
            run: echo "Encoded: $(echo ${{ secrets.LAB_SECRET }} | base64)"
    What to notice: you are about to do the thing Drill 04 fails people for — deliberately, with a throwaway value, to see the failure mode with your own eyes.
  3. Merge or push to your default branch, then run it from the Actions tab and read the log.

    On screen: step one prints The secret is *** — the exact value was caught and masked. Step two prints a base64 string, fully readable. Decode it in any terminal (echo <string> | base64 -d) and there is your "secret", in plain view of anyone who can see the log.
  4. Delete the workflow and the secret, and say the rule out loud.

    The lesson: masking catches exact matches only. One trivial transformation defeated it. The rule is behavioural, not technical: no step prints a secret, and any echo near secrets. in a diff is a review finding — which is exactly how you will read workflow PRs from now on.
Note · public repository logs are public. Do this lab in a private repository, or with the throwaway value only — and never, in any lab in this bootcamp, put a real credential where a log can see it.
On the job

A merge, not a ritual

You · Cloud Engineer · deploys are manual and scary

Someone still deploys by running commands on their laptop, and it shows. You move it into a GitHub Actions workflow: push to main, tests run, the deploy happens the same way every time. The next release is a merge, not a ritual — and when it is 2am, the pipeline does not have a bad day the way a tired human does.

Class Twenty-Two

Examination

Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.

Drill 01Recall · triggers
A workflow must run validation on every pull request targeting main, and also let an engineer start it manually for debugging. Which trigger configuration is right?
Marked

B. Two needs, two triggers: pull_request filtered to main makes the check automatic where it matters, and workflow_dispatch adds the manual button. A runs on every push to every branch — noise, and it does not attach the check to the PR the way branch protection needs. C burns runner minutes polling for changes that events already announce. D is the pre-automation world wearing a YAML costume: the whole point is that validation stops depending on a human remembering. Matching triggers to intent is most of workflow design.

Drill 02Recall · jobs & runners
A workflow has two jobs: build writes a file to disk, and deploy expects to read it. There is no needs: between them. What actually happens?
Marked

Jobs are parallel strangers by default. Each job gets its own fresh runner, started at the same time unless needs: says otherwise — so deploy boots a machine that has never heard of build, and the file is simply not there. GitHub validates syntax, not intent, so nothing warns you (B). The fix is two-part: needs: build for ordering, and an artifact upload/download to carry the file between machines. Steps share a runner; jobs do not. Engineers who blur that line lose an afternoon to it, exactly once.

Drill 03Select three
Which three does a GitHub environment named production genuinely give you?
Marked

Scoped credentials, gates, and history. An environment is a named target with three real powers: its secrets exist only for jobs that pass its rules, its protection rules can hold a job until a named human approves, and it keeps a record of every run that touched it. It is not a quality guarantee — an approved deploy of wrong code is still wrong code — and it is not a copy of anything; the repository stays singular. If you selected the false options you were crediting the gate with powers only testing and review have. The environment controls who may pull the trigger and with which keys — never whether the aim was good.

Drill 04Spot the error
This deploy job passes review at a glance, but one line is a serious mistake. Which?
# deploy.yml (excerpt)
1.  jobs:
2.    deploy:
3.      runs-on: ubuntu-latest
4.      environment: production
5.      steps:
6.        - uses: actions/checkout@v4
7.        - run: echo "Deploying with key ${{ secrets.DEPLOY_KEY }}"
8.        - run: ./deploy.sh --key "${{ secrets.DEPLOY_KEY }}"
Marked

Line seven. The step exists only to print the deploy key into a log that every collaborator — on a public repository, every human alive — can read. Masking will scrub the exact string, and Lab 2 showed you how little that promise is worth: any transformation, encoding, or partial interpolation slips through, and debug modes widen the leak further. The log also outlives the run; a key printed today is compromised on the day someone reads last month's logs.

The rest is sound: ubuntu-latest is fine (A), environment: production is exactly right and is what scopes the secret (B), and passing a secret as an argument to the script that needs it is the normal pattern (D). The reviewer's reflex from §5 applies: an echo adjacent to secrets. is a finding, every time, even when it is "just for debugging". Especially then.

Situation 01Write before you reveal
A release goes out with every check green — and production breaks within the hour. In the retrospective, a manager asks, genuinely puzzled: "CI passed. How can the deploy be bad? What is all this automation actually proving?" Answer the question honestly.
Green proves the checks passed. Start by saying exactly what the checks check.
Reasoning

A strong answer concedes before it argues. The manager's puzzlement is fair, because "CI passed" is routinely oversold. Say plainly what green means: every check that exists, passed. Nothing more. CI is a list of specific questions asked automatically — does the Bicep compile, do the tests pass, does the linter object — and a green tick is the answer "no" to each. It has never been a certificate that the release is good; it is a certificate that the release is not bad in the ways we thought to ask about.

Then locate the actual failure: a missing question. Production broke in a way no check covered — a config value only production has, a dependency only production exercises, a load only real traffic produces. That is not automation failing; that is automation faithfully reporting on an incomplete list. The retrospective's real output is one new question added to the list, so this exact failure can never ship quietly again. Teams that do this after every incident compound; their CI is an archive of every way production has ever surprised them.

End with the reframe. CI's honest job is to make the cheap mistakes impossible so humans can spend review attention on the expensive ones — and, per §5, the pipeline itself is code that can be wrong. "What does it prove?" has a one-line answer worth saying verbatim: green proves we asked our questions and got good answers; it cannot prove we asked enough. The manager should leave less impressed by green ticks and more interested in what is on the list — which is exactly the right amount of impressed.

Situation 02Write before you reveal
A teammate is annoyed that their workflow-file changes keep getting held up in review: "It's YAML, not code. It doesn't even run in production — it just runs checks. Can we exempt .github/ from required review?" What do you say?
What can a workflow file read, and where can it send what it reads?
Reasoning

The trap is the premise. "It doesn't run in production" is exactly backwards. The workflow file runs with the repository's secrets in reach and, next class, with credentials into Azure itself. It decides what gets validated, what gets skipped, and where things get deployed. It is not near production power — for this repository it is among the most privileged text anyone writes. The friction your teammate resents is not bureaucracy misapplied to YAML; it is review applied precisely where it pays most.

Make the attack concrete, because abstractions do not persuade annoyed people. A one-line change to on: can make a workflow run on events from outside contributors. One added step can echo or upload a secret — Lab 2 defeated masking with a single base64. One deleted line can remove the what-if gate that stands between a typo and a production delete. Each looks like harmless YAML housekeeping in a diff. Exempting .github/ from review means changes to who-can-do-what ship unread — the least reviewable path guarding the most powerful file.

Then fix the legitimate complaint underneath. Slow review is a real problem with a better solution than no review. Offer the §5 checklist — trigger, permissions, steps, gates — which turns a workflow diff into a two-minute read, and add a CODEOWNERS line from Class Eighteen routing .github/ to the people who review it fastest and best. The answer to "review is slow" is making review fast; keep the requirement, fix the speed.

Examination record · first attempt
0/4
Class Twenty-Two · Complete
Retain this much

Five things worth carrying out of this class

  1. A workflow is a YAML file in .github/workflows/: a trigger (on:), jobs that run in parallel on fresh runners unless needs: orders them, and steps that run in sequence and share a machine.
  2. Contexts carry the facts of the run — github.actor, github.event_name, secrets.NAME — read with the expression syntax, gated with if:. Print the github context first when debugging.
  3. Secrets are encrypted and masked; variables are plain configuration; environments bundle both behind protection rules, so the production credential does not exist for a job that has not passed the production gate.
  4. Masking scrubs exact matches only — one transformation defeats it. The rule is behavioural: no step prints a secret, and an echo near secrets. in a diff is a review finding.
  5. The workflow file is production code: it reads secrets and directs deploys. Review its diffs with four questions — trigger, permissions, steps, gates — because green runs prove the checks passed, not that the checks are right.
Notes
  1. GitHub-hosted runners are ephemeral virtual machines — created for the job, destroyed after it — with Linux, Windows, and macOS options and a generous free allowance for public repositories and small private use. The ephemerality is a feature: nothing from the previous run survives, so every run starts from a provable clean slate. Class Twenty-Four covers what hosted runners cannot do, and the security bill you take on when you host your own.
  2. GitHub's own documentation is candid that secret redaction is best-effort: exact-match scrubbing of registered secret values, defeated by encoding, splitting, or structural transformation of the value. Treat masking as a backstop against accidents, never as permission to handle secrets loosely — the leaks that hurt are precisely the ones that do not look like the secret.
  3. This class teaches GitHub Actions because Campux's code lives on GitHub and hiring demand is broad — but the AZ-400 exam and plenty of employers also expect Azure Pipelines, its sibling inside Azure DevOps. The concepts map almost one-to-one (triggers, stages, agents, service connections), so treat the second system as an afternoon of vocabulary, not a new discipline. Which one "wins" varies by shop and by year; the model transfers, as usual.