diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..f495df8 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,44 @@ +name: Course quality gates + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: course-quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate ${{ matrix.os }} / Python ${{ matrix.python }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python: ["3.12", "3.14"] + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python }} + + - name: Validate course structure and links + run: python scripts/validate_repo.py + + - name: Compile validator + run: python -m py_compile scripts/validate_repo.py + + - name: Verify controlled CI demo + working-directory: examples/actions/demo + run: python -m unittest discover -s tests -v + + - name: Verify Git conflict and reflog behavior + run: python scripts/smoke_git_behaviors.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4743a07 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +Thanks for helping improve this course. + +This repository is a textbook and reference implementation. Learner practice belongs in a separate disposable `git-github-lab` repository unless a contribution specifically improves the curriculum itself. + +## Before opening a change + +1. Read `SAFETY.md` if the change teaches recovery, reset, rebase, force operations, or history rewriting. +2. Keep the course beginner-accessible. Prefer a precise mental model and a small reproducible exercise over adding more commands. +3. Do not add claims of mastery that the learner cannot demonstrate. +4. Do not add secrets, personal tokens, real credentials, or private repository data. +5. Keep external resources supplementary; the core lesson must remain understandable without hunting through tutorials. + +## Required local check + +Run: + +```bash +python scripts/validate_repo.py +``` + +The same validator runs on both Linux and Windows in GitHub Actions. + +## Pull request standard + +A useful pull request should explain: + +- what learner problem it solves +- what behavior or understanding changes +- how the change was tested +- any safety implications + +For changes that add or modify a Git command exercise, include the expected observable repository state before and after the exercise. + +## Course design rule + +The learning loop is: + +> **LEARN → PREDICT → DO → INSPECT → BREAK/RECOVER → EXPLAIN → PROVE** + +Not every lesson needs every stage as a heading, but major capabilities should eventually require evidence rather than passive reading. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..526bad7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Adham Mahmood + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Module_0_Setup/Exercises/.gitkeep b/Module_0_Setup/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_0_Setup/Exercises/README.md b/Module_0_Setup/Exercises/README.md new file mode 100644 index 0000000..cf734e0 --- /dev/null +++ b/Module_0_Setup/Exercises/README.md @@ -0,0 +1,26 @@ +# Module 0 Exercises — Setup and Mental Model + +Use your disposable `git-github-lab` repository. Do not perform these exercises in the course repository. + +## Challenge 1 — Prove your environment + +Without copying commands from the guide, prove that: + +- Git is installed and callable from your terminal +- your Git identity is configured intentionally +- you know which directory you are currently in +- you can distinguish a normal folder from a Git repository + +**Evidence:** record the commands you chose and explain what each output proved. + +## Challenge 2 — Repository boundary + +Create one normal folder and one Git repository. Put a file in each. Predict what `git status` will do in both locations, then test your prediction. + +**Pass when:** you can explain what `.git` represents and why Git commands care about repository boundaries. + +## Challenge 3 — Rebuild from memory + +Delete only your disposable practice repository, recreate it from scratch, initialize Git, create a first file, inspect state, make the first commit, and show the history. + +**Do not advance** if you need a command-by-command recipe to complete this challenge. diff --git a/Module_1_Daily_Core/Exercises/.gitkeep b/Module_1_Daily_Core/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_1_Daily_Core/Exercises/README.md b/Module_1_Daily_Core/Exercises/README.md new file mode 100644 index 0000000..9b9ccda --- /dev/null +++ b/Module_1_Daily_Core/Exercises/README.md @@ -0,0 +1,41 @@ +# Module 1 Exercises — Daily Core + +Work only in your disposable student lab. + +## Challenge 1 — Three states of one file + +Create a file, stage it, then modify it again before committing. + +Before running any corrective command, predict what each of these will show: + +```bash +git status +git diff +git diff --staged +``` + +Explain why the same file can appear in both staged and unstaged state. + +## Challenge 2 — Selective commit + +Create changes in at least three files. Make one focused commit that intentionally includes only part of the work. + +**Evidence:** show the staged diff before the commit and explain why the omitted changes did not belong in it. + +## Challenge 3 — Ignore-rule trap + +Track a harmless fake configuration file, then add its pattern to `.gitignore`. + +Predict whether Git stops tracking it. Test the prediction and explain why `.gitignore` is not a secret-removal mechanism. + +## Boss check — Inspect before acting + +Have another person or future-you make several mixed changes in the lab. Without immediately staging anything, determine: + +- what changed +- what is staged +- what is not staged +- what is untracked +- what belongs in the next commit + +**Pass when:** your first instinct is inspection, not `git add .`. diff --git a/Module_2_Branching/Exercises/.gitkeep b/Module_2_Branching/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_2_Branching/Exercises/README.md b/Module_2_Branching/Exercises/README.md new file mode 100644 index 0000000..27d5b05 --- /dev/null +++ b/Module_2_Branching/Exercises/README.md @@ -0,0 +1,33 @@ +# Module 2 Exercises — Branching + +Use the disposable student lab. + +## Challenge 1 — Predict branch movement + +Create two commits on `main`, create a feature branch, add two more commits there, and draw the commit graph you expect before inspecting it. + +Then run: + +```bash +git log --oneline --graph --decorate --all +``` + +Explain what the branch names point to. + +## Challenge 2 — Fast-forward vs merge commit + +Produce one merge that can fast-forward and one merge where both branches have diverged. + +Before each merge, predict the resulting graph. + +**Pass when:** you can explain why the outcomes differ without describing branches as folders or copies. + +## Challenge 3 — Branch cleanup + +Merge a finished feature, verify the work is reachable from `main`, then delete the local feature branch. + +Explain why deleting the branch name does not delete commits that remain reachable from `main`. + +## Boss check — Unknown graph + +Create a small history with at least three branches and six commits. Leave it for a while, return without notes, inspect it, and explain which work is merged and which work remains isolated before running a merge command. diff --git a/Module_3_Remotes/Exercises/.gitkeep b/Module_3_Remotes/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_3_Remotes/Exercises/README.md b/Module_3_Remotes/Exercises/README.md new file mode 100644 index 0000000..79d084f --- /dev/null +++ b/Module_3_Remotes/Exercises/README.md @@ -0,0 +1,33 @@ +# Module 3 Exercises — Remotes + +Use your disposable student lab plus its GitHub remote. + +## Challenge 1 — Local vs remote truth + +Create a local commit without pushing it. Then inspect: + +```bash +git status +git log --oneline --decorate --graph --all +git branch -vv +``` + +Explain what exists locally and what GitHub does not know yet. + +## Challenge 2 — Remote changes first + +Make a harmless change on GitHub, then return to your local clone. + +Do **not** immediately run `git pull`. + +First fetch and inspect the difference between your local branch and its remote-tracking branch. Decide how to integrate only after you understand the state. + +## Challenge 3 — Two clones + +Clone the same practice repository into two separate folders. Make a commit in clone A and push it. In clone B, inspect the stale state, fetch, explain what changed, then integrate deliberately. + +## Boss check — Divergence diagnosis + +Create one unpublished local commit and one different remote commit so the branch diverges. Before integrating anything, draw the graph and explain your available choices. + +**Pass when:** `fetch → inspect → decide` feels natural and `pull` is no longer a mystery command. diff --git a/Module_4_Collaboration/Exercises/.gitkeep b/Module_4_Collaboration/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_4_Collaboration/Exercises/README.md b/Module_4_Collaboration/Exercises/README.md new file mode 100644 index 0000000..8200a61 --- /dev/null +++ b/Module_4_Collaboration/Exercises/README.md @@ -0,0 +1,33 @@ +# Module 4 Exercises — Collaboration + +Use your disposable lab or a repository where collaboration is welcome. + +## Challenge 1 — Issue to pull request + +Create an Issue describing a small, testable change. Create a branch from current `main`, implement only that scope, push it, and open a pull request that links the Issue. + +**Evidence:** the PR explains what changed, how it was checked, and what remains out of scope. + +## Challenge 2 — Review without rubber-stamping + +Inspect a harmless practice PR as if you were its reviewer. Record at least: + +- one question about intent or behavior, +- one concrete improvement request, +- the final decision you would make after the concern is resolved and why. + +If a real collaborator is available, perform the review on their PR. If you are studying solo, keep the review notes in your lab; GitHub does not allow you to approve your own PR, and this course does not require another person just to pass the exercise. + +Explain the difference between reviewing a change and merely confirming that a diff exists. + +## Challenge 3 — Update the same PR + +Respond to your review feedback by committing to the existing PR branch. Do not open a replacement PR. + +Explain why the pull request updates automatically. + +## Boss check — Fork and upstream + +Fork a practice repository, clone your fork, add the original repository as `upstream`, fetch from it, and show which refs belong to `origin` versus `upstream`. + +**Pass when:** you can describe the contributor workflow without confusing forks, branches, remotes, and pull requests. diff --git a/Module_5_Fixing_Mistakes/Exercises/.gitkeep b/Module_5_Fixing_Mistakes/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_5_Fixing_Mistakes/Exercises/README.md b/Module_5_Fixing_Mistakes/Exercises/README.md new file mode 100644 index 0000000..f2df129 --- /dev/null +++ b/Module_5_Fixing_Mistakes/Exercises/README.md @@ -0,0 +1,30 @@ +# Module 5 Exercises — Fixing Mistakes and Recovery + +Use only your disposable student lab. Read [`../../SAFETY.md`](../../SAFETY.md) before destructive commands. + +## Challenge 1 — Choose the least destructive tool + +Create four separate mistakes: + +- staged the wrong file +- changed a tracked file but want its last committed version back +- committed the right change with the wrong message +- committed something that should be undone after it has been shared + +For each case, state your intended final state **before** choosing a command. + +## Challenge 2 — Stash deliberately + +Create tracked work in progress, stash it, verify the working tree state, make an unrelated commit, then restore the stash and resolve any conflict if one appears. + +Explain what the stash protected and what it did not. + +## Challenge 3 — Recover a displaced commit + +Create a disposable commit, move the branch away from it, then recover it using reflog by creating a recovery branch at the correct commit. + +**Pass when:** you can prove the commit is reachable again and explain why reflog was useful. + +## Boss check — Shared-history decision + +Given a mistake already pushed to a branch another person may have pulled, explain why `revert` is usually safer than rewriting that history. Then demonstrate the safe repair in your practice repository. diff --git a/Module_6_Real_World/Exercises/.gitkeep b/Module_6_Real_World/Exercises/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Module_6_Real_World/Exercises/README.md b/Module_6_Real_World/Exercises/README.md new file mode 100644 index 0000000..4cf8cc1 --- /dev/null +++ b/Module_6_Real_World/Exercises/README.md @@ -0,0 +1,33 @@ +# Module 6 Exercises — Real-World Git + +Use the disposable student lab. Read [`../../SAFETY.md`](../../SAFETY.md) before rewriting history. + +## Challenge 1 — Conflict from divergence + +Create a genuine same-line conflict by changing one committed line differently on two diverged branches. Before resolving it, explain: + +- what `HEAD` represents +- what each conflict side contains +- what final content you intend to keep + +Resolve, commit, and prove the working tree is clean. + +## Challenge 2 — Rebase boundary + +Rebase an unpublished feature branch onto updated `main`. Compare commit IDs before and after. + +Then explain why doing the same rewrite to a shared branch can disrupt collaborators. + +## Challenge 3 — Release evidence + +Create an annotated version tag in your lab, inspect it, push it, and create a GitHub Release with concise notes tied to actual changes. + +## Challenge 4 — Controlled CI lab + +Complete the self-contained exercise in [`../../examples/actions/README.md`](../../examples/actions/README.md). You must be able to explain the workflow trigger, job, steps, and why the intentionally failing test blocks the check. + +## Boss check — Disaster lab + +Combine at least three failures in one practice repository: a merge conflict, remote divergence, and a displaced commit recoverable through reflog. Diagnose before repairing. + +**Pass when:** you can narrate repository state first, choose the least destructive appropriate operation, and verify the final graph and working tree. diff --git a/Module_6_Real_World/Module_6_Guide.md b/Module_6_Real_World/Module_6_Guide.md index 1d689fa..8d997f1 100644 --- a/Module_6_Real_World/Module_6_Guide.md +++ b/Module_6_Real_World/Module_6_Guide.md @@ -100,6 +100,8 @@ Rebase creates new commit identities for the replayed commits. Therefore the imp Rebasing your own feature/PR branch is common in many teams. Rebasing a shared stable branch is a very different risk. +If a team workflow explicitly allows you to rewrite your own published feature branch, `git push --force-with-lease` is safer than raw `--force` because it refuses to overwrite unexpected remote work. It is still a history rewrite and should not be used casually. + ### DO Create divergence: @@ -178,23 +180,30 @@ A workflow normally lives under: .github/workflows/*.yml ``` -At this stage you do not need to become a CI engineer. You need to be able to open a workflow and identify: +This course uses its own workflow at [`../.github/workflows/quality.yml`](../.github/workflows/quality.yml). It validates the repository on both Linux and Windows and also proves that the controlled CI demo still runs. + +At this stage you do not need to become a CI engineer. You do need to identify: -- what triggers it (`on`) +- what triggers a workflow (`on`) - its jobs (`jobs`) +- the runner used by each job - the steps each job performs (`steps`) -- whether a failed check should stop a merge +- the command that determines whether a check passes +- why a failed required check should block a merge A professional GitHub profile is useful too, but profile cosmetics are not evidence of engineering ability. Strong repositories, clear READMEs, useful commits, and real contributions matter more. -### DO -1. Open a workflow from one of your own or a reputable public repository. -2. Identify the trigger, jobs, and major steps. -3. Find a commit or pull request with automated checks and inspect what passed/failed. -4. If you want a profile README, create a public repo named exactly your username and add a truthful README. Do not claim technologies you have not actually used. +### DO — controlled lab first + +1. Open this course's [quality workflow](../.github/workflows/quality.yml) and identify its trigger, job, matrix, and validation commands. +2. Complete the self-contained [`examples/actions`](../examples/actions/README.md) lab in your disposable `git-github-lab` repository. +3. Make the demo workflow pass. +4. Break the demo test deliberately, push it, and inspect the red check. +5. Repair it and verify the check returns to green. +6. Inspect one additional workflow from a reputable project only after you understand the controlled example. ### TRANSITION CONDITION -You can explain what CI/Actions does, locate a workflow, identify its trigger/jobs/steps, and explain why a green check is useful but does not prove software correctness by itself. +You can explain what CI/Actions does, read a basic workflow, produce both a passing and intentionally failing check in your own lab, and explain why green CI proves only the checks that actually ran. --- @@ -203,7 +212,8 @@ You can explain what CI/Actions does, locate a workflow, identify its trigger/jo - [ ] You can deliberately create and resolve a real merge conflict - [ ] You understand divergence rather than treating conflicts as random errors - [ ] You can rebase your own branch and explain the shared-history boundary +- [ ] You can explain when `--force-with-lease` is safer than `--force` and why both still rewrite history - [ ] You can tag and release a version deliberately -- [ ] You can read a basic GitHub Actions workflow +- [ ] You can read and deliberately break/repair a basic GitHub Actions workflow -Then complete **Gate 6 — Real-World Git / Disaster Lab** in [`../ASSESSMENTS.md`](../ASSESSMENTS.md). Pass it before beginning the capstone. \ No newline at end of file +Then complete **Gate 6 — Real-World Git / Disaster Lab** in [`../ASSESSMENTS.md`](../ASSESSMENTS.md). Pass it before beginning the capstone. diff --git a/README.md b/README.md index 0e47b59..c88a5d5 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ Every module ends in evidence, not just reading. | **3 — Remotes** | clone, push, fetch, pull, tracking, remote/local relationships | | **4 — Collaboration** | forks, PRs, reviews, Issues, contribution etiquette | | **5 — Recovery** | restore, stash, amend, revert, reset, reflog, recovery branches | -| **6 — Real World** | conflicts, divergence, rebase boundaries, tags/releases, CI awareness | +| **6 — Real World** | conflicts, divergence, rebase boundaries, tags/releases, controlled CI | | **Capstone** | complete a real contribution workflow and demonstrate recovery skills | -The original course contains 28 lessons. v2 keeps the accessible lesson-based structure while strengthening technical accuracy, safety, cumulative assessment, and professional workflow. +The course contains 28 lessons, module challenge sheets, six cumulative gates plus capstone assessment, and a self-contained GitHub Actions lab. ## Important: use a separate practice repository @@ -68,9 +68,21 @@ You should be able to: - use reflog to recover displaced committed work - understand the shared-history boundary for rebase/reset/force operations - tag a version and understand GitHub Releases -- read a basic GitHub Actions workflow +- read, run, deliberately fail, and repair a basic GitHub Actions workflow - make a small, respectful open-source contribution +## The repository practices what it teaches + +This course has its own cross-platform GitHub Actions quality gate. On every push and pull request it checks the required course structure, all 28 lesson logs, module exercise sheets, local Markdown links, file hygiene, and the runnable CI demo. + +Run the same core validation locally: + +```bash +python scripts/validate_repo.py +``` + +The controlled Actions lab lives at [`examples/actions/README.md`](examples/actions/README.md). + ## Core commands are not the course You will use these constantly: @@ -95,6 +107,12 @@ But competence comes from knowing **what state they change and why**, not from m - [Pro Git](https://git-scm.com/book/en/v2) — free official Git book - [GitHub Docs](https://docs.github.com/en/get-started) — GitHub workflows and product documentation +## Contributing and license + +Contributions should preserve the inspection-first, evidence-based learning model. See [`CONTRIBUTING.md`](CONTRIBUTING.md). + +Licensed under the [`MIT License`](LICENSE). + ## Begin Open **[`START_HERE.md`](START_HERE.md)** and create your student lab before Lesson 1. diff --git a/START_HERE.md b/START_HERE.md index c7d6939..f891acb 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -26,15 +26,20 @@ All practice, deliberate mistakes, lesson logs, branches, conflicts, resets, and See **[STUDENT_LAB.md](STUDENT_LAB.md)** for the exact setup. -## 3. How every lesson works +## 3. How the course works -Each lesson uses this loop: +Use this loop: -> **LEARN → PREDICT → DO → INSPECT → BREAK/RECOVER → EXPLAIN → TRANSITION CHECK** +> **LEARN → PREDICT → DO → INSPECT → BREAK/RECOVER → EXPLAIN → PROVE** -A lesson is finished only when you can pass its Transition Condition without copying commands from the guide. +For each module: -When a lesson contains a destructive command, first read **[SAFETY.md](SAFETY.md)**. +1. work through the module guide in your student lab, +2. complete the module's `Exercises/README.md` challenges, +3. complete the matching cumulative gate in [ASSESSMENTS.md](ASSESSMENTS.md), +4. advance only when you can do the required work without copying a recipe. + +When a task contains a destructive or history-rewriting command, first read **[SAFETY.md](SAFETY.md)**. ## 4. Course order @@ -44,7 +49,7 @@ When a lesson contains a destructive command, first read **[SAFETY.md](SAFETY.md 4. **Module 3 — Remotes**: clone, push, fetch, pull, upstream tracking, divergence 5. **Module 4 — Collaboration**: forks, pull requests, review, issues, contribution etiquette 6. **Module 5 — Recovery**: restore, stash, amend, revert, reset, reflog -7. **Module 6 — Real World**: conflicts, rebase, tags/releases, Actions awareness +7. **Module 6 — Real World**: conflicts, rebase, tags/releases, controlled GitHub Actions lab 8. **Final Capstone**: complete a real contribution workflow and demonstrate recovery skills ## 5. What to do with the lesson logs @@ -77,7 +82,8 @@ You are ready to move on when you can: - run it, - inspect whether your prediction was correct, - explain the result, -- recover if you deliberately create a failure. +- recover if you deliberately create a failure, +- pass the module challenge and cumulative gate without command-by-command copying. Finishing pages is not mastery. @@ -89,4 +95,4 @@ Finishing pages is not mastery. If you get lost later, return to this sentence: -> **Read the lesson. Work in the lab. Inspect constantly. Pass the transition check from memory.** +> **Read the lesson. Work in the lab. Inspect constantly. Complete the challenge. Prove the gate.** diff --git a/examples/actions/README.md b/examples/actions/README.md new file mode 100644 index 0000000..24c25cb --- /dev/null +++ b/examples/actions/README.md @@ -0,0 +1,43 @@ +# Controlled GitHub Actions Lab + +This lab removes the need to hunt for a random public workflow. Run it in your disposable `git-github-lab` repository. + +## 1. Copy the demo + +Copy these files into your student lab: + +- `examples/actions/demo/app.py` → `app.py` +- `examples/actions/demo/tests/test_app.py` → `tests/test_app.py` +- `examples/actions/basic-python-ci.yml` → `.github/workflows/ci.yml` + +Commit and push them. + +## 2. Inspect before running + +Open `.github/workflows/ci.yml` and identify: + +- the events under `on` +- the job under `jobs` +- the runner +- each step +- the command that decides whether the check passes + +## 3. Prove green + +Open the Actions tab or the commit checks. Confirm the tests pass. + +## 4. Break it deliberately + +In `tests/test_app.py`, change the expected result for `add(2, 3)` from `5` to `6`, commit, and push. + +Predict the result before checking GitHub. + +Confirm the workflow fails and inspect the failing test output. + +## 5. Repair + +Restore the correct expectation, commit, push, and verify the workflow returns to green. + +## Pass condition + +You can explain why CI is useful, what caused the red check, and why a green check only proves the checks that actually ran—not all possible correctness. diff --git a/examples/actions/basic-python-ci.yml b/examples/actions/basic-python-ci.yml new file mode 100644 index 0000000..98627b0 --- /dev/null +++ b/examples/actions/basic-python-ci.yml @@ -0,0 +1,23 @@ +name: Python CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Run tests + run: python -m unittest discover -s tests -v diff --git a/examples/actions/demo/app.py b/examples/actions/demo/app.py new file mode 100644 index 0000000..e1829c3 --- /dev/null +++ b/examples/actions/demo/app.py @@ -0,0 +1,2 @@ +def add(a: int, b: int) -> int: + return a + b diff --git a/examples/actions/demo/tests/test_app.py b/examples/actions/demo/tests/test_app.py new file mode 100644 index 0000000..f4c8738 --- /dev/null +++ b/examples/actions/demo/tests/test_app.py @@ -0,0 +1,12 @@ +import unittest + +from app import add + + +class AddTests(unittest.TestCase): + def test_adds_two_numbers(self) -> None: + self.assertEqual(add(2, 3), 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/smoke_git_behaviors.py b/scripts/smoke_git_behaviors.py new file mode 100644 index 0000000..a7aa1a8 --- /dev/null +++ b/scripts/smoke_git_behaviors.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import subprocess +import tempfile +from pathlib import Path + + +def run(repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + cwd=repo, + check=check, + text=True, + capture_output=True, + ) + + +def write(path: Path, text: str) -> None: + path.write_text(text, encoding="utf-8") + + +def main() -> None: + with tempfile.TemporaryDirectory() as temp: + repo = Path(temp) / "lab" + repo.mkdir() + run(repo, "init", "-b", "main") + run(repo, "config", "user.name", "Course Smoke Test") + run(repo, "config", "user.email", "course-smoke@example.invalid") + + color = repo / "color.txt" + write(color, "Color: blue\n") + run(repo, "add", "color.txt") + run(repo, "commit", "-m", "Add baseline color") + + run(repo, "switch", "-c", "change-color") + write(color, "Color: red\n") + run(repo, "add", "color.txt") + run(repo, "commit", "-m", "Change color to red") + + run(repo, "switch", "main") + write(color, "Color: green\n") + run(repo, "add", "color.txt") + run(repo, "commit", "-m", "Change color to green") + + conflict = run(repo, "merge", "change-color", check=False) + if conflict.returncode == 0: + raise AssertionError("expected same-line merge conflict") + if "UU color.txt" not in run(repo, "status", "--short").stdout: + raise AssertionError("conflicted path was not reported as unmerged") + + write(color, "Color: purple\n") + run(repo, "add", "color.txt") + run(repo, "commit", "-m", "Resolve color conflict") + if run(repo, "status", "--porcelain").stdout.strip(): + raise AssertionError("working tree should be clean after conflict resolution") + + recoverable = repo / "recoverable.txt" + write(recoverable, "recover me\n") + run(repo, "add", "recoverable.txt") + run(repo, "commit", "-m", "Add recoverable commit") + recoverable_sha = run(repo, "rev-parse", "HEAD").stdout.strip() + + run(repo, "reset", "--hard", "HEAD^") + reflog = run(repo, "reflog", "--format=%H").stdout.splitlines() + if recoverable_sha not in reflog: + raise AssertionError("reflog did not retain the displaced commit") + + run(repo, "branch", "recovery", recoverable_sha) + recovered = run(repo, "show", "recovery:recoverable.txt").stdout + if recovered != "recover me\n": + raise AssertionError("recovery branch did not restore displaced committed work") + + print("GIT BEHAVIOR SMOKE TEST: PASS") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_repo.py b/scripts/validate_repo.py new file mode 100644 index 0000000..d0b25b1 --- /dev/null +++ b/scripts/validate_repo.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Repository quality gates for the Git & GitHub course. + +The validator intentionally uses only the Python standard library so a fresh clone can +run the same checks locally and in CI without installing project dependencies. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from urllib.parse import unquote + +ROOT = Path(__file__).resolve().parents[1] + +REQUIRED_ROOT_FILES = { + "README.md", + "START_HERE.md", + "STUDENT_LAB.md", + "SAFETY.md", + "ASSESSMENTS.md", + "CHEATSHEET.md", + "CONTRIBUTING.md", + "LICENSE", +} + +MODULES = [ + ("Module_0_Setup", "Module_0_Guide.md", range(1, 4)), + ("Module_1_Daily_Core", "Module_1_Guide.md", range(4, 9)), + ("Module_2_Branching", "Module_2_Guide.md", range(9, 13)), + ("Module_3_Remotes", "Module_3_Guide.md", range(13, 17)), + ("Module_4_Collaboration", "Module_4_Guide.md", range(17, 21)), + ("Module_5_Fixing_Mistakes", "Module_5_Guide.md", range(21, 25)), + ("Module_6_Real_World", "Module_6_Guide.md", range(25, 29)), +] + +MARKDOWN_LINK = re.compile(r"(? None: + errors.append(message) + + +def validate_structure(errors: list[str]) -> None: + for relative in sorted(REQUIRED_ROOT_FILES): + if not (ROOT / relative).is_file(): + fail(errors, f"Missing required root file: {relative}") + + capstone = ROOT / "Capstone_First_Contribution" / "README.md" + if not capstone.is_file(): + fail(errors, "Missing capstone README") + + for module_dir, guide_name, lessons in MODULES: + module = ROOT / module_dir + if not module.is_dir(): + fail(errors, f"Missing module directory: {module_dir}") + continue + + if not (module / guide_name).is_file(): + fail(errors, f"Missing module guide: {module_dir}/{guide_name}") + + exercises = module / "Exercises" + exercise_docs = list(exercises.glob("*.md")) if exercises.is_dir() else [] + if not exercise_docs: + fail(errors, f"Module has no exercise sheet: {module_dir}/Exercises") + + for lesson in lessons: + log = module / "Logs" / f"Lesson_{lesson:02d}.md" + if not log.is_file(): + fail(errors, f"Missing lesson log: {log.relative_to(ROOT)}") + + +def validate_markdown_links(errors: list[str]) -> None: + for md in sorted(ROOT.rglob("*.md")): + text = md.read_text(encoding="utf-8") + for match in MARKDOWN_LINK.finditer(text): + raw_target = match.group(1).strip() + if not raw_target or raw_target.startswith(("http://", "https://", "mailto:", "#")): + continue + + target = raw_target.split(maxsplit=1)[0].strip("<>") + target = unquote(target).split("#", 1)[0] + if not target: + continue + + resolved = (md.parent / target).resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + fail(errors, f"Link escapes repository: {md.relative_to(ROOT)} -> {raw_target}") + continue + + if not resolved.exists(): + fail(errors, f"Broken local link: {md.relative_to(ROOT)} -> {raw_target}") + + +def validate_file_hygiene(errors: list[str]) -> None: + for path in sorted(ROOT.rglob("*")): + if not path.is_file(): + continue + + relative = path.relative_to(ROOT) + if ".git" in relative.parts: + continue + + if path.stat().st_size == 0: + fail(errors, f"Unexpected empty file: {relative}") + continue + + if path.suffix.lower() not in TEXT_SUFFIXES: + continue + + text = path.read_text(encoding="utf-8") + if path.suffix.lower() == ".md": + match = PLACEHOLDER.search(text) + if match: + fail(errors, f"Placeholder marker {match.group(0)!r} in {relative}") + + for label, pattern in SECRET_PATTERNS: + if pattern.search(text): + fail(errors, f"Possible {label} committed in {relative}") + + +def main() -> int: + errors: list[str] = [] + validate_structure(errors) + validate_markdown_links(errors) + validate_file_hygiene(errors) + + if errors: + print("QUALITY GATES: FAIL") + for error in errors: + print(f" - {error}") + return 1 + + print("QUALITY GATES: PASS") + print("Verified structure, 28 lesson logs, exercises, local links, placeholders, empty files, and secret-pattern hygiene.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())