Skip to content
Merged
106 changes: 106 additions & 0 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Operations

How this repository is run. It ships no application code, so its operations are the fleet audit, the local gates that mirror CI, and the script that applies repository configuration.

## Runbooks

### Run the gates the way CI runs them

CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py <file>` runs `DEFAULT_RULES`, which omits `comment-wrap` and `comment-case`. A bare run therefore under-reports against what CI checks, and a clean result from it proves less than it appears to. Run the CI invocations:

```sh
python3 scripts/test_prose_lint.py
python3 scripts/test_repo_gate.py
python3 scripts/test_pr_review.py
python3 spec/audit.py --selftest
python3 scripts/repo_gate.py
python3 scripts/prose_lint.py . --check charset --check dupword --check spelling
python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --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
```

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. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it.

Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally.

The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem.

Scope a run to what changed, which matches the correct-as-next-edited rule:

```sh
python3 scripts/prose_lint.py . --diff origin/develop
```

Whole-tree discovery reads only files git tracks, so `python3 scripts/prose_lint.py .` and `--diff` do not see a new file until it is staged, and a clean whole-tree run proves nothing about an unstaged one. An explicit path is always read, tracked or not, so name a new file directly to check it before staging.

### Audit the fleet

```sh
python3 spec/audit.py # every cataloged repo
python3 spec/audit.py <RepoName> # one repo
python3 spec/audit.py --issue <RepoName>
```

Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md).

### Apply or verify repository configuration

```sh
repo-config/configure.sh check <owner>/<repo> <release|operational>
repo-config/configure.sh apply <owner>/<repo> <release|operational>
```

**Always pass the command.** A bare `repo-config/configure.sh` with no arguments defaults to `apply` against the current repo, so an invocation meant to test whether the script runs performs a live write instead. Never run it without a command.

`check` is read-only and exits non-zero on drift. `apply` is idempotent and drives entirely from the committed payloads, so it is a no-op on a conformant repo.

`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, and creates or updates both branch rulesets. On a repository that has deliberately drifted it silently reasserts the fleet configuration.

The model argument selects which develop payload is applied, so passing the wrong one applies the wrong ruleset.

## Backup and Recovery

The repository is the record, and GitHub holds it. Nothing here keeps state outside git.

A deleted branch is recoverable from any full clone that still has the commit, which is the recovery path when a branch is deleted while another pull request is based on it:

```sh
git push origin <sha>:refs/heads/<branch>
```

Never use `--depth 1` on a clone that will amend or force-push, because a shallow clone severs the merge base and orphans the branch.

## Logs and Debugging

Workflow runs are the log. `gh run list --branch <branch>` and `gh run view <id> --log-failed` reach them.

A local gate reproduces a CI failure exactly, because CI runs the same commands listed under Runbooks against the same committed configuration. Reproduce locally before reading workflow logs.

## Tool Usage

The Docker linters pull `:latest` deliberately, so a local run matches whatever CI resolved:

```sh
docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir davidanson/markdownlint-cli2:latest "**/*.md"
docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md
```

Both commands are the canonical invocations from [GOVERNANCE.md](./GOVERNANCE.md). markdownlint reads every markdown file, while cspell reads `README.md` and `HISTORY.md` only. That narrower spelling scope is deliberate, since gating every markdown file would mean padding `cspell.json` with technical terms without end, and broad live spell-check is the editor extension's job. Widening it here produces noise that no gate acts on.

The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action.

Two `gh` limitations on the current host, both worked around rather than fixed:

- `gh pr checks` carries no `--json` flag on the installed `gh` 2.46.0, so a watcher built on it prints nothing and a quiet result reads as a passing one. Read the checks from `gh pr view --json statusCheckRollup` instead.
- `gh pr edit --base` fails with a Projects-classic deprecation error. Use `gh api --method PATCH repos/<owner>/<repo>/pulls/<number> -f base=<branch>` instead.

## Configuration Layout

