Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ desktop.ini
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
!.vscode/README.md

# --- transient work (kept out of tracking for now) ----------------------------
.temp/
Expand Down
35 changes: 35 additions & 0 deletions .vscode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Editor integration

VS Code workspace wiring so the linters that gate commits also surface live in
the editor. See [`/AGENTS.md`](../AGENTS.md) for the overall architecture.

| File | Purpose |
| ----------------- | ------------------------------------------------------- |
| `settings.json` | tooling-only workspace settings, wired to `.config/` |
| `extensions.json` | recommended extensions matching the commit-time linters |

## `settings.json` — same rules, live

Live markdown linting `extends` the same [`.config/markdownlint.jsonc`](../.config/markdownlint.jsonc)
the commit hook enforces — editor and commit never disagree. File endings and
final newlines honour `.editorconfig` intent for files VS Code creates.

`editor.formatOnSave` stays **off** by design: formatting is a commit concern
here (markdownlint `--fix`, shfmt), and an editor formatter would fight the
linters.

## `extensions.json` — surface the gate early

Recommends one extension per commit-time linter (EditorConfig, markdownlint,
YAML, PowerShell/PSScriptAnalyzer, AsciiDoc, shellcheck, shfmt) so problems
appear while typing, not first at commit. VS Code prompts contributors to
install them on open.

## Conventions

- Keep `settings.json` strictly about wiring linters to `.config/` — no
personal preferences (theme, fonts). Extend it freely in a project, but keep
the split.
- `.gitignore` tracks only the shipped files (`settings.json`,
`extensions.json`, this README) and ignores everything else in `.vscode/`,
so personal editor state never lands in the repo.
25 changes: 4 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,10 @@ drifted). Git tracks symlinks natively.

## Retrofit an existing repo

To bring another repo (e.g. one using split per-language workflows and a
hand-rolled `.githooks/` hook) up to this architecture:

1. **Adopt the orchestrator.** Copy `.pre-commit-config.yaml` and adapt the hook
list to the repo's languages. Remove any `core.hooksPath`/`.githooks/` setup —
pre-commit replaces it. Don't set `core.hooksPath`; the two conflict.
2. **Relocate configs** into `.config/`, referenced explicitly via hook `args`
(keep root-forced files at root). Factor markdown rules into
`markdownlint.jsonc` so the editor and CLI share them.
3. **Move custom checks into shared scripts.** Any bespoke lint/test logic that
lived in CI *and* a local hook becomes one `.config/scripts/*.ps1` invoked as
a `repo: local` hook — run identically local and in CI. Delete the duplicate.
4. **Collapse CI to one workflow** that runs `pre-commit run` (scoped on
`pull_request`, full on `merge_group`). Replace N split per-language workflows;
path-filtering is no longer needed because pre-commit scopes by file.
5. **Wire the editor** (`.vscode/settings.json`) and add recommendations.
6. **Copy the docs** (this file + the per-folder READMEs), trimming to the
retrofitted repo's actual tools.
7. **Purge phantoms.** Verify every tool exists (e.g. there is **no**
`asciidoctor-lint` gem — use `asciidoctor --failure-level` for syntax + Vale
for prose).
The step-by-step guide for bringing a repo **not** created from this template
up to this architecture lives in [`MIGRATION.md`](MIGRATION.md) — the entry
document that lists every concept, orders the migration (orchestrator first,
server-side protection last), and links each concept's reference doc.

Anti-pattern this replaces: local hook logic and CI workflows re-implementing
"the same" checks separately, which drift apart. One orchestrator, one source.
Expand Down
116 changes: 116 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# MIGRATION.md — adopting this template in an existing repo

How to migrate this repo's concepts into a repo that was **not** created from
this template. This is the entry document: it names each concept, gives the
order to adopt them in, and links the reference doc that explains each one in
depth — read those as you reach each step.

For the architecture itself (what you are adopting and why), read
[`AGENTS.md`](AGENTS.md) first.

## The concepts and where they are explained

| Concept | Lives in | Reference doc |
| ------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
| Orchestration (the local workflow) | `.pre-commit-config.yaml` | [`AGENTS.md`](AGENTS.md) + inline comments |
| Tool configs | `.config/` | [`.config/README.md`](.config/README.md) |
| Shared lint/test scripts | `.config/scripts/` | [`.config/scripts/README.md`](.config/scripts/README.md) |
| Policy rules (custom analyzer rules) | `.config/PSScriptAnalyzerRules/` | [`.config/scripts/README.md`](.config/scripts/README.md) |
| Opt-in overlays | `.config/overlays/` | [`.config/overlays/vale/README.md`](.config/overlays/vale/README.md) |
| CI (GH Actions workflow) | `.github/workflows/lint.yml` | [`.github/README.md`](.github/README.md) |
| GitHub automation beyond CI | `.github/` (Dependabot, CODEOWNERS, PR/issue templates) | [`.github/README.md`](.github/README.md) |
| Editor integration | `.vscode/` | [`.vscode/README.md`](.vscode/README.md) |
| Root convention files | `.editorconfig`, `.gitattributes`, `.gitignore`, `.claudeignore` | self-documenting (declarative) |
| One-time server-side setup | `setup/` | [`setup/README.md`](setup/README.md) |
| Agent entry docs | `CLAUDE.md`, `AGENTS.md`, per-folder READMEs | themselves |

