Learn › CI gates ›
Written for Rasputin 2026.08.5
What a passing check proves
Beginner · about 25 minutes · hands-on lab in a folder you create
In the tracks: Software developer, DevOps.
What you need
- A terminal on macOS or Linux. On Windows, WSL (Windows Subsystem for Linux) gives you a Linux terminal. This lesson was tested on macOS with Git 2.50.1 and 2.54.0, and on Linux (Debian) with Git 2.47.3. It was not tested on Windows.
- Git, the tool most software projects use to track changes. Type
git --versionand press Return. If you see a version number, you have it. If not: on macOS, install Git from git-scm.com; on Linux, install the packagegit. Installing it may need administrator rights on macOS and does need them on Linux. sh,echoandchmod, already part of macOS and Linux.
You do not need Rasputin hardware, Rasputin code, or an account anywhere.
The idea
A software project changes one proposed edit at a time. Each change is reviewed and then merged: added to the main copy of the code.
Continuous integration (CI) checks every change automatically, as soon as it is proposed. A separate computer takes the change and runs a list of checks: programs that confirm, for example, that the code builds and its tests pass. A test is a small program that runs part of the code with a known input and compares the result with the expected answer.
Every program, a check included, ends with an exit status: a number it hands back when it
finishes. 0 means success; anything else means failure. The words a check prints are for
people. The exit status is what CI reads.
A gate is a check allowed to stop a change: if it fails, the change cannot be merged. Other checks only report: they show a result and let the change through either way. Which checks are gates is a separate decision, usually recorded somewhere other than the list of checks. On GitHub, the file that says what to run does not say what must pass; the repository’s settings do.
So a passing check proves only that what it checked came out as expected. It does not prove the code is right where no test looks, and a result alone does not say whether a failure would have blocked anything.
A real example: Rasputin
Rasputin is an open-source system for running a small group of computers at home. Its control-plane repository runs a CI workflow on every proposed change, with checks that include tests and a build. A comment in the file notes that what blocks a merge is “configured outside this file”, in the repository’s settings. Those settings name required checks, so, as another comment records, renaming one check once left a change blocked while “all twelve checks were green”.
Try it
You will build a tiny program, a check for it, and a gate. The gate is a Git pre-commit hook: a script Git runs before saving each commit, a snapshot of your files. If the script fails, Git refuses the commit. In CI the checks run on a separate computer; here your own Git plays that part.
1. Make a sandbox. Run these one at a time:
mkdir ci-lab
cd ci-lab
git init
git config user.name "Your Name"
git config user.email "you@example.com"
mkdir makes a new, empty folder; cd moves you into it; git init turns it into a Git project.
It prints a line beginning Initialized empty Git repository, and may print hint: lines;
neither is an error. With no --global flag, the name
and email apply only inside ci-lab; the placeholders are fine.
2. Write the program.
echo 'echo $(( $1 * 2 ))' > double.sh
sh double.sh 3
6
double.sh prints double the number you give it: $1 is that number, and $(( )) does the
arithmetic. The single quotes keep the line exactly as typed, and > sends it into the file.
3. Write a check. Copy all of these lines at once:
cat > check.sh <<'EOF'
if [ "$(sh double.sh 2)" = "4" ]; then
echo "check passed"
else
echo "check FAILED"
exit 1
fi
EOF
sh check.sh
echo $?
check passed
0
cat > check.sh <<'EOF' saves every line up to EOF into check.sh. The check runs the program
with 2 and compares the answer with 4; if they differ, exit 1 ends it with a failure status.
echo $? prints the exit status of the command before it.
4. Make the check a gate.
echo 'sh check.sh' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
git add double.sh check.sh
git commit -q -m "Add double"
check passed
.git/hooks/pre-commit is where Git looks for this hook, and chmod +x marks it as runnable.
git add chooses files for the commit. In git commit, -m gives the commit a message, and -q
stops Git printing its own summary, so what you see comes from the hook.
5. Break the program.
echo 'echo $(( $1 * 3 ))' > double.sh
git commit -q -a -m "Triple it"
git log --format=%s
check FAILED
Add double
-a includes every changed file Git already tracks. git log --format=%s lists saved commits by
message, newest first. Only Add double is there: the gate refused the change.
6. Predict, then commit. Will the gate accept this one?
echo 'echo $(( $1 + 2 ))' > double.sh
sh double.sh 3
git commit -q -a -m "Add two"
5
check passed
Double 3 is 6, and the program says 5. The gate accepted it anyway, because 2 + 2 and 2 * 2
are both 4. The check proved one thing, the answer for 2, and nothing else.
7. Turn the gate into a report.
echo 'sh check.sh || true' > .git/hooks/pre-commit
echo 'echo $(( $1 * 3 ))' > double.sh
git commit -q -a -m "Triple it"
git log --format=%s
check FAILED
Triple it
Add two
Add double
|| means “if the command before this failed, run the one after”, and true always succeeds.
The check still ran and still printed check FAILED, but the hook now always exits 0, so Git
saved the commit. Nothing in the output shows that the check lost its power to block. Only reading
the hook, where that authority is written down, tells you.
Apart from hint: lines, every step prints the same on macOS and Linux.
8. Clean up.
cd ..
rm -rf ci-lab
rm deletes; -r includes everything inside the folder, and -f skips the questions. It cannot
be undone, so check you typed ci-lab.
Check yourself
- A check prints
check FAILED. What does CI look at to decide whether it failed? - In step 6, the check passed. What did that prove about
double.sh? - A proposed change shows one failing check, and the page still lets you merge it. Is something broken?
Answers
- Its exit status. In step 7 the check exited
1, but the hook threw that away and exited0, and Git went by the hook. - Only that it prints
4when given2. For3it was wrong. - Not necessarily: that check may only report. Whether a check blocks is a separate setting, so read the setting rather than guess from the result.
Where to go next
- A real list of checks: Rasputin’s CI workflow, whose comments explain why a check’s name matters.
- Checks running: the workflow’s recent runs in the public repository.
Reviewed 2026-09-12. Something wrong or unclear in this lesson? Tell us.