From 6374ceb1c5cc6edefd15a8a2c12c3632dd132088 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 17 Jul 2026 21:21:00 -0700 Subject: [PATCH 1/2] Split carry-process out of the carried repo-config README into a hub-only doc (#345) Finding 1: repo-config/README.md is carried verbatim by every fleet repo, but it shipped carry-*process* narration - the Downstream Carry rules, the ruleset import/regen procedure, and the maintainer brownfield migration - which is meta-guidance for whoever performs the carry, not a current fact about the repo the file lands in. (This is also how the private-repo link reached public carriers; #344 removed the link, this removes the narration that hosted it.) - New hub-only docs/repo-config-carry.md holds the carry/apply/regen process: Downstream Carry, Applying the Config, Regenerating the Payloads, Brownfield Migration. Not in spec/files.json baseline, so never carried downstream; being hub-only it may name repos and link the hub freely. - repo-config/README.md trims to current facts about a repo's own config: the file descriptions, what the rulesets enforce, the secrets each mechanism needs, the general settings. - Cross-refs fixed: AGENTS.md (carried) plain-text-mentions the hub doc for the full procedure/brownfield instead of linking it, and drops two stale anchors ("Rules / Rulesets", "Template - GitHub Setup"); STANDUP.md and spec/files.json (both hub-only) point at the new doc. Finding 2: the adapted spec/secrets.json shape rule now lives in the hub doc - a source-only repo whose targets all map to null carries just baseline (no lone targetMechanisms map, which read as a schema bug that tripped Copilot 3x); a repo with a real mechanism carries mechanisms + its targetMechanisms routing. spec/validate.py passes; markdownlint, editorconfig-checker, and the CI-scoped cspell all clean; no dangling references. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 6 ++--- STANDUP.md | 4 +-- cspell.json | 1 + docs/repo-config-carry.md | 53 +++++++++++++++++++++++++++++++++++++++ repo-config/README.md | 46 ++++----------------------------- spec/files.json | 4 +-- 6 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 docs/repo-config-carry.md diff --git a/AGENTS.md b/AGENTS.md index fa771603..f20ab9c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,14 +37,14 @@ The specific rules in this file implement a few governing principles. Read these - **Both rulesets intentionally omit "Require branches to be up to date before merging".** The flag is off on `main` and on `develop`, for related but distinct reasons. - *Main:* the check is graph-based - it asks whether main's tip commit is reachable from develop, not whether the two branches have the same content. After any develop -> main release, main's tip is a brand-new merge commit that develop's history doesn't contain. Forward-only develop never adds it (no back-merge of main into develop), so the check would fail on every subsequent release. Other technical workarounds - rebasing develop onto main, or rewriting develop's history - exist but contradict the squash-only develop ruleset and the linearity invariant. - *Develop:* the check stalls bot auto-merge when two bot PRs against develop land within the same window. As soon as the first merges, the second flips to `mergeStateStatus: BEHIND` and GitHub's auto-merge will not fire while strict is on. The merge-bot only *enables* auto-merge on `opened`/`reopened` (see below) and never auto-updates bot branches, and Dependabot's rebase isn't real-time, so the second PR sits OPEN with all checks green indefinitely. Squash mechanics still rebase the diff onto develop's tip on merge, `required_linear_history` still enforces linearity, textual conflicts still block `mergeable: CONFLICTING`, and the required `Check pull request workflow status job` still gates merges - the only thing lost is pre-merge detection of *semantic-but-not-textual* conflicts, which the post-merge develop CI run catches anyway. - - See [`README.md`](./repo-config/README.md) "Rules / Rulesets" for the configured state. -- **Configuring branch protection on a fleet repo: don't hand-build the rules.** Reconstructing the rules by hand is error-prone and has gone wrong on past ports. First delete **all** legacy classic branch-protection rules and any stray rulesets (this template uses rulesets *only*), then create **exactly two rulesets named `develop` and `main`** by exporting the template's two rulesets and re-importing them via `gh api -X POST .../rulesets` (`gh ruleset` is read-only). The names are load-bearing - this file and the workflows reference them. Operational repos import [`operational/develop.json`](./repo-config/operational/develop.json) as their `develop` ruleset (the `main` ruleset is shared); [`configure.sh`](./repo-config/configure.sh) selects the right develop payload from the registry `workflowModel` automatically. Full export/import procedure: [README "Rules / Rulesets"](./repo-config/README.md). **Brownfield repos** (pre-existing history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the ruleset - see the [brownfield migration procedure](./repo-config/README.md) in that section. + - See [`repo-config/README.md`](./repo-config/README.md) "Rulesets" for the configured state. +- **Configuring branch protection on a fleet repo: don't hand-build the rules.** Reconstructing the rules by hand is error-prone and has gone wrong on past ports. First delete **all** legacy classic branch-protection rules and any stray rulesets (this template uses rulesets *only*), then create **exactly two rulesets named `develop` and `main`** by exporting the template's two rulesets and re-importing them via `gh api -X POST .../rulesets` (`gh ruleset` is read-only). The names are load-bearing - this file and the workflows reference them. Operational repos import [`operational/develop.json`](./repo-config/operational/develop.json) as their `develop` ruleset (the `main` ruleset is shared); [`configure.sh`](./repo-config/configure.sh) selects the right develop payload from the registry `workflowModel` automatically. The full export/import procedure and the regen snippet live in the hub-only `docs/repo-config-carry.md`. **Brownfield repos** (pre-existing history) need an extra step: `Require signed commits` rejects legacy unsigned commits and the admin bypass does not cover `git push --force`, so re-signing requires temporarily disabling the ruleset - the maintainer-only brownfield migration procedure is in that same hub doc. - **Bots (Dependabot and codegen) target both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch) and [`catalog/snippets/workflows/run-codegen-pull-request-task.yml`](./catalog/snippets/workflows/run-codegen-pull-request-task.yml) runs as a matrix over both branches with branch names `codegen-main` and `codegen-develop`. Each branch absorbs its own bot PRs independently, so neither falls behind, and the forward-only rule still holds (nothing is back-merged from main to develop - both branches receive their updates directly). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs (CVE-driven) always open against the repo default branch (`main`) regardless of `target-branch` - the same `case` statement covers them. The merge-bot auto-merges **every** Dependabot tier including semver-major (no ecosystem or update-type guard): the required CI checks are the gate, not the bump magnitude, so a major that breaks the build fails its checks and never merges. - **Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's `merge-dependabot` and `merge-codegen` jobs only fire on `opened` / `reopened` events (auto-merge is enabled exactly once per PR). When a maintainer pushes commits to a bot's branch (a `synchronize` event with an actor that isn't the same bot), the merge-bot's `disable-auto-merge-on-maintainer-push` job fires and calls `gh pr merge --disable-auto`. The maintainer's commits stay in the PR but won't auto-merge with the bot's content; re-enable auto-merge manually (`gh pr merge --auto ` or the GitHub UI) when ready. - **Why parallel dual-target rather than develop-only with eventual flow-through:** push-distribution channels (HACS for Home Assistant integrations, Linux distros that vendor from `main`, etc.) consume `main` directly. A develop-only model would leave `main` running stale code during long-running develop features. Codegen content can also be production-critical (live API-derived data, language lists, build catalogs) rather than just sample/demo content, so both branches need fresh codegen on their own cadence. - **Codegen regenerates committed files; its output must be deterministic from its inputs, never per-run state.** The codegen workflow is a mechanism to refresh files that are checked into the repo: it runs a matrix over `main` and `develop`, each leg regenerating against its own checkout and opening its own PR (`codegen-main -> main`, `codegen-develop -> develop`). For the two legs not to conflict on `develop -> main`, the generated output must depend only on its inputs - never on per-invocation state (timestamps, GUIDs, build IDs), which would diverge every run and conflict on every release. **What** a repo regenerates (data files, source, or both; code changes or pure data) and **how** (download and process an external source, transform local inputs, whatever) is entirely its own concern - the template constrains only that the output be input-deterministic, not how it is produced. - *Reference:* the codegen workflow tasks are kept under [`catalog/snippets/workflows/`](./catalog/snippets/workflows/) (`run-codegen-pull-request-task.yml` and its scheduler). A repo adopting codegen supplies its own input-deterministic generator; this repo ships none. -- **App-token workflows use Client ID, not App ID.** `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0; the template uses `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. When adding new App-token call sites, use the same form - do not reintroduce `app-id` / `CODEGEN_APP_ID`. See [README "Template - GitHub Setup"](./repo-config/README.md) for the secret-setup procedure. +- **App-token workflows use Client ID, not App ID.** `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0; the template uses `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. When adding new App-token call sites, use the same form - do not reintroduce `app-id` / `CODEGEN_APP_ID`. See [`repo-config/README.md`](./repo-config/README.md) "Secrets" for which secrets each mechanism needs. ## Release Model diff --git a/STANDUP.md b/STANDUP.md index 3b47392d..a869ecd6 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -18,7 +18,7 @@ Implement the Actions that satisfy [`WORKFLOW.md`][workflow] for the repo's type ## 4. Apply Settings, Rulesets, and Secrets -Run `repo-config/configure.sh [owner/repo] [release|operational]` (the repo defaults to the current one, the model to the registry lookup) to apply the fleet settings and the two rulesets idempotently (import the JSON, never hand-build - see [`repo-config/README.md`][repo-config-readme]). Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s) - Actions, and 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. +Run `repo-config/configure.sh [owner/repo] [release|operational]` (the repo defaults to the current one, the model to the registry lookup) to apply the fleet settings and the two rulesets idempotently (import the JSON, never hand-build - see [`docs/repo-config-carry.md`][repo-config-carry]). Configure every required secret per [`spec/secrets.json`][secrets] (the registry `requiredSecrets[]` list plus the implicit baseline) in the right store(s) - Actions, and 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. ## 5. Verify - Run the Audit @@ -55,7 +55,7 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo; its [matrix]: ./reports/conformance-matrix.md [project-types]: ./spec/project-types.json [repo-config]: ./repo-config/ -[repo-config-readme]: ./repo-config/README.md +[repo-config-carry]: ./docs/repo-config-carry.md [repos]: ./registry/repos.json [secrets]: ./spec/secrets.json [spec]: ./spec/ diff --git a/cspell.json b/cspell.json index e77a9ac1..a6d471e0 100644 --- a/cspell.json +++ b/cspell.json @@ -100,6 +100,7 @@ "pyrightconfig", "pytest", "quoteoftheday", + "regen", "resharper", "rhysd", "Rubba", diff --git a/docs/repo-config-carry.md b/docs/repo-config-carry.md new file mode 100644 index 00000000..77cddf20 --- /dev/null +++ b/docs/repo-config-carry.md @@ -0,0 +1,53 @@ +# repo-config: Carry, Apply, and Regenerate (Hub-Only) + +The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** - it is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`](../repo-config/README.md) states only the current facts about a repo's own config; this carry/apply/regen procedure lives here so it never ships into a downstream copy. + +## Downstream Carry + +Every fleet repo carries the `repo-config/` directory; the hub keeps the canonical copy. Rules for the carried copy: + +- **Carry only your model's `develop` variant.** A `release` repo carries `develop.json`; an `operational` repo carries `operational/develop.json` instead. `main.json` and `settings.json` are shared by both models. `configure.sh` aborts when the payload its model needs is missing rather than applying a partial configuration. +- **Carried files reference no other fleet repo.** A carried file names no sibling fleet repo as an example and links none (any fleet repo may be private, so a cross-repo link 404s in a public carrier; and it couples the repos). See [AGENTS.md "Documentation Style Conventions"](../AGENTS.md#documentation-style-conventions). To point at a current good example, name it in the onboarding/conformance issue or the hub-only [`reports/conformance-matrix.md`](../reports/conformance-matrix.md). +- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo - adapt this shape, don't invent. A current well-formed example is named in the onboarding/conformance issue. +- **Adapted `spec/secrets.json` shape.** The repo-scoped adaptation carries `baseline` (the App pair, which every fleet repo needs for the merge-bot) plus a `mechanisms` entry for each publish mechanism the repo actually uses, and the `targetMechanisms` routing entries for those mechanisms. **A source-only repo whose publish targets all map to a null mechanism (nothing to route) carries just `baseline` (plus a `note`)** - it omits `targetMechanisms` and `mechanisms` entirely, because a lone `targetMechanisms` map with no `mechanisms` reads as a schema bug (the audit enumerates `baseline` + `mechanisms`, never `targetMechanisms`, so an all-null routing map is dead weight). A `release` repo that uses a real mechanism (e.g. `nuget-oidc`, `docker-hub`, `codecov`) carries that `mechanisms` entry **and** its `targetMechanisms`/`typeMechanisms` routing, which the audit then picks up. +- **The regen snippet targets the current repo**, so it works unchanged in a carried copy. + +## Applying the Config + +**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`** - the names are load-bearing (`AGENTS.md` and the workflows reference them); only the `develop` *content* varies by model. + +First remove all legacy classic branch-protection rules and any stray rulesets, then run `configure.sh` (which picks the `develop` payload from the repo's `workflowModel` and applies `settings.json` alongside the rulesets): + +```sh +repo-config/configure.sh [owner/repo] [release|operational] +``` + +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; 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 + +To change the canonical rulesets, edit the live rulesets (fleet-wide changes happen at the hub), then regenerate the committed files from the current repo: + +```sh +repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" +# Paginate so a name match on a later page is never missed - the same trap configure.sh guards against. +# --paginate with --jq '.[]' emits one JSON object per ruleset across all pages; jq -s re-assembles them +# into the single array the selections below expect. +rulesets=$(gh api --paginate "repos/$repo/rulesets" --jq '.[]' | jq -s '.') +for name in develop main; do + out="repo-config/$name.json" + # An operational carry keeps its develop payload at operational/develop.json (develop.json is absent). + [ "$name" = "develop" ] && [ ! -f "$out" ] && out="repo-config/operational/develop.json" + # Exactly one ruleset per name: zero or duplicates is declared drift - fail loudly, never regen from a guess. + count=$(jq --arg n "$name" '[.[] | select(.name==$n)] | length' <<<"$rulesets") + [ "$count" -eq 1 ] || { echo "expected exactly 1 ruleset named $name, found $count (drift)" >&2; exit 1; } + id=$(jq --arg n "$name" '.[] | select(.name==$n) | .id' <<<"$rulesets") + gh api "repos/$repo/rulesets/$id" \ + --jq '{name, target, enforcement, bypass_actors, conditions, rules}' \ + | jq -S --indent 4 '.' > "$out" +done +``` + +## Brownfield Migration (Maintainer Only) + +`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [AGENTS.md "Git and Commit Rules"](../AGENTS.md#git-and-commit-rules) forbids agents from running - **an agent must never execute it; surface it to the maintainer**. Greenfield repos where signing is live before the first commit never hit this. diff --git a/repo-config/README.md b/repo-config/README.md index 1c9d8f4b..8562d8c1 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -2,18 +2,9 @@ Repository and branch configuration held as committed files, kept out of `.github/` (which holds the GitHub-consumed configuration - workflows, Dependabot). This mirrors the layout the fleet repos use. -- `main.json` plus one `develop` variant - the branch rulesets as the writable API subset (`name`, `target`, `enforcement`, `bypass_actors`, `conditions`, `rules`). The `develop` payload is `develop.json` (`release` repos) or `operational/develop.json` (`operational` repos); the hub keeps both, a carried copy only its own model's (see "Downstream Carry"). These are the canonical expected payloads that the audit (the hub's fleet-wide `AUDIT.md`, or a carried repo-scoped adaptation - see "Downstream Carry") diffs the live rulesets against. -- `operational/develop.json` - the `develop` ruleset for **operational** repos (registry `workflowModel: operational`): direct signed pushes, no PR gate. Present at the hub and in operational carries only - a carried `release` repo does not have it. See "Rulesets" below. -- `configure.sh` - applies the rulesets to a repository via the GitHub API (create or full-payload update, idempotent). Run `repo-config/configure.sh [owner/repo] [release|operational]`; the model may also be passed as the sole argument (`repo-config/configure.sh operational`). The model defaults to the registry `workflowModel` lookup at the hub; in a downstream carry (no registry) it is inferred from which `develop` payload is carried, and an ambiguous layout (both or neither payload) aborts rather than guesses. - -## Downstream Carry - -Every fleet repo carries this directory; the hub keeps the canonical copy. Rules for the carried copy: - -- **Carry only your model's `develop` variant.** A `release` repo carries `develop.json`; an `operational` repo carries `operational/develop.json` instead. `main.json` and `settings.json` are shared by both models. `configure.sh` aborts when the payload its model needs is missing rather than applying a partial configuration. -- **Hub-only references stay plain text.** The hub is a private repo: never URL-link it from a downstream repo - the link 404s for anyone without hub access. Files whose canonical fleet-wide form lives only at the hub are mentioned by name, not linked; links into files every repo carries (`AGENTS.md`) resolve everywhere and are fine. -- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative, and the local copies link no other repo. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo - adapt this shape, don't invent. A `release` repo keeps its `develop` payload at `repo-config/develop.json`, and its `spec/secrets.json` keeps the baseline App pair plus the secret names for its own publish mechanisms (from the hub's canonical `spec/secrets.json`). The fleet audit letter-checks both carried files (`spec/files.json`). A current well-formed example is named in the onboarding/conformance issue, not linked here. -- **The regen snippet targets the current repo**, so it works unchanged in a carried copy. +- `main.json` plus one `develop` variant - the branch rulesets as the writable API subset (`name`, `target`, `enforcement`, `bypass_actors`, `conditions`, `rules`). The `develop` payload is `develop.json` (`release` repos) or `operational/develop.json` (`operational` repos). These are the canonical expected payloads that the self-audit (`AUDIT.md`) diffs the live rulesets against. +- `operational/develop.json` - the `develop` ruleset for **operational** repos (registry `workflowModel: operational`): direct signed pushes, no PR gate. Present in operational repos only - a `release` repo does not have it. See "Rulesets" below. +- `configure.sh` - applies the rulesets and settings to a repository via the GitHub API (create or full-payload update, idempotent). Run `repo-config/configure.sh [owner/repo] [release|operational]`; the model may also be passed as the sole argument (`repo-config/configure.sh operational`). The model defaults to the registry `workflowModel` lookup where a registry is present, else it is inferred from which `develop` payload is present (an ambiguous layout aborts rather than guesses). ## Rulesets @@ -24,33 +15,11 @@ Two workflow models share `main.json` but differ on `develop` (registry `workflo `main` (both models) requires merge-commit merges (no linear-history rule), signed commits, a passing `Check pull request workflow status job`, resolved review threads, and Copilot review, and blocks force-pushes and deletion - so a `develop -> main` promotion is always gated even when `develop` takes direct commits. Every ruleset intentionally leaves "Require branches to be up to date before merging" **off** - see [AGENTS.md "Branching Model"][agents-branching-model]. -**Configure by importing these JSON files, 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`** - the names are load-bearing (`AGENTS.md` and the workflows reference them); only the `develop` *content* varies by model. First remove all legacy classic branch-protection rules and any stray rulesets, then run `configure.sh` (which picks the `develop` payload from the repo's `workflowModel`), or `gh api -X POST repos///rulesets --input repo-config/.json` per file (operational repos use `operational/develop.json` for `develop`). `gh ruleset` is read-only; 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 ruleset, GET it, change the field, and PUT the whole writable subset back (a partial PUT `422`s). - -To change the canonical rulesets, edit the live rulesets (fleet-wide changes happen at the hub), then regenerate the committed files from the current repo: - -```sh -repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" -# Paginate so a name match on a later page is never missed - the same trap configure.sh guards against. -# --paginate with --jq '.[]' emits one JSON object per ruleset across all pages; jq -s re-assembles them -# into the single array the selections below expect. -rulesets=$(gh api --paginate "repos/$repo/rulesets" --jq '.[]' | jq -s '.') -for name in develop main; do - out="repo-config/$name.json" - # An operational carry keeps its develop payload at operational/develop.json (develop.json is absent). - [ "$name" = "develop" ] && [ ! -f "$out" ] && out="repo-config/operational/develop.json" - # Exactly one ruleset per name: zero or duplicates is declared drift - fail loudly, never regen from a guess. - count=$(jq --arg n "$name" '[.[] | select(.name==$n)] | length' <<<"$rulesets") - [ "$count" -eq 1 ] || { echo "expected exactly 1 ruleset named $name, found $count (drift)" >&2; exit 1; } - id=$(jq --arg n "$name" '.[] | select(.name==$n) | .id' <<<"$rulesets") - gh api "repos/$repo/rulesets/$id" \ - --jq '{name, target, enforcement, bypass_actors, conditions, rules}' \ - | jq -S --indent 4 '.' > "$out" -done -``` +The result is **exactly two rulesets named `develop` and `main`** - the names are load-bearing (`AGENTS.md` and the workflows reference them); only the `develop` *content* varies by model. The required check binds by name and only turns green after the repo's PR workflow runs once. ## Secrets -Publish credentials required per mechanism are enumerated in `spec/secrets.json` (canonical at the hub; a downstream repo carries a repo-scoped adaptation - see "Downstream Carry"). A repo needs only the mechanisms its own publish targets use - a source-only repo needs none of the publish credentials below. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key; the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). 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; the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. +Publish credentials required per mechanism are enumerated in `spec/secrets.json`. A repo needs only the mechanisms its own publish targets use - a source-only repo needs none of the publish credentials below. NuGet and PyPI use keyless OIDC Trusted Publishing (no stored key; the publish job needs `id-token: write`, and PyPI additionally an `environment: pypi` gate). 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; the app must be installed, not just created). App-token call sites use `client-id`, never the deprecated `app-id`. ## Repo Settings @@ -63,12 +32,7 @@ The fleet-standard general settings live in [`settings.json`][settings-json] and - **Wikis and Projects off. Discussions on public repos only** (off on private). **Sponsorships off** - the button is driven by `.github/FUNDING.yml`, not a REST toggle, and the fleet ships none. - **Actions / General**: allow GitHub Actions to create and approve pull requests (for the bots). -## Brownfield Migration (Maintainer Only) - -`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [AGENTS.md "Git and Commit Rules"][agents-git-and-commit-rules] forbids agents from running - **an agent must never execute it; surface it to the maintainer**. Greenfield repos where signing is live before the first commit never hit this. - [agents-branching-model]: ../AGENTS.md#branching-model -[agents-git-and-commit-rules]: ../AGENTS.md#git-and-commit-rules [settings-json]: ./settings.json diff --git a/spec/files.json b/spec/files.json index a40ca8a4..b622105d 100644 --- a/spec/files.json +++ b/spec/files.json @@ -16,8 +16,8 @@ { "path": "version.json", "intentRef": "WORKFLOW.md#d3---versioning-and-classification", "appliesTo": "*" }, { "path": "repo-config/develop.json", "intentRef": "repo-config/README.md", "appliesTo": "*" }, { "path": "repo-config/main.json", "intentRef": "repo-config/README.md", "appliesTo": "*" }, - { "path": "AUDIT.md", "intentRef": "repo-config/README.md", "appliesTo": "*" }, - { "path": "spec/secrets.json", "intentRef": "repo-config/README.md", "appliesTo": "*" }, + { "path": "AUDIT.md", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, + { "path": "spec/secrets.json", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, { "path": ".github/dependabot.yml", "appliesTo": "*" }, { "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks.json", "appliesTo": ["csharp"] }, { "path": ".vscode/tasks.json", "sections": ["clean-compile task group"], "reference": "catalog/snippets/configs/vscode-tasks-python.json", "appliesTo": ["python"] }, From 218566d2659bab445e89a2af3a48e79ad96cb5c2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 17 Jul 2026 21:25:06 -0700 Subject: [PATCH 2/2] Use reference-style links in the new hub doc (Copilot #349) docs/repo-config-carry.md now uses reference-style links with the definitions grouped at the bottom, per the AGENTS.md Markdown rule (inline links are reserved for AGENTS.md and copilot-instructions.md). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/repo-config-carry.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/repo-config-carry.md b/docs/repo-config-carry.md index 77cddf20..1a7e7d01 100644 --- a/docs/repo-config-carry.md +++ b/docs/repo-config-carry.md @@ -1,13 +1,13 @@ # repo-config: Carry, Apply, and Regenerate (Hub-Only) -The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** - it is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`](../repo-config/README.md) states only the current facts about a repo's own config; this carry/apply/regen procedure lives here so it never ships into a downstream copy. +The **process** for carrying the `repo-config/` baseline to a fleet repo, applying it, and regenerating the canonical payloads. This doc is **hub-only** - it is not carried downstream (it describes what the hub does *to* a repo, not a fact about any one repo). The carried [`repo-config/README.md`][repo-config-readme] states only the current facts about a repo's own config; this carry/apply/regen procedure lives here so it never ships into a downstream copy. ## Downstream Carry Every fleet repo carries the `repo-config/` directory; the hub keeps the canonical copy. Rules for the carried copy: - **Carry only your model's `develop` variant.** A `release` repo carries `develop.json`; an `operational` repo carries `operational/develop.json` instead. `main.json` and `settings.json` are shared by both models. `configure.sh` aborts when the payload its model needs is missing rather than applying a partial configuration. -- **Carried files reference no other fleet repo.** A carried file names no sibling fleet repo as an example and links none (any fleet repo may be private, so a cross-repo link 404s in a public carrier; and it couples the repos). See [AGENTS.md "Documentation Style Conventions"](../AGENTS.md#documentation-style-conventions). To point at a current good example, name it in the onboarding/conformance issue or the hub-only [`reports/conformance-matrix.md`](../reports/conformance-matrix.md). +- **Carried files reference no other fleet repo.** A carried file names no sibling fleet repo as an example and links none (any fleet repo may be private, so a cross-repo link 404s in a public carrier; and it couples the repos). See [AGENTS.md "Documentation Style Conventions"][agents-documentation-style]. To point at a current good example, name it in the onboarding/conformance issue or the hub-only [`reports/conformance-matrix.md`][conformance-matrix]. - **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo - adapt this shape, don't invent. A current well-formed example is named in the onboarding/conformance issue. - **Adapted `spec/secrets.json` shape.** The repo-scoped adaptation carries `baseline` (the App pair, which every fleet repo needs for the merge-bot) plus a `mechanisms` entry for each publish mechanism the repo actually uses, and the `targetMechanisms` routing entries for those mechanisms. **A source-only repo whose publish targets all map to a null mechanism (nothing to route) carries just `baseline` (plus a `note`)** - it omits `targetMechanisms` and `mechanisms` entirely, because a lone `targetMechanisms` map with no `mechanisms` reads as a schema bug (the audit enumerates `baseline` + `mechanisms`, never `targetMechanisms`, so an all-null routing map is dead weight). A `release` repo that uses a real mechanism (e.g. `nuget-oidc`, `docker-hub`, `codecov`) carries that `mechanisms` entry **and** its `targetMechanisms`/`typeMechanisms` routing, which the audit then picks up. - **The regen snippet targets the current repo**, so it works unchanged in a carried copy. @@ -50,4 +50,11 @@ done ## Brownfield Migration (Maintainer Only) -`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [AGENTS.md "Git and Commit Rules"](../AGENTS.md#git-and-commit-rules) forbids agents from running - **an agent must never execute it; surface it to the maintainer**. Greenfield repos where signing is live before the first commit never hit this. +`Require signed commits` rejects any pre-existing unsigned commit, so the first `develop -> main` release on a repo with unsigned history is blocked. Re-signing that history is a non-fast-forward that the `Block force pushes` rule rejects, **and the admin bypass does not cover `git push --force`**. Completing it requires temporarily disabling the ruleset and a maintainer force-push. This is a one-time, maintainer-performed migration that deliberately uses the force-push [AGENTS.md "Git and Commit Rules"][agents-git-and-commit-rules] forbids agents from running - **an agent must never execute it; surface it to the maintainer**. Greenfield repos where signing is live before the first commit never hit this. + + + +[agents-documentation-style]: ../AGENTS.md#documentation-style-conventions +[agents-git-and-commit-rules]: ../AGENTS.md#git-and-commit-rules +[conformance-matrix]: ../reports/conformance-matrix.md +[repo-config-readme]: ../repo-config/README.md