Career OS

Anatomy of a Workflow

A workflow file looks intimidating until you see it’s just a recipe: when this happens, on this kind of machine, do these steps in this order. This lecture takes the file apart noun by noun, then you write one from a blank page.

🎯 Learning objectives

By the end you can:

  1. Write the three-part skeleton of any workflow (name, on, jobs) from memory.
  2. Choose the right event (push, pull_request, schedule, workflow_dispatch) for a trigger.
  3. Explain the difference between a run: step (shell command) and a uses: step (action).
  4. Describe the runner lifecycle and why steps in a job share files but jobs don’t.
  5. Author and push a working CI workflow that builds and tests on every push.

Mental model: a recipe card

NAME:    "Bake a cake"            ← what humans call this workflow
WHEN:    someone shouts "birthday!" ← the EVENT
KITCHEN: a brand-new rented kitchen ← the RUNNER
STEPS:
  1. get the ingredients           ← a STEP (an ACTION: "checkout")
  2. preheat oven                   ← a STEP (a command: "run: ...")
  3. mix and bake
  4. take a photo of the result     ← a STEP (an ACTION: "upload artifact")

YAML is just how we write that recipe in a format the robot can read. Indentation is meaningful in YAML (like Python) — two spaces, never tabs. That’s the #1 source of beginner errors.

The skeleton — every workflow has these three keys

name: My First Workflow      # 1. a human-friendly label (optional but do it)

on: push                     # 2. WHEN should this run? (the event)

jobs:                        # 3. WHAT should it do? (one or more jobs)
  say-hello:                 #    job id (you pick the name)
    runs-on: ubuntu-latest   #    which runner OS
    steps:
      - run: echo "Hello, world!"

Save that as .github/workflows/hello.yml, push it, and GitHub runs it. That’s a complete, valid workflow. Everything else in this lecture is elaboration on these three keys.

Where the file must live

It must be in .github/workflows/ at the repo root, and end in .yml or .yaml. GitHub only looks there. A workflow file anywhere else is just a text file that does nothing.

Key 2, deep: events (on:)

The on: key answers “what makes this run?” The most common triggers:

on:
  push:                      # someone pushed commits
    branches: [main]         #   ...but only to the main branch
  pull_request:              # someone opened/updated a PR
  workflow_dispatch:          # a manual "Run" button in the Actions tab
  schedule:
    - cron: '0 3 * * *'       # every day at 03:00 UTC (a "cron" timer)
EventFires when…Classic use
pushCommits land on a branchBuild + test + deploy
pull_requestA PR is opened or updatedRun checks before merging
workflow_dispatchYou click “Run workflow”Manual deploys, one-off jobs
scheduleA cron time arrivesNightly backups, link-checkers

Filters narrow the trigger

push alone fires on every branch. Adding branches: [main] means “only pushes to main.” You can also filter by paths: (only when certain files change) — e.g. don’t rebuild the site when only the README changed.

Key 3, deep: jobs

A job is a set of steps that run together on one runner. A workflow can have many jobs, and here’s the crucial default:

Jobs run in PARALLEL by default

If you list three jobs, GitHub runs all three at the same time on three separate machines. That’s fast — but if deploy must wait for build to finish, you have to say so explicitly with needs:.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "building..."

  deploy:
    needs: build              # ⬅️ wait for `build` to succeed first
    runs-on: ubuntu-latest
    steps:
      - run: echo "deploying..."

needs: build turns two parallel jobs into a pipeline: build → then deploy. This is exactly the shape of your site’s workflow.

runs-on — picking the machine

runs-on: ubuntu-latest    # Linux — fastest, cheapest, the default choice
# runs-on: windows-latest # when you need Windows
# runs-on: macos-latest   # when you need macOS (e.g. building iOS apps)

Use ubuntu-latest unless you have a specific reason not to — it’s the cheapest in minutes and starts fastest.

Steps: the two kinds

Inside a job, steps: is an ordered list. Every step is one of two things:

steps:
  # KIND 1 — run: a shell command on the runner
  - name: Install dependencies
    run: npm ci

  # KIND 2 — uses: a prebuilt ACTION (someone else's reusable step)
  - name: Check out my code
    uses: actions/checkout@v4
run:uses:
What it isA shell command you writeA packaged, reusable action
Examplerun: npm testuses: actions/checkout@v4
AnalogyCooking from scratchUsing a kitchen appliance
WhenProject-specific commandsCommon tasks others already solved

What is an “action” really?

An action is reusable code someone published so you don’t reinvent it. actions/checkout@v4 means:

  • actions/checkout → the action’s name (org actions, repo checkout)
  • @v4 → the version you’re pinning to

