diff --git a/config/README.adoc b/config/README.adoc new file mode 100644 index 000000000..ace28b0d3 --- /dev/null +++ b/config/README.adoc @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell += Estate canon: rulesets, settings, autolinks +:toc: macro + +toc::[] + +== What this is + +The single source of truth for how every repository in the estate is +*configured* โ€” as opposed to what runs in it (`.github/workflows/*-reusable.yml`) +or what the code must satisfy (the Mustfiles). Design spec: +`docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md`. +Tier rules: `docs/CICD-SIGNAL-DISCIPLINE.adoc`, section "Estate canon". + +[cols="1,3",options="header"] +|=== +| Path | Holds + +| `rulesets/base.json` +| The one branch ruleset. Applied to every repo, both owners. Template: the + applier fills `required_status_checks` (see `gates.json`). + +| `rulesets/gates.json` +| Which workflow *files* are ๐Ÿ”ด GATE per profile, and how contexts are derived. + +| `rulesets/gates-only.json` +| Owner decision O6 only: a second ruleset carrying just the status-check rule + with a short bypass list, so AI-reviewer apps cannot merge around gates. + *Not applied unless O6 is ruled.* + +| `rulesets/immutable-tags.json` +| The tag ruleset. Tags are created by an admin or by the estate App only. + +| `settings/repo.json` +| Repository settings PATCH body plus the Actions-permission endpoints. + +| `settings/actions-allowlist.json` +| The estate Actions allowlist. *Apply at the tail of the sweep, never before.* + +| `autolinks/*.json` +| Autolink references per profile; `base` plus language/proof additions. +|=== + +== Identity is the target, never the name + +Live rulesets (2026-09-02) are called `Optimus-Branch` on every sampled repo; +older waves were called `Base`, `Backup`, `Pages-fix`. Names drift. The applier +and the verifier identify *the* branch ruleset as: the active ruleset whose +target is `branch` and whose include list is exactly `["~DEFAULT_BRANCH"]`. +Exactly one such ruleset must exist; zero or two is a verifier failure. The +same rule for tags with `["~ALL"]`. The `name` field in these files is what a +fresh POST uses; an existing ruleset is PUT by id and keeps whatever name it has. + +== What the base ruleset deliberately drops from the live copy + +[cols="1,2",options="header"] +|=== +| Live rule | Why it is gone + +| `code_scanning` (CodeQL, Hypatia, Scorecard alerts) +| Doubles the CodeQL/Hypatia gates and makes Scorecard โ€” a PERIODIC โ€” block PRs. + +| `code_quality`, `copilot_code_review`, `code_coverage` 95 %, `required_deployments` github-pages +| Nothing in the estate satisfies them, so every merge went through `--admin`, + which bypasses everything else too (spec ยง4). + +| `update` +| "Restrict updates" makes the default branch writable by bypass actors only; + the `pull_request` rule already forces changes through PRs. + +| `require_code_owner_review`, `required_review_thread_resolution`, `require_extra_approval_for_unattributed_changes` +| No CODEOWNERS estate-wide; thread resolution and attribution approvals were + unsatisfiable for bot PRs. + +| bypass mode `always` on apps and RepositoryRole 2 (maintain) +| `always` lets an app push straight to the default branch. All Integration + bypass is `pull_request`; the maintain role is dropped; admin (5) keeps + `pull_request` โ€” the emergency path is the owner flipping the ruleset, not a + standing push right. + +| Integration ids 56611 (codacy), 827041 (gitar-bot), 254 (codecov), 2740 (renovate), 57789 (advanced-security), 1561, 85455, 946600 +| R1/R4 removals, advanced-security needs no bypass, the last three are + unresolved (owner decision O5). +|=== + +Kept: `deletion`, `non_fast_forward`, `required_signatures`, `pull_request` +(0 approvals, squash only pending O8), `required_status_checks`. + +`strict_required_status_checks_policy` is *false*, decided: with strict on, +every PR must be rebased onto the tip of the default branch before merge, which +on 400 repos with bot PRs means permanent `BEHIND` states (PR #714 in this repo +sat BEHIND on the day this was written). Gates test the change; freshness is +Dependabot's job. + +== Contexts are derived, never typed + +Hand-typed contexts produced seven spellings of CodeQL and four of Hypatia +across the estate. `gates.json` names workflow *files*; the applier reads the +check-run names those files emitted on the latest default-branch run and writes +exactly those, with `integration_id` 15368. A file with no run yet contributes +nothing and is reported. A repo with no derivable contexts gets no +`required_status_checks` rule and is reported as UNGATED โ€” an empty rule would +be a fake gate. + +Canonical thin callers carry the tier in their `name:` (`๐Ÿ”ด GATE: Governance`, +`๐Ÿ“… PERIODIC: Scorecard`); `scripts/check-gate-tiers.sh` keys on that prefix. +Filenames never change (renames register phantom workflows); names and job ids do. + +== Bypass binds the whole ruleset + +GitHub applies bypass per ruleset, not per rule, so every actor in the bypass +list can merge around every GATE. Nine apps are listed (owner ruling R3). That +means a GATE binds humans and unlisted bots; the AI-reviewer apps can merge a +red PR. `gates-only.json` is the fix if the owner wants it (O6). + +== Tags and the App credential + +`immutable-tags.json` drops the live copy's `required_status_checks`, +`required_deployments` and `required_linear_history` โ€” none can be satisfied +at tag-creation time, which is why no workflow in the estate could create a +tag. Creation is reserved to admins and to OikosBot (2538504) in `always` +mode, so release workflows create tags *through the App*. + +That App credential does not exist yet. `hyperpolymath/standards` holds no +`APP_ID` variable and no `APP_PRIVATE_KEY` secret (checked 2026-09-02; +`signed-push-smoke.yml` has failed on every run since 2026-08-24 for that +reason). Until the owner plants them (decision O11), the periodic +`lock-refresh` and App-created tags cannot run; tags stay admin-only. + +== Settings notes + +* `secret_scanning` and `secret_scanning_push_protection` are not available on + private repos of a Free account; the applier drops that block on + `visibility: private` and reports it. Rulesets *do* work on private Free + repos (planted POST on `dev-notes-vault`, 2026-09-02), so there is no classic + branch-protection fallback and no `base-classic.json`. +* `allow_merge_commit` / `allow_rebase_merge` are false pending O8. Rebase + merges cannot be signed; merge commits are the owner's call. +* `sha_pinning_required` is set through the Actions-permissions endpoint; + `actions.lock` is the file-level pin (both, per R2). + +== Allowlist: order matters + +`actions-allowlist.json` is 92 patterns, down from 118 live. It is applied +*last*, after the sweep has removed every workflow that references a pruned +action. Applied first it kills those workflows silently (the 87 %-dead-runs +incident in memory). `hyperpolymath/*` subsumes the 20 explicit entries that +were on the live list, including four coordinates that no longer exist. + +The prune is hygiene, not enforcement: `verified_allowed` is true, so Marketplace- +verified creators (Snyk, Codecov, SonarSource, Semgrep) run regardless of the +list. R1 is enforced by deleting the workflows in the sweep. Setting +`verified_allowed` to false is O12, decided only after a `uses:` census shows +every verified-creator action still in use is on the list. + +== Autolinks + +Audit 2026-09-02, all 428 repos with workflows: + +[cols="3,1",options="header"] +|=== +| Finding | Repos + +| The copied six-prefix set (GHSA, PROV, CVE, ADR, RUSTSEC, RFC) | 335 +| Same set minus RFC (`cloudguard-cli`) | 1 +| No autolinks at all | 92 (57 hyperpolymath, 35 metadatastician โ€” nearly the whole org) +| ADR- pointing at a *different* repo (renamed after the trial was copied) | 10 +|=== + +`ADR-` is repo-local on every repo, not estate-central; `base.json` templates +it with `{{OWNER}}/{{REPO}}`. The live template ends in `ADR-.adoc`, but +ADR files are named `ADR--.adoc`, so every copied ADR- link is a 404 +(verified: `ADR-003.adoc` on standards). The canon uses a code-search URL that +keeps `` and lands on the slugged file. `PROV-` and `RUSTSEC-` move out of the base set +into the `proof` and `rust` profiles. The applier adds missing prefixes and +rewrites wrong templates by default; removing extras is `--prune`, opt-in, +because 300 repos carrying a harmless `PROV-` is not worth 300 API writes. + +== Apply order (per repo) + +. settings PATCH + Actions permissions + workflow permissions +. autolinks +. branch ruleset (POST if none matches the identity rule, PUT by id if one does; refuse if two) +. tag ruleset +. verifier: identity rule, phantom contexts = 0, live โ‰ก canonical +. allowlist โ€” *tail of the sweep only* diff --git a/config/autolinks/base.json b/config/autolinks/base.json new file mode 100644 index 000000000..83881ea32 --- /dev/null +++ b/config/autolinks/base.json @@ -0,0 +1,33 @@ +{ + "profile": "base", + "applies_to": "every repo", + "placeholders": "{{OWNER}} and {{REPO}} are substituted by the applier; ADR- is repo-local (audit 2026-09-02: 336/336 repos point ADR- at their own docs/decisions; 10 point at a renamed repo, which the applier corrects).", + "autolinks": [ + { + "key_prefix": "GHSA-", + "url_template": "https://github.com/advisories/GHSA-", + "is_alphanumeric": true + }, + { + "key_prefix": "CVE-", + "url_template": "https://nvd.nist.gov/vuln/detail/CVE-", + "is_alphanumeric": true + }, + { + "key_prefix": "OSV-", + "url_template": "https://osv.dev/vulnerability/OSV-", + "is_alphanumeric": true + }, + { + "key_prefix": "RFC-", + "url_template": "https://www.rfc-editor.org/rfc/rfc.html", + "is_alphanumeric": false + }, + { + "key_prefix": "ADR-", + "url_template": "https://github.com/{{OWNER}}/{{REPO}}/search?q=ADR-+path%3Adocs%2Fdecisions&type=code", + "is_alphanumeric": false + } + ], + "adr_note": "ADR files are named ADR--.adoc (standards: ADR-003-workflow-pin-staleness-window.adoc), so the live template .../docs/decisions/ADR-.adoc returns 404 on every repo (verified 2026-09-02: ADR-003.adoc = HTTP 404). The search URL resolves to the slugged file; code search needs a signed-in viewer, which every issue/PR reader is." +} diff --git a/config/autolinks/elixir.json b/config/autolinks/elixir.json new file mode 100644 index 000000000..68ec8cfb2 --- /dev/null +++ b/config/autolinks/elixir.json @@ -0,0 +1,7 @@ +{ + "profile": "elixir", + "extends": "base", + "detect": ["mix.exs"], + "autolinks": [], + "why_empty": "Hex and the Erlang Ecosystem Foundation publish advisories as GHSA/OSV entries; no Hex-specific id prefix with a stable URL was found. GHSA- and OSV- in base cover them." +} diff --git a/config/autolinks/julia.json b/config/autolinks/julia.json new file mode 100644 index 000000000..50b65c2bc --- /dev/null +++ b/config/autolinks/julia.json @@ -0,0 +1,7 @@ +{ + "profile": "julia", + "extends": "base", + "detect": ["Project.toml"], + "autolinks": [], + "why_empty": "Julia advisories are published as GHSA entries against the General registry; no Julia-specific prefix exists that resolves to a stable URL. GHSA- and OSV- in base cover them. Add a prefix here only once a URL template has been verified against a real advisory." +} diff --git a/config/autolinks/proof.json b/config/autolinks/proof.json new file mode 100644 index 000000000..4d8c8b155 --- /dev/null +++ b/config/autolinks/proof.json @@ -0,0 +1,9 @@ +{ + "profile": "proof", + "extends": "base", + "detect_workflows": ["proofs.yml", "abi-ffi-gate.yml", "spark-theatre-gate.yml"], + "detect_dependency": "hyperpolymath/proven", + "autolinks": [ + { "key_prefix": "PROV-", "url_template": "https://github.com/hyperpolymath/proven/issues/", "is_alphanumeric": false } + ] +} diff --git a/config/autolinks/rust.json b/config/autolinks/rust.json new file mode 100644 index 000000000..cf5cd34d9 --- /dev/null +++ b/config/autolinks/rust.json @@ -0,0 +1,8 @@ +{ + "profile": "rust", + "extends": "base", + "detect": ["Cargo.toml"], + "autolinks": [ + { "key_prefix": "RUSTSEC-", "url_template": "https://rustsec.org/advisories/RUSTSEC-.html", "is_alphanumeric": true } + ] +} diff --git a/config/rulesets/base.json b/config/rulesets/base.json new file mode 100644 index 000000000..55e47eaa0 --- /dev/null +++ b/config/rulesets/base.json @@ -0,0 +1,49 @@ +{ + "name": "Base", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" }, + { "actor_id": 1236702, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 29110, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 15368, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 347564, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 46505, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 1143301, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 1144995, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 12526, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 2538504, "actor_type": "Integration", "bypass_mode": "pull_request" } + ], + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { "type": "required_signatures" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "require_extra_approval_for_unattributed_changes": false, + "required_reviewers": [], + "allowed_merge_methods": ["squash"] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [] + } + } + ] +} diff --git a/config/rulesets/gates-only.json b/config/rulesets/gates-only.json new file mode 100644 index 000000000..80410ad23 --- /dev/null +++ b/config/rulesets/gates-only.json @@ -0,0 +1,28 @@ +{ + "name": "Gates", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" }, + { "actor_id": 1236702, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 29110, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 15368, "actor_type": "Integration", "bypass_mode": "pull_request" }, + { "actor_id": 2538504, "actor_type": "Integration", "bypass_mode": "pull_request" } + ], + "rules": [ + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [] + } + } + ] +} diff --git a/config/rulesets/gates.json b/config/rulesets/gates.json new file mode 100644 index 000000000..029c4465d --- /dev/null +++ b/config/rulesets/gates.json @@ -0,0 +1,99 @@ +{ + "version": 1, + "purpose": "Which workflow FILES are ๐Ÿ”ด GATE per repo profile. The applier turns these into required_status_checks contexts by reading the check-run names the latest default-branch run of each file actually emitted (integration_id 15368). Contexts are never typed by hand.", + "context_derivation": { + "source": "GET /repos/{o}/{r}/actions/workflows/{file}/runs?branch=&per_page=1 then GET /repos/{o}/{r}/actions/runs/{id}/jobs", + "context_shape": "job name; for a reusable caller it is ' / '", + "integration_id": 15368, + "if_no_run_yet": "omit that file's contexts and report it; never write a context nothing has emitted", + "if_zero_contexts_overall": "do not write the required_status_checks rule at all; report the repo as UNGATED" + }, + "profiles": { + "base": { + "applies_to": "every repo", + "gate_workflows": [ + "governance.yml", + "secret-scanner.yml", + "hypatia-scan.yml", + "codeql.yml" + ] + }, + "rust": { + "detect": [ + "Cargo.toml" + ], + "gate_workflows": [ + "rust-ci.yml" + ] + }, + "elixir": { + "detect": [ + "mix.exs" + ], + "gate_workflows": [ + "elixir-ci.yml" + ] + }, + "julia": { + "detect": [ + "Project.toml" + ], + "gate_workflows": [ + "julia-ci.yml" + ] + }, + "ada": { + "detect": [ + "alire.toml", + "*.gpr" + ], + "gate_workflows": [ + "ada-ci.yml" + ] + }, + "zig": { + "detect": [ + "build.zig" + ], + "gate_workflows": [ + "zig-ci.yml" + ] + }, + "bun": { + "detect": [ + "bun.lock", + "bun.lockb" + ], + "gate_workflows": [ + "bun-ci.yml" + ] + }, + "proof": { + "detect_workflows": [ + "proofs.yml", + "abi-ffi-gate.yml", + "spark-theatre-gate.yml" + ], + "gate_workflows": [ + "proofs.yml", + "abi-ffi-gate.yml", + "spark-theatre-gate.yml" + ] + } + }, + "never_required_workflows": [ + "scorecard.yml", + "labels.yml", + "mirror.yml", + "main-estate-audit.yml", + "stale.yml", + "oikosbot.yml", + "readme-derive.yml", + "sonarqube.yml" + ], + "never_required_contexts": [ + "Allowlist Preflight", + "SonarCloud Code Analysis", + "CodeQL (default setup)" + ] +} diff --git a/config/rulesets/immutable-tags.json b/config/rulesets/immutable-tags.json new file mode 100644 index 000000000..a3c86813a --- /dev/null +++ b/config/rulesets/immutable-tags.json @@ -0,0 +1,22 @@ +{ + "name": "immutable-tags", + "target": "tag", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~ALL"], + "exclude": [] + } + }, + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" }, + { "actor_id": 2538504, "actor_type": "Integration", "bypass_mode": "always" } + ], + "rules": [ + { "type": "creation" }, + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { "type": "update" }, + { "type": "required_signatures" } + ] +} diff --git a/config/settings/actions-allowlist.json b/config/settings/actions-allowlist.json new file mode 100644 index 000000000..8f00d2040 --- /dev/null +++ b/config/settings/actions-allowlist.json @@ -0,0 +1,125 @@ +{ + "version": 1, + "purpose": "Estate Actions allowlist (PUT /repos/{o}/{r}/actions/permissions/selected-actions; org-level for metadatastician). APPLY ONLY AT THE TAIL OF THE SWEEP (design spec ยง10 step 5): pruning before the workflows that reference the pruned actions are gone recreates the 87%-dead-runs incident.", + "pruned_from_live_2026_09_02": { + "R1/R4/R5 removals": [ + "snyk/actions@*", + "snyk/actions/node@*", + "codecov/codecov-action@*" + ], + "Deno banned": [ + "denoland/setup-deno@*" + ], + "Semgrep DROP (spec ยง5.2)": [ + "returntocorp/semgrep-action@*" + ], + "push-email-notify DROP (spec ยง5.5)": [ + "dawidd6/action-send-mail@*" + ], + "covered by hyperpolymath/*": "20 explicit hyperpolymath/... entries, including the dead panic-attacker, a2ml-validate-action, k9-validate-action, deno-ci-reusable and rsr-antipattern-reusable coordinates" + }, + "review_candidates_not_pruned": [ + "astral-sh/setup-uv@*", + "pypa/gh-action-pypi-publish@*", + "PyO3/maturin-action@*", + "ad-m/github-push-action@*", + "swatinem/rust-cache@* (case duplicate of Swatinem/rust-cache@*)" + ], + "github_owned_allowed": true, + "verified_allowed": true, + "patterns_allowed": [ + "8398a7/action-slack@*", + "actions-rust-lang/setup-rust-toolchain@*", + "ad-m/github-push-action@*", + "alire-project/setup-alire@*", + "anchore/sbom-action@*", + "anchore/scan-action@*", + "android-actions/setup-android@*", + "anthropics/claude-code-action@*", + "aquasecurity/trivy-action@*", + "astral-sh/setup-uv@*", + "awalsh128/cache-apt-pkgs-action@*", + "azure/webapps-deploy@*", + "benchmark-action/github-action-benchmark@*", + "cachix/install-nix-action@*", + "cbrgm/cleanup-stale-branches-action@*", + "codespell-project/actions-codespell@*", + "cometkim/rclone-actions@*", + "DavidAnson/markdownlint-cli2-action@*", + "dawidd6/action-download-artifact@*", + "DeLaGuardo/setup-clojure@*", + "dependabot/fetch-metadata@*", + "dependency-check/Dependency-Check_Action@*", + "devcontainers/ci@*", + "dlang-community/setup-dlang@*", + "docker/build-push-action@*", + "docker/login-action@*", + "docker/metadata-action@*", + "docker/setup-buildx-action@*", + "docker/setup-qemu-action@*", + "dtolnay/rust-action@*", + "dtolnay/rust-toolchain@*", + "editorconfig-checker/action-editorconfig-checker@*", + "EnricoMi/publish-unit-test-result-action@*", + "erlef/setup-beam@*", + "extractions/setup-just@*", + "gitleaks/gitleaks-action@*", + "google/clusterfuzzlite/actions/build_fuzzers@*", + "google/clusterfuzzlite/actions/run_fuzzers@*", + "google/clusterfuzzlite@*", + "goto-bus-stop/setup-zig@*", + "hadolint/hadolint-action@*", + "hashicorp/setup-terraform@*", + "haskell-actions/hlint-run@*", + "haskell-actions/hlint-setup@*", + "haskell-actions/setup@*", + "hyperpolymath/*", + "ibiqlik/action-yamllint@*", + "ionos-deploy-now/deploy-to-ionos-action@*", + "ionos-deploy-now/retrieve-project-info-action@*", + "ionos-deploy-now/template-renderer-action@*", + "jetli/wasm-pack-action@*", + "jlumbroso/free-disk-space@*", + "julia-actions/cache@*", + "julia-actions/julia-processcoverage@*", + "julia-actions/setup-julia@*", + "JuliaRegistries/TagBot@*", + "KSXGitHub/github-actions-deploy-aur@*", + "KyleMayes/install-llvm-action@*", + "leanprover/lean-action@*", + "ludeeus/action-shellcheck@*", + "lycheeverse/lychee-action@*", + "mlugg/setup-zig@*", + "mozilla-actions/sccache-action@*", + "ocaml/setup-ocaml@*", + "orhun/git-cliff-action@*", + "ossf/scorecard-action@*", + "oven-sh/setup-bun@*", + "peaceiris/actions-gh-pages@*", + "peaceiris/actions-hugo@*", + "peter-evans/create-pull-request@*", + "peter-evans/dockerhub-description@*", + "peter-evans/repository-dispatch@*", + "pnpm/action-setup@*", + "PyO3/maturin-action@*", + "pypa/gh-action-pypi-publish@*", + "r-lib/actions/setup-r@*", + "r-lib/actions@*", + "reactivecircus/android-emulator-runner@*", + "ruby/setup-ruby@*", + "rustsec/audit-check@*", + "shivammathur/setup-php@*", + "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@*", + "slsa-framework/slsa-github-generator@*", + "softprops/action-gh-release@*", + "SonarSource/sonarcloud-github-action@*", + "SonarSource/sonarqube-scan-action@*", + "superfly/flyctl-actions/setup-flyctl@*", + "Swatinem/rust-cache@*", + "swatinem/rust-cache@*", + "taiki-e/install-action@*", + "trufflesecurity/trufflehog@*", + "webfactory/ssh-agent@*" + ], + "enforcement_note": "verified_allowed is TRUE, so Marketplace-verified creators (Snyk, Codecov, SonarSource, Semgrep, ...) run whether or not they appear in patterns_allowed. The 118โ†’92 prune is therefore HYGIENE: R1 is enforced by deleting the workflows in the sweep, not by this list. Flipping verified_allowed to false is a separate decision (O12) taken only after a uses: census proves every verified-creator action still in use is on patterns_allowed." +} diff --git a/config/settings/repo.json b/config/settings/repo.json new file mode 100644 index 000000000..767e4c790 --- /dev/null +++ b/config/settings/repo.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "purpose": "Universal repository settings. One PATCH body plus the three sibling endpoints. Per-repo deltas are limited to the keys listed under per_repo_deltas_allowed; everything else is canonical.", + "repo": { + "allow_squash_merge": true, + "allow_merge_commit": false, + "allow_rebase_merge": false, + "delete_branch_on_merge": true, + "allow_auto_merge": true, + "allow_update_branch": true, + "web_commit_signoff_required": true, + "squash_merge_commit_title": "PR_TITLE", + "squash_merge_commit_message": "PR_BODY", + "security_and_analysis": { + "secret_scanning": { "status": "enabled" }, + "secret_scanning_push_protection": { "status": "enabled" }, + "dependabot_security_updates": { "status": "enabled" } + } + }, + "repo_private_overrides": { + "note": "secret_scanning and push_protection are not available on private repos of a Free account; the applier drops the security_and_analysis block on visibility=private and reports it.", + "security_and_analysis": { "dependabot_security_updates": { "status": "enabled" } } + }, + "actions_permissions": { + "endpoint": "PUT /repos/{o}/{r}/actions/permissions", + "enabled": true, + "allowed_actions": "selected", + "sha_pinning_required": true + }, + "actions_workflow_permissions": { + "endpoint": "PUT /repos/{o}/{r}/actions/permissions/workflow", + "default_workflow_permissions": "read", + "can_approve_pull_request_reviews": false + }, + "vulnerability_alerts": { "endpoint": "PUT /repos/{o}/{r}/vulnerability-alerts", "enabled": true }, + "automated_security_fixes": { "endpoint": "PUT /repos/{o}/{r}/automated-security-fixes", "enabled": true }, + "per_repo_deltas_allowed": ["description", "homepage", "topics", "has_wiki", "has_projects", "has_discussions", "has_pages", "visibility"], + "open_owner_decisions": { + "O8": "allow_merge_commit / allow_rebase_merge are false here (squash-only). If the owner keeps merge commits, flip allow_merge_commit to true and add \"merge\" to allowed_merge_methods in rulesets/base.json." + } +} diff --git a/docs/CICD-SIGNAL-DISCIPLINE.adoc b/docs/CICD-SIGNAL-DISCIPLINE.adoc index 9e0b6bc79..96cc3a8c2 100644 --- a/docs/CICD-SIGNAL-DISCIPLINE.adoc +++ b/docs/CICD-SIGNAL-DISCIPLINE.adoc @@ -334,3 +334,86 @@ overstates its coverage. . *Rename to tiered names*, one concept at a time, following the safe sequence. . *Classify and remove fake gates.* Deleting a check that cannot fail is strictly better than repairing it. + +== Estate canon โ€” ratified 2026-09-02 + +The tiers above were a taxonomy; this section makes them a *rule set* for the +estate. Design spec: `docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md`. +Machine-readable form: `config/rulesets/gates.json`. + +=== MUST list (every repository) + +[cols="2,1,1,3",options="header"] +|=== +| Workflow file | Tier | Required? | Notes + +| `governance.yml` โ†’ `governance-reusable.yml` +| ๐Ÿ”ด GATE +| yes, one context per job +| N named jobs so a red names its cause; includes `actions-lock-verify` + +| `secret-scanner.yml` โ†’ `secret-scanner-reusable.yml` +| ๐Ÿ”ด GATE +| yes +| + +| `hypatia-scan.yml` โ†’ `hypatia-scan-reusable.yml` +| ๐Ÿ”ด GATE +| yes +| Partial until the engine runs all 34 rule modules and sees `.github/`; say so + +| `codeql.yml` โ†’ `codeql-reusable.yml` +| ๐Ÿ”ด GATE +| yes +| One mode estate-wide; default setup off + +| `-ci.yml` (rust, elixir, julia, ada, zig, bun) +| ๐Ÿ”ด GATE +| yes, for the repo's languages +| One thin caller per language present + +| `proofs.yml`, `abi-ffi-gate.yml`, `spark-theatre-gate.yml` +| ๐Ÿ”ด GATE +| yes, where present +| The hyperpolymath core; opt-in by repo type + +| `scorecard.yml`, `labels.yml`, `mirror.yml`, `main-estate-audit.yml`, `stale.yml` +| ๐Ÿ“… PERIODIC +| never +| Schedule or push only; never on `pull_request` + +| `oikosbot.yml`, `readme-derive.yml`, container build +| ๐ŸŸก CHECK +| never +| + +| CodeRabbit, Sonar, GitGuardian, Copilot review +| โšช ADVISORY +| never +| App-driven; no workflow file + +| `actions.lock` +| โ€” +| โ€” +| Present everywhere (R2); regenerated in the same PR as any `uses:` change +|=== + +=== Naming convention for canonical callers + +The canonical thin caller's `name:` is ` `: +`๐Ÿ”ด GATE: Governance`, `๐Ÿ”ด GATE: Secret Scanner`, `๐Ÿ”ด GATE: Hypatia Scan`, +`๐Ÿ”ด GATE: CodeQL`, `๐Ÿ”ด GATE: Rust CI`, `๐Ÿ“… PERIODIC: Scorecard`, +`๐Ÿ“… PERIODIC: Labels`. `scripts/check-gate-tiers.sh` keys on the prefix. +Filenames are frozen; only `name:` and job ids change. + +=== Two invariants added + +[start=6] +. *Required contexts are derived, never typed.* The applier reads the check-run + names the gate files emitted on the latest default-branch run and writes + exactly those. A file with no run contributes nothing; a repo with nothing + derivable gets no status-check rule and is reported UNGATED. +. *A GATE blocks merge for non-bypass actors.* Bypass applies to the whole + ruleset, so every listed app can merge around every gate. The tier claim + "Always" in the table above is true of humans and unlisted bots; owner + decision O6 (`config/rulesets/gates-only.json`) extends it to the apps. diff --git a/docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md b/docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md index 85df597f2..ee9d9fcbd 100644 --- a/docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md +++ b/docs/superpowers/specs/2026-09-02-cicd-regularisation-design.md @@ -427,3 +427,26 @@ O9 metadatastician installs with no ruling: slack, microsoft-teams-for-github, thanks-dev, linear-data-importer, linear-code (keep or uninstall) ยท O10 `workflow-templates/` on a User-account `.github` (plant one, check the "New workflow" page); if invisible, hyperpolymath distribution = rsr-template-repo only. + +## 13. Amendments from step 1 (2026-09-02, same day) + +Facts found while writing the canonical artefacts; each overrides the section it names. + +| ยง | Was | Now | Evidence | +|---|---|---|---| +| 10 step 1 paths | `standards/rulesets/`, `settings/`, `autolinks/` at repo root | **`config/rulesets/`, `config/settings/`, `config/autolinks/`** + `config/README.adoc` | `config/` already holds the estate gitleaks baseline; no new root directories; Mustfile root rules constrain only loose `.contractile` files and `REGISTRY.a2ml` | +| 7.1 P-priv | classic branch protection fallback, `base-classic.json` | **Dropped.** Rulesets work on private Free repos | Planted POST+DELETE on `dev-notes-vault` (private), 2026-09-02 | +| 7.3 name | ruleset `Base` | Identity = active branch ruleset targeting exactly `["~DEFAULT_BRANCH"]`; name irrelevant. Live name everywhere sampled is `Optimus-Branch`; no `Base` exists | `gh api repos/{o}/{r}/rulesets` on standards, hypatia, verisimdb | +| 7.3 rules | `required_linear_history`; `update` unmentioned | **No `required_linear_history`** (live has none; O8 decides squash-only vs merge commits). **`update` dropped** (live has it; it makes main writable by bypass actors only) | live ruleset 14285635 | +| 7.3 bypass modes | unspecified | all Integration + admin = `pull_request`; RepositoryRole 2 (maintain) dropped; `always` nowhere on the branch ruleset | `config/README.adoc` | +| 7.3 strict | unspecified | `strict_required_status_checks_policy: false`, decided | PR #714 sat BEHIND | +| 7.3 tags | "keep `tag-protection.json`" | `config/rulesets/immutable-tags.json`: drop `required_status_checks`, `required_deployments`, `required_linear_history` (unsatisfiable at tag creation โ†’ no workflow could create tags); bypass = admin + OikosBot `always` | live ruleset 18110117 | +| 6.4 credential | "the single App credential it already holds" | **standards holds no App credential.** No `APP_ID` variable, no `APP_PRIVATE_KEY` secret; `signed-push-smoke.yml` red on every run since 2026-08-24. `lock-refresh` and App-created tags are blocked on **O11** | `gh secret list`, `gh variable list`, run 32717664038 | +| 7.6 ADR | ADR- "estate-central โ†’ standards/docs/decisions" | **ADR- is repo-local** on all 336 repos with autolinks; 10 point at a renamed repo (rename residue). Templated `{{OWNER}}/{{REPO}}` | full audit `$CLAUDE_JOB_DIR/tmp/autolinks-all.tsv`, 428 repos, 0 errors | +| 7.6 audit | 28/40 sampled | 335 repos identical six-prefix set, 1 minus RFC (`cloudguard-cli`), **92 none** (57 hyperpolymath, 35 metadatastician = almost the whole org) | same | +| 7.6 profiles | JLSEC/HEX prefixes proposed | `julia.json` and `elixir.json` ship **empty** with a stated reason: no verified advisory prefix with a stable URL; GHSA-/OSV- in base cover both. `OSV-` added to base | rustsec/osv URL shapes verified; nothing invented | +| 7.2 allowlist | prune list | 118 โ†’ 92 patterns; `hyperpolymath/*` subsumes 20 explicit entries; Python-adjacent trio and `ad-m/github-push-action` listed as review candidates, not pruned | `config/settings/actions-allowlist.json` | +| 12 | O1โ€“O10 | **O11 added**: create or pick the estate GitHub App (OikosBot 2538504 is the owner's own), plant `APP_ID` (variable) + `APP_PRIVATE_KEY` (secret) on `hyperpolymath/standards` | โ€” | +| 7.6 ADR template | `docs/decisions/ADR-.adoc` | ADR files are `ADR--.adoc`; the live template 404s on every repo. Canon = code-search URL `search?q=ADR-+path%3Adocs%2Fdecisions&type=code` | `ADR-003.adoc` on standards = HTTP 404, 2026-09-02 | +| 7.2 allowlist enforcement | "prune" implied enforcement | `verified_allowed` is true, so verified creators bypass the list; prune is hygiene, R1 is enforced by deleting workflows. **O12 added**: flip `verified_allowed` to false after a `uses:` census | `config/settings/actions-allowlist.json` | +| 7.3 direct push | unstated | With every bypass at `pull_request` and no `always` actor, main is PR-only for everyone including the owner; emergency path = disable the ruleset. FYI posted on #715 | `config/rulesets/base.json` |