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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Applies to code and workflow (`#`) comments alike.

- **[`.editorconfig`](./.editorconfig) sets the line ending:** `[*] end_of_line = crlf` is the **default** - every file type is CRLF unless pinned otherwise - with **LF** pinned for the execution-sensitive exceptions - `*.sh`, Dockerfiles, and any individual `.py` executed directly via its shebang (pinned **by path**, e.g. `spec/validate.py`; vanilla `.py` stays CRLF, since Python's universal newlines accept it and it is commonly edited on Windows). Only the LF exceptions are declared; the redundant per-type CRLF rules are intentionally omitted. `.gitattributes` mirrors it: `* -text` (git stores the exact bytes you commit and will **not** normalize) plus the matching LF pins.
- **Choosing an ending for a new file type:** CRLF is the **default** - cross-platform editors on Windows produce it, and it is harmless on Linux for everything except shell. Use LF only when the type **requires** it or CRLF **breaks how it is consumed**: executable scripts/shebangs (`*.sh`, s6, husky), Dockerfiles (CRLF breaks `RUN` heredocs/continuations), and tool-owned formats with a native LF ending (KiCad). **Non-workflow YAML stays CRLF** - GitHub Actions' parser tolerates it (a repo that also runs yamllint sets `new-lines: disable` to defer to `.editorconfig`). **Workflow YAML (`.github/workflows/*.{yml,yaml}`) is pinned LF** in `.editorconfig` - Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This (and the catalog snippet workflows in `catalog/snippets/workflows/*`, pinned LF the same way) is an LF class **not** backed by a `.gitattributes` pin: git keeps `* -text` (no normalization), and CI's `editorconfig-checker` (EOL-only) catches a mismatch instead. Distinguish where a file is *consumed* from where it is *edited*: consumption on Linux alone does not force LF. A config or pattern file consumed by a Linux tool stays CRLF when the tool tolerates a trailing CR: `.dockerignore` and `.gitignore` are CRLF (their parsers strip the CR), and only a *Dockerfile* - interpreted, where a CR breaks `RUN` heredocs and line continuations - is LF.
- **Operational (config) repos: the global default follows the consuming application's native platform, not the fleet CRLF default.** A config repo (registry `workflowModel: operational`) is a *view into an application's configuration directory* - often the exact tree mounted into that app's container - so its files must use the ending the app itself reads and writes, and forcing the fleet CRLF default would fight the app. Set the `[*] end_of_line` default to the app's native ending and record it in the registry [`lineEndings`](./registry/repos.json) field (`lf` | `crlf`): **LF** for a Linux-native app whose config lives in a Linux container - ESPHome, Home Assistant, a devcontainer-only or HACS config - and **CRLF** for a Windows-native editor - e.g. Vantage-Config, edited by Design Center on Windows. The execution-sensitive LF pins (`*.sh`, Dockerfiles, workflow YAML) still apply on top, and `.gitattributes` still mirrors the chosen default. This override is for operational repos only; `release` repos keep the `[*] end_of_line = crlf` fleet default above. Do **not** re-normalize such a repo to the fleet default - that is exactly the over-normalization these per-repo endings prevent.
- **Scripts and extensionless executables must be LF - and pinned in `.gitattributes`, not just configured.** A CRLF shebang (`#!/usr/bin/env bash\r`) breaks execution. `.editorconfig` sets `[*.sh] = lf`, but that extension-based rule does not match **extensionless** executables (s6 service scripts `run`/`up`/`finish`, husky/git hook scripts like `.husky/pre-commit`), and `* -text` enforces nothing - so a broad normalization pass or an editor can silently flip them to CRLF (it has). `.gitattributes` is the enforcement layer: it carries `*.sh text eol=lf`, and any repo whose tooling ships extensionless scripts **adds the matching path pin** - e.g. `Docker/s6-overlay/** text eol=lf` for s6 init, `.husky/pre-commit text eol=lf` for husky hooks - so git holds them at LF on checkout and `--renormalize`. This pin is mandatory for any repo that overrides s6 init, uses husky/git hooks, or otherwise ships executable scripts. The same explicit-pin rule extends to **tool-owned file formats the base config doesn't key on**: pin them to whatever ending the tool reads and writes so a normalization sweep can't churn them - e.g. KiCad project/footprint/3D files (`*.kicad_mod`, `*.kicad_sym`, `*.step`), which KiCad writes LF (`*.kicad_mod text eol=lf`, ...). The principle is general: a file class the `.editorconfig` extension rules and `* -text` don't cover needs an explicit `.gitattributes` pin matching its tool's native ending.
- **Pair each such pin with a matching `.editorconfig` override - the git pin alone is not enough.** `.gitattributes` governs **git** (checkout, commit, `--renormalize`); the **editor** follows `.editorconfig`, where the `[*] end_of_line = crlf` default still applies to any file no extension rule covers. So even with the git pin, the editor writes a CRLF shebang into an extensionless hook (breaking it when run from the working tree) or re-ends/trims a byte-sensitive data file. Give every extensionless **executable** an editorconfig LF override beside its `.gitattributes` pin (`[.husky/pre-commit] end_of_line = lf`); and for a **byte-preserve data directory** (downloaded or opaque source whose exact bytes the consumer may depend on) disable *all* editor normalization, not just EOL - `[<dir>/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` is EditorConfig's spec-defined special value that removes an inherited property, so the editor enforces neither the global `charset` nor `end_of_line` on that path). Keep these overrides with the line-ending governance (above any `.NET-only` divider), not in the language-style section.
- **New files:** create them with the `.editorconfig`-mandated ending.
Expand Down
4 changes: 4 additions & 0 deletions registry/repos.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"types": ["source-only"],
"groundTruthBranch": "develop",
"workflowModel": "operational",
"lineEndings": "lf",
"hasDevelop": true,
"publish": [{ "target": "github-release", "mechanism": "none" }],
"requiredSecrets": [],
Expand Down Expand Up @@ -203,6 +204,7 @@
"types": ["source-only"],
"groundTruthBranch": "develop",
"workflowModel": "operational",
"lineEndings": "lf",
"hasDevelop": true,
"publish": [{ "target": "github-release", "mechanism": "none" }],
"requiredSecrets": [],
Expand All @@ -217,6 +219,7 @@
"types": ["source-only"],
"groundTruthBranch": "main",
"workflowModel": "operational",
"lineEndings": "lf",
"hasDevelop": true,
"publish": [{ "target": "github-release", "mechanism": "none" }],
"requiredSecrets": [],
Expand Down Expand Up @@ -283,6 +286,7 @@
"types": ["source-only"],
"groundTruthBranch": "develop",
"workflowModel": "operational",
"lineEndings": "crlf",
"hasDevelop": true,
"publish": [{ "target": "github-release", "mechanism": "none" }],
"requiredSecrets": [],
Expand Down
9 changes: 9 additions & 0 deletions registry/repos.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@
"$defs": {
"releaseTrigger": { "enum": ["two-phase", "publish-on-merge", "dispatch-only", "none"] },
"workflowModel": { "enum": ["release", "operational"] },
"lineEndings": { "enum": ["lf", "crlf"] },
"mechanism": { "enum": ["oidc", "static-secret", "none"] },
"target": { "enum": ["nuget", "pypi", "docker", "github-release"] },
"repo": {
"type": "object",
"required": ["name", "url", "status"],
"allOf": [
{
"comment": "An operational repo must declare its line endings (release repos use the fleet CRLF default).",
"if": { "properties": { "workflowModel": { "const": "operational" } }, "required": ["workflowModel"] },
"then": { "required": ["lineEndings"] }
}
],
Comment on lines +34 to +40
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
Expand All @@ -39,6 +47,7 @@
"classificationPending": { "type": "boolean" },
"groundTruthBranch": { "type": "string" },
"workflowModel": { "$ref": "#/$defs/workflowModel" },
"lineEndings": { "$ref": "#/$defs/lineEndings" },
"hasDevelop": { "type": "boolean" },
Comment thread
ptr727 marked this conversation as resolved.
"publish": {
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion spec/project-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
{ "id": "recurring.comments", "verdict": "letter", "assert": "Comments are concise, only the non-obvious, no prose narration, and do not grow on re-edit.", "intentRef": "AGENTS.md#comments" },
{ "id": "recurring.charset", "verdict": "letter", "assert": "ASCII only in agent-authored text: no em-dash (use a spaced hyphen), no smart quotes, no stray non-ASCII.", "intentRef": "AGENTS.md#character-set" },
{ "id": "recurring.spelling", "verdict": "letter", "assert": "US English spelling; the shared cspell.json sets language en-US (a bare en accepts British spellings too).", "intentRef": "CODESTYLE.md#markdown-and-spelling" },
{ "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow .editorconfig, which carries a global [*] end_of_line = crlf default plus LF pins for execution-sensitive files (shell, Dockerfiles, shebang-executable .py by path), plus workflow YAML in .github/workflows/* enforced by editorconfig-checker in CI; a per-extension-only form lacking the global [*] default is a drift finding. Edits preserve the file's endings.", "intentRef": "AGENTS.md#line-endings" }
{ "id": "recurring.eol", "verdict": "letter", "assert": "Line endings follow .editorconfig, which carries a global [*] end_of_line default plus LF pins for execution-sensitive files (shell, Dockerfiles, shebang-executable .py by path), plus workflow YAML in .github/workflows/* enforced by editorconfig-checker in CI; a per-extension-only form lacking the global [*] default is a drift finding. The global default is CRLF for release repos, or the consuming application's native platform for an operational (config) repo as recorded in the registry lineEndings field (LF for Linux-native/container config e.g. ESPHome/Home Assistant, CRLF for a Windows-native editor e.g. Vantage/Design Center) - do not re-normalize such a repo to CRLF. Edits preserve the file's endings.", "intentRef": "AGENTS.md#line-endings" }
]
},
"readme-structure": {
Expand Down
8 changes: 8 additions & 0 deletions spec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ def check_secret_set(label, entry, need_kind):
if model is not None and model not in ("release", "operational"):
errors.append(f"{name}: workflowModel '{model}' invalid (expected release or operational)")

eol = repo.get("lineEndings")
if eol is not None and eol not in ("lf", "crlf"):
errors.append(f"{name}: lineEndings '{eol}' invalid (expected lf or crlf)")
Comment thread
ptr727 marked this conversation as resolved.
# An operational repo's endings follow the consuming app's platform, so they must be declared; a release
# repo omits the field and uses the fleet CRLF default.
if model == "operational" and eol is None:
errors.append(f"{name}: operational repo must declare lineEndings (lf or crlf)")
Comment on lines +127 to +133

required = set(repo.get("requiredSecrets", []))
for pub in repo.get("publish", []):
if not isinstance(pub, dict) or "target" not in pub or "mechanism" not in pub:
Expand Down