RizTech Academy logo
RizTech Academy
CI/CDLesson 2 of 635 min

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 .yml file; 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 with uses:.

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 to main and 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 on ubuntu-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@v4 clones your repo; actions/setup-node@v4 installs 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

  1. Label the workflow / jobs / steps / actions in the greetings ci.yml and explain the nesting.
  2. Explain the difference between a run: step and a uses: step, with an example of each.
  3. Explain what runs-on: ubuntu-latest gives you and why a fresh machine each time matters.
  4. Add a needs: dependency so a deploy job runs only after a test job passes.
  5. Explain why actions/checkout@v4 is pinned to @v4 rather than left unpinned.
  6. Describe what you would see in the Actions tab after a push, and where a failure's cause appears.

Official documentation

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