Computing FoundationsGit and GitHub › Day 35

Hands-on lab — Day 35: Git Workflows for Real Projects

Commands

Setup

cd labs/sections/computing-foundations/day-035-git-workflows-for-real-projects

Run

bash examples/workflow_demo.sh
bash starter/workflow_demo.sh

Test

bash tests/run_tests.sh

File tree

examples/workflow_demo.sh
expected-output/FIELDS.md
expected-output/sample-run.txt
expected-output/tests-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/workflow_demo.sh
starter/workflow-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 035 lab — A Real Git Workflow

Day number: 35 of 365. This lab runs a complete team git workflow end to end — branch, atomic commits, reviewed merge, and a semantic-versioned release tag — entirely on your own machine, with no network and no account.

Lesson

Purpose

Day 35's lesson explains how a team puts git to work: branching strategies, commit hygiene, release tags, and clean history. This lab makes it real. You simulate GitHub Flow against a local bare "origin" — create a short-lived feature branch, make atomic commits with Conventional Commit messages, merge back with --no-ff (as if merging a pull request), and tag a v1.0.0 release with an annotated tag — then read the history you built. It is exactly the workflow the Versioned Notes Repository project asks for.

Learning objectives

  • Run a full GitHub Flow cycle: branch, commit, merge, tag.
  • Write atomic commits with feat: and fix: Conventional Commit messages.
  • Merge a feature branch with --no-ff so the merge is recorded like a pull request.
  • Create an annotated release tag (git tag -a) and choose a semantic version.
  • See a .gitignore keep a file out of git, and contrast atomic with sloppy commits.

Prerequisites

  • The Day 35 lesson (read it first — it explains every concept this lab exercises).
  • Days 29-34: commits, branching and merging, remotes, pull requests, and undoing changes.
  • git (2.23+) and a terminal. No programming experience required; every command is given.

Supported operating systems

  • macOS — fully supported (tested on macOS, Apple Silicon).
  • Linux — fully supported (any distribution with git and bash).
  • Windows — run inside WSL and follow the Linux path; native PowerShell is not supported for these bash scripts.

Hardware requirements

Any computer that can run git. The lab creates a few tiny text files in a temporary directory and needs no special CPU, RAM, disk, or GPU.

Required software

  • git 2.23 or newer (for git switch; older git works with the git checkout equivalents noted in troubleshooting.md).
  • bash 3.2 or newer, plus standard utilities (mktemp, printf, grep, awk, sed) — all preinstalled.

Free and open-source options

Everything here is free and open source: git and every command used ship with your OS or are open-source tools. No account, key, or purchase is needed, and the lab never touches the network — the "remote" is a local bare repository.

Installation

None beyond git. Clone the repository (or copy this directory) and change into it:

cd labs/sections/computing-foundations/day-035-git-workflows-for-real-projects

File structure

day-035-git-workflows-for-real-projects/
├── README.md                        ← you are here
├── metadata.yml                     ← machine-readable lab metadata
├── starter/
│   ├── workflow_demo.sh             ← YOUR working file (5 exercises)
│   └── workflow-worksheet.md        ← record your branch/merge/tag/semver steps
├── examples/
│   └── workflow_demo.sh             ← completed reference implementation
├── tests/
│   └── run_tests.sh                 ← automated behavioral checks
├── expected-output/
│   ├── sample-run.txt               ← real captured run of the reference
│   ├── tests-run.txt                ← real captured test run
│   └── FIELDS.md                    ← what a correct run must contain
├── requirements/
│   └── README.md                    ← dependency statement (git + shell)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished workflow execute first
bash examples/workflow_demo.sh

