GitHub Actions: workflows, jobs and steps
To build a pipeline you need a CI/CD tool, and this course uses GitHub Actions — because it is built into GitHub (where your code already is), it is free for public repositories and generous for private ones, and its concepts transfer directly to every other CI system (GitLab CI, CircleCI, Jenkins). This lesson is the anatomy of a GitHub Actions workflow — the structure you will write, using the greetings app's real pipeline.
Where a workflow lives, and its structure
A GitHub Actions pipeline is defined in a YAML file in your repository under .github/workflows/. GitHub
watches that folder and runs the workflows when their triggers fire. The structure has a clear hierarchy:
- A workflow — one
.ymlfile; the whole pipeline. - Triggers (
on:) — what starts it (a push, a pull request, a schedule). - Jobs — independent units of work; by default they run in parallel, each on a fresh machine.
- Steps — the ordered commands within a job; they run in sequence on that job's machine.
- Actions — reusable pre-built steps (like
actions/checkout) you pull in withuses:.
So: a workflow has jobs, a job has steps, and a step either runs a command (run:) or uses a
prebuilt action (uses:). Hold that nesting and any workflow file is readable.
The greetings workflow, explained
Here is the real, verified CI workflow for the greetings app (.github/workflows/ci.yml):
name: CI
on: # triggers
push:
branches: [main] # run on pushes to main
pull_request: # and on every pull request
jobs:
build-and-test: # one job
runs-on: ubuntu-latest # on a fresh Ubuntu machine
steps:
- uses: actions/checkout@v4 # step: check out the repo (a prebuilt action)
- uses: actions/setup-node@v4 # step: install Node
with:
node-version: 20
cache: npm
- run: npm ci # step: install dependencies (a command)
- run: npm test # step: run the tests
- run: docker build -t greetings:${{ github.sha }} . # step: build the image
Walk the hierarchy in it:
on:— this workflow triggers on a push tomainand on any pull request. Every PR now gets built and tested automatically (the Git module's PR checks, realised).jobs.build-and-test— one job, running onubuntu-latest(GitHub gives you a fresh, clean VM each time — reproducibility, like a container).steps— run in order: check out the code, install Node, install dependencies, run the tests, build the image. If any step fails, the job fails and the ones after it do not run — the gate from the last lesson.
That is a complete, working CI pipeline: on every change, a clean machine checks out the code, tests it, and confirms the image builds.
run versus uses
Two kinds of step, and knowing when to use each:
run:executes a shell command on the job's machine —npm ci,npm test,docker build. This is how you run your build and test commands.uses:pulls in a prebuilt action — a reusable, shared step.actions/checkout@v4clones your repo;actions/setup-node@v4installs Node. Actions save you from scripting common setup by hand, and the ecosystem has one for almost everything (setting up languages, caching, deploying to clouds).
The @v4 pins the action to a major version — the same reproducibility discipline as pinning a Docker base
image or a dependency. A typical workflow is a mix: uses: for standard setup (checkout, language, caching)
and run: for your project's own commands.
Jobs, parallelism and dependencies
By default, jobs run in parallel on separate machines — useful when you have independent work (lint, test,
build) that can happen at once for speed. When one job must wait for another (deploy after test passes), you
express the dependency with needs::
jobs:
test:
runs-on: ubuntu-latest
steps: [ ... ]
deploy:
needs: test # deploy runs ONLY if test succeeded
runs-on: ubuntu-latest
steps: [ ... ]
needs: test makes deploy wait for test and run only if it passed — which is exactly how you build the
pipeline's gate across jobs (test, then deploy). You will use this in the deploy-pipeline lesson to ensure
nothing deploys unless the tests are green.
Seeing it run
When you push the workflow file and then push a change (or open a PR), GitHub runs the workflow and shows the result in the repository's Actions tab — each job, each step, with logs, and a green tick or red cross. A failed step's log shows exactly what went wrong (the QA course's "make failures actionable"). On a pull request, the check appears right on the PR, and with branch protection (the Git module) a red pipeline blocks the merge. This is CI/CD in daily use: you push, the pipeline runs, you watch it go green — and if it goes red, you fix it before merging.
Check your work
Location/structure: workflows live in .github/workflows/*.yml. Hierarchy: workflow (a file) →
triggers (on:) → jobs (parallel by default, each on a fresh machine) → steps (sequential) →
steps run a command (run:) or a prebuilt action (uses:).
The greetings workflow: on: push-to-main + PR (every PR built/tested); one build-and-test job on
ubuntu-latest (fresh clean VM); steps checkout → setup-node → npm ci → npm test → docker build. A
failed step fails the job and stops later steps (the gate).
run vs uses: run: = a shell command (your build/test commands); uses: = a reusable action
(actions/checkout@v4, actions/setup-node@v4), pinned with @v4 for reproducibility. Mix both.
Jobs/parallelism: jobs run in parallel by default (speed for independent work); needs: makes one job wait
for another and run only if it passed (deploy needs: test) — the cross-job gate.
Seeing it: results in the Actions tab (jobs/steps/logs, green/red); the check shows on the PR and, with branch protection, a red pipeline blocks merge.
Practice
- Label the workflow / jobs / steps / actions in the greetings
ci.ymland explain the nesting. - Explain the difference between a
run:step and auses:step, with an example of each. - Explain what
runs-on: ubuntu-latestgives you and why a fresh machine each time matters. - Add a
needs:dependency so adeployjob runs only after atestjob passes. - Explain why
actions/checkout@v4is pinned to@v4rather than left unpinned. - Describe what you would see in the Actions tab after a push, and where a failure's cause appears.
Official documentation
- GitHub Actions — Understanding workflows — Workflows, jobs, steps and triggers.
- GitHub Actions — Workflow syntax — Every field in a workflow file.
- GitHub Actions — Events that trigger workflows — The
on:triggers.
Next: a pipeline that builds and tests.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship