From 52cee66a5a892d07cd7ea8c8c6a3eb6863449627 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Tue, 12 May 2026 13:36:19 -0700 Subject: [PATCH] chore(318): add markdownlint gate to pre-push hook and align docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Gate 2: markdownlint-cli2 check in pre-push hook - Renumber downstream gates (2→3, 3→4, 4→5, 5→6) - Fix/simplify .github/workflows/lint-markdown.yml - Update .squad/playbooks/pre-push-process.md for 6-gate process - Align docs/CONTRIBUTING.md with new gate structure - Improve scripts/install-hooks.sh hook installation - Add *.sh and .github/hooks/* eol=lf to .gitattributes Closes #318 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 6 +++ .github/hooks/pre-push | 37 ++++++++++++++--- .github/workflows/lint-markdown.yml | 5 ++- .squad/playbooks/pre-push-process.md | 62 +++++++++++++++++++--------- docs/CONTRIBUTING.md | 27 ++++++------ scripts/install-hooks.sh | 11 ++--- 6 files changed, 103 insertions(+), 45 deletions(-) diff --git a/.gitattributes b/.gitattributes index a6c3c3ad..777a2592 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,9 @@ .squad/agents/*/history.md merge=union .squad/log/** merge=union .squad/orchestration-log/** merge=union + +# Line ending normalization — keep LF everywhere to match dotnet format expectations +* text=auto eol=lf +*.sh text eol=lf +.github/hooks/* text eol=lf +*.cs text eol=lf diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index c344819f..b74bad95 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Pre-push gate: mirrors CI checks to catch failures before they reach GitHub -# Runs: branch protection → untracked-file check → dotnet format → Release build → unit/architecture tests → integration tests +# Runs: branch protection → untracked-file check → markdownlint → dotnet format → Release build → unit/architecture tests → integration tests # NOTE: git provides refspecs on stdin; interactive prompts must use /dev/tty. # ⚠️ BYPASS POLICY: git push --no-verify is PROHIBITED without prior written # approval from Ralph + Aragorn documented in a GitHub issue comment. @@ -51,7 +51,34 @@ if [[ -n "$UNTRACKED_SRC" ]]; then fi fi -# ── Gate 2: dotnet format check ──────────────────────────────────────────── +# ── Gate 2: markdownlint check ───────────────────────────────────────────── +echo -e "\n${CYAN}📝 Checking Markdown lint (markdownlint-cli2)...${RESET}" +if [[ -x "$ROOT/node_modules/.bin/markdownlint-cli2" ]]; then + "$ROOT/node_modules/.bin/markdownlint-cli2" "**/*.md" \ + "!**/node_modules/**" \ + "!**/bin/**" \ + "!**/obj/**" \ + "!.squad/**" \ + "!.copilot/**" \ + "!.github/agents/**" \ + "!.github/skills/**" \ + "!.github/copilot-instructions.md" \ + --config "$ROOT/.markdownlint.json" + MD_EXIT=$? +else + echo -e "${YELLOW}⚠️ markdownlint-cli2 not found at node_modules/.bin — run 'npm install'.${RESET}" + exit 1 +fi + +if [[ $MD_EXIT -ne 0 ]]; then + echo -e "${RED}❌ Markdownlint violations detected.${RESET}" + echo -e "${YELLOW} Fix: npx markdownlint-cli2 \"**/*.md\"${RESET}" + echo -e "${YELLOW} Then: git add -u && git commit (or --amend), then re-push.${RESET}" + exit 1 +fi +echo -e "${GREEN}✅ Markdown lint OK.${RESET}" + +# ── Gate 3: dotnet format check ──────────────────────────────────────────── echo -e "\n${CYAN}🎨 Checking code formatting (dotnet format --verify-no-changes)...${RESET}" dotnet format MyBlog.slnx --verify-no-changes 2>&1 FORMAT_EXIT=$? @@ -78,7 +105,7 @@ if [[ $FORMAT_EXIT -ne 0 ]]; then fi echo -e "${GREEN}✅ Formatting OK.${RESET}" -# ── Gate 3: Release build (mirrors CI exactly) ───────────────────────────── +# ── Gate 4: Release build (mirrors CI exactly) ───────────────────────────── MAX_ATTEMPTS=3 BUILD_ATTEMPT=0 BUILD_OK=false @@ -114,7 +141,7 @@ if [[ "$BUILD_OK" != true ]]; then exit 1 fi -# ── Gate 4: Unit + Architecture tests ────────────────────────────────────── +# ── Gate 5: Unit + Architecture tests ────────────────────────────────────── TEST_PROJECTS=( "tests/Architecture.Tests/Architecture.Tests.csproj" "tests/Domain.Tests/Domain.Tests.csproj" @@ -150,7 +177,7 @@ if [[ "$TESTS_OK" != true ]]; then exit 1 fi -# ── Gate 5: Docker-backed integration tests ──────────────────────────────── +# ── Gate 6: Docker-backed integration tests ──────────────────────────────── # Requires Docker daemon for Testcontainers-backed dependencies. INTEGRATION_PROJECTS=( "tests/Web.Tests.Integration/Web.Tests.Integration.csproj" diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml index d03c4004..0691e7d7 100644 --- a/.github/workflows/lint-markdown.yml +++ b/.github/workflows/lint-markdown.yml @@ -4,10 +4,8 @@ name: Lint Markdown on: push: branches: [dev, insider] - paths: ['**.md', '.markdownlint.json', '.github/workflows/lint-markdown.yml'] pull_request: branches: [dev, preview, main, insider] - paths: ['**.md', '.markdownlint.json', '.github/workflows/lint-markdown.yml'] permissions: contents: read @@ -24,6 +22,9 @@ jobs: with: globs: | **/*.md + !**/node_modules/** + !**/bin/** + !**/obj/** !.squad/** !.copilot/** !.github/agents/** diff --git a/.squad/playbooks/pre-push-process.md b/.squad/playbooks/pre-push-process.md index d23afb6c..fa319c54 100644 --- a/.squad/playbooks/pre-push-process.md +++ b/.squad/playbooks/pre-push-process.md @@ -2,7 +2,7 @@ **Owner:** Boromir (DevOps) + Aragorn (Lead) **Ref:** `.github/hooks/pre-push`, `CONTRIBUTING.md` -**Last Updated:** 2026-05-11 +**Last Updated:** 2026-05-12 --- @@ -20,7 +20,7 @@ ## Overview -The pre-push hook (`.github/hooks/pre-push`) enforces 6 gates that mirror CI. This playbook documents what agents must do before pushing and how to troubleshoot failures. +The pre-push hook (`.github/hooks/pre-push`) enforces 7 gates that mirror CI. This playbook documents what agents must do before pushing and how to troubleshoot failures. ## Pre-Flight Checklist (Before `git push`) @@ -41,7 +41,19 @@ Before running `git push`, verify: git add ``` -3. **Code is formatted** — Gate 2 runs `dotnet format --verify-no-changes` +3. **Markdown lint passes** — Gate 2 runs `markdownlint-cli2` + + ```bash + npx markdownlint-cli2 "**/*.md" \ + "!**/node_modules/**" \ + "!.squad/**" \ + "!.copilot/**" \ + "!.github/agents/**" \ + "!.github/skills/**" \ + "!.github/copilot-instructions.md" + ``` + +4. **Code is formatted** — Gate 3 runs `dotnet format --verify-no-changes` ```bash dotnet format MyBlog.slnx --verify-no-changes @@ -55,7 +67,7 @@ Before running `git push`, verify: git commit # or --amend ``` -4. **Release build passes locally** — Gate 3 runs Release (not Debug) +5. **Release build passes locally** — Gate 4 runs Release (not Debug) ```bash dotnet build MyBlog.slnx --configuration Release @@ -63,7 +75,7 @@ Before running `git push`, verify: If build fails, run `.github/prompts/build-repair.prompt.md` to fix. -5. **Unit tests pass** — Gate 4 runs 4 test projects +6. **Unit tests pass** — Gate 5 runs 4 test projects ```bash dotnet test tests/Architecture.Tests/Architecture.Tests.csproj --configuration Release --no-build @@ -72,13 +84,13 @@ Before running `git push`, verify: dotnet test tests/Web.Tests.Bunit/Web.Tests.Bunit.csproj --configuration Release --no-build ``` -6. **Docker is running** — Gate 5 requires Docker for integration tests +7. **Docker is running** — Gate 6 requires Docker for integration tests ```bash docker info &>/dev/null && echo "Docker OK" || echo "Docker NOT running" ``` -## The 6 Gates (What the Hook Runs) +## The 7 Gates (What the Hook Runs) When you execute `git push`, the hook runs automatically: @@ -86,12 +98,13 @@ When you execute `git push`, the hook runs automatically: | ----- | ---------------------- | ------------------------------------------------------------------------ | | **0** | Branch protection | Current branch is `main` or `dev` | | **1** | Untracked source files | `.razor`/`.cs` files not staged (prompts y/N) | -| **2** | dotnet format | Any file requires formatting changes (prompts auto-fix y/N) | -| **3** | Release build | `dotnet build --configuration Release` fails (3 attempts) | -| **4** | Unit/Arch/bUnit tests | Any of 4 test projects fail (3 attempts) | -| **5** | Integration tests | Any of 2 integration test projects fail; Docker not running (3 attempts) | +| **2** | markdownlint-cli2 | Any Markdown lint violation | +| **3** | dotnet format | Any file requires formatting changes (prompts auto-fix y/N) | +| **4** | Release build | `dotnet build --configuration Release` fails (3 attempts) | +| **5** | Unit/Arch/bUnit tests | Any of 4 test projects fail (3 attempts) | +| **6** | Integration tests | Any of 2 integration test projects fail; Docker not running (3 attempts) | -### Gate 4 — Test Projects (Unit) +### Gate 5 — Test Projects (Unit) ```text tests/Architecture.Tests/Architecture.Tests.csproj @@ -100,7 +113,7 @@ tests/Web.Tests/Web.Tests.csproj tests/Web.Tests.Bunit/Web.Tests.Bunit.csproj ``` -### Gate 5 — Integration Test Projects (Docker Required) +### Gate 6 — Integration Test Projects (Docker Required) ```text tests/Web.Tests.Integration/Web.Tests.Integration.csproj @@ -111,7 +124,7 @@ These use Testcontainers (mongo:7.0) and Aspire DCP (`DistributedApplicationTest ## Retry Behavior -The hook allows **3 attempts** for Gates 3, 4, and 5. Between attempts: +The hook allows **3 attempts** for Gates 4, 5, and 6. Between attempts: - The hook pauses and prompts "Fix the errors and press Enter to retry, or Ctrl+C to abort" - Fix the failing code, then press Enter @@ -119,7 +132,15 @@ The hook allows **3 attempts** for Gates 3, 4, and 5. Between attempts: ## Troubleshooting -### Formatting Failure (Gate 2) +### Markdownlint Failure (Gate 2) + +| Symptom | Fix | +| -------------------------- | ----------------------------------------------------------------------- | +| MD013 / line-length errors | Reflow paragraphs or use explicit markdownlint disable comments | +| Lint binary missing | Run `npm install` to restore `markdownlint-cli2` dev dependency | +| Unexpected lint scope | Use `.markdownlint.json` and keep hook globs aligned with CI exclusions | + +### Formatting Failure (Gate 3) | Symptom | Fix | | ------------------------ | ----------------------------------------------------------------- | @@ -127,7 +148,7 @@ The hook allows **3 attempts** for Gates 3, 4, and 5. Between attempts: | Analyzer rule violation | Run `dotnet format MyBlog.slnx --diagnostics ` to debug | | dotnet format not found | Install .NET SDK matching `global.json`; format ships with SDK | -### Build Failure (Gate 3) +### Build Failure (Gate 4) | Symptom | Fix | | ------------------------ | ----------------------------------------------------- | @@ -137,7 +158,7 @@ The hook allows **3 attempts** for Gates 3, 4, and 5. Between attempts: **Escalation:** Run `.github/prompts/build-repair.prompt.md` for automated fix. -### Test Failure (Gate 4) +### Test Failure (Gate 5) | Symptom | Fix | | ------------------------- | ------------------------------------------------------------------------------------------------------------------ | @@ -145,7 +166,7 @@ The hook allows **3 attempts** for Gates 3, 4, and 5. Between attempts: | bUnit test failure | Verify Blazor component rendering; check `Render()` not `RenderComponent()` (bUnit 2.x) | | DateTime equality failure | Assert individual fields, not whole-record equality (UtcNow varies between calls) | -### Integration Test Failure (Gate 5) +### Integration Test Failure (Gate 6) | Symptom | Fix | | ------------------------- | --------------------------------------------------------------- | @@ -165,9 +186,10 @@ chmod +x .git/hooks/pre-push ## Anti-Patterns - ❌ **Bypassing the hook** with `git push --no-verify` — CI will catch it, wasting time -- ❌ **Committing unformatted code** — Gate 2 blocks the push; run `dotnet format MyBlog.slnx` first +- ❌ **Committing broken Markdown** — Gate 2 blocks the push; run `npx markdownlint-cli2` +- ❌ **Committing unformatted code** — Gate 3 blocks the push; run `dotnet format MyBlog.slnx` first - ❌ **Running Debug build only** — CI uses Release; Debug hides missing files -- ❌ **Pushing without Docker** — Gate 4 will block; start Docker first +- ❌ **Pushing without Docker** — Gate 6 will block; start Docker first - ❌ **Ignoring untracked files** — They're invisible to CI and will cause failures - ❌ **Committing to `main` directly** — Gate 0 blocks this; use `squad/{issue}-{slug}` branches diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ef59e522..0d1d49eb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -28,18 +28,19 @@ gate stays current as the repo evolves. ### What the Pre-Push Gate Enforces The pre-push hook automatically runs before every `git push` and enforces -**6 sequential gates**: +**7 sequential gates**: | Gate | Rule | Enforced Behavior | |------|------|--------| | **0** | Squad branch naming | Rejects pushes on non-`squad/{issue}-{slug}` branches; blocks `main` and `dev` | | **1** | Untracked source files | Warns if `.razor` or `.cs` files exist but are not staged; prompts to confirm before proceeding | -| **2** | dotnet format | Runs `dotnet format --verify-no-changes`; prompts to auto-fix if formatting issues found | -| **3** | Release build | Runs `dotnet build MyBlog.slnx --configuration Release`; zero warnings or errors required | -| **4** | Unit & architecture tests | Runs `Architecture.Tests`, `Domain.Tests`, `Web.Tests`, `Web.Tests.Bunit` (Release configuration) | -| **5** | Integration tests | Runs `Web.Tests.Integration`, `AppHost.Tests` (Release configuration; Docker daemon required) | +| **2** | Markdown lint | Runs `markdownlint-cli2`; blocks push on Markdown lint violations | +| **3** | dotnet format | Runs `dotnet format --verify-no-changes`; prompts to auto-fix if formatting issues found | +| **4** | Release build | Runs `dotnet build MyBlog.slnx --configuration Release`; zero warnings or errors required | +| **5** | Unit & architecture tests | Runs `Architecture.Tests`, `Domain.Tests`, `Web.Tests`, `Web.Tests.Bunit` (Release configuration) | +| **6** | Integration tests | Runs `Web.Tests.Integration`, `AppHost.Tests` (Release configuration; Docker daemon required) | -**Retry logic:** Gates 3–5 allow up to **3 attempts**. Between failures, the +**Retry logic:** Gates 4–6 allow up to **3 attempts**. Between failures, the hook pauses and prompts you to fix errors, then retries automatically. ### Branch Naming (Strict) @@ -69,7 +70,7 @@ wastes CI cycles and bypasses team quality gates. Fix locally first. ### Prerequisites - **.NET 10 SDK** — [Download](https://dotnet.microsoft.com/en-us/download) -- **Docker daemon** — Required for integration tests (Gate 5: `tests/Web.Tests.Integration`, `tests/AppHost.Tests`) +- **Docker daemon** — Required for integration tests (Gate 6: `tests/Web.Tests.Integration`, `tests/AppHost.Tests`) - **Auth0 account** — See [AUTH0_SETUP.md](AUTH0_SETUP.md) for Auth0 configuration ### Building and Testing Locally @@ -78,10 +79,10 @@ wastes CI cycles and bypasses team quality gates. Fix locally first. # Restore dependencies dotnet restore MyBlog.slnx -# Build the solution (Release config, as Gate 3 does) +# Build the solution (Release config, as Gate 4 does) dotnet build MyBlog.slnx --configuration Release -# Run all tests (as Gates 4 and 5 do) +# Run all tests (as Gates 5 and 6 do) dotnet test MyBlog.slnx --configuration Release # Run the application (via Aspire AppHost) @@ -127,7 +128,7 @@ dotnet test tests/Architecture.Tests --configuration Release --no-build dotnet test tests/Domain.Tests --configuration Release --no-build dotnet test tests/Web.Tests --configuration Release --no-build dotnet test tests/Web.Tests.Bunit --configuration Release --no-build -# Run integration tests (Gate 5 — requires Docker) +# Run integration tests (Gate 6 — requires Docker) dotnet test tests/Web.Tests.Integration --configuration Release --no-build dotnet test tests/AppHost.Tests --configuration Release --no-build ``` @@ -237,7 +238,7 @@ on a new issue branch keeps the repository clean and your work tracking obvious. ## Troubleshooting -### Build Failures (Gate 3) +### Build Failures (Gate 4) - **Warnings treated as errors:** The Release config enforces `TreatWarningsAsErrors=true`. Fix warnings first. @@ -245,14 +246,14 @@ on a new issue branch keeps the repository clean and your work tracking obvious. `git add`, then retry. - **NuGet restore failure:** Run `dotnet restore` manually and retry. -### Test Failures (Gates 4 & 5) +### Test Failures (Gates 5 & 6) - **Architecture test failure:** Check naming conventions (commands → `Command`, queries → `Query`, handlers → `Handler`, validators → `Validator`). - **DateTime equality failures:** Assert individual fields instead of whole-record equality; `UtcNow` changes between calls. -- **Docker not running (Gate 5):** Start Docker Desktop and retry. +- **Docker not running (Gate 6):** Start Docker Desktop and retry. - **Container startup timeout:** Increase Docker resources and verify images are pulled. diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index 9f22c16e..e6b0acd3 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -81,14 +81,15 @@ echo "" echo "Pre-commit hook gates on every 'git commit':" echo " • Runs markdownlint on staged .md files (degrades gracefully if not installed)" echo "" -echo "Pre-push hook enforces 6 gates on every 'git push':" +echo "Pre-push hook enforces 7 gates on every 'git push':" echo " 0. Enforces branch naming — squad/{issue}-{slug} runs all gates;" echo " sprint/{N}-{slug} passes Gate 0 and exits (skips feature gates)" echo " 1. Warns about untracked .razor/.cs source files" -echo " 2. dotnet format --verify-no-changes (formatting check; offers auto-fix)" -echo " 3. Release build (dotnet build MyBlog.slnx --configuration Release)" -echo " 4. Unit/arch tests (tests/Architecture.Tests, tests/Domain.Tests, tests/Web.Tests, tests/Web.Tests.Bunit)" -echo " 5. Integration tests (tests/Web.Tests.Integration, tests/AppHost.Tests — Docker required)" +echo " 2. markdownlint-cli2 (fails on Markdown lint violations)" +echo " 3. dotnet format --verify-no-changes (formatting check; offers auto-fix)" +echo " 4. Release build (dotnet build MyBlog.slnx --configuration Release)" +echo " 5. Unit/arch tests (tests/Architecture.Tests, tests/Domain.Tests, tests/Web.Tests, tests/Web.Tests.Bunit)" +echo " 6. Integration tests (tests/Web.Tests.Integration, tests/AppHost.Tests — Docker required)" echo "" echo "⚠️ git push --no-verify is prohibited without prior written approval from Ralph + Aragorn." echo " Document the bypass in a GitHub issue comment before using --no-verify."