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
24 changes: 24 additions & 0 deletions .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ jobs:
mapfile -t scripts < <(git ls-files '*.sh')
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable "${scripts[@]}"

# The peer of the step above, built the same way: the file list comes from git, and the checker runs as a container rather than an install.
# The module version is pinned beside the image because the image alone does not fix it, and a floating install would make this a different check here than the one a maintainer runs locally.
# 1.23.0 rather than the newest: 1.24.0 needs a newer System.Management.Automation than this image carries, so it installs and then fails to import, which reads as a broken gate rather than a version mismatch.
# The count is printed because a checker that read no files reports the same clean as one that read them all, which is how this step first passed having read nothing.
# The list splits on whitespace rather than on a newline, because the same command is documented for a local run and a PowerShell caller joins the file list with spaces where a shell joins it with newlines.
# Splitting on the newline alone hands the analyzer one path holding every file, which it reports as one file it cannot find and then a count of one and no findings, having analyzed nothing at all.
- name: Check PowerShell scripts step
run: |
set -Eeuo pipefail
PS_SCRIPTS="$(git ls-files '*.ps1')"
docker run --rm --pull=always -e PS_SCRIPTS="$PS_SCRIPTS" -v "$PWD":/mnt --workdir /mnt mcr.microsoft.com/powershell:latest \
pwsh -NoProfile -Command '
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module PSScriptAnalyzer -RequiredVersion 1.23.0 -Force -Scope AllUsers
Import-Module PSScriptAnalyzer
$files = $env:PS_SCRIPTS -split "\s+" | Where-Object { $_ }
if (-not $files) { Write-Host "no PowerShell scripts are tracked"; exit 0 }
$found = @()
foreach ($file in $files) { $found += Invoke-ScriptAnalyzer -Path $file -Settings ./PSScriptAnalyzerSettings.psd1 }
Write-Host "Checked $($files.Count) file(s)"
if ($found) { $found | Format-Table RuleName,Severity,ScriptName,Line,Message -AutoSize | Out-String -Width 200 | Write-Host; exit 1 }
Write-Host "no findings"
'

- name: Validate registry and spec step
run: |
set -Eeuo pipefail
Expand Down
24 changes: 23 additions & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i

**Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`):

- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below.
- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** and **PSScriptAnalyzer** via Docker `:latest` (editorconfig-checker's action only installs the CLI, and PSScriptAnalyzer has no action, so the Docker one-liner is what actually runs each check). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below.
- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way.
- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks.

Expand Down Expand Up @@ -401,6 +401,28 @@ The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc
docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md
```

- **PSScriptAnalyzer** (PowerShell, the peer of the shellcheck step, with the excluded rules and their reasons in [`PSScriptAnalyzerSettings.psd1`](./PSScriptAnalyzerSettings.psd1)):

```sh
docker run --rm --pull=always -e PS_SCRIPTS="$(git ls-files '*.ps1')" -v "$PWD":/mnt --workdir /mnt mcr.microsoft.com/powershell:latest \
pwsh -NoProfile -Command '
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module PSScriptAnalyzer -RequiredVersion 1.23.0 -Force -Scope AllUsers
Import-Module PSScriptAnalyzer
$files = $env:PS_SCRIPTS -split "\s+" | Where-Object { $_ }
if (-not $files) { Write-Host "no PowerShell scripts are tracked"; exit 0 }
$found = @()
foreach ($file in $files) { $found += Invoke-ScriptAnalyzer -Path $file -Settings ./PSScriptAnalyzerSettings.psd1 }
Write-Host "Checked $($files.Count) file(s)"
if ($found) { $found | Format-Table RuleName,Severity,ScriptName,Line,Message -AutoSize | Out-String -Width 200 | Write-Host; exit 1 }
Write-Host "no findings"
'
```

The module version is pinned beside the image, because the image alone does not fix it and a floating install makes a local run a different check from CI. 1.23.0 rather than the newest, since 1.24.0 needs a newer `System.Management.Automation` than the image carries and fails to import after installing cleanly. The file list comes from `git ls-files` for the same reason the shellcheck step uses it, and the count is printed because a run that read no files reports the same clean as one that read them all.

**The list splits on whitespace rather than on a newline, and the regex is double-quoted.** A shell joins the file list with newlines and PowerShell joins it with spaces, so a newline-only split hands the analyzer one path holding every file, which it reports as one file it cannot find followed by a clean run over nothing. The double quotes are what let the whole invocation stay inside the single-quoted `-Command` a shell passes, since PowerShell escapes with a backtick and leaves the backslash alone. Run verbatim it reports `Checked 5 file(s)` from either shell.

In a configured editor the davidanson extension is enough. Use the Docker CLI when there's no IDE (agent/headless) or to confirm a clean run before pushing.

When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`.
Expand Down
4 changes: 4 additions & 0 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ python3 scripts/prose_lint.py . --check charset-unknown --summary
for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done
python3 spec/validate.py
docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest
scripts=(); while IFS= read -r f; do scripts+=("$f"); done < <(git ls-files '*.sh'); docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable "${scripts[@]}"
docker run --rm --pull=always -e PS_SCRIPTS="$(git ls-files '*.ps1')" -v "$PWD":/mnt --workdir /mnt mcr.microsoft.com/powershell:latest pwsh -NoProfile -Command 'Set-PSRepository PSGallery -InstallationPolicy Trusted; Install-Module PSScriptAnalyzer -RequiredVersion 1.23.0 -Force -Scope AllUsers; Import-Module PSScriptAnalyzer; $files = $env:PS_SCRIPTS -split "\s+" | Where-Object { $_ }; if (-not $files) { Write-Host "no PowerShell scripts are tracked"; exit 0 }; $found = @(); foreach ($f in $files) { $found += Invoke-ScriptAnalyzer -Path $f -Settings ./PSScriptAnalyzerSettings.psd1 }; Write-Host "Checked $($files.Count) file(s)"; if ($found) { $found | Format-Table RuleName,Severity,ScriptName,Line,Message -AutoSize | Out-String -Width 200 | Write-Host; exit 1 }; Write-Host "no findings"'
```

The two container lines that take a file list differ from the workflow in **form** and not in what they check, and both differences exist because this runbook runs on a developer's machine where the workflow runs on `ubuntu-latest`. The shell list is collected with a `while read` loop rather than the workflow's `mapfile`, since `mapfile` arrives in bash 4 and macOS ships 3.2, and it stays an array so a path carrying whitespace is still passed as one argument. The PowerShell list splits on whitespace rather than on a newline, since a shell joins `git ls-files` output with newlines and PowerShell joins it with spaces, and splitting on the newline alone hands the analyzer one argument holding every path, which it reports as one file it cannot find and a clean run over nothing.

Two gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. The second is that `sentence-split` is implemented and tested but named by no invocation, so nothing runs it.

Run the `editorconfig-checker` line before pushing a new file, and before pushing an existing file that a script rewrote rather than an editor. This repository defaults to CRLF and most tooling writes LF, so a new file fails that check on its first CI run rather than locally. A scripted rewrite is the same hazard on a file that was already correct, since reading and rewriting a whole file in text mode converts every line ending in it, which no prose or Markdown gate reports.
Expand Down
13 changes: 13 additions & 0 deletions PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@{
# Every default rule runs, and two are excluded with their reasons, matching how the shell gate takes shellcheck's defaults and disables a finding inline where the finding is wrong for the program being quoted.
ExcludeRules = @(
# The scripts under host-setup/windows write their report to the console as their whole purpose, mirroring the printf calls in their Linux siblings.
# Write-Output is not merely a different spelling here: these functions return exit codes through the pipeline, so report text on the same stream would arrive at the caller as a return value.
'PSAvoidUsingWriteHost'

# Every script here already carries a preview and a consent step, in the shape its Linux sibling uses: -DryRun prints what would run, and a confirm prompt asks before the host changes.
# Supporting ShouldProcess would add -WhatIf and -Confirm beside them, so a reader would face two spellings of preview and two of consent, one of them undocumented.
# A function is named for what it does instead, and the rule is declined here rather than worked around by renaming setters into something they are not.
'PSUseShouldProcessForStateChangingFunctions'
)
}
33 changes: 33 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,33 @@ One pull request writing down the agent-to-agent messaging this fleet has now us
- **Settled** - A peer's finding is checked rather than adopted. Two of those four did not reproduce at the hub, the `AGENTS.md` anchor rewrite and the settings-diff exposure, and one did and shipped as #653. So the write-up states verification as a step rather than as a courtesy.
- **Settled** - The boundary that matters most is not politeness but permission. A peer cannot widen what the asking session may do, so work blocked in one session goes back to the maintainer rather than sideways to another agent.

### What Building the Windows Host Tooling Surfaced

Three findings raised while writing [`host-setup/windows/`][host-setup-windows], each about the Linux side or the fleet rather than about the new scripts, and none blocking them.

**State** `ready` for the first two, `decision` for the third. **Touches** [`docs/host-setup.md`][host-setup-doc], and the three scripts under `host-setup/linux/`. **Cost** one hub edit each, and no re-vendor, since nothing under `host-setup/` is carried.