- [spec/](./spec/) is the machine-readable ground truth, holding project types, the file and section baseline, and required or forbidden secrets.
- [registry/repos.json](./registry/repos.json) is the fleet registry, naming every project with its types, publish mechanism, and status.
- [repo-config/](./repo-config/) holds the branch rulesets and the apply script. It sits outside `.github/`, which is Actions-owned.
- [catalog/](./catalog/) holds reference snippets the audit compares implementations against.
- [reports/](./reports/) holds per-repo audit output.
- [scripts/](./scripts/) holds the gates that run in CI and locally.
4 changes: 2 additions & 2 deletions STANDUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's

- [`CODESTYLE.md`][codestyle]: the repo's language and formatting conventions beyond the carried rules.
- `ARCHITECTURE.md`: how a code repo is built, its module layout, data flow, and design decisions.
- `OPERATIONS.md`: how an operational repo is run, covering runbooks, backup, log and debug procedures, tool-usage notes, and config layout.
- `OPERATIONS.md`: how the repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`.
- `TODO.md`: the repo's running backlog, per [`spec/readme-structure.md`][readme-structure]. It keeps open work out of the README's section order, where it does not belong and changes on a different cadence from everything around it.

**`OPERATIONS.md` is required on an `operational` repo**, not optional, so it appears in the baseline above with `appliesTo: ["operational"]`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file. It is the operational-repo analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one.
**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning those five headings with no content under them, for which this repo's own `OPERATIONS.md` is the worked example. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one.

Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place.

Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Running backlog for this repo, kept in a committed file so the guidance survives
- Refresh the README (it has gone stale) and evaluate a lower-maintenance structure, for example a per-section index that points into each doc with a one-line description, keeping the README as the adoption and audit-instruction entry point with pointers to the other docs. A per-section index trades brevity for a sync obligation: it must track what the docs contain.
- Add a linter-only Python project type for codegen/boilerplate Python, code that runs during another tool's build to emit generated source (e.g. ESPHome codegen that produces enriched C++ at compile time), so it ships no unit tests and no coverage and needs only the linter. Keep it distinct from the existing `python` type, which is utility code that can and should carry unit tests and coverage (as in PlexCleaner). Until it exists, ESPHome-Config stays `source-only` and its `+python` reclassification is deferred, so accept its one outstanding validation finding meanwhile.
- Add a fleet-standard clang-format config for the `cpp` type: a catalog snippet plus a CODESTYLE C++ section defining the style, the C++ analogue of the shared ruff config, so the `cpp` clang-format check references one canonical style rather than each repo inventing its own. Base it on the ESPHome-Config agent's proposed `.clang-format`.
- Document in [`STANDUP.md`][standup] that the initial onboarding commits belong on a feature branch and reach `develop` by squash, never as direct commits to `develop`. Pre-creating `main` and `develop` is right, and committing onto `develop` from there is the wrong next step. Section 0 already teaches the same shape of trap for signing, that the window closes at the first commit, so branch placement belongs beside it. The two reasons do not carry equally across the workflow models, which is the part worth stating rather than assuming. On a `release` repo `repo-config/develop.json` carries a `pull_request` rule, so a direct commit is simply blocked and an agent discovers the rule by hitting it. On an `operational` repo `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so a direct commit succeeds and nothing corrects it, which is exactly why the instruction has to carry the weight there. The reason that holds for both is that the squash collapses the exploratory onboarding history, which is where PII, secrets and noise commits accumulate, and a public repo treats that history as a hard gate. There is no recovery afterwards, since `non_fast_forward` is set on both `develop` payloads, so history that reaches `develop` cannot be rewritten without disabling the ruleset. Blog hit this during its standup and moved to a feature branch.
- Give [`STANDUP.md`][standup] an explicit branch-bootstrap step, because it currently says nothing about how `main` and `develop` come into being and an agent therefore commits onto whichever branch it finds. The sequence that avoids every cleanup problem is: create `main` and leave it carrying nothing, create `develop` from `main` and leave it carrying nothing, create the first feature branch from `develop` and do the whole standup there, add the repo to GitHub and apply the repo config while still on that branch, then open a normal pull request to `develop` when the repo is ready. Nothing ever has to be cleaned off `main` or `develop`, because nothing ever reached them without review. Note the mechanical wrinkle when writing this up: a git branch cannot exist without a commit, so "carrying nothing" means exactly one signed empty root commit (`git commit --allow-empty`), and section 0's signing window applies to that commit like any other. The alternative of committing onto `develop` and squashing afterwards does not work: `non_fast_forward` is set on both `develop` payloads, so the history cannot be rewritten without disabling the ruleset, and Blog was correctly blocked when it tried. Worth stating that the protection is uneven, since a `release` repo's `repo-config/develop.json` carries a `pull_request` rule that blocks a direct commit outright, while `repo-config/operational/develop.json` carries only `deletion`, `non_fast_forward` and `required_signatures`, so on an operational repo a direct commit succeeds and only the instruction stands between the agent and an unfixable history. The reason it matters on a public repo is that the exploratory standup history is where PII, secrets and noise commits accumulate, and squashing a feature branch is the one chance to leave them out.
- Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first.
- Re-vendor `repo-config/configure.sh` across the fleet. The hub swept it to one sentence per line, and it is carried `verbatim` with `appliesTo: "*"`, so every repo already holding a copy is byte-mismatched against the hub until it takes the new one.

