Computing Foundations › Systems Foundations: Storage, Observability, and Tooling › Day 41
Hands-on lab — Day 41: Thinking in Automation: Scripts, Hooks, and Pipelines
- ← Back to the Day 41 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-041-thinking-in-automation-scripts-hooks-and/
Commands
Setup
cd labs/sections/computing-foundations/day-041-thinking-in-automation-scripts-hooks-and Run
bash examples/hook_demo.sh
bash examples/pipeline.sh
bash examples/pipeline.sh test
bash starter/hook_demo.sh Test
bash tests/run_tests.sh File tree
examples/hook_demo.sh examples/pipeline.sh examples/pre-commit expected-output/demo-output.txt expected-output/FIELDS.md expected-output/tests-output.txt metadata.yml README.md requirements/README.md security.md starter/automation-worksheet.md starter/hook_demo.sh tests/run_tests.sh troubleshooting.md
Lab README
Day 041 lab — A Real Pre-Commit Hook
Lesson
- Lesson title: Thinking in Automation: Scripts, Hooks, and Pipelines
- Day number: 41 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-041-thinking-in-automation-scripts-hooks-and
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-041-thinking-in-automation-scripts-hooks-andwhen the site is running.
Purpose
Day 41's lesson is about the automation mindset — scripts, hooks, and pipelines.
This lab makes it real. You install a working git pre-commit hook, watch it
block a bad commit and allow a clean one, and run a tiny fail-fast
pipeline that stops at the first failing stage. Everything runs offline in a
throwaway repository the scripts create and delete for you, so you can experiment
without touching any real project.
Learning objectives
- Install a git
pre-commithook and explain why it must be executable. - Watch a hook reject a commit (non-zero exit) and allow a clean one (zero exit).
- Read a hook and point to the line that aborts a commit.
- Run a pipeline of ordered stages and observe fail-fast behavior.
- Complete four exercises that build a hook check and extend a pipeline.
Prerequisites
- The Day 41 lesson (read it first — it explains hooks, pipelines, and fail-fast).
- Basic git from Week 5: staging and committing.
- A terminal with
gitandbash(macOS, Linux, or WSL on Windows).
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon).
- Linux — fully supported (any distribution with git and bash).
- Windows — use WSL (Windows Subsystem for Linux) and follow the Linux path; native PowerShell is not supported for this lab.
Hardware requirements
Any computer that runs git. The scripts create tiny temporary repositories and need no meaningful RAM, disk, or GPU.
Required software
git(any recent version — check withgit --version).bash3.2 or newer (preinstalled on macOS and Linux).- Standard utilities:
mktemp,grep,printf,chmod— all part of the base system.
Free and open-source options
Everything here is free and open source: git and bash ship with or install freely on every supported OS. No account, API key, network, or purchase is needed. The lesson surveys free tools you can grow into — the pre-commit framework, hosted CI free tiers, and Makefiles — but this lab needs none of them.
Installation
None. Clone the repository (or copy this directory) and you are ready:
cd labs/sections/computing-foundations/day-041-thinking-in-automation-scripts-hooks-and
File structure
day-041-thinking-in-automation-scripts-hooks-and/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── hook_demo.sh ← YOUR working file (4 exercises)
│ └── automation-worksheet.md ← worksheet for the practice assignment
├── examples/
│ ├── pre-commit ← the reference hook (installed by the demo and tests)
│ ├── hook_demo.sh ← full reference walkthrough
│ └── pipeline.sh ← the tiny fail-fast pipeline
├── tests/
│ └── run_tests.sh ← automated checks (real behavior)
├── expected-output/
│ ├── demo-output.txt ← real captured demo run (macOS)
│ ├── tests-output.txt ← real captured test run (macOS)
│ └── FIELDS.md ← what a correct run must show, all platforms
├── requirements/
│ └── README.md ← dependency statement (git + bash only)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the whole thing work end to end (blocks bad, allows clean, runs pipeline)
bash examples/hook_demo.sh
## 2. Watch the pipeline: first all green, then forced to fail fast at 'test'
bash examples/pipeline.sh
bash examples/pipeline.sh test
## 3. Your task: complete the four exercises in the starter, then run it
bash starter/hook_demo.sh
## 4. Check everything
bash tests/run_tests.sh
What the commands do
bash examples/hook_demo.sh— creates a temporary git repo, installsexamples/pre-commit, attempts a commit of a file with trailing whitespace (rejected), fixes it and commits cleanly (allowed), then runs the pipeline green and failing, and deletes the temp repo on exit.bash examples/pipeline.sh— runs the stages lint, test, build, deploy in order; with no argument all pass (exit 0). Pass a stage name (bash examples/pipeline.sh test) to make that stage fail and see the pipeline stop before the later stages (exit 1) — failing fast.bash starter/hook_demo.sh— the same idea as a skeleton with four numbered exercises: complete a hook check, block a bad commit, commit clean, and add a pipeline stage.bash tests/run_tests.sh— installs the hook in a fresh temp repo and verifies real behavior: the hook blocks trailing whitespace and a syntax error, allows a clean commit, and the pipeline fails fast while succeeding when all stages pass.
Expected output
See expected-output/demo-output.txt and
expected-output/tests-output.txt — real
captured runs. The demo's two key lines are:
Result: the hook BLOCKED the bad commit (exit non-zero), as intended.
Result: the hook ALLOWED the clean commit.
Your temporary paths will differ; the shape must match.
expected-output/FIELDS.md lists exactly what a
correct run must show on every platform.
Validation steps
- Run
bash examples/hook_demo.sh— confirm you see both the BLOCKED and ALLOWED results. - Run
bash examples/pipeline.sh— it must print all four stages and exit 0. - Run
bash examples/pipeline.sh test— it must stop attest, never printbuildordeploy, and exit non-zero. - Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 6 checks, 0 failure(s). The command exits 0 on success and
non-zero on any failure, so it runs unchanged in CI.
Cleanup
Nothing to clean up. Every script does its work inside a temporary directory
(mktemp -d) and deletes it on exit, leaving your machine untouched. To reset the
starter file to its original state, restore it from git:
git checkout -- starter/hook_demo.sh.
Troubleshooting
See troubleshooting.md for the full list (hook not
executable, --no-verify, missing git, invisible trailing whitespace, WSL notes).
Security notes
See security.md. Short version: the scripts make no network calls, need no elevated privileges, and work only in a self-deleting temp directory — but remember that hooks run code, so always read a hook before installing one from an untrusted source.
Extension exercises
- Make the pipeline observable: copy
examples/pipeline.shto your own file and have every run append a timestamped line topipeline.logrecording which stage it reached and whether it succeeded. - Add a check to your hook that rejects a commit if a staged
.shfile fails abash -nsyntax check, then prove it blocks a broken file and allows a fixed one. - Sketch a CI version: write (do not run) a short list of the same stages your pipeline runs, and note which checks you would keep local for speed and which you would move to CI as the authoritative gate.
Navigation
- Previous day: Day 40 — Observability: Logs, Metrics, Traces, and Dashboards (
labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards/). - Next day: Day 42 — Section Review: Your Computing Foundations Toolkit (
labs/sections/computing-foundations/day-042-section-review-your-computing-foundations-toolkit/).
Expected output
FIELDS.md
# Expected output — what a correct run produces (all platforms)
This directory holds real captured runs from the authoring machine (macOS,
Apple Silicon, 2026-07-12). Your paths will differ; the shape must match.
## `bash examples/hook_demo.sh` must show, in order
1. `Hook installed at .git/hooks/pre-commit (executable).`
2. A **blocked** bad commit: the hook prints `pre-commit: trailing whitespace in bad.sh`, then `pre-commit: commit rejected.`, and the demo reports `Result: the hook BLOCKED the bad commit`.
3. An **allowed** clean commit: `pre-commit: quality gate passed.` followed by `Result: the hook ALLOWED the clean commit.`
4. The pipeline run green (all four stages, then `Pipeline succeeded: every stage green.`) and then forced to fail (stops after `test`, printing `Pipeline stopped at 'test'.`).
The two lines that prove the gate works in both directions are
`BLOCKED the bad commit` and `ALLOWED the clean commit`.
## `bash examples/pipeline.sh` (no argument)
Prints `lint`, `test`, `build`, `deploy`, then `Pipeline succeeded: every stage green.` — exit code `0`.
## `bash examples/pipeline.sh test`
Prints `lint`, then `test`, then the failure and `Pipeline stopped at 'test'. Later stages did not run.` — `build` and `deploy` never appear — exit code `1`. This is failing fast.
## `bash tests/run_tests.sh`
Ends with `6 checks, 0 failure(s).` and exits `0`.
## Platform notes
- **macOS and Linux:** identical output; the lab needs only `git` and `bash`, both preinstalled or trivially installed.
- **Temporary paths:** every script works in a `mktemp -d` directory and deletes it on exit, so the path printed after "Temporary repository at" changes on every run and differs between macOS (`/var/folders/...`) and Linux (`/tmp/...`). That line is the only run-to-run difference.
- **Windows:** run inside WSL (Windows Subsystem for Linux) and the Linux output applies. Native PowerShell is not supported for this lab.
demo-output.txt
$ bash examples/hook_demo.sh
=== 1. Installing the pre-commit hook ===
Hook installed at .git/hooks/pre-commit (executable).
=== 2. Attempting a BAD commit (trailing whitespace) ===
pre-commit: trailing whitespace in bad.sh
pre-commit: commit rejected.
pre-commit: fix the issues above, or bypass with 'git commit --no-verify' (discouraged).
Result: the hook BLOCKED the bad commit (exit non-zero), as intended.
=== 3. Fixing the file and committing CLEAN ===
pre-commit: quality gate passed.
Result: the hook ALLOWED the clean commit.
=== 4. Running the pipeline ===
--- pipeline: all stages ---
==> stage: lint
==> stage: test
==> stage: build
==> stage: deploy
Pipeline succeeded: every stage green.
--- pipeline: forced failure at 'test' ---
==> stage: lint
==> stage: test
test: FAILED
Pipeline stopped at 'test'. Later stages did not run.
Demo complete. Temporary repository at /var/folders/.../tmp.XXXXXXXX will now be removed.
# Captured on macOS (Apple Silicon), 2026-07-12. The temporary path after
# "Temporary repository at" varies on every run and between platforms.
tests-output.txt
$ bash tests/run_tests.sh
Testing the pre-commit hook ...
ok: hook blocks a commit with trailing whitespace
ok: hook allows a clean commit
ok: hook blocks a commit with a shell syntax error
Testing the pipeline ...
ok: pipeline succeeds when all stages pass
ok: pipeline fails fast on a failing stage
ok: later stages are skipped after a failure
6 checks, 0 failure(s).
$ echo $?
0
# Captured on macOS (Apple Silicon), 2026-07-12.
# The command exits 0 when all checks pass and non-zero on any failure, so it
# runs unchanged in CI. Output is identical on Linux (git + bash only).
Source files
examples/hook_demo.sh (2276 bytes)
#!/usr/bin/env bash
# hook_demo.sh — a complete, offline walkthrough of a real pre-commit hook.
#
# It creates a throwaway git repository in a temporary directory, installs the
# reference pre-commit hook, then:
# 2. attempts to commit a file with trailing whitespace -> the hook BLOCKS it
# 3. fixes the file and commits cleanly -> the hook ALLOWS it
# 4. runs the fail-fast pipeline both green and forced to fail
# The temporary repository is deleted on exit. No network, no sudo, no changes
# outside the temp directory.
set -uo pipefail
# Locate this script's directory so we can find pre-commit and pipeline.sh
# even after we cd into the temporary repository.
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create an isolated, self-cleaning workspace.
work="$(mktemp -d 2>/dev/null || mktemp -d -t hookdemo)"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
cd "${work}"
git init -q
# A local identity so commits work without touching your global git config.
git config user.email "learner@example.local"
git config user.name "Automation Learner"
echo "=== 1. Installing the pre-commit hook ==="
mkdir -p .git/hooks
cp "${here}/pre-commit" .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Hook installed at .git/hooks/pre-commit (executable)."
echo
echo "=== 2. Attempting a BAD commit (trailing whitespace) ==="
# The three trailing spaces after "hello" are the defect the hook catches.
printf 'echo "hello" \n' > bad.sh
git add bad.sh
if git commit -q -m "add bad.sh" 2>&1; then
echo "UNEXPECTED: the bad commit was allowed."
else
echo "Result: the hook BLOCKED the bad commit (exit non-zero), as intended."
fi
echo
echo "=== 3. Fixing the file and committing CLEAN ==="
printf 'echo "hello"\n' > bad.sh # same file, trailing whitespace removed
git add bad.sh
if git commit -q -m "add clean script" 2>&1; then
echo "Result: the hook ALLOWED the clean commit."
else
echo "UNEXPECTED: the clean commit was blocked."
fi
echo
echo "=== 4. Running the pipeline ==="
echo "--- pipeline: all stages ---"
bash "${here}/pipeline.sh" || true
echo "--- pipeline: forced failure at 'test' ---"
bash "${here}/pipeline.sh" test || true
echo
echo "Demo complete. Temporary repository at ${work} will now be removed."
examples/pipeline.sh (1085 bytes)
#!/usr/bin/env bash
# pipeline.sh — a tiny fail-fast pipeline.
#
# It runs four stages in order: lint, test, build, deploy. Each stage prints a
# line and "passes" — unless you name a stage as the first argument, in which
# case that stage fails, the pipeline stops there, and the later stages never
# run. This demonstrates the core property of a real CI pipeline: fail fast.
#
# Usage:
# bash pipeline.sh # every stage passes, exit 0
# bash pipeline.sh test # the 'test' stage fails, pipeline stops, exit 1
#
# Runs offline; writes nothing outside its own console output.
set -uo pipefail
fail_stage="${1:-none}" # the stage to simulate a failure at (default: none)
run_stage() {
local name="$1"
echo "==> stage: ${name}"
if [ "${name}" = "${fail_stage}" ]; then
echo " ${name}: FAILED" >&2
return 1
fi
return 0
}
for stage in lint test build deploy; do
if ! run_stage "${stage}"; then
echo "Pipeline stopped at '${stage}'. Later stages did not run." >&2
exit 1
fi
done
echo "Pipeline succeeded: every stage green."
exit 0
examples/pre-commit (1416 bytes)
#!/usr/bin/env bash
# pre-commit — a quality gate git runs before recording a commit.
#
# It inspects only the files staged for this commit and rejects the commit
# (by exiting non-zero) if any of them has a problem. This is the reference
# hook installed by hook_demo.sh and by the test suite. It runs offline and
# touches nothing outside the repository it is invoked in.
#
# Checks:
# 1. trailing whitespace on any line of a staged text file
# 2. shell syntax errors in staged *.sh files (via `bash -n`, which parses
# a script without running it)
set -u
fail=0
# List files staged for this commit that were Added, Copied, or Modified.
staged="$(git diff --cached --name-only --diff-filter=ACM)"
for f in ${staged}; do
[ -f "${f}" ] || continue
# Check 1: trailing whitespace (one or more spaces/tabs at end of a line).
if grep -nE '[ ]+$' "${f}" >/dev/null 2>&1; then
echo "pre-commit: trailing whitespace in ${f}" >&2
fail=1
fi
# Check 2: shell syntax for *.sh files.
case "${f}" in
*.sh)
if ! bash -n "${f}" 2>/dev/null; then
echo "pre-commit: shell syntax error in ${f}" >&2
fail=1
fi
;;
esac
done
if [ "${fail}" -ne 0 ]; then
echo "pre-commit: commit rejected." >&2
echo "pre-commit: fix the issues above, or bypass with 'git commit --no-verify' (discouraged)." >&2
exit 1
fi
echo "pre-commit: quality gate passed."
exit 0
metadata.yml (639 bytes)
lesson_id: D041
day: 41
kind: shell-scripting
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-041-thinking-in-automation-scripts-hooks-and
run_commands:
- bash examples/hook_demo.sh
- bash examples/pipeline.sh
- bash examples/pipeline.sh test
- bash starter/hook_demo.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- '# nothing to clean: every script works in a self-deleting temp directory'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 6 checks, 0 failure(s)'
requirements/README.md (865 bytes)
# Dependencies — Day 041 lab
**Only a POSIX shell and git.** This lab deliberately has zero installable
dependencies beyond what a developer machine already has:
- `bash` ≥ 3.2 — preinstalled on macOS and every mainstream Linux distribution.
- `git` — the version-control tool from Week 5. Check with `git --version`.
- Standard utilities used by the scripts: `mktemp`, `grep`, `printf`, `chmod`,
and `bash -n` — all part of the base system.
No Python, Node, package installs, network access, API keys, or `sudo` are
required. Every script runs entirely inside a temporary directory it creates and
deletes, so it changes nothing on your machine.
If `git` is missing:
- **macOS:** `xcode-select --install` (installs the command-line developer tools, including git).
- **Debian/Ubuntu:** `sudo apt install git`.
- **Fedora:** `sudo dnf install git`.
starter/automation-worksheet.md (1841 bytes)
# Automation worksheet — Day 041
Fill this in as you work through the lab and the lesson. Keep it: the Week 6
project reuses these decisions.
## 1. What does your pre-commit hook check?
List every check your `pre-commit` hook performs, and state exactly what makes a
commit fail each one.
- Check 1: _______________________________________________
- A commit fails it when: ______________________________
- Check 2: _______________________________________________
- A commit fails it when: ______________________________
Which single line in the hook actually causes git to abort the commit?
- Answer: ________________________________________________
## 2. One thing you WOULD automate in this course's workflow
Name a real, repeated task in how you work through this course (for example:
running the day's tests before committing, or reformatting your notes).
- Task: __________________________________________________
- Which rung of the ladder fits it (script / scheduled / hook / CI)? _________
- Why that rung, and not a higher or lower one? __________
________________________________________________________
## 3. One thing you would NOT automate
Name a task you have decided to leave manual, and justify it with the heuristic
from the lesson (rare? steps keep changing? needs human judgment? irreversible?).
- Task: __________________________________________________
- Why leave it manual: ___________________________________
________________________________________________________
## 4. Reflection (2–3 sentences)
Where would you draw the line between what your local hook checks and what a
shared CI pipeline checks, and why?
- Answer: ________________________________________________
________________________________________________________
________________________________________________________
starter/hook_demo.sh (3779 bytes)
#!/usr/bin/env bash
# hook_demo.sh (STARTER) — build your own pre-commit hook and pipeline.
#
# This skeleton already sets up a throwaway git repository and installs a hook
# with ONE check (trailing whitespace). Complete the four numbered exercises
# below. The finished reference version is examples/hook_demo.sh — try the
# exercises yourself first, then compare.
#
# Run it at any time to see your progress:
# bash starter/hook_demo.sh
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
work="$(mktemp -d 2>/dev/null || mktemp -d -t hookstart)"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
cd "${work}"
git init -q
git config user.email "learner@example.local"
git config user.name "Automation Learner"
mkdir -p .git/hooks
# -------------------------------------------------------------------------
# EXERCISE 1: write a hook check.
# The hook below already rejects trailing whitespace. Add a SECOND check that
# rejects any staged *.sh file with a shell syntax error. Inside the `case`
# block, use `bash -n "${f}"` (it parses a script without running it) and set
# `fail=1` plus an explanatory message if it fails. Uncomment and complete the
# marked lines.
# -------------------------------------------------------------------------
cat > .git/hooks/pre-commit <<'HOOK'
#!/usr/bin/env bash
set -u
fail=0
staged="$(git diff --cached --name-only --diff-filter=ACM)"
for f in ${staged}; do
[ -f "${f}" ] || continue
# Check 1 (provided): trailing whitespace.
if grep -nE '[ ]+$' "${f}" >/dev/null 2>&1; then
echo "pre-commit: trailing whitespace in ${f}" >&2
fail=1
fi
# Check 2 (EXERCISE 1): shell syntax for *.sh files.
case "${f}" in
*.sh)
# if ! bash -n "${f}" 2>/dev/null; then
# echo "pre-commit: shell syntax error in ${f}" >&2
# fail=1
# fi
: # remove this no-op line once you complete the check above
;;
esac
done
if [ "${fail}" -ne 0 ]; then
echo "pre-commit: commit rejected." >&2
exit 1
fi
echo "pre-commit: quality gate passed."
exit 0
HOOK
chmod +x .git/hooks/pre-commit
echo "Hook installed."
# -------------------------------------------------------------------------
# EXERCISE 2: make it block a bad commit.
# Create a file with trailing whitespace, stage it, and try to commit. The hook
# should reject it. The commands are given; run the script to see the result.
# -------------------------------------------------------------------------
printf 'echo "hello" \n' > bad.sh # note the trailing spaces
git add bad.sh
if git commit -q -m "add bad.sh" 2>&1; then
echo "The bad commit was allowed — check your hook."
else
echo "Good: the hook BLOCKED the bad commit."
fi
# -------------------------------------------------------------------------
# EXERCISE 3: fix the file and commit clean.
# Overwrite bad.sh with a clean version (no trailing whitespace), stage it, and
# commit. The hook should allow it.
# -------------------------------------------------------------------------
printf 'echo "hello"\n' > bad.sh
git add bad.sh
if git commit -q -m "add clean script" 2>&1; then
echo "Good: the hook ALLOWED the clean commit."
else
echo "The clean commit was blocked — check your hook."
fi
# -------------------------------------------------------------------------
# EXERCISE 4: add a pipeline stage.
# The stage list below runs lint -> test -> build. Add a fourth stage, "deploy",
# so the list reads: lint test build deploy. Then run the script and watch the
# pipeline print your new stage.
# -------------------------------------------------------------------------
echo "--- pipeline ---"
for stage in lint test build; do # EXERCISE 4: add 'deploy' to this list
echo "==> stage: ${stage}"
done
echo "Pipeline finished."
tests/run_tests.sh (3015 bytes)
#!/usr/bin/env bash
# Tests for the Day 041 lab. Run from anywhere:
# bash tests/run_tests.sh
#
# Verifies real behavior, not just file existence:
# - the pre-commit hook BLOCKS a commit with trailing whitespace (non-zero)
# - the pre-commit hook ALLOWS a clean commit (zero)
# - the pre-commit hook BLOCKS a commit with a shell syntax error
# - the pipeline FAILS FAST on a failing stage (non-zero, later stages skipped)
# - the pipeline SUCCEEDS when every stage passes (zero)
# Offline; needs only git and bash. Exits 0 on success, non-zero on any failure.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# --- Set up a throwaway repo with the reference hook installed. ---
work="$(mktemp -d 2>/dev/null || mktemp -d -t hooktest)"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
cd "${work}"
git init -q
git config user.email "test@example.local"
git config user.name "Test Runner"
mkdir -p .git/hooks
cp "${lab_dir}/examples/pre-commit" .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Testing the pre-commit hook ..."
# 1. Bad commit: trailing whitespace must be rejected.
printf 'echo "hi" \n' > bad.sh
git add bad.sh
if git commit -q -m "bad" >/dev/null 2>&1; then
check "hook blocks a commit with trailing whitespace" "no"
else
check "hook blocks a commit with trailing whitespace" "yes"
fi
# 2. Clean commit: no defects must be allowed.
printf 'echo "hi"\n' > bad.sh
git add bad.sh
if git commit -q -m "clean" >/dev/null 2>&1; then
check "hook allows a clean commit" "yes"
else
check "hook allows a clean commit" "no"
fi
# 3. Syntax-error commit: broken shell must be rejected.
printf 'if [ 1 -eq 1 ]; then\n echo oops\n' > broken.sh # missing 'fi'
git add broken.sh
if git commit -q -m "broken" >/dev/null 2>&1; then
check "hook blocks a commit with a shell syntax error" "no"
else
check "hook blocks a commit with a shell syntax error" "yes"
fi
echo "Testing the pipeline ..."
# 4. Pipeline succeeds when every stage passes.
if bash "${lab_dir}/examples/pipeline.sh" >/dev/null 2>&1; then
check "pipeline succeeds when all stages pass" "yes"
else
check "pipeline succeeds when all stages pass" "no"
fi
# 5. Pipeline fails fast on a failing stage.
if bash "${lab_dir}/examples/pipeline.sh" test >/dev/null 2>&1; then
check "pipeline fails fast on a failing stage" "no"
else
check "pipeline fails fast on a failing stage" "yes"
fi
# 6. Fail fast really skips later stages: build/deploy must NOT appear.
ff_out="$(bash "${lab_dir}/examples/pipeline.sh" test 2>&1)"
if echo "${ff_out}" | grep -q "stage: build"; then
check "later stages are skipped after a failure" "no"
else
check "later stages are skipped after a failure" "yes"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 041 lab
The hook does not run — my bad commit went through
A git hook only runs if it is executable. If you installed the hook by copying a file, make it executable:
chmod +x .git/hooks/pre-commit
Then stage and commit again. This is the single most common hook problem: git
silently ignores a non-executable hook and does not warn you. The lab's demo and
tests set chmod +x for you; you only hit this when installing a hook by hand.
git commit --no-verify skipped my hook — is that a bug?
No. --no-verify deliberately bypasses pre-commit and commit-msg hooks. It
exists for genuine emergencies (for example, committing a work-in-progress fix
while a hook is temporarily broken). Avoid it as a habit, because the whole
point of the hook is to run every time: skip it routinely and broken changes
slip through exactly when you are in a hurry and most likely to make mistakes.
This is also why teams back a local hook with CI — CI runs on a shared server
and cannot be skipped with --no-verify, so it catches what a bypassed hook
missed.
command not found: git
Install git (see requirements/README.md): macOS xcode-select --install;
Debian/Ubuntu sudo apt install git. Verify with git --version.
mktemp: illegal option or a temp-directory error
The scripts try mktemp -d first and fall back to mktemp -d -t hookdemo for
older/BSD variants. If both fail, your mktemp is unusual — set a temp dir
manually and re-run, or run inside WSL on Windows.
The pipeline never fails
pipeline.sh only fails when you name the stage to fail as its first argument:
bash examples/pipeline.sh test fails at test. With no argument, every stage
passes and it exits 0 — that is the intended "all green" run.
My commit is blocked but I cannot see any trailing whitespace
Trailing whitespace is invisible by design. Show it with grep -nE '[ \t]+$' yourfile.sh, or configure your editor to reveal or strip trailing whitespace on
save. The hook is catching real characters at the ends of lines.
Windows: bash is not recognized
Use WSL: run wsl --install, open Ubuntu, and follow the Linux instructions.
Native PowerShell is not supported for this lab because git hooks here are shell
scripts.
Security notes
Security notes — Day 041 lab
-
What the scripts do: create a git repository in a fresh temporary directory (
mktemp -d), install a hook, make commits, run a pipeline, and delete the temporary directory on exit. They make no network connections, need no elevated privileges, and write nothing outside their own temp directory. -
Temp dir only: every script confines its work to a temporary directory it owns and removes. Your real repositories, your global git config, and your home directory are untouched. The scripts set a local git identity (
git config user.email ...without--global) so they never alter your global git settings. -
Hooks run code — review before installing untrusted hooks. A git hook is a program git executes automatically on your machine. Installing a hook (or a CI action, or a pre-commit config) from an untrusted source means running a stranger's code with your permissions, on every commit. Read any hook before you install it. The hooks in this lab are short and commented for exactly that reason; read
examples/pre-commitbefore trusting it. -
--no-verifyis a foot-gun, not a feature to lean on. Bypassing hooks routinely defeats the safety they provide. Use it only in a real emergency, and rely on CI as the backstop that cannot be individually skipped. -
Secrets: this lab uses none, which is the point — automation that needs a password, key, or token must read it from the environment or a secret store, never hard-code it in a script or commit it to a repository, because git history is permanent and copied to every clone.