The full engineering corpus lives in docs/ — this file is the
short, branch-protection-and-PR-hygiene companion.
Configure these in GitHub → Settings → Branches → Branch protection rules
→ Branch name pattern: main so the corpus matches what GitHub enforces:
Two settings are deferred — maintainer decision, 2026-08-10. While LearnStack has one active contributor, the live
mainrule sets Require approvals: 0 and leaves Do not allow bypassing off. A second approver on a single-contributor repository blocks every merge, and admin enforcement with no second admin blocks the only person who could unblock it. The settings below are the target state, not the current one.Trigger: a second active contributor gains write access. That is the condition that makes the rule mean something, and it is self-evidencing — nobody has to remember to check a date. Activating it is the same discipline as the deferred status checks below: set both settings, and drop this note in the same pull request.
Everything else in this section — required status checks, linear history, no force pushes, no direct pushes — is live today. Only the two named settings are deferred.
Required checks repaired — 2026-09-14 (P02d-1). The live rule now requires the current
metaname andbackend integration (Testcontainers). A fresh API read verified all five check names and their GitHub Actions attribution, with strict up-to-date checking and every other protection setting preserved. P02d-1's delivery record carries the dated verification.
- Require a pull request before merging
- Require approvals: 1 (raise to 2 once the team grows past two active contributors).
- Dismiss stale approvals when new commits are pushed: on.
- Require review from CODEOWNERS: off (no CODEOWNERS file yet).
- Require status checks to pass before merging
-
Require branches to be up to date before merging: on.
-
Required status checks (the job names from
.github/workflows/ci.yml):backend (build + unit + arch + contract)frontend (typecheck + lint + build + test)secret scan (leakwatch)meta (compose + commit hygiene + link audit)backend integration (Testcontainers)— activated in Phase 02a Packet 6 with the four-role provisioning suite, one packet earlier than planned: Packet 6 ships the first Docker-bound test and is therefore the packet that has to split them. The job carries novars.ENABLE_*gate and no placeholder step, so it runs on every pull request. Required since P02d-1, 2026-09-14. The check cannot pass empty: both backend jobs end withscripts/assert-tests-ran.py, which fails a run whose filter matched no test —dotnet test --filterexits 0 when nothing matches, and a renamedRequires=Dockertrait would otherwise leave the whole Docker suite unrun behind a green check.openapi diff— required since P02d-4, 2026-10-04. The always-running job uses pinned policy fixtures, verified base/head snapshots and actual first-baseline proof. Its real PR run passed before activation. API read-back verifies all six GitHub Actions contexts, strict up-to-date checking and every other protection setting unchanged. The packet delivery record records the successful job and activation proof.public demo + lighthouse— required since P02d-7, 2026-10-11. The first complete real PR run passes before activation. Downloaded artifacts confirm all nine pages, 27 audits and owned cleanup. Full API read-back confirms seven GitHub Actions contexts, strict up-to-date checking, all six previous contexts retained and every unrelated protection setting unchanged. The CI and exit closeout records execution, artifact validation and activation proof. The separate reviewed-head verification identifies the exact implementation head inspected after activation; subsequent heads still require all seven passing checks.
-
P02d-5 keeps the existing check names. The frontend job uses the guarded workspace test runner and native production/cleanup controls. Backend integration also builds a disposable Next app against the real API/PostgreSQL; Node 22.23.1/pnpm, frozen frontend dependencies and OpenSSL are required by both that job and the frontend native-ingress proof. P5 keeps its synthetic transport probe; P6 adds unchanged product-page modes for HTML/RSC isolation, pagination, presentation, freshness and recovery. These socket/DOM proofs do not claim a browser or manual accessibility audit. ADR-0055 adds a separate narrow stock-Next navigation probe and development HMR admission controls. Docker integration requires an installed stock Chromium browser; CI explicitly checks
/usr/bin/google-chromeand its version before the suite. Local supported binaries are Chrome/Brave on macOS and Chrome/Chromium on Linux; a missing browser fails rather than skips. The fixture uses an isolated disposable profile and certificate-specific public-key trust. Its Flight-to-document navigation and fresh HMR admission scope does not deliver P7's browser demo, full Playwright/axe or Lighthouse. The Step 3 record owns completed implementation, reviews and execution evidence. -
P02d-7 replaces the disabled Lighthouse placeholder with the always-running
public demo + lighthousejob under ADR-0056. It installs the locked browser tools and Playwright-managed Chromium, then runsmake demo DEMO_MODE=ciover the same lifecycle as local use. Nine actual pages, 27 JSON/HTML audits, normalized gzip/CLS/automated-accessibility checks, real quota observation and confirmed cleanup are mandatory. Only public reports/screenshots and sanitized receipts are uploaded; raw logs, profiles and private TLS/env files are excluded. The authorized seventh-context activation followed the complete successful real PR run on 2026-10-11. The Step 4 record owns the actual run and verified activation/read-back.GitHub matches required checks by name. The current CI and exit record owns execution and dated API read-back; a workflow edit alone is not a protection-setting change.
-
- Require conversation resolution before merging: on.
- Require signed commits: optional (off until the team rolls out signing keys).
- Require linear history: on (we use squash-merge or rebase-merge, never bubble).
- Do not allow bypassing the above settings: on (admins included).
- Restrict who can push to matching branches: off (PRs only — no direct push).
- Allow force pushes: off.
- Allow deletions: off.
The ordinary fast checks retain their approximately three-minute target.
ADR-0056's full-stack job deliberately includes real limiter-window waits: its
20-minute job ceiling includes setup/build, a 12-minute serial proof and separate
30-second cleanup. Timing ceilings are not performance targets. A failing or
slow check is investigated rather than skipped on main.
Per CLAUDE.md § Commit conventions:
- Conventional Commits:
type(scope): subjectwith subject in imperative mood, ≤ 72 chars. docs(scope)for doc-only commits; scope ∈architecture | decisions | standards | roadmapor omitted for cross-cutting changes.
- Title mirrors the primary commit's subject.
- Follow Git Workflow § Description for the canonical structure, including migration/rollback details for schema or configuration changes and links to the related ADR / phase documents.
make install # one-time per clone: deps + git hooks
make lint # dotnet format --verify + ESLint
make typecheck # tsc --noEmit
make test # unit + arch + contract + integration + vitestmake test depends on install since Phase 02a Packet 10, so it restores what it is
about to run rather than reporting a failure that is really a missing package. The cost
is that a workspace whose frontend/pnpm-lock.yaml disagrees with its package.json —
or an offline clone missing a package from the local store — now stops at that
prerequisite. For a backend-only change, make test-backend runs the .NET suites without
it.
make test starts Testcontainers since Phase 02a Packet 6, so it needs a Docker
socket and takes noticeably longer than it did. The Docker-bound cases are split
out by [Trait("Requires","Docker")]; to skip them, run
dotnet test backend/LearnStack.slnx --filter "Requires!=Docker", which is
exactly what CI's backend job runs.
The pre-commit hook (activated by make install) runs, on staged files only:
dotnet format on *.cs; prettier on JS / TS / JSON / Markdown under
frontend/; shared-subject next lint --fix on web JS/TS (including .mts and
.cts); SDK/UI workspace lint when their JS/TS changes; and, when the binary is
on PATH, leakwatch scan fs <staged-file>. Web source, scripts and root configs
share one subject census with CI; generated next-env.d.ts, .next and .server
are excluded deliberately. So the
three commands above are mostly a sanity check. There is deliberately no
make secret-scan to pair with them — the hook and CI are the scanner's only
runners, and CI re-runs every check as a hard gate, so a bypassed local commit
will fail the PR build.
The commit-msg hook (same activation) checks the subject against
Git Workflow § Commits: Conventional
Commits shape and 72 characters, judged on the subject git will store. CI's meta job
runs the same hook on every commit of a pull request, so the point is not a second
rule but where the failure lands: CI catches a bad subject after a push, and a subject is
only fixable by rewriting history; locally it is a retry. A merge is skipped, and git's
autosquash markers (fixup!, squash!, amend!) pass locally so a fixup workflow works
— CI refuses them, so squash before review. Git's own Revert "…" subject is refused
everywhere; § Reverts says what a revert's subject is. The ADR: and Module: trailers
are deliberately not checked: whether a commit owes one depends on judgement a hook does
not have, and a hook that guessed is a hook people disable.
Prettier stops at the frontend/ boundary, and the root .prettierignore
is what enforces it in your editor — .vscode/settings.json maps [markdown]
to the Prettier extension, which honours that file, so neither format-on-save
nor an explicit Format Document reaches docs/. Keep it that way: prettier's
Markdown printer rewrites hard-wrapped prose — it joins blockquote lines and
reads * inside a code span as emphasis — and it corrupted a roadmap record
doing exactly that. Nothing verifies its output outside the frontend workspace
anyway, since no CI job runs prettier and make format invokes it from
frontend/.
Leakwatch builds before v1.6.0 only accept a directory target. CI pins v1.8.0, which accepts files; the hook detects an older local build and skips the scan with an upgrade hint rather than failing your commit. CI scans the whole tree either way.
The secret scanner is Leakwatch
— MIT licensed, verifier-equipped, hybrid Aho-Corasick + regex + entropy
detection engine. Config lives at .leakwatch.yaml + .leakwatchignore
at the repo root. Install once for the local pre-commit scan (CI runs it
regardless, this is just earlier feedback):
brew install HodeTech/tap/leakwatch # macOS (Homebrew)
# or:
go install github.com/HodeTech/leakwatch@v1.8.0The version is pinned, and to the same one CI installs. @latest on the old
cemililik/ path resolves to v1.5.0 — the module renamed its path at v1.6.0, so
nothing newer is installable there — and v1.5.0 does not understand
leakwatch:ignore, so a developer following an unpinned instruction would get a
scanner that disagrees with the one gating their pull request.
If Leakwatch flags an intentional dev credential, prefer:
- Inline ignore at the literal —
# leakwatch:ignore(or# leakwatch:ignore:<detector-id>for a targeted skip) at the end of the line carrying the dev credential. Lowest blast radius. .leakwatchignorepath entry — for whole files where every value is dev-only (env templates, the LiveKit / Coturn confs)..leakwatch.yamlconfig tweak — last resort; document the why.
git push --forcetomain. Branch protection blocks it; do not work around it.- Bypass the pre-commit hook (
--no-verify) for anything but a documented emergency — CI will catch it and the PR will fail. - Edit an Accepted ADR's body outside the two bounded mechanisms in Documentation Standards § Correcting and Amending ADRs (ADR-0041): an inline erratum by default, in-place replacement only for a canonical artifact for reuse, both only for a statement false when it entered the record, and both owing a dated Amendment in every Accepted ADR the diff changes. A changed decision is a new ADR that supersedes the old one, with the same number rule preserved.