There’s a whole Marketplace of them: check out code, set up Node/Python/Java, cache files, upload artifacts, deploy to AWS, post to Slack. Using actions is how a 20-line workflow does an hour of work.

Always pin a version

uses: actions/checkout@v4 pins to major version 4. Never use an action with no version — you’d silently run whatever code its author pushes next, which is both a stability risk and a security risk (Lecture 3 covers pinning to a full commit SHA for untrusted actions).

The runner lifecycle — the model that prevents 90% of confusion

When a job starts, GitHub:

1. Boots a BRAND-NEW virtual machine (the runner)
2. Runs your steps top-to-bottom, all in the SAME working folder
3. Tears the machine down and deletes EVERYTHING

Two consequences you must internalize:

Within a job: steps share a workspace

Step 1 does git checkout → the files land in the working folder. Step 2 does npm ci → it can see those files. Step 3 does npm run build → it sees the installed packages. Steps in one job share the same disk. That’s why order matters: checkout before build.

Between jobs: nothing is shared

build and deploy run on different machines. The files build created are gone when deploy starts. To hand data from one job to the next, you must explicitly upload an artifact in build and download it in deploy (Lecture 3). This is the single most common “but the file was right there!” confusion.

🧪 Lab — write your first real workflow

Goal: a CI workflow that, on every push, checks out your code, installs deps, and builds the site — proving the build isn’t broken before you rely on it.

Create .github/workflows/ci.yml:

name: CI — Build Check

on:
  push:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the code
        uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build the site
        run: npm run build

      - name: Say it worked
        run: echo "✅ Build passed — safe to deploy."

Then:

git add .github/workflows/ci.yml
git commit -m "[ci]: add build-check workflow"
git push

Open the Actions tab and watch it run. Now break it on purpose: introduce a typo in a doc link (your config throws on broken links), push, and watch the build go ❌ red — and notice it tells you exactly which step failed. Fix it, push, watch it go ✅. That red→green loop is CI.

Why with:?

with: passes inputs to an action — like function arguments. actions/setup-node accepts node-version and cache. Each action’s README documents its inputs.

⚠️ Common misconceptions

Warning

  • “Tabs are fine in YAML.” They are not. YAML forbids tabs for indentation — use 2 spaces. Most “invalid workflow” errors are this.
  • “Jobs run in order top to bottom.” No — parallel by default. Use needs: to sequence them.
  • “The next job can see my built files.” No — new machine, empty disk. Use artifacts.
  • run and uses can be in the same step.” No — a step is either a run or a uses, never both.
  • actions/checkout happens automatically.” It does not. If you forget it, the runner has no copy of your code and everything downstream fails. It’s almost always step 1.

✅ Self-check

1. What are the three top-level keys every workflow has?

name (label), on (the event/trigger), and jobs (what to do). Only on and jobs are strictly required.

2. You have jobs `test` and `deploy`. You want deploy to run only after test passes. What do you add?

needs: test inside the deploy job. Without it, they’d run in parallel on separate machines.

3. Step 2 runs `npm ci` but fails with "no package.json found." What did you forget?

actions/checkout@v4 as an earlier step. Without checkout, the runner never pulled your code, so there’s no package.json on the empty machine.

4. Why can't a `deploy` job simply read the files that the `build` job created?

Because each job runs on its own fresh runner with its own disk, destroyed after the job. To pass files between jobs you upload them as an artifact in build and download it in deploy.

5. What does `@v4` mean in `uses: actions/checkout@v4`?

It pins the action to major version 4 — a specific, stable line of releases — instead of silently running whatever the author publishes next.

📚 Graded resources

Why AI can’t do this for you

AI generates flawless-looking YAML. But it can’t feel the difference between “jobs run in parallel” and “jobs run in order” until your deploy ships before your build finished and breaks production. The recipe is easy to copy; knowing why each line is there — so you can change it safely — is what you’re building here.

🔑 Key terms

TermMeaning
YAMLThe indentation-based config format workflows are written in (2 spaces, no tabs).
on:The key that declares the triggering event(s).
jobs:The key listing the units of work.
runs-onWhich runner OS a job uses (ubuntu-latest, etc.).
needs:Declares that a job must wait for another to succeed.
steps:The ordered list of instructions in a job.
run:A step that executes a shell command.
uses:A step that runs a reusable action.
with:Inputs (arguments) passed to an action.
ArtifactA file bundle saved from a job, downloadable by later jobs (or you).
cronThe time-schedule syntax used by schedule: triggers.

Previous: ← Overview · Next: Advanced + Dissecting This Site →

Saves your progress on this device.