## 2. Your task: complete the five exercises in the starter, then run it
bash starter/workflow_demo.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/workflow_demo.sh — creates a throwaway working repo and a local bare origin in a mktemp directory, sets a repository-local git identity, commits a .gitignore (chore:), branches off main (feat/notes-search), makes a feat: and a fix: atomic commit, merges back with git merge --no-ff, tags v1.0.0 with git tag -a, pushes to the local origin, illustrates atomic-vs-sloppy commits, then prints git tag and git log --oneline --decorate --graph. The temp directory is deleted on exit.
  • bash starter/workflow_demo.sh — the same skeleton with five numbered exercises. Each __FILL_ME_IN__ line names the exact git command to write in its place. Edit the file, then run it.
  • bash tests/run_tests.sh — runs the workflow and checks the resulting history: a feature branch merged into main, an annotated v1.0.0 tag, and feat:/fix: Conventional Commit messages; plus that the starter names all five exercises and that no lab script contains a network URL.

Expected output

See expected-output/sample-run.txt — a real captured run. The history section looks like this (hashes and temp paths will differ):

=== Tags ===
v1.0.0
=== History (main) ===
*   87ff7e2 (HEAD -> main, tag: v1.0.0, origin/main) Merge branch 'feat/notes-search'
|\
| * 7f8bc99 (feat/notes-search) fix: handle empty query without crashing
| * 3c188b4 feat: add case-insensitive note search
|/
* 538d3a1 chore: initialize notes repository with .gitignore
=== Workflow demo complete ===

The merge commit sits on top with v1.0.0 pinned to it; the feat: and fix: commits are on the merged branch below; the initial chore: commit is at the base. See expected-output/FIELDS.md for the full list of what every correct run must contain.

Validation steps

  1. Run bash starter/workflow_demo.sh after completing the exercises — it must exit without errors and print the Tags and History sections.
  2. Confirm the History shows a merge commit at the top (proof of --no-ff).
  3. Confirm the Tags section lists v1.0.0 and the log shows tag: v1.0.0.
  4. Confirm you see one feat: and one fix: commit message.
  5. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line: 8 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. It needs no network.

Cleanup

Nothing to clean up: both scripts do all their work inside a mktemp directory and delete it on exit. To reset your edited starter, restore it from git: git checkout -- starter/workflow_demo.sh.

Troubleshooting

See troubleshooting.md for the full list (git version and git switch, identity errors, fast-forward merges, annotated vs lightweight tags, deleting a tag, and WSL notes).

Security notes

See security.md. Short version: everything runs in a temporary directory that is deleted on exit, there are no network calls or credentials, your global git identity is never modified, and the fake .env is a teaching prop that is never committed.

Extension exercises

  1. Add a second feature branch, merge it with --no-ff, and cut a v1.1.0 tag — then note why the bump is MINOR, not PATCH or MAJOR.
  2. Generate a changelog by hand: git log --oneline and group commits under Features (feat:) and Fixes (fix:), as changelog tools do.
  3. Inspect the two tag kinds: create a lightweight tag (git tag tmp) and compare git show tmp with git show v1.0.0 to see what the annotation adds.
  • Previous day: Day 34 — Undoing Things: Reset, Revert, and Reflog (labs/sections/computing-foundations/day-034-undoing-things-reset-revert-and-reflog/).
  • Next day: Day 36 — Choosing and Configuring a Code Editor (labs/sections/computing-foundations/day-036-choosing-and-configuring-a-code-editor/, to be written).

Expected output

FIELDS.md

# Expected output — Day 035 lab

This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, git 2.50.x, 2026-07-12). Commit hashes, the temporary
directory name, and the run date will differ on your machine — that is
expected. What must be the same is the *shape* of the result.

## `sample-run.txt`

A full run of `examples/workflow_demo.sh`. A correct run always shows, in order:

1. `=== 1. Initial commit with a .gitignore ===` and a line confirming that
   `.env` is on disk but ignored by git (not tracked).
2. `=== 2. ... ===` with `on branch: feat/notes-search`.
3. `=== 3. ... ===` — two atomic commits are made (no output of their own).
4. `=== 4. ... ===` — the feature branch is merged into `main` with `--no-ff`.
5. `=== 5. ... ===` — an annotated `v1.0.0` tag is created and pushed to the
   local bare `origin`.
6. An atomic-vs-sloppy illustration on a scratch branch.
7. `=== Tags ===` listing `v1.0.0`.
8. `=== History (main) ===` — a `git log --oneline --decorate --graph` showing
   a merge commit at the top carrying `tag: v1.0.0`, the `feat:` and `fix:`
   commits underneath on the merged branch, and the `chore:` initial commit at
   the base.
