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
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ end_of_line = lf
[catalog/snippets/workflows/*.{yml,yaml}]
end_of_line = lf

# The husky pre-commit snippet is a shebang script; keep it LF so a copied snippet runs (a CRLF shebang breaks).
[catalog/snippets/husky/pre-commit]
end_of_line = lf

# Linux scripts
[*.sh]
end_of_line = lf
Expand Down
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# and `--renormalize`. Any repo whose tooling ships extensionless scripts adds the matching path pin, e.g. s6-overlay
# init `Docker/s6-overlay/** text eol=lf` or husky/git hooks `.husky/pre-commit text eol=lf`.
*.sh text eol=lf
# The husky pre-commit snippet is an extensionless shebang script (like a copied .husky/pre-commit).
catalog/snippets/husky/pre-commit text eol=lf

# Vanilla `.py` follows the CRLF default - Python's universal newlines accept CRLF, and it is
# commonly edited on Windows. Pin LF only for a `.py` executed directly via its shebang, by path -
Expand Down
17 changes: 11 additions & 6 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ jobs:
- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

# Docker images are digest-pinned (version in the trailing comment) so CI is reproducible.
# Doc linters run as pinned action wrappers. editorconfig-checker's action is install-only, so it runs via Docker.
- name: Lint Markdown step
run: docker run --rm -v "$PWD":/workdir davidanson/markdownlint-cli2@sha256:0ed9a5f4c77ef447da2a2ac6e67caf74b214a7f80288819565e8b7d2ac148fe5 "**/*.md" # markdownlint-cli2 v0.22.1
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
with:
globs: '**/*.md'

- name: Spell check step
run: docker run --rm -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell@sha256:cb2eab4ec34956aca554e35615da65401d510fa5983cce2773391e2fd9f4fc20 "**/*.md" # cspell v10.0.1
uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0
with:
files: '**/*.md'
incremental_files_only: false

- name: Lint workflows step
run: docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 -color # actionlint v1.7.12
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0

- name: Check line endings step
run: docker run --rm -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker@sha256:67b9e9b16a674e36f7c05919da789f03a01d343ca8423eb8797179399af07c00 # editorconfig-checker v3.4.0
- name: Check EditorConfig step
run: docker run --rm -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest
Comment on lines +42 to +43

- name: Validate registry and spec step
run: |
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ dist/
.pytest_cache/
.ruff_cache/
.pyright/

# Coverage output (dotnet XPlat/coverlet, Python coverage.py)
coverage/
[Tt]est[Rr]esults/
*.cobertura.xml
*.coverage
coverage.xml
.coverage
.coverage.*
htmlcov/
28 changes: 24 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul

## Communicating with the User

- **Reference every pull request as a clickable link.** When you mention a PR - in chat, a summary, or a report - render it as a markdown link to the PR (`[#123](https://github.com/<owner>/<repo>/pull/123)`), never a bare `#123`. The same applies to issues and commits.
- **Reference every pull request as a clickable link.** When you mention a PR - in chat, a summary, or a report - render it as a markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits.
- **Ask for input as a numbered list.** When you need the user to decide or answer, present the questions - and any options - as a numbered list so they can reply per number. A single inline question is fine; two or more are always numbered.

## Workflow YAML Conventions
Expand All @@ -225,7 +225,21 @@ These conventions describe the target state. New and modified workflows must res

### Running the Linters Locally (Known-Working Invocations)

The CI lint job runs these tools (workflow YAML and Markdown), but run them locally before pushing to catch issues early, so an agent must know how to actually run them. Some linters are not obvious to invoke and their non-Docker install paths (curl-pipe installers, global npm) are frequently blocked in sandboxes or fail on WSL. **Prefer the Docker invocations below; they are the known-working path and need no local toolchain.** Both tools auto-discover their targets from the working directory.
CI runs the full lint set, but run the linters locally before pushing to catch issues early, so an agent must know how to invoke them. Their non-Docker install paths (curl-pipe installers, global npm) are frequently blocked in sandboxes or fail on WSL, so **prefer the Docker invocations below, the known-working path that needs no local toolchain.** These tools auto-discover their targets from the working directory.

**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).
- **The [`.husky/pre-commit`](./catalog/snippets/husky/pre-commit) hook** runs **language formatting only** - CSharpier + `dotnet format` (or ruff) via native tooling, no Docker and no doc linters, so it stays fast.
- **The VS Code [Lint tasks](./catalog/snippets/configs/vscode-tasks.json)** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks.
Comment on lines +230 to +234

The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc or headless (agent) runs.

- **editorconfig-checker** (line endings + charset across the tree):

```sh
docker run --rm -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest
```

- **actionlint** (GitHub Actions workflow YAML - run after any `.github/workflows/` edit, since workflow-only changes are not smoke-built):

