It is Friday evening and you push a small cleanup before logging off. The change looks trivial, so you skip the local run and head home. On Monday the team finds the design broken, and the fix takes an hour that a three-minute check would have saved.
How do you make sure every change is checked automatically, even when you forget?
The answer is continuous integration, universally shortened to CI: a service that runs your checks on a clean machine for every push and every pull request, which is a proposal to merge one branch into another. You stop relying on memory, and the machine stops forgetting.
Your laptop is a comfortable liar. It holds old build files, leftover environment settings, and tools installed months ago in ways nobody recorded. A check that passes there may pass because of that history rather than because of your change.
CI removes the comfortable parts. Each run starts from a fresh machine image, installs the tools from scratch, checks out your exact change, and runs the loop. When that run is green, the result belongs to the change alone, and anyone on the team can reproduce it by running the same commands.
The workflow is written in YAML, a configuration format where each line is a key followed by a value and indentation shows structure. Two spaces deeper means inside the line above; get the indentation wrong and the key lands in the wrong place. Read the project's file below. It is thirteen lines, and each one is explained after it.
name: RTL smoke
on: [push, pull_request]
jobs:
smoke:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4 # course note: pin to a verified commit SHA
- name: Install tools
run: sudo apt-get update && sudo apt-get install -y iverilog verilator yosys make
- name: Report tool versions
run: make check-tools
- name: Lint, simulate, synthesize
run: make allThis is the project's smoke-test workflow: four steps on a fresh machine, ending in the same full loop you run locally.
Read the picture below from top to bottom. Each box is one stage of the run, and the last box splits into the two possible endings. The labels under the boxes say what the machine actually does at each stage.
The name line gives the run its display name: RTL smoke. The on line lists the events that start a run: every push and every pull_request. Nothing about this file runs on a timer or by hand; a change arriving is what wakes it up.
One job called smoke runs on a fresh Ubuntu 24.04 machine, through four steps in order: checkout fetches your change, install fetches the simulator, the linter, and the synthesis tool, versions records the tool versions with make check-tools, and the final step runs make all.
A step that exits nonzero stops the job immediately and marks it red. That fail-fast behavior is the whole enforcement mechanism: the counts_by_two failure from the previous lesson would stop the final step, and no green badge would follow. Later steps never run after a failure, so a red run always names the first thing that broke.
The last step runs the same command you ran locally in the previous two lessons. Here is what that command shows on the good design:
# captured terminal output
$ make all 2>&1 | grep -E '^(PASS|iverilog|vvp|verilator|yosys)' | cut -c1-110
verilator --lint-only -Wall --top-module mini_controller rtl/mini_controller.sv
iverilog -g2012 -Wall -s tb_mini_controller -o build/sim.vvp rtl/mini_controller.sv tb/tb_mini_controller.sv
PASS: mini_controller
yosys -Q -T -p 'read_verilog -sv rtl/mini_controller.sv; hierarchy -check -top mini_controller; proc; opt; cheThis output is the local run of the workflow's final step, measured with GNU Make 3.81 on macOS: the linter line, the compile line, the verdict line starting with PASS:, and the synthesis line. The project also passes the same loop on Ubuntu 24.04 with GNU Make 4.3 and that distribution's own packages: Icarus 12.0, Verilator 5.020, and Yosys 0.33.
This workflow file has not been run on a hosted CI service for this course. The evidence above is local: the project was run on macOS and inside an Ubuntu 24.04 container with that distribution's own packages, where make all passed. Treat the workflow as reviewed but unexecuted until you watch it run on your own repository.
In your own repository this file lives at .github/workflows/rtl-smoke.yml, and the hosting service discovers it there automatically.
All of them. CI never skips steps for changes that look safe. The full loop reruns on the fresh machine and reports green against the change alone.
Imagine the counts_by_two mutant from the previous lesson reaching the workflow. The first three steps pass: the code is fetched, the tools install, the versions are reported. Then the final step runs the simulation, the testbench reports mismatches, the verdict gate finds no verdict line, and make exits 2:
# captured terminal output
$ make sim > /dev/null 2>&1; echo "make sim exit status with a counts_by_two bug: $?"
make sim exit status with a counts_by_two bug: 2This output is the local form of the red run, measured with GNU Make 3.81 on macOS: the step that runs the loop fails, so the job stops and the run is marked red. The fix is the same as locally: restore the design, push again, and watch the new run go green.
The log ends with the tool lines, the mismatch reports, and the make error. A red run always names the first thing that broke, so read a CI log from the first failure, not the last line.
The file works, but three of its choices can move under you. The runner label names an image family rather than an exact copy, and the install step takes whatever tool versions the package mirror offers that day. The checkout step is referenced by a tag, which can be moved to different code after you read it.
Each of those is a way for today's green run to differ from next month's without any change from you. The next lesson shows what can be pinned down: exact tool versions, verified action code, and a recorded environment, so a rerun means the same run.
A workflow that installs tools but never runs the tests. The log shows a successful install and the badge goes green, while no check ever ran. End every workflow with the step that runs the loop, and read a green badge as a statement about that step.
Merging despite a red build. A red run that gets merged anyway teaches the team that red is advice. Treat red as a closed gate: fix the change or fix the check, then rerun before merging.
Letting unpinned versions drift. When the runner image or the tool packages update silently, a new failure may belong to the environment rather than the change. Record versions in the log with the versions step, and pin what the next lesson shows you to pin.
Rule: every push gets a fresh machine, the full loop, and a red or green answer.
The fourth step, Lint, simulate, synthesize, turns red. The first three steps pass: checkout, install, and versions all succeed. The log ends with the simulation tool lines, the mismatch reports, and the make error from the failed recipe, and the run is marked red at that step.
The runner image behind the Ubuntu label, which the host updates over time, and the tool versions the package installer fetches, which track whatever the mirror offers that day. The checkout tag is a third: a tag can be moved to different code after you first read it.
At .github/workflows/rtl-smoke.yml. The hosting service scans that directory for workflow files on every push and pull request, so placing the file there is what connects your change arriving to the checks running.
CI runs the full loop on a fresh machine for every push and pull request, so no change depends on anyone remembering to check it.
The workflow checks out the change, installs tools, records versions, and runs make all, failing fast at the first nonzero step.
Runner labels, package versions, and action tags can drift, so record versions now and pin them next.