From 6b9486f972f22ce9c5d5f86a8dc92a2b49e324a7 Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:35:31 +0100 Subject: [PATCH 1/6] docs(adr): propose a repository settings baseline by class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Status PROPOSED. Changes no repository by itself. ADR-0009 declares a required, release-gating test suite for class-2 repositories. zsh-lint is class 2 and its default branch has no required status checks, no required reviews, and admins unenforced. The requirement is real policy enforced by nothing — the #464 pattern moved from CI to repository configuration. A survey of all 86 active public repositories found 75 with no rules at all on the default branch, and only two configured beyond a single rule. Those two disagree with each other, because no written standard exists: nothing in decisions, runbooks, or AGENTS.md mentions branch protection or rulesets, and the new-repository runbook never configures them. Organization-level rulesets would be the right mechanism but return 403 Upgrade to GitHub Team on the current plan, so per-repository configuration is the only option and drift is structural rather than accidental. That makes a declared baseline worth having. Records the costs honestly: required checks can deadlock, as wiki did this week; rules do not stop administrators; and enforce_admins is deliberately excluded because enabling it would have made that deadlock unrecoverable. --- .../0013-repository-settings-baseline.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 decisions/0013-repository-settings-baseline.md diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md new file mode 100644 index 000000000..bdecf2142 --- /dev/null +++ b/decisions/0013-repository-settings-baseline.md @@ -0,0 +1,174 @@ +# 13. Repository Settings Baseline by Class + +- **Status:** PROPOSED +- **Date:** 2026-07-22 +- **Deciders:** ss-o +- **Supersedes:** None +- **Superseded by:** None + +## Context + +The organization declares requirements that nothing verifies. + +ADR-0009 states that class-2 repositories have a test suite that is +**required** and that **gates release tags**. `z-shell/zsh-lint` is class 2. Its +default branch reports: + +```json +{ "required_status_checks": null, "required_approving_review_count": 0, "enforce_admins": false } +``` + +The requirement is real policy and is enforced by nothing. This is the same +failure recorded in #464, where two documents described a commit-trailer check +that no workflow ran, generalized from CI to repository configuration. + +A survey of all 86 active, public, non-fork repositories found: + +| Measure | Count | +| --- | --- | +| No rules at all on the default branch | **75 (87%)** | +| No classic branch protection | 60 (70%) | +| No rulesets | 73 (85%) | +| Any rule present | 11 — six of which have only `copilot_code_review` | +| Repositories with more than one kind of rule | **2** (`zi`, `wiki`) | +| Required status checks configured | `zi` 5, `wiki` 1, all others 0 | +| Default branch still `master` | `z-shell/web` | + +The two configured repositories do not agree with each other, and neither was +derived from a written standard. Nothing in `decisions/`, `runbooks/`, or +`AGENTS.md` mentions branch protection or rulesets, and +`runbooks/new-repository.md` bootstraps labels, CI templates, and dependency +automation without ever configuring protection. Settings have therefore been set +per repository, by hand, at different times, against no shared reference. + +Two mechanisms are in play simultaneously. Classic branch protection and +repository rulesets are **independent** systems whose effective behaviour is +their union. `z-shell/wiki` reports `required_linear_history: false` under +classic protection while a ruleset enforces it on the same branch. Reading +either source alone gives a wrong answer, and 19 repositories still carry +classic protection with no ruleset at all. + +**Organization-level rulesets are not available on the current plan** +(`GET /orgs/z-shell/rulesets` returns `403 Upgrade to GitHub Team`), and no +repository custom properties are defined. Central enforcement is therefore not +an option today: per-repository configuration is the only mechanism, so drift is +structural rather than accidental. + +## Decision + +Adopt a required settings baseline, expressed per ADR-0007 repository class, as +the reference that repositories are configured against and audited for. + +### Baseline by class + +`R` = required. `S` = recommended, not required. `—` = not required. + +| Setting | 1. Deployed | 2. Versioned | 3. Git-consumed | 4. Meta | +| --- | --- | --- | --- | --- | +| Default branch named `main` | R | R | R | R | +| Pull request required to the default branch | R | R | R | R | +| Deletion of the default branch blocked | R | R | R | R | +| Force push to the default branch blocked | R | R | R | R | +| Required status checks | R | R | S | S | +| Linear history | R | S | S | S | +| Signed commits | S | S | S | S | +| Copilot code review | R | R | S | R | + +Rationale for the differences: + +- **Required status checks** are mandatory only where a failing artifact reaches + users automatically (class 1) or is published under a version tag (class 2). + Class 3 is consumed from source at a ref the consumer chooses. +- **Linear history** is required only for class 1, where the deployed branch must + be trivially bisectable against what is live. Elsewhere it constrains merge + strategy for little benefit — and see the cost recorded below. +- **Copilot code review** is required wherever a change reaches users or other + repositories without a second human necessarily reading it. + +### Expressed as rulesets, not classic protection + +New configuration uses repository rulesets. Classic branch protection is treated +as legacy: it is not extended, and where both exist the ruleset is authoritative. +Migration of the 19 classic-only repositories is not required by this ADR and +should not be bundled with adopting the baseline. + +### Repositories with no CI + +23 repositories have no workflow files. "Required status checks" is unsatisfiable +there. Those repositories are conformant on every other row and are reported as +`n/a` for that row rather than as failures. + +## Rollout and rollback + +1. Accept this ADR. It changes no repository by itself. +2. Add a settings step to `runbooks/new-repository.md` so new repositories start + conformant. +3. Build a read-only audit that reports drift per repository against this table, + following the `scripts/labels-sync.rb` pattern: read-only by default, an apply + mode behind both `--apply` and `--confirm-apply`, and a pilot allowlist. +4. Apply per repository, deliberately, starting with class 1 and class 2. + +Rollback is per repository and immediate: rulesets can be deleted without +touching repository contents. Nothing in this ADR is irreversible. + +## Consequences + +### Positive + +- A declared standard exists, so "is this repository configured correctly?" has + an answer that is not a matter of opinion. +- Drift becomes measurable. Given per-repository configuration is the only + mechanism available on this plan, measurement is the only alternative to + discovering gaps by accident. +- The declared-versus-enforced gap in ADR-0009 becomes visible rather than + latent. + +### Negative / costs + +- Requiring pull requests on repositories that currently allow direct pushes will + change day-to-day habits for single-maintainer repositories. +- Required status checks can deadlock. `z-shell/wiki` required a check whose + failure could only be cleared by the merge that the check was blocking; the fix + needed a direct-to-`main` commit. Any class-1 repository can reach this state, + and the escape route must stay available. +- Rules do not stop administrators. An admin push reports + `Bypassed rule violations` and succeeds. The baseline raises the floor; it does + not make configuration self-enforcing. +- Configuring 86 repositories is real work even when automated, and every applied + rule is a way to lock a maintainer out of their own repository. + +### Neutral + +- `enforce_admins` is deliberately left out of the baseline. Enabling it would + have made the `wiki` deadlock unrecoverable without changing settings under + pressure. +- The baseline says nothing about branch names other than the default, so + ADR-0008's `next` branch model is unaffected. + +## Alternatives considered + +- **Organization-level rulesets.** The correct mechanism, and what this ADR would + otherwise recommend instead of a per-repository baseline. Unavailable on the + current plan; revisit if the organization moves to GitHub Team. +- **Do nothing and fix repositories as problems surface.** This is the status + quo, and it is what produced a class-2 repository with an unenforced "required" + test suite. The cost is paid in incidents rather than in setup. +- **One uniform policy for all repositories.** Simpler to state and to audit, but + it would require status checks on 23 repositories that have no CI, and would + impose class-1 constraints on plugins consumed from source. +- **Automated remediation across all repositories.** Rejected: applying + protection unattended to 86 repositories risks locking maintainers out, and the + labels rollout showed that a per-repository confirm step is affordable. + +## References + +- `decisions/0007-release-publication-flow.md` — the repository classes this + baseline is keyed to. +- `decisions/0009-testing-ci-strategy.md` — declares the class-2 test requirement + that is currently unenforced. +- `runbooks/new-repository.md` — bootstrap procedure that should gain a settings + step. +- `runbooks/release.md` — records that classic protection and rulesets are + independent systems whose effective rule is their union. +- `scripts/labels-sync.rb` — the audit-then-gated-apply pattern the eventual + settings audit should follow. From 98654428ed7f7518eb425b29339bb9b6fc776402 Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:43:13 +0100 Subject: [PATCH 2/6] docs(adr): correct the classic-versus-ruleset precedence claim Review feedback. The decision section said a ruleset is authoritative where both it and classic branch protection exist, contradicting the context section, which correctly states the effective rule is their union. The contradiction mattered: treating a ruleset as superseding classic protection is exactly the misreading that led to a wrong conclusion about wiki's main branch, where classic protection reports required_linear_history false while a ruleset enforces it. Neither system is authoritative. Both apply until classic protection is explicitly deleted, so an audit must read both sources and a repository counts as migrated only once the classic settings are removed. --- decisions/0013-repository-settings-baseline.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index bdecf2142..ae5b11b89 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -88,9 +88,18 @@ Rationale for the differences: ### Expressed as rulesets, not classic protection New configuration uses repository rulesets. Classic branch protection is treated -as legacy: it is not extended, and where both exist the ruleset is authoritative. +as legacy and is not extended. + +Where both exist, **neither one is authoritative**: the two systems apply +simultaneously and the effective rule is their union. A ruleset does not +override, disable, or supersede classic protection — the classic settings keep +applying until they are explicitly removed. Any audit must therefore read both +sources, and a repository is only fully migrated once its classic protection is +deleted, not merely once a ruleset exists alongside it. + Migration of the 19 classic-only repositories is not required by this ADR and -should not be bundled with adopting the baseline. +should not be bundled with adopting the baseline. Until a repository is +migrated, expect its behaviour to reflect both systems at once. ### Repositories with no CI From 8947e5a9e964cf9599b9dbd442ddf3d8d6979d3e Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:47:20 +0100 Subject: [PATCH 3/6] docs(adr): use US spelling to match the other ADRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback. decisions/ uses US spelling exclusively — behavior appears six times and behaviour none, organization eighteen times and organisation none. This ADR introduced the only two British spellings in the directory, which also makes the term harder to grep for. --- decisions/0013-repository-settings-baseline.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index ae5b11b89..478a4b6ae 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -42,7 +42,7 @@ automation without ever configuring protection. Settings have therefore been set per repository, by hand, at different times, against no shared reference. Two mechanisms are in play simultaneously. Classic branch protection and -repository rulesets are **independent** systems whose effective behaviour is +repository rulesets are **independent** systems whose effective behavior is their union. `z-shell/wiki` reports `required_linear_history: false` under classic protection while a ruleset enforces it on the same branch. Reading either source alone gives a wrong answer, and 19 repositories still carry @@ -99,7 +99,7 @@ deleted, not merely once a ruleset exists alongside it. Migration of the 19 classic-only repositories is not required by this ADR and should not be bundled with adopting the baseline. Until a repository is -migrated, expect its behaviour to reflect both systems at once. +migrated, expect its behavior to reflect both systems at once. ### Repositories with no CI From 24ef1d500776f9b2973a03f6257287b43a48a334 Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:51:28 +0100 Subject: [PATCH 4/6] docs(adr): name the classes exactly as ADR-0007 does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback. The baseline table abbreviated three of the four class names — Deployed, Versioned and Meta against ADR-0007's Continuously deployed artifacts, Versioned tools and packages and Meta/infrastructure — which makes it easy to misread which class a row applies to. The four names now appear verbatim in a mapping above the table, with the columns keyed by number so the table stays narrow. Also drops the legend entry for a not-required symbol that no cell used, since it implied a distinction the baseline does not make. --- decisions/0013-repository-settings-baseline.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index 478a4b6ae..ca0f0bff5 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -61,9 +61,18 @@ the reference that repositories are configured against and audited for. ### Baseline by class -`R` = required. `S` = recommended, not required. `—` = not required. +Classes are exactly those defined in +`decisions/0007-release-publication-flow.md`, referred to here by number so the +table stays readable: -| Setting | 1. Deployed | 2. Versioned | 3. Git-consumed | 4. Meta | +1. **Continuously deployed artifacts** — `wiki`, `src`, `zd` images +2. **Versioned tools and packages** — `zunit`, `zsh-lint`, packaged `zsh` +3. **Git-consumed source** — `zi`, most plugins and annexes +4. **Meta/infrastructure** — `.github` + +`R` = required. `S` = recommended, not required. + +| Setting | Class 1 | Class 2 | Class 3 | Class 4 | | --- | --- | --- | --- | --- | | Default branch named `main` | R | R | R | R | | Pull request required to the default branch | R | R | R | R | From 43006843d95b7376b3d79775a572cf03f7c54f2c Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:53:52 +0100 Subject: [PATCH 5/6] docs(adr): clarify the no-CI exemption sentence Review feedback on subject-verb agreement. The quoted phrase named a table row, so singular agreement was defensible, but the sentence was ambiguous about whether it described the row or status checks in general. Naming the row explicitly removes the ambiguity instead of trading one reading for the other, and matches the following sentence, which already refers to rows. --- decisions/0013-repository-settings-baseline.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index ca0f0bff5..b1b168b20 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -112,9 +112,10 @@ migrated, expect its behavior to reflect both systems at once. ### Repositories with no CI -23 repositories have no workflow files. "Required status checks" is unsatisfiable -there. Those repositories are conformant on every other row and are reported as -`n/a` for that row rather than as failures. +23 repositories have no workflow files, so the **Required status checks** row is +unsatisfiable there — there is no check to require. Those repositories are +conformant on every other row and are reported as `n/a` for that row rather than +as failures. ## Rollout and rollback From 13c7a4783d48c91307625da91d1e04c6383df7cc Mon Sep 17 00:00:00 2001 From: Sall Date: Wed, 22 Jul 2026 03:57:56 +0100 Subject: [PATCH 6/6] docs(adr): correct the rollback claim for the default-branch row Review feedback, and the claim was wrong rather than merely incomplete. Rollback was described as immediate because rulesets can be deleted. The baseline's first row is not a ruleset: renaming a default branch is not undone by deleting anything, GitHub's redirect does not update existing clones or references that name the branch in other repositories' workflows, documentation, or pinned uses: lines, and reverting means renaming back and repairing those references a second time. The row is now marked audit-only and excluded from any automated apply, the rollback section separates ruleset-expressed settings from the rename, and the irreversibility claim is scoped to what the audit actually applies. Only z-shell/web is non-conformant on that row, and it is called out as a deliberate one-off. --- .../0013-repository-settings-baseline.md | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/decisions/0013-repository-settings-baseline.md b/decisions/0013-repository-settings-baseline.md index b1b168b20..e8a62ba84 100644 --- a/decisions/0013-repository-settings-baseline.md +++ b/decisions/0013-repository-settings-baseline.md @@ -74,7 +74,7 @@ table stays readable: | Setting | Class 1 | Class 2 | Class 3 | Class 4 | | --- | --- | --- | --- | --- | -| Default branch named `main` | R | R | R | R | +| Default branch named `main` (audit only, never auto-applied) | R | R | R | R | | Pull request required to the default branch | R | R | R | R | | Deletion of the default branch blocked | R | R | R | R | | Force push to the default branch blocked | R | R | R | R | @@ -127,8 +127,26 @@ as failures. mode behind both `--apply` and `--confirm-apply`, and a pilot allowlist. 4. Apply per repository, deliberately, starting with class 1 and class 2. -Rollback is per repository and immediate: rulesets can be deleted without -touching repository contents. Nothing in this ADR is irreversible. +Rollback differs by setting, and the distinction matters. + +Every row except the first is expressed as a ruleset. Those are reversible per +repository and immediately: deleting the ruleset restores the prior state without +touching repository contents. + +**Renaming a default branch is not in that category** and must not be treated as +one. It is not a ruleset, so deleting rulesets does not undo it. GitHub retargets +open pull requests and installs a redirect, but existing clones keep tracking the +old name, and anything referencing the branch by name — workflow triggers in +other repositories, documentation links, external CI, `uses:` references pinned +to a branch — is not updated. Reverting means renaming back and repairing the +same references twice. + +The default-branch row is therefore **excluded from any automated apply**. Only +`z-shell/web` is currently non-conformant, and it should be migrated as a +deliberate one-off with its references audited first, not swept up in a +conformance run. + +With that row excluded, nothing the audit applies is irreversible. ## Consequences