Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 32 additions & 5 deletions .github/hooks/pre-push
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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

Comment on lines +56 to +72
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=$?
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/lint-markdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +22,9 @@ jobs:
with:
globs: |
**/*.md
!**/node_modules/**
!**/bin/**
!**/obj/**
!.squad/**
!.copilot/**
!.github/agents/**
Expand Down
62 changes: 42 additions & 20 deletions .squad/playbooks/pre-push-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand All @@ -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`)

Expand All @@ -41,7 +41,19 @@ Before running `git push`, verify:
git add <files>
```

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"
```
Comment on lines +47 to +54

4. **Code is formatted** — Gate 3 runs `dotnet format --verify-no-changes`

```bash
dotnet format MyBlog.slnx --verify-no-changes
Expand All @@ -55,15 +67,15 @@ 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
```

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
Expand All @@ -72,26 +84,27 @@ 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:

| Gate | What | Blocks Push If |
| ----- | ---------------------- | ------------------------------------------------------------------------ |
| **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
Expand All @@ -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
Expand All @@ -111,23 +124,31 @@ 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
- The gate re-runs from scratch

## 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 |
| ------------------------ | ----------------------------------------------------------------- |
| Files differ from format | Run `dotnet format MyBlog.slnx`, then `git add -u && git commit` |
| Analyzer rule violation | Run `dotnet format MyBlog.slnx --diagnostics <rule-id>` 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 |
| ------------------------ | ----------------------------------------------------- |
Expand All @@ -137,15 +158,15 @@ 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 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Architecture test failure | Check naming conventions (commands → `Command`, queries → `Query`, handlers → `Handler`, validators → `Validator`) |
| bUnit test failure | Verify Blazor component rendering; check `Render<T>()` not `RenderComponent<T>()` (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 |
| ------------------------- | --------------------------------------------------------------- |
Expand All @@ -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

Expand Down
27 changes: 14 additions & 13 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Comment on lines +131 to 132
dotnet test tests/AppHost.Tests --configuration Release --no-build
```
Expand Down Expand Up @@ -237,22 +238,22 @@ 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.
- **Missing file references:** Stage any new `.razor` or `.cs` files with
`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.

Expand Down
11 changes: 6 additions & 5 deletions scripts/install-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Loading