9. `=== Workflow demo complete ===`.

## `tests-run.txt`

A full run of `bash tests/run_tests.sh`. It ends with `8 checks, 0 failure(s).`
and exits 0. The checks assert that the reference produced a merged feature
branch, an annotated `v1.0.0` tag, and `feat:`/`fix:` Conventional Commit
messages, that the starter names all five exercises, and that no lab script
contains a network URL (the lab is entirely local).

## Platform notes

- **Linux:** identical output. The scripts use only portable git and shell
  features and set a repository-local git identity, so nothing depends on macOS.
- **Windows:** run inside WSL and the output matches the Linux case. Native
  PowerShell is not supported for these bash scripts.
- The scripts create everything under a `mktemp` directory and delete it on
  exit, so no files are left on your machine after a run.

sample-run.txt

=== 1. Initial commit with a .gitignore ===
  .env is present on disk but correctly ignored by git (not tracked)

=== 2. Create a short-lived feature branch (GitHub Flow) ===
  on branch: feat/notes-search

=== 3. Two atomic commits with Conventional Commit messages ===

=== 4. Merge back to main with --no-ff (like merging a pull request) ===
  merged; main now has a merge commit recording the reviewed branch

=== 5. Tag an annotated release and push everything to origin ===

=== Atomic vs sloppy commit (illustration on a scratch branch) ===
  Atomic history on this branch:
    a2f33af refactor: add tidy() helper
    682c5bf feat: add MAX_RESULTS setting
  (A sloppy alternative would cram both changes into one 'stuff' commit,
   which you could not revert or bisect independently.)

=== Tags ===
v1.0.0
=== History (main) ===
*   87ff7e2 (HEAD -> main, tag: v1.0.0, origin/main) Merge branch 'feat/notes-search'
|\  
| * 7f8bc99 (feat/notes-search) fix: handle empty query without crashing
| * 3c188b4 feat: add case-insensitive note search
|/  
* 538d3a1 chore: initialize notes repository with .gitignore
=== Workflow demo complete ===

tests-run.txt

Testing <repo>/labs/sections/computing-foundations/day-035-git-workflows-for-real-projects/examples/workflow_demo.sh ...
  ok: script exits successfully
  ok: feature branch merged into main (merge commit present)
  ok: annotated v1.0.0 tag pinned in history
  ok: git tag lists v1.0.0
  ok: has a Conventional Commit feat: message
  ok: has a Conventional Commit fix: message
Testing starter/workflow_demo.sh ...
  Note: starter still has unfilled exercises — checking structure only.
  ok: starter names all five exercises
  ok: lab scripts contain no network URLs

8 checks, 0 failure(s).

Source files

examples/workflow_demo.sh (4204 bytes)
#!/usr/bin/env bash
# Day 035 lab — completed reference implementation: a real git workflow.
#
# Simulates GitHub Flow entirely on your own machine, with no network and no
# account. It creates a throwaway working repository and a local bare "origin"
# in a temporary directory, sets a LOCAL git identity (never touching your
# global config), then:
#   1. commits an initial .gitignore  (chore:)
#   2. branches off main               (feat/notes-search)
#   3. makes two atomic commits        (feat: , fix:)
#   4. merges back with --no-ff        (like merging a pull request)
#   5. tags an annotated release       (git tag -a v1.0.0)
# It also demonstrates that an ignored file stays out of git, and contrasts an
# atomic commit sequence with a sloppy one. The temp directory is removed on
# exit, so the demo leaves nothing behind.
set -euo pipefail

# --- Create an isolated workspace and clean it up on exit -------------------
work_root="$(mktemp -d "${TMPDIR:-/tmp}/day035-demo.XXXXXX")"
cleanup() { rm -rf "${work_root}"; }
trap cleanup EXIT

origin="${work_root}/origin.git"   # local bare repo standing in for a remote
repo="${work_root}/notes"          # our working clone