## Migration, in order

The order matters: the orchestrator comes first because every later step hangs
hooks off it, and server-side protection comes last because it requires a CI
run to have happened.

### 1. Adopt the orchestrator

Copy `.pre-commit-config.yaml` to the target repo's root and adapt the hook
list to its languages — drop hooks for languages the repo doesn't have, keep
the structure (managed hooks for off-the-shelf linters, `repo: local` hooks
for bespoke checks, stages as cadence). Remove any existing
`core.hooksPath`/`.githooks/` mechanism — pre-commit replaces it, and the two
conflict.

### 2. Relocate configs into `.config/`

Move tool configs out of root into `.config/`, referenced explicitly via hook
`args` (see [`.config/README.md`](.config/README.md) for which file feeds
which tool). Only files a tool *forces* to root stay there. Factor markdown
rules into `markdownlint.jsonc` so editor and CLI share one rule set.

### 3. Move custom checks into shared scripts

Any bespoke lint/test logic that lived in CI *and* a local hook becomes one
`.config/scripts/*.ps1` invoked as a `repo: local` hook — run identically in
both places; delete the duplicate. Follow the script contract in
[`.config/scripts/README.md`](.config/scripts/README.md): fail fast, never
self-install, no-op cleanly on nothing to check. Copy
`Initialize-DevEnvironment.ps1` (bootstrap) and `Invoke-TestLane.ps1` (test
lanes) as-is; tag the repo's Pester tests `Fast`/`Standard`/`Thorough` by
measured cost.

### 4. Collapse CI to one workflow

Replace split per-language workflows with one `lint.yml` that runs
`pre-commit run` — scoped to the diff on `pull_request`, full on `merge_group`
(see [`.github/README.md`](.github/README.md)). Path-filtering becomes
unnecessary: pre-commit scopes by file. Keep the full-history gitleaks scan as
the one CI-only step, pin third-party actions by commit SHA, and bring over
`dependabot.yml`, `CODEOWNERS`, and the PR/issue templates, adapting owners
and paths.

### 5. Wire the editor

Copy `.vscode/settings.json` and `extensions.json`
(see [`.vscode/README.md`](.vscode/README.md)) so the same rules surface live
while typing. Keep the `.gitignore` pattern that tracks only the shipped
files.

### 6. Copy the docs and root conventions

Copy `AGENTS.md`, `CLAUDE.md`, and the per-folder READMEs, trimming each to
the tools the target repo actually uses. Copy the root convention files
(`.editorconfig`, `.gitattributes`, `.gitignore`, `.claudeignore`) and merge
with what exists. Where the same file must exist twice, prefer a symlink over
a copy (rationale and the Windows caveat: [`AGENTS.md`](AGENTS.md)).

### 7. Purge phantoms

Verify every referenced tool actually exists and every hook actually runs —
e.g. there is no `asciidoctor-lint` gem; AsciiDoc is checked via
`asciidoctor --failure-level` plus Vale for prose. A hook that silently checks
nothing is worse than no hook.

### 8. Apply server-side protection, then delete `setup/`

After the first push and one green CI run (the `lint` check must exist),
run the one-time scripts from [`setup/README.md`](setup/README.md) with
admin + `gh`:

```pwsh
pwsh -NoProfile -File setup/Protect-MainBranch.ps1
pwsh -NoProfile -File setup/Enable-RepoSecurity.ps1
```

They create the merge-queue ruleset on the default branch and enable the
server-side security settings. Then delete the `setup/` folder — it has
served its purpose.

## Verify the migration

- `pre-commit run --all-files` passes locally.
- A test PR shows the `lint` check running scoped to the diff, and merging
routes through the merge queue (full run at the gate).
- The editor surfaces the same findings the commit hook enforces.

Anti-pattern this migration removes: local hook logic and CI workflows
re-implementing "the same" checks separately, which drift apart. One
orchestrator, one source.
5 changes: 5 additions & 0 deletions setup/Protect-MainBranch.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ Describe 'Get-ProtectionRuleset' -Tag 'Fast' {
$script:R.rules.type | Should -Contain 'merge_queue'
}

It 'waits at most 1 minute before merging queued entries' {
($script:R.rules | Where-Object type -EQ 'merge_queue').parameters.min_entries_to_merge_wait_minutes |
Should -Be 1
}

It 'requires review threads to be resolved (holds auto-merge on open comments)' {
($script:R.rules | Where-Object type -EQ 'pull_request').parameters.required_review_thread_resolution |
Should -BeTrue
Expand Down
2 changes: 1 addition & 1 deletion setup/Protect-MainBranch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function Get-ProtectionRuleset {
max_entries_to_build = 5
min_entries_to_merge = 1
max_entries_to_merge = 5
min_entries_to_merge_wait_minutes = 5
min_entries_to_merge_wait_minutes = 1
check_response_timeout_minutes = 60
}
}
Expand Down