Expand All @@ -238,7 +252,13 @@ The CI lint job runs these tools (workflow YAML and Markdown), but run them loca
- **markdownlint-cli2** (Markdown - mirrors the davidanson VS Code extension via the shared [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc), so the CLI and IDE agree):

```sh
docker run --rm -v "$PWD":/workdir davidanson/markdownlint-cli2:latest "**/*.md"
docker run --rm -v "$PWD":/workdir --workdir /workdir davidanson/markdownlint-cli2:latest "**/*.md"
```

- **cspell** (spelling in user-facing docs; word list + exclusions in [`cspell.json`](./cspell.json)):

```sh
docker run --rm -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress "**/*.md"
```

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.
Expand All @@ -258,7 +278,7 @@ Contributors commit to this repo with signed commits; the SSH-signing setup live
## Editor and Tasks

- **VS Code is the primary IDE, and the experience favors it.** Prefer VS Code tasks and launch configurations for building, running, and testing over ad-hoc shell scripts; a script is the fallback, not the default.
- The `.code-workspace` file carries the shared editor settings and the recommended-extension set. A **standard set** of extensions applies to every repo (markdownlint, cspell, editorconfig, markdown-all-in-one, better-todo-tree, github-actions, actionlint, shellcheck, claude-code); **language-specific** extensions are added per project (.NET: csdevkit, csharpier; Python: python, pylance, ruff, mypy; Docker: the Docker extension). The catalog holds the full set and per-language additions: [`catalog/snippets/vscode/`](./catalog/snippets/vscode/).
- The `.code-workspace` file carries the shared editor settings and the recommended-extension set. **All VS Code settings and extension recommendations live only here, never in a standalone `.vscode/settings.json` or `.vscode/extensions.json`** (`.vscode/` holds only `tasks.json` and `launch.json`). A **standard set** of extensions applies to every repo (markdownlint, cspell, editorconfig, markdown-all-in-one, better-todo-tree, github-actions, actionlint, shellcheck, claude-code); **language-specific** extensions are added per project (.NET: csdevkit, csharpier; Python: python, pylance, ruff, mypy; Docker: the Docker extension). The catalog holds the full set and per-language additions: [`catalog/snippets/vscode/`](./catalog/snippets/vscode/).
- The Table of Contents is maintained by the Markdown All in One extension; `markdown.extension.toc.levels` in the workspace sets which heading levels it includes (see the Markdown rules for the authoring convention and the `<!-- omit from toc -->` exclusion marker).
- **Agents: editing the active `.code-workspace` can reload the VS Code window and drop the agent's session.** Commit all state first, prefer opening the folder rather than the workspace while editing it, or leave workspace edits to the maintainer (a maintainer edit does not reload).