# --- A local bare repo acts as "origin" (no network involved) ---------------
git init --quiet --bare -b main "${origin}"
git clone --quiet "${origin}" "${repo}" 2>/dev/null
cd "${repo}"

# LOCAL identity only: these writes stay inside ${repo}/.git/config and never
# alter your machine's global git identity.
git config user.name  "Workflow Demo"
git config user.email "demo@example.invalid"

echo "=== 1. Initial commit with a .gitignore ==="
printf '%s\n' 'node_modules/' '*.log' '.env' > .gitignore
printf '%s\n' '# Course Notes' '' 'Notes for the 365 Days of AI course.' > README.md
git add .gitignore README.md
git commit --quiet -m "chore: initialize notes repository with .gitignore"

# Prove .gitignore works: a matching file must NOT appear in git status.
echo 'SECRET_KEY=do-not-commit-me' > .env
if git status --porcelain | grep -q '\.env'; then
  echo "  UNEXPECTED: .env showed up in git status" >&2
else
  echo "  .env is present on disk but correctly ignored by git (not tracked)"
fi

echo
echo "=== 2. Create a short-lived feature branch (GitHub Flow) ==="
git switch --quiet -c feat/notes-search
echo "  on branch: $(git branch --show-current)"

echo
echo "=== 3. Two atomic commits with Conventional Commit messages ==="
printf '%s\n' 'def search(notes, query):' '    return [n for n in notes if query.lower() in n.lower()]' > search.py
git add search.py
git commit --quiet -m "feat: add case-insensitive note search"

printf '%s\n' 'def search(notes, query):' '    if not query:' '        return []' '    return [n for n in notes if query.lower() in n.lower()]' > search.py
git add search.py
git commit --quiet -m "fix: handle empty query without crashing"

echo
echo "=== 4. Merge back to main with --no-ff (like merging a pull request) ==="
git switch --quiet main
git merge --no-ff --quiet -m "Merge branch 'feat/notes-search'" feat/notes-search
echo "  merged; main now has a merge commit recording the reviewed branch"

echo
echo "=== 5. Tag an annotated release and push everything to origin ==="
git tag -a v1.0.0 -m "First release: note search"
git push --quiet origin main
git push --quiet origin v1.0.0

echo
echo "=== Atomic vs sloppy commit (illustration on a scratch branch) ==="
git switch --quiet -c demo/atomic-vs-sloppy
# Atomic: two separate logical changes -> two commits you can revert or bisect.
echo 'MAX_RESULTS = 50' > config.py
git add config.py
git commit --quiet -m "feat: add MAX_RESULTS setting"
printf '%s\n' 'def tidy(s):' '    return s.strip()' > utils.py
git add utils.py
git commit --quiet -m "refactor: add tidy() helper"
echo "  Atomic history on this branch:"
git log --oneline -2 | sed 's/^/    /'
git switch --quiet main
git branch -D demo/atomic-vs-sloppy >/dev/null 2>&1
echo "  (A sloppy alternative would cram both changes into one 'stuff' commit,"
echo "   which you could not revert or bisect independently.)"

echo
echo "=== Tags ==="
git tag
echo "=== History (main) ==="
git log --oneline --decorate --graph
echo "=== Workflow demo complete ==="
metadata.yml (585 bytes)
lesson_id: D035
day: 35
kind: shell-scripting
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-035-git-workflows-for-real-projects
run_commands:
  - bash examples/workflow_demo.sh
  - bash starter/workflow_demo.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - '# nothing to clean up: scripts work only in a mktemp dir removed on exit'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), git 2.50.x, bash tests/run_tests.sh → 8 checks, 0 failure(s)'
requirements/README.md (895 bytes)
# Dependencies — Day 035 lab

**Just git and a POSIX shell.** This lab has no installable dependencies:

- `git` (2.23 or newer — for the `git switch` command; any git from the last
  few years works). Check with `git --version`.
- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution).
- Standard OS utilities used by the scripts: `mktemp`, `printf`, `grep`, `awk`,
  `sed` — all part of the base system.

There is no `requirements.txt` or `package.json`, no account, and no API key.
Every git operation runs against a **local bare repository** created in a
temporary directory, so the lab needs **no network connection** at all.