- **Record why the host tooling carries no linter category, or decide that it should.** No installer on either platform manages `markdownlint`, `cspell`, `actionlint`, `editorconfig-checker`, `shellcheck`, `PSScriptAnalyzer` or `ruff`, and nothing states that as a decision, so the absence is correct and reachable only by inference.
- **Blocked by** - Nothing.
- **Issue** - [#671][issue-671].
- **Checked** - `main` at `1d5b076` on 2026-08-11, where `readonly TOOLS=(git gh jq git-restore-mtime node python uv dotnet)` names no linter and no comment says why.
- **Settled** - The reasoning holds and is worth writing down once rather than per platform: each linter runs as a pinned image or through `uvx`, so the image tag fixes the version and a local run matches CI, and installing native copies would put a second unpinned version on the host and break exactly that.
- **Open** - Whether it belongs in [`docs/host-setup.md`][host-setup-doc] as a fleet fact, which is what covers Linux by the same sentence, or stays per platform where only the Windows README states it today.

- **Report the `gh` git protocol in `setup-github.sh`, as its Windows peer does.** A host can pass every check the fleet runs while `gh` is configured for https, and a checkout made through `gh` then authenticates by token where every other checkout on that host authenticates by key.
- **Blocked by** - Nothing.
- **Issue** - [#672][issue-672].
- **Checked** - Measured on the maintainer's Windows host on 2026-08-11, where `gh auth status` reports `Git operations protocol: https` against `gh` 2.97.0, an SSH key that signs and verifies, and `scripts/host_gate.py` exiting 0 over all seven declared tools.
- **Settled** - Reported rather than written, since rewriting a working authentication configuration is the operator's call, and `setup-github.sh` touches `gh` nowhere today.
- **Open** - Whether `--configure` should set it, which is the only part where the two platforms could still diverge.

- **Align the Linux scripts onto "name one action" instead of "the last one given wins".** Overwriting `MODE` in the arg loop discards an intent silently, and it discards it in the dangerous direction: `--report --install` drops the safe action and keeps the one that changes the host.
- **Blocked by** - Nothing.
- **Issue** - [#673][issue-673].
- **Checked** - `main` at `1d5b076` on 2026-08-11, where all three scripts document last-wins, no documented example passes two actions, `bootstrap.sh` passes exactly one per `run_tool` call, and no test asserts the behavior.
- **Settled** - The Windows tooling already refuses this way. That began as a constraint, since a PowerShell `param()` block records which switches were given and not their order, and the constraint produced the better behavior.
- **Open** - Nothing about the change itself, which is three `usage()` heredocs and three `parse_args()` bodies. The decision is only whether the fleet wants the stricter contract, and taking it deletes the differences-table row in [`host-setup/windows/README.md`][host-setup-windows] rather than leaving a permanent divergence.

## Standalone Chores

Small work with no research to preserve, selectable one bullet at a time.
Expand Down Expand Up @@ -483,6 +510,7 @@ Regenerate [reports/divergences.md][divergences-report] before using it as the w
- **Finish the host rollout and fill the tooling matrix, which are one visit each.** The rollout needs the matrix to be repeatable and the matrix is only worth filling if the rollout uses it.
- **Hub state** - Done for the documentary half, verified `develop` at `1ed0cc8` on 2026-08-03.
- **Outstanding** - Four machines, WSL2 Ubuntu, the MacBook Air and both ThinkPads, plus any headless or cron environment running with the token. macOS needs someone on that platform, the Proxmox question is whether that host also runs containers which decides whether Docker is required there, and the engine-inside-the-distro variant of the WSL2 Docker cell is unverified.
- **Detail** - The Windows half is a visit rather than a visit plus an unwritten script, since [`host-setup/windows/`][host-setup-windows] now carries the tooling and it was written and run on a Windows host.
- **Issue** - [#365][issue-365] and [#483][issue-483].
- **Rides with** - Nothing on the hub, since the write-guard newline fix has landed on `develop` and a machine keeps running the old hook until the installer is re-run there.
- **Detail** - A ticked row means the host-wide rules text and not the hook, since only running the installer deploys both layers, and the proxmox host proved that distinction by carrying the documentary half alone for eight days on the machine where the incident originated.
Expand Down Expand Up @@ -529,6 +557,9 @@ Nothing is awaiting close today. [#578][issue-578] was the last entry here and c
[issue-623]: https://github.com/ptr727/ProjectTemplate/issues/623
[issue-633]: https://github.com/ptr727/ProjectTemplate/issues/633
[issue-639]: https://github.com/ptr727/ProjectTemplate/issues/639
[issue-671]: https://github.com/ptr727/ProjectTemplate/issues/671
[issue-672]: https://github.com/ptr727/ProjectTemplate/issues/672
[issue-673]: https://github.com/ptr727/ProjectTemplate/issues/673

<!-- Pull requests -->

Expand All @@ -553,6 +584,8 @@ Nothing is awaiting close today. [#578][issue-578] was the last entry here and c
[fidelity-honesty]: ./spec/fidelity_honesty.py
[files]: ./spec/files.json
[governance]: ./GOVERNANCE.md
[host-setup-doc]: ./docs/host-setup.md
[host-setup-windows]: ./host-setup/windows/
[install-tools]: ./host-setup/linux/install-tools.sh
[markdownlint]: ./.markdownlint-cli2.jsonc
[matrix]: ./reports/conformance-matrix.md
Expand Down
Loading