Expand Down
3 changes: 2 additions & 1 deletion reports/conformance-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug
| Shape | Reference repo | Cold-standup | Last audited | First gap / notes |
|---|---|---|---|---|
| `python` + `source-only` | Financial-Modeling | not-tested | - | Reference for the source-release (dispatch-only) profile; the downstream standup issue is open. |
| `source-only` + `release` | Blog | not-tested | - | Hugo static site (#456), the next standup and the first cold test of `STANDUP.md` step 0. Its deploy target has no declared type yet, so `releaseTrigger` is `none` and `publish[]` is empty until a deploy has actually run. |
| `csharp` + `console` | - | not-tested | - | |
| `csharp` + `docker` | - | not-tested | - | |
| `csharp` + `python` | PlexCleaner | not-tested | - | First mixed-language shape (#339). Python is a stdlib-only `uvx` **scripts** profile subtree (`RegressionTests/`): no `uv.lock`, `pyproject.toml` lint/type config only, mypy checker, `python.uvlock.pinned` + `python.coverage.codecov` N/A; `codecov.yml` stays required for the C# side. Both language rule-sets apply (CODESTYLE.md "Two profiles"). |
Expand All @@ -19,7 +20,7 @@ The primary shapes are stood up as whole repos; the **composable targets** (`nug
| `upstream-wrapper` | - | not-tested | - | Tag from a committed state file, not SemVer2. |
| `codegen` | - | not-tested | - | Deterministic matrix over both branches. |
| `docs` | ProjectTemplate | not-tested | - | Governance hub; CI is lint-only. |
| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Carries a required `OPERATIONS.md` (`appliesTo: ["operational"]`, presence-checked) for its runbooks. Blog (#456) is the next standup and the first cold test of `STANDUP.md` step 0. |
| `operational` config | - | not-tested | - | `workflowModel: operational`, direct signed commits to `develop`, promotion-PR gate. Its `develop` ruleset carries no `pull_request` rule, so the branch discipline rests on the instruction rather than the gate. |

## Composable Targets

Expand Down
2 changes: 1 addition & 1 deletion spec/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{ "path": "WORKFLOW.md", "fidelity": "intent", "whole": true, "intentRef": "WORKFLOW.md", "appliesTo": "*" },
{ "path": "README.md", "appliesTo": "*" },
{ "path": "HISTORY.md", "appliesTo": "*" },
{ "path": "OPERATIONS.md", "appliesTo": ["operational"] },
{ "path": "OPERATIONS.md", "appliesTo": "*" },
{ "path": ".github/copilot-instructions.md", "fidelity": "intent", "whole": true, "sections": ["Commit Messages and Pull Request Titles", "Reviewing Carried Fleet Content", "GitHub Copilot Review Runbook"], "placeholders": ["<owner>", "<repo>", "<N>"], "appliesTo": "*" },
{ "path": ".editorconfig", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" },
{ "path": ".editorconfig-checker.json", "fidelity": "intent", "whole": true, "intentRef": "GOVERNANCE.md#line-endings", "appliesTo": "*" },
Expand Down
Loading
Loading