Expand Down
2 changes: 1 addition & 1 deletion CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions][analyzer-d
logger.LogError(exception, "{Function}", function);
```

2. **Libraries log through abstractions, never a concrete backend.** A NuGet **library** depends only on `Microsoft.Extensions.Logging.Abstractions` and exposes an `ILoggerFactory` seam - a settable global factory defaulting to `NullLoggerFactory` (fallback `NullLogger.Instance`) with `SetFactory`/`TrySetFactory`, and/or an `ILoggerFactory`/`ILogger` parameter in its API. It must **not** reference Serilog or any sink - that forces a logging framework on every consumer and drags in AOT-incompatible dependencies. The consuming **application** owns the concrete logger (Serilog is fine there), bridges it to `ILoggerFactory` (e.g. `SerilogLoggerFactory` from `Serilog.Extensions.Logging`), and injects it. Reference: `LanguageTags` - `LogOptions` in the library; the CLI's `LoggerFactory` builds the Serilog-backed factory and injects it via `LogOptions.SetFactory`.
2. **Libraries log through abstractions, never a concrete backend.** A NuGet **library** depends only on `Microsoft.Extensions.Logging.Abstractions` and exposes an `ILoggerFactory` seam - a settable global factory defaulting to `NullLoggerFactory.Instance` (fallback `NullLogger.Instance`) with `SetFactory`/`TrySetFactory`, and/or an `ILoggerFactory`/`ILogger` parameter in its API. It must **not** reference Serilog or any sink - that forces a logging framework on every consumer and drags in AOT-incompatible dependencies. The consuming **application** owns the concrete logger (Serilog is fine there), bridges it to `ILoggerFactory` (e.g. `SerilogLoggerFactory` from `Serilog.Extensions.Logging`), and injects it. Reference: `LanguageTags` - `LogOptions` in the library; the CLI's `LoggerFactory` builds the Serilog-backed factory and injects it via `LogOptions.SetFactory`.

3. **CallerMemberName**: Use for automatic function name tracking

Expand Down
2 changes: 1 addition & 1 deletion WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input
- **D1.3 Smoke never publishes and never uploads.** Input: `smoke: true`. Output: full compile/lint/test, but no registry/image push, no release, and **no** artifact uploads (every `upload-artifact`, including any aggregation job, is gated `!smoke`). *Prevents: a PR publishing; orphaned artifacts churning the storage quota.*
- **D1.4 Workflow-file changes are not smoke-built.** Input: a PR changing only `.github/workflows/**`. Output: the paths-filter excludes workflow files, so smoke-build skips. *Implication: a workflow-only change is not smoke-built, but actionlint still validates it in CI.*
- **D1.5 One required aggregator gates merge.** Input: any PR. Output: a single aggregator job must **succeed**, `needs:` the changes job and the validation job, treat a **skipped** smoke build as pass, and **block** on `failure`/`cancelled`. Its name is ruleset-bound: the job `name:` and the ruleset `context:` are the same string and MUST be renamed together, never independently. *Prevents: a paths-filter error letting a target-changing PR merge unbuilt.*
- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --collect:"XPlat Code Coverage"` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate); `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests (see `spec/secrets.json` `typeMechanisms`). *Prevents: coverage silently going unreported; a stale, unused token.*
- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --collect:"XPlat Code Coverage"` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate); `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests (see `spec/secrets.json` `typeMechanisms`). The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR - a distinct knob from `fail_ci_if_error` (which only guards the upload step) - and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`; a repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact - `.gitignore` excludes it (e.g. `coverage/`, `*.cobertura.xml`; `.gitignore` is the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.*

### D2 - Input/State Validation at Entry

Expand Down
16 changes: 16 additions & 0 deletions catalog/snippets/configs/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Codecov configuration. Codecov's coverage statuses are advisory - they report and trend coverage, never gating a PR.
# - informational: true makes project and patch statuses advisory (always pass), so a coverage delta never blocks a PR.
# - Distinct from the upload step's fail_ci_if_error: false, which only guards upload errors.
# - A repo may override this to enforce a coverage threshold.
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true

# Exclude code intentionally not unit-tested from the coverage denominator. Add the repo's own paths.
# ignore:
# - "Sandbox/**"
57 changes: 57 additions & 0 deletions catalog/snippets/configs/vscode-tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,63 @@
"showReuseMessage": false,
"clear": false
}
},
// Lint group - the local full doc-lint surface (Docker at :latest), matching the CI lint set.
// Run on demand. The pre-commit hook does language formatting only. Every repo carries these.
{
"label": "Lint: EditorConfig",
"type": "process",
"command": "docker",
"args": [ "run", "--rm", "-v", "${workspaceFolder}:/check", "-w", "/check", "mstruebing/editorconfig-checker:latest" ],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Workflows",
"type": "process",
"command": "docker",
"args": [ "run", "--rm", "-v", "${workspaceFolder}:/repo", "-w", "/repo", "rhysd/actionlint:latest", "-color" ],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Markdown",
"type": "process",
"command": "docker",
"args": [ "run", "--rm", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "davidanson/markdownlint-cli2:latest", "**/*.md" ],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Spelling",
"type": "process",
"command": "docker",
"args": [ "run", "--rm", "-v", "${workspaceFolder}:/workdir", "-w", "/workdir", "ghcr.io/streetsidesoftware/cspell:latest", "--no-progress", "**/*.md" ],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: All",
"dependsOrder": "sequence",
"dependsOn": [
"Lint: EditorConfig",
"Lint: Workflows",
"Lint: Markdown",
"Lint: Spelling"
],
"problemMatcher": []
}
]
}
7 changes: 7 additions & 0 deletions catalog/snippets/husky/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Husky snippet

`pre-commit` is the reference git pre-commit hook (installed under `.husky/` by Husky). It runs **language formatting and style only** - CSharpier and `dotnet format` style via `dotnet husky run` for .NET, or ruff for a Python repo - kept fast with native tooling and no Docker.

Full linting (line endings, workflow YAML, Markdown, spelling) is **not** run in the hook. It runs in CI as pinned action wrappers, and on demand via the VS Code **Lint** tasks in `catalog/snippets/configs/vscode-tasks.json` (Docker at `:latest`). Keeping the doc linters out of the hook is what keeps it simple.

A copied `.husky/pre-commit` is an extensionless shebang script, so pin it to **LF** in both `.gitattributes` (`.husky/pre-commit text eol=lf`) and `.editorconfig` (`[.husky/pre-commit] end_of_line = lf`) - a CRLF shebang breaks execution. Drop the `dotnet husky run` line in a non-.NET repo.
9 changes: 9 additions & 0 deletions catalog/snippets/husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Local pre-commit: language formatting and style only (no Docker). Full lint runs in CI and the VS Code Lint tasks.

# .NET: CSharpier + dotnet format style via Husky.Net. A Python repo runs ruff here instead.
if command -v dotnet >/dev/null 2>&1; then
dotnet husky run
fi
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"LINQ",
"logfile",
"mktemp",
"mstruebing",
"mypy",
"nameof",
"nbgv",
Expand Down
Loading