diff --git a/.github/actions/prose-gate/prose_lint.py b/.github/actions/prose-gate/prose_lint.py index a7a62be4..49bee6ba 100755 --- a/.github/actions/prose-gate/prose_lint.py +++ b/.github/actions/prose-gate/prose_lint.py @@ -353,6 +353,7 @@ def path_candidate(token: str, in_span: bool = True) -> str | None: "repo-config/main.json", "repo-config/README.md", "repo-config/settings.json", + "repo-config/labels.json", "spec/secrets.json", ".github/workflows/get-version-task.yml", ".github/workflows/publish-plan-task.yml", diff --git a/AUDIT.md b/AUDIT.md index 43777731..190a6b95 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -98,7 +98,7 @@ Run [`WORKFLOW.md`][workflow]'s methodology against the repo's **own** Actions: ## 6. Validate Settings, Rulesets, and Secrets -- **General settings and rulesets** - fetch the hub and check out `main`. Run `repo-config/configure.sh check / release|operational` from that checkout. Pass the target repository and its registry `workflowModel` explicitly. The command checks the shared settings, state-dependent settings, Dependabot security features, and both rulesets against the hub payloads. It preserves and reports `bypass_actors` without asserting them because bypass authority is a per-repository human decision. +- **General settings, labels, and rulesets** - fetch the hub and check out `main`. Run `repo-config/configure.sh check / release|operational` from that checkout. Pass the target repository and its registry `workflowModel` explicitly. The command checks what `configure.sh apply` writes: the declared settings, the derived settings and the registry description, the declared labels, the Dependabot security features, the shared `main` ruleset, and the `develop` ruleset the model selects. It preserves and reports `bypass_actors` without asserting them because bypass authority is a per-repository human decision. - **Secrets** - from the same hub checkout, run [`spec/audit.py`][audit-runner] `[repo]` and read its Secrets section. It resolves the required set from the hub's own [`spec/secrets.json`][secrets] plus the registry entry's `publish[]`/`types[]`/`requiredSecrets[]`, confirming each required name exists (name only, not the values) in the Actions store and, where the mechanism needs it (Docker Hub, codegen App), the Dependabot store too. diff --git a/OPERATIONS.md b/OPERATIONS.md index e71f7110..fe3c719d 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -87,7 +87,7 @@ repo-config/configure.sh apply owner/repo release|operational `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. +`apply` is not a narrow toggle. One run patches every key in `repo-config/settings.json`, sets the default branch, enables both Dependabot features, creates or updates every label in `repo-config/labels.json`, 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. diff --git a/STANDUP.md b/STANDUP.md index 27e33f11..c19eba56 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -207,7 +207,7 @@ Three conditions fail here, and the two commands together are what separate them Each is step 0A's escalation rather than something to work around. -Run `repo-config/configure.sh apply owner/repo release|operational` from a hub checkout at `main`, naming the repo being stood up and its model, to apply the fleet settings, Dependabot security features, and two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config.md`][repo-config-doc]). Then run `repo-config/configure.sh check owner/repo release|operational` from the same checkout. Pass the model explicitly because the repository is outside the registry during this step. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once, which is why this step follows step 3 rather than preceding it. A ruleset requiring a name no run has ever reported leaves the first pull request waiting on a status nothing produces, and on an operational repo the `develop -> main` promotion is a pull request too, so the same wait applies there. +Run `repo-config/configure.sh apply owner/repo release|operational` from a hub checkout at `main`, naming the repo being stood up and its model, to apply the fleet settings, Dependabot security features, label set, and two rulesets idempotently (import the JSON, never hand-build it, per [`docs/repo-config.md`][repo-config-doc]). Then run `repo-config/configure.sh check owner/repo release|operational` from the same checkout. Pass the model explicitly because the repository is outside the registry during this step. Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s), meaning Actions plus Dependabot where the mechanism needs it, and confirm no forbidden secret is present. The required check binds by name (`Check pull request workflow status job`) and turns green only after the PR workflow has run once, which is why this step follows step 3 rather than preceding it. A ruleset requiring a name no run has ever reported leaves the first pull request waiting on a status nothing produces, and on an operational repo the `develop -> main` promotion is a pull request too, so the same wait applies there. For a **private** repo, confirm the account-wide toggle at `https://github.com/settings/security_analysis`, `Dependabot on self-hosted runners`, is off, along with `Automatically enable for new repositories` beside it. If self-hosted routing is wanted instead, register a matching self-hosted runner rather than disabling the toggle. Left on with no self-hosted runner registered on the account, Dependabot's own update jobs queue for up to 24 hours, then get cancelled. That cancelled-with-zero-steps pattern is the only Actions-API-visible signal, not an explicit cause, and ordinary CI is unaffected. The account-setting root cause surfaces only as a `Self-hosted runner unavailable` message on the repo's own Dependabot page. GitHub never routes a public repo through this setting, so a public standup is unaffected (ptr727/ProjectTemplate#1015). A repo standing up from a **partial state** may already carry queued or cancelled jobs from before this check ran. Fixing the toggle does not rerun those. A manual `Check for Updates` click on the repo's own Dependabot page does. diff --git a/docs/repo-config.md b/docs/repo-config.md index 8c7ffcee..766199a9 100644 --- a/docs/repo-config.md +++ b/docs/repo-config.md @@ -10,6 +10,7 @@ The hub holds all fleet-wide repository configuration: - `main.json` declares the shared `main` ruleset. - `develop.json` declares the release-model `develop` ruleset. - `operational/develop.json` declares the operational-model `develop` ruleset. +- `labels.json` declares the fleet label set. - `configure.sh` applies or checks those payloads through the GitHub API. Downstream repositories carry no `repo-config/` directory. The registry's `workflowModel` selects the `develop` payload. Commands that operate before registry enrollment pass the model explicitly. @@ -24,13 +25,13 @@ Downstream repositories carry no copy of `spec/secrets.json`. `baseline` applies **Configure by importing the JSON payloads, never by hand-building the rules** (hand reconstruction has gone wrong on past setups). The result must be **exactly two rulesets named `develop` and `main`**, and the names are load-bearing (`AGENTS.md` and the workflows reference them). Only the `develop` *content* varies by model. -Remove all classic branch-protection rules and stray rulesets. Run `configure.sh apply` from a hub checkout at `main`, naming the target repository and its model. The script applies `settings.json`, the Dependabot security features, and both rulesets. A registered repository can omit the model and use the registry lookup. A repository outside the registry passes the model explicitly: +Remove all classic branch-protection rules and stray rulesets. Run `configure.sh apply` from a hub checkout at `main`, naming the target repository and its model. The script applies `settings.json`, the Dependabot security features, the label set, and both rulesets. A registered repository can omit the model and use the registry lookup. A repository outside the registry passes the model explicitly: ```sh repo-config/configure.sh apply owner/repo release|operational ``` -Then validate the result with `repo-config/configure.sh check owner/repo release|operational`, run from the same checkout, which asserts every applied ruleset, setting, and security feature and exits non-zero on drift (the ruleset and settings checks are driven by the committed payloads, so they stay repo-agnostic). Or import each ruleset by hand with `gh api -X POST repos///rulesets --input repo-config/.json` (operational repos use `operational/develop.json` for `develop`). `gh ruleset` is read-only, so creation goes through `gh api`. The required check binds by name and only turns green after the repo's PR workflow runs once. To edit a live ruleset, GET it, change the field, and PUT the whole writable subset back (a partial PUT `422`s). +Then validate the result with `repo-config/configure.sh check owner/repo release|operational`, run from the same checkout, which asserts every applied ruleset, setting, label, and security feature and exits non-zero on drift (the ruleset and settings checks are driven by the committed payloads, so they stay repo-agnostic). Or import each ruleset by hand with `gh api -X POST repos///rulesets --input repo-config/.json` (operational repos use `operational/develop.json` for `develop`). `gh ruleset` is read-only, so creation goes through `gh api`. The required check binds by name and only turns green after the repo's PR workflow runs once. To edit a live ruleset, GET it, change the field, and PUT the whole writable subset back (a partial PUT `422`s). ## Regenerating the Payloads diff --git a/repo-config/README.md b/repo-config/README.md index c8b01a7d..a0d3820f 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -3,7 +3,8 @@ Hub-only repository and branch configuration held as committed files, kept out of `.github/` (which holds the GitHub-consumed configuration: workflows, Dependabot). Downstream repositories carry no `repo-config/` directory. Apply and check commands run from a hub checkout at `main` and name the target repository. - `main.json`, `develop.json`, and `operational/develop.json`: the canonical branch rulesets as the managed part of the writable API subset (`name`, `target`, `enforcement`, `conditions`, `rules`). `main.json` is shared. `develop.json` serves release repos, and `operational/develop.json` serves operational repos. `repo-config/configure.sh check owner/repo release|operational` compares the selected payloads with the live rulesets. `bypass_actors` is writable and deliberately unmanaged, so no payload declares one and nothing diffs it: who may bypass a ruleset is a human decision taken in the UI, which `repo-config/configure.sh` preserves on `apply` and reports without asserting on `check`. -- `configure.sh`: run from a hub checkout at `main`, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. It resolves every payload path against the hub's `repo-config/` directory. Name the target repository explicitly, since the command defaults to whichever repository the shell is sitting in. `repo-config/configure.sh apply owner/repo release|operational` creates or updates the settings, Dependabot security features, and rulesets idempotently. `repo-config/configure.sh check owner/repo release|operational` is the read-only inverse and exits non-zero on drift. The model defaults to the registry `workflowModel` lookup. Pass it explicitly for a repository outside the registry. +- `labels.json`: the fleet label set, one `name`, `color`, and `description` per label. `repo-config/configure.sh apply owner/repo release|operational` creates or updates every declared label by name and deletes nothing, so a label a repo adds of its own stays. `check` asserts each declared label on all three fields and reports the undeclared ones without judging them. +- `configure.sh`: run from a hub checkout at `main`, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. It resolves every payload path against the hub's `repo-config/` directory. Name the target repository explicitly, since the command defaults to whichever repository the shell is sitting in. `repo-config/configure.sh apply owner/repo release|operational` creates or updates the settings, Dependabot security features, labels, and rulesets idempotently. `repo-config/configure.sh check owner/repo release|operational` is the read-only inverse and exits non-zero on drift. The model defaults to the registry `workflowModel` lookup. Pass it explicitly for a repository outside the registry. ## Rulesets @@ -20,6 +21,18 @@ The result is **exactly two rulesets named `develop` and `main`**, and the names Publish credentials required per mechanism are enumerated in `spec/secrets.json`. A repo needs only the mechanisms its own publish targets use, so a source-only repo needs none of the publish credentials below. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key, so the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). That publish job belongs to the repo's own workflow file, since trusted publishing validates the OIDC token's `job_workflow_ref` claim against the repository owning the package and rejects a reusable workflow's ref, so `id-token: write` is granted at that one entry point and nowhere else. The registry-side policy is the other half of that pairing and is configured on nuget.org or PyPI rather than here: it names the repository and the workflow file the push runs from, so moving the push between workflow files means repointing the policy in the same change. Docker Hub has no OIDC equivalent and uses a stored `DOCKER_HUB_USERNAME` + `DOCKER_HUB_ACCESS_TOKEN` in both the Actions and Dependabot secret stores. Codegen and merge-bot repos add a GitHub App (`CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` in both stores, and the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. +## Labels + +The triage labels classify an issue by the kind of work it needs, so a backlog sweep can pick the gates and scripts, which converge, ahead of the prose defects, which re-enter the review loop when worked one bundle at a time. An issue carries exactly one of these five, or `enhancement` for a feature, beside whatever surface labels it also carries. + +- **`gate`**: a rule that exists in prose with no mechanical check, or a check that misses a shape. +- **`script`**: a defect in hub tooling. +- **`prose`**: a defect in rule or procedure text. +- **`decision`**: needs the maintainer's decision before it can be worked. +- **`chore`**: registry, labels, rollout, and other fleet housekeeping. + +The class labels `introduced` and `pre-existing` record which class, per the `local-strict-review` Skill's "Disposing of Findings", a filed review finding carried. A `style` finding is declined rather than filed, so it has no label. `agents`, `skills`, and `codegen` mark the surface, and the rest are GitHub's own defaults and the Dependabot pair, declared so every fleet repo carries at least this set. + ## Repo Settings The fleet-standard general settings live in [`settings.json`][settings-json] and are applied idempotently by `repo-config/configure.sh apply owner/repo release|operational` alongside the rulesets (`gh api PATCH /repos/{owner}/{repo}`). The two settings that depend on per-repo state, `has_discussions` (visibility) and `default_branch` (main-must-exist), are computed by the script, not stored in the file. `apply` also enables Dependabot vulnerability alerts and automated security updates, fleet policy applied via the API rather than a `settings.json` key. `repo-config/configure.sh check owner/repo release|operational` validates all of these and exits non-zero on drift. diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 7caa264f..f2853c4f 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Configure or validate a repository against the committed fleet config in this directory, via the GitHub API. # -# Apply: repo-config/configure.sh apply [owner/repo] [release|operational] # create-or-update settings + rulesets (writes) +# Apply: repo-config/configure.sh apply [owner/repo] [release|operational] # create-or-update settings + labels + rulesets (writes) # Check: repo-config/configure.sh check [owner/repo] [release|operational] # validate an existing repo, non-zero on drift (reads) # # Both modes need admin on the repo, because the rulesets endpoints require it. @@ -9,14 +9,15 @@ # The model may be passed as the sole positional, as in `configure.sh check operational`. # The command may be omitted for the apply default, so `configure.sh owner/repo` still applies. # -# The apply mode writes three groups, in order. +# The apply mode writes four groups, in order. # First settings.json via PATCH, plus has_discussions (public repos only) and default_branch (main, only when it exists). # Then the Dependabot vulnerability alerts and automated security updates. +# Then the fleet label set from labels.json, create-or-update by name, leaving any label the payload does not declare alone. # Then the branch rulesets, main.json shared and the model-specific develop ruleset, create-or-update by name. # The develop ruleset is develop.json where the model is PR-gated, or operational/develop.json for direct signed pushes. # Applying the same configuration twice changes nothing, so the mode is idempotent. # -# The check mode is the read-only inverse, and it verifies the same three groups apply writes. +# The check mode is the read-only inverse, and it verifies the same four groups apply writes. # The ruleset and static-settings assertions are driven by the committed payloads, so they stay repo-agnostic. # A ruleset is checked on enforcement, on the rule-type set compared in both directions, and on the whole parameters object of every parameterized rule. # Comparing the parameters object rather than named fields means a parameter added to a payload is audited with no change here. @@ -24,7 +25,8 @@ # That still survives the GitHub API normalizing a stored ruleset, since the comparison is over parsed JSON with sorted keys rather than a byte diff. # The derived settings apply computes are asserted by name rather than from a payload, meaning has_discussions and default_branch. # The two Dependabot security features are asserted the same way, since apply enables them and no payload declares them. -# What is unaudited is a static setting absent from settings.json, since only that group is payload-driven. +# A label is checked on name, color, and description against labels.json, and a label the payload never declared is reported without being asserted, since a repo may carry labels of its own. +# What is unaudited is a static setting absent from settings.json and a label labels.json does not declare, since those two groups are asserted in the payload's direction only, where a ruleset's rule-type set is compared both ways. # Secret names are checked separately, by spec/audit.py from a hub checkout. # This script leaves them a manual-verify note for values, which are never readable via the API. set -Eeuo pipefail @@ -75,6 +77,7 @@ operational) develop_ruleset="$script_dir/operational/develop.json" ;; esac main_ruleset="$script_dir/main.json" settings_file="$script_dir/settings.json" +labels_file="$script_dir/labels.json" # ----- Resolve the declared description (optional, shared by apply and check) ----- # Absence keeps the About panel following the README. @@ -170,15 +173,41 @@ apply_ruleset() { # payload-file - create-or-update the ruleset by name fi } +# Test with `labels_payload_ok`, which is true only when labels.json parses to a non-empty array whose every label meets the API's field contract. +# That is a non-empty name, a six-digit hex color, and a description of at most 100 characters, each a string holding no tab or line break. +# The type test keeps a missing description from rendering as the literal string null, the contract tests keep a label from failing at the API partway through the loop, and the character test keeps a value from splitting the tab-joined rows the two label loops read. +labels_payload_ok() { + jq -e 'type=="array" and length > 0 and all(.[]; (.name|type=="string") and (.color|type=="string") and (.description|type=="string") and (.name|length) > 0 and (.color|test("^[0-9a-fA-F]{6}$")) and (.description|length) <= 100 and ((.name+.color+.description)|test("[\t\r\n]")|not))' "$labels_file" >/dev/null 2>&1 +} + +apply_labels() { # create-or-update every label labels.json declares, by name + # `gh label create --force` updates a label that exists and creates one that does not, so the write is idempotent by name. + # A label the payload does not declare is left standing, because a repo may carry labels of its own and this script deletes nothing. + # Every field is read from the payload, so a label added there reaches every fleet repo on the next apply with no change here. + # The payload is parsed into a variable before the loop for the reason check_settings gives: a jq failure inside `done < <(...)` skips the body silently. + local rows lname color desc + # The payload was validated by cmd_apply's pre-flight, before any write, so this read cannot be the first to find it malformed. + rows="$(jqr '.[] | "\(.name)\t\(.color)\t\(.description)"' "$labels_file")" + while IFS=$'\t' read -r lname color desc; do + gh label create "$lname" --repo "$repo" --color "$color" --description "$desc" --force >/dev/null + done <<<"$rows" + echo "Applied $(wc -l <<<"$rows" | tr -d ' ') labels from labels.json" +} + cmd_apply() { local f private disc payload # Pre-flight every required payload before any write, so a partial carry aborts before it half-applies. - for f in "$settings_file" "$develop_ruleset" "$main_ruleset"; do + for f in "$settings_file" "$labels_file" "$develop_ruleset" "$main_ruleset"; do if [ ! -e "$f" ]; then echo "Required payload $f not found. Aborting to avoid a partially-applied configuration." >&2 exit 1 fi done + # The label payload's content is validated here too, since apply_labels runs after the settings and Dependabot writes and an abort there would leave them applied. + if ! labels_payload_ok; then + echo "Label payload $labels_file did not parse, is empty, or holds a label outside the field contract (non-empty name, six-digit hex color, description of at most 100 characters, no tab or line break). Aborting before any write." >&2 + exit 1 + fi echo "Applying configuration to $repo (model: $model)" # The writes below silence stdout only, because the success-response JSON is noise. # They still fail loud, since gh errors go to stderr and a failed write aborts the script. @@ -206,6 +235,8 @@ cmd_apply() { gh api --method PUT "repos/$repo/vulnerability-alerts" >/dev/null gh api --method PUT "repos/$repo/automated-security-fixes" >/dev/null echo "Enabled Dependabot vulnerability alerts + automated security updates" + # ----- Fleet label set ----- + apply_labels # ----- Branch rulesets (main shared, develop selected by workflow model) ----- apply_ruleset "$develop_ruleset" apply_ruleset "$main_ruleset" @@ -388,12 +419,44 @@ check_security() { fi } +check_labels() { + local live rows lname color desc got extra + if [ ! -e "$labels_file" ]; then + fail "label payload $labels_file missing" + return + fi + # The list is paginated, since a repo carrying more labels than one page holds would otherwise report a later-page label as missing. + if ! live="$(gh api --paginate "repos/$repo/labels" --jq '.[]' | jq -s '.')"; then + fail "could not read repository labels" + return + fi + if ! labels_payload_ok; then + fail "label payload $labels_file did not parse, is empty, or holds a label outside the field contract" + return + fi + rows="$(jqr '.[] | "\(.name)\t\(.color)\t\(.description)"' "$labels_file")" + # Each declared label is asserted on all three fields, so a color or description edited by hand reads as drift. + while IFS=$'\t' read -r lname color desc; do + # shellcheck disable=SC2016 # $n is a jq --arg variable, not a shell expansion + got="$(jqr --arg n "$lname" '[.[] | select(.name == $n)] | first // empty | "\(.color)\t\(.description // "")"' <<<"$live")" + assert "label '$lname' = $color '$desc'" test "$got" = "$color"$'\t'"$desc" + done <<<"$rows" + # A label the payload never declared is reported rather than asserted, matching the bypass list: the fleet set is a floor, and a repo may add its own. + # shellcheck disable=SC2016 # $want is a jq --slurpfile variable, not a shell expansion + if ! extra="$(jqr --slurpfile want "$labels_file" '[.[].name] - [$want[0][].name] | join(", ")' <<<"$live")"; then + fail "could not compute the undeclared label list" + return + fi + note "labels not declared by labels.json: ${extra:-none} (left alone by this script)" +} + cmd_check() { echo "Validating configuration for $repo (model: $model)" check_ruleset "$develop_ruleset" check_ruleset "$main_ruleset" check_settings check_security + check_labels # Secret names are asserted by spec/audit.py, not here. # Values are never readable via the API regardless. note "run spec/audit.py [RepoName] (the registry name, not owner/repo) for required secret names, then verify by hand that their values are valid" diff --git a/repo-config/labels.json b/repo-config/labels.json new file mode 100644 index 00000000..2a877709 --- /dev/null +++ b/repo-config/labels.json @@ -0,0 +1,18 @@ +[ + { "name": "agents", "color": "f86915", "description": "Agents instructions" }, + { "name": "bug", "color": "d73a4a", "description": "Something isn't working" }, + { "name": "chore", "color": "c2e0c6", "description": "Registry, labels, rollout, and other fleet housekeeping" }, + { "name": "codegen", "color": "3526d4", "description": "Codegen bot" }, + { "name": "decision", "color": "b60205", "description": "Needs the maintainer's decision before it can be worked" }, + { "name": "dependencies", "color": "0366d6", "description": "Pull requests that update a dependency file" }, + { "name": "documentation", "color": "0075ca", "description": "Improvements or additions to documentation" }, + { "name": "duplicate", "color": "cfd3d7", "description": "This issue or pull request already exists" }, + { "name": "enhancement", "color": "a2eeef", "description": "New feature or request" }, + { "name": "gate", "color": "5319e7", "description": "A rule with no mechanical check, or a check that misses a shape" }, + { "name": "github_actions", "color": "000000", "description": "Pull requests that update GitHub Actions code" }, + { "name": "introduced", "color": "e99695", "description": "Review finding classed introduced per local-strict-review Disposing of Findings" }, + { "name": "pre-existing", "color": "bfd4f2", "description": "Review finding classed pre-existing per local-strict-review Disposing of Findings" }, + { "name": "prose", "color": "fbca04", "description": "A defect in rule or procedure text" }, + { "name": "script", "color": "1d76db", "description": "A defect in hub tooling" }, + { "name": "skills", "color": "a25957", "description": "Agent skill" } +] diff --git a/reports/canonical-review.json b/reports/canonical-review.json index 1ff4f613..0c129203 100644 --- a/reports/canonical-review.json +++ b/reports/canonical-review.json @@ -593,6 +593,14 @@ "hubCommit": "78898becaa2b1a62cb4c806d86273210c6390ac5", "stamp": "2026-09-01T14:48:23Z" }, + { + "unit": "AUDIT.md > 6. Validate Settings, Rulesets, and Secrets", + "digest": "sha256:cadbf5a3b36bdc9ec871eb907565c582bed70c2e4098c6a99fd756cd2b623489", + "reviewer": "agent-skill", + "findings": 2, + "hubCommit": "db9e5695f1501cc894953bdd57db46e4faf74159", + "stamp": "2026-09-05T02:20:07Z" + }, { "unit": "CODESTYLE.md > General", "digest": "sha256:54fcc07fc743507089e95ab8f1e7aeb64f0d83e31716514dadb666871a495684", diff --git a/spec/divergences.json b/spec/divergences.json index a283cd1d..9321c79c 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -11,6 +11,7 @@ { "path": "repo-config/operational/develop.json", "disposition": "retire", "reason": "The hub hosts the operational-model develop ruleset payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, { "path": "repo-config/main.json", "disposition": "retire", "reason": "The hub hosts the shared main ruleset payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, { "path": "repo-config/settings.json", "disposition": "retire", "reason": "The hub hosts the shared repository settings payload. A downstream repository is checked against the hub's main payload.", "tracking": null }, + { "path": "repo-config/labels.json", "disposition": "retire", "reason": "The hub hosts the fleet label set. A downstream repository is checked against the hub's main payload.", "tracking": null }, { "path": "repo-config/README.md", "disposition": "retire", "reason": "The hub hosts the repository-configuration reference beside the payloads and script it documents.", "tracking": null }, { "path": "spec/secrets.json", "disposition": "retire", "reason": "The adapted baseline/mechanisms carry never varied per repo: baseline applies to every fleet repo by definition, and mechanisms/targetMechanisms/typeMechanisms are computed centrally by spec/audit.py from the hub's own spec/secrets.json plus registry/repos.json, which a downstream copy could only restate or let drift. A downstream repository is checked against the hub's copy by running spec/audit.py from a hub checkout instead (ptr727/ProjectTemplate#993).", "tracking": null }, { "path": ".github/workflows/build-release-task.yml", "disposition": "retire", "reason": "The release chain is hub-hosted as a workflow_call task, per docs/reusable-workflows.md \"Stage 4: The Release Chain and the Docker Core\", so a downstream copy of this filename is retired rather than re-vendored: the caller stub a repo carries after adoption is publish-release.yml and test-pull-request.yml calling the hub task by pin, and no adopting repo carries a same-named local file. The ten carriers measured on develop at hub 7c67328 are PhotoCleaner, PlexCleaner, LanguageTags, MediaTools, Utilities, aiopurpleair, ESPHome-NonRoot, VSCode-Server-DotNetCore, KiCadLibrary, and homeassistant-purpleair. Delete the copy as each repo adopts the hub task; adoption is a separate, later change per repo (docs/reusable-workflows.md \"Rollout\" Stage 4).", "tracking": null },