If `git` is missing:

- **macOS:** `xcode-select --install` (installs the command-line tools), or
  install git from your package manager of choice.
- **Debian/Ubuntu:** `sudo apt install git`.
- **Fedora:** `sudo dnf install git`.
starter/workflow_demo.sh (3645 bytes)
#!/usr/bin/env bash
# Day 035 lab — YOUR working file: run a real git workflow yourself.
#
# This starter builds the same isolated, network-free setup as the reference
# (a throwaway working repo plus a local bare "origin" in a temp directory,
# with a LOCAL git identity so your global config is never touched). Your job
# is to complete the five numbered exercises below, replacing each
#   : "__FILL_ME_IN__ exercise N ..."   # a harmless no-op placeholder line
# with the real git command named in its comment. Try it before peeking at
# examples/workflow_demo.sh.
#
# Run it with:   bash starter/workflow_demo.sh
# Check it with: bash tests/run_tests.sh
set -euo pipefail

# --- Isolated workspace, cleaned up on exit (already done for you) ----------
work_root="$(mktemp -d "${TMPDIR:-/tmp}/day035-starter.XXXXXX")"
cleanup() { rm -rf "${work_root}"; }
trap cleanup EXIT

origin="${work_root}/origin.git"
repo="${work_root}/notes"

git init --quiet --bare -b main "${origin}"
git clone --quiet "${origin}" "${repo}" 2>/dev/null
cd "${repo}"
git config user.name  "Workflow Learner"
git config user.email "learner@example.invalid"

echo "=== 1. Initial commit with a .gitignore ==="
printf '%s\n' 'node_modules/' '*.log' '.env' > .gitignore
printf '%s\n' '# Course Notes' '' 'Notes for the 365 Days of AI course.' > README.md
git add .gitignore README.md
# Exercise 1: make the initial commit with a chore: Conventional Commit message.
#   Use: git commit -m "chore: initialize notes repository with .gitignore"
: "__FILL_ME_IN__ exercise 1 — replace this line with the git commit command above"

echo
echo "=== 2. Create a short-lived feature branch (GitHub Flow) ==="
# Exercise 2: create AND switch to a feature branch named feat/notes-search.
#   Use: git switch -c feat/notes-search
: "__FILL_ME_IN__ exercise 2 — replace this line with the git switch command above"
echo "  on branch: $(git branch --show-current)"

echo
echo "=== 3. Two atomic commits with Conventional Commit messages ==="
printf '%s\n' 'def search(notes, query):' '    return [n for n in notes if query.lower() in n.lower()]' > search.py
git add search.py
# Exercise 3a: commit this as a feature.
#   Use: git commit -m "feat: add case-insensitive note search"
: "__FILL_ME_IN__ exercise 3a — replace this line with the feat: commit command above"
printf '%s\n' 'def search(notes, query):' '    if not query:' '        return []' '    return [n for n in notes if query.lower() in n.lower()]' > search.py
git add search.py
# Exercise 3b: commit the bug fix separately (this is what makes it atomic).
#   Use: git commit -m "fix: handle empty query without crashing"
: "__FILL_ME_IN__ exercise 3b — replace this line with the fix: commit command above"

echo
echo "=== 4. Merge back to main with --no-ff (like merging a pull request) ==="
git switch --quiet main
# Exercise 4: merge the feature branch WITHOUT fast-forward so a merge commit
# is recorded (the shape of a merged pull request).
#   Use: git merge --no-ff -m "Merge branch 'feat/notes-search'" feat/notes-search
: "__FILL_ME_IN__ exercise 4 — replace this line with the git merge --no-ff command above"

echo
echo "=== 5. Tag an annotated release ==="
# Exercise 5: create an ANNOTATED tag for the first release. Choose the semver
# deliberately and record your reasoning in starter/workflow-worksheet.md.
#   Use: git tag -a v1.0.0 -m "First release: note search"
: "__FILL_ME_IN__ exercise 5 — replace this line with the git tag -a command above"

echo
echo "=== Tags ==="
git tag
echo "=== History (main) ==="
git log --oneline --decorate --graph
echo "=== Workflow demo complete ==="
starter/workflow-worksheet.md (2152 bytes)
# Workflow worksheet — Day 035

Fill this in as you complete `starter/workflow_demo.sh` (or as you run the
commands by hand in your own throwaway repository). It is the record of the
GitHub Flow cycle you ran, and it feeds the Versioned Notes Repository project.

## 1. Branch

- **Feature branch name I created:** `______________________`
  (Convention used in this lab: `feat/notes-search`. A good branch name is
  short and says what the work is.)
- **Command I used to create and switch to it:**

  ```bash
  ______________________________________________
  ```

## 2. Atomic commits (Conventional Commits)

Record each commit message you wrote. At least one `feat:` and one `fix:`.

| # | Type | Full commit message |
| - | ---- | ------------------- |
| 1 | chore | `chore: initialize notes repository with .gitignore` |
| 2 | feat  | `______________________________________________` |
| 3 | fix   | `______________________________________________` |

Why is each of these commits *atomic*? (One sentence.)

> ______________________________________________________________

## 3. Merge (like merging a pull request)

- **Merge command I used:**

  ```bash
  ______________________________________________
  ```

- **Why `--no-ff`?** (One sentence — what does the merge commit record?)

  > ______________________________________________________________

## 4. Release tag

- **Tag command I used:**

  ```bash
  ______________________________________________
  ```

- **Annotated or lightweight? Why that choice?**

  > ______________________________________________________________

## 5. The semantic version I chose — and why

I chose the version **`v______`**.

Justify each part as if explaining it to a teammate who will depend on the
number (2–4 sentences):

- **MAJOR (___):** ______________________________________________
- **MINOR (___):** ______________________________________________
- **PATCH (___):** ______________________________________________

## 6. Reflection

If you added a backward-compatible feature next, what would the version become,
and why?

> ______________________________________________________________
tests/run_tests.sh (3230 bytes)
#!/usr/bin/env bash
# Tests for the Day 035 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Runs the workflow scripts (each in its own throwaway, network-free repo) and
# verifies the resulting history: a feature branch was merged into main, an
# annotated v1.0.0 tag exists, and the commits follow the feat:/fix:
# Conventional Commits convention. 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
}

# A workflow script proves its behavior through the history it prints. We run
# it, capture output, and assert on the merge, the tag, and the commit types.
run_workflow_checks() {
  local script="$1" output
  echo "Testing ${script} ..."
  if ! output="$(bash "${script}" 2>&1)"; then
    check "script exits successfully" "no"
    echo "${output}" | sed 's/^/    /'
    return
  fi
  check "script exits successfully" "yes"

  echo "${output}" | grep -q "Merge branch 'feat/notes-search'" \
    && check "feature branch merged into main (merge commit present)" "yes" \
    || check "feature branch merged into main (merge commit present)" "no"

  echo "${output}" | grep -q "tag: v1.0.0" \
    && check "annotated v1.0.0 tag pinned in history" "yes" \
    || check "annotated v1.0.0 tag pinned in history" "no"

  echo "${output}" | awk '/^=== Tags ===/{f=1;next} /^===/{f=0} f' | grep -qx "v1.0.0" \
    && check "git tag lists v1.0.0" "yes" \
    || check "git tag lists v1.0.0" "no"

  echo "${output}" | grep -q "feat: add case-insensitive note search" \
    && check "has a Conventional Commit feat: message" "yes" \
    || check "has a Conventional Commit feat: message" "no"

  echo "${output}" | grep -q "fix: handle empty query without crashing" \
    && check "has a Conventional Commit fix: message" "yes" \
    || check "has a Conventional Commit fix: message" "no"
}

# 1) The reference implementation must satisfy every behavioral check.
run_workflow_checks "${lab_dir}/examples/workflow_demo.sh"

# 2) The starter: only run it once the learner has completed the exercises;
#    while placeholders remain it cannot build a valid repo, so we check
#    structure instead.
echo "Testing starter/workflow_demo.sh ..."
if grep -q '__FILL_ME_IN__' "${lab_dir}/starter/workflow_demo.sh"; then
  echo "  Note: starter still has unfilled exercises — checking structure only."
  count="$(grep -c 'Exercise [0-9]' "${lab_dir}/starter/workflow_demo.sh")"
  [ "${count}" -ge 5 ] \
    && check "starter names all five exercises" "yes" \
    || check "starter names all five exercises" "no"
else
  run_workflow_checks "${lab_dir}/starter/workflow_demo.sh"
fi

# 3) Hard rule: lab scripts stay fully local — no network URLs anywhere.
if grep -rEn 'https?://' "${lab_dir}/examples" "${lab_dir}/starter" "${lab_dir}/tests" >/dev/null 2>&1; then
  check "lab scripts contain no network URLs" "no"
else
  check "lab scripts contain no network URLs" "yes"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 035 lab

git: command not found

Git is not installed or not on your PATH. See requirements/README.md for install instructions, then re-run. Confirm with git --version.

error: unknown switch or git switch not recognized

Your git is older than 2.23. Either update git, or replace git switch -c NAME with git checkout -b NAME and git switch NAME with git checkout NAME in the scripts — the behavior is identical.

Author identity unknown / a commit is refused

Git needs a name and email to record a commit. The scripts set a local identity inside the temporary repo:

git config user.name  "Workflow Learner"
git config user.email "learner@example.invalid"

These writes go only into the temp repo's .git/config — they never change your global identity. If you copied commands out of order and skipped these two lines, the first commit will fail; run them (inside the repo) first.

The merge fast-forwards and no merge commit appears

You omitted --no-ff. Without it, git slides main forward and the branch shape disappears from the log. Always use git merge --no-ff ... when you want the merge recorded as a distinct commit (the shape of a merged pull request).

git tag prints nothing

Either the tag command did not run, or you created it in a way that did not persist. Re-run the annotated form and confirm:

git tag -a v1.0.0 -m "First release"
git tag

Annotated vs lightweight tags

  • Lightweight (git tag v1.0.0): just a name pointing at a commit. No author, date, or message.
  • Annotated (git tag -a v1.0.0 -m "..."): a full object storing the tagger, date, and message. Use annotated tags for releases.

Inspect the difference with git show v1.0.0 (an annotated tag shows the tagger and message header; a lightweight tag jumps straight to the commit).

Deleting a tag

If you tagged the wrong commit or chose the wrong version:

git tag -d v1.0.0                 # delete the local tag
git push origin :refs/tags/v1.0.0 # delete it on a remote you already pushed to

Then re-create the tag on the correct commit. (In this lab the "remote" is the local bare origin, so the second command works offline too.)

The starter script errors out on git log

That is expected while exercises are unfilled: with the placeholders in place, no commits are made, so git log has nothing to show. Complete the five exercises (replace every __FILL_ME_IN__ line) and the script runs cleanly. The test detects the placeholders and checks the starter's structure only until you finish.

Windows: bash is not recognized

Use WSL (wsl --install, then open your Linux distribution and follow the Linux path). These are bash scripts; native PowerShell is not supported here.

Security notes

Security notes — Day 035 lab

  • Everything happens in a temporary directory. Both scripts create their repositories under a mktemp directory (for example /tmp/day035-demo.XXXXXX) and delete it on exit via a trap. Nothing is written to your home directory, your existing repositories, or anywhere permanent.
  • No network, no account, no keys. The "origin" is a local bare repository in that same temp directory. The scripts make no network calls and need no credentials, so there is nothing to leak.
  • Your global git identity is untouched. The scripts set user.name and user.email with a repository-local git config inside the temp repo. Your machine's global git configuration is never read or modified.
  • The .env demonstration is a teaching prop. The scripts create a fake .env file containing an obviously non-real value to show that .gitignore keeps it out of git. It is never committed and is destroyed with the temp directory. This mirrors the real rule: never commit secrets — exclude them with .gitignore, because anything committed lives in history forever, even after a later deletion.
  • Read before you run. Both scripts are short and commented. Reading a script before executing it is a habit this course reinforces; you can see exactly what these do (create files, make commits, delete a temp dir).