From ddb4b643b2ff52c89c3669c3b7058d0a27025b4b Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 28 Jun 2026 06:51:33 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20implement=20issue=20#970=20?= =?UTF-8?q?=E2=80=94=20[Phase=203]=20Docs=20cutover=20+=20end-to-end=20new?= =?UTF-8?q?-repo=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/bootstrap/new-repo-validation.md | 127 ++++++++++++++++++++++++++ scripts/seed-repo-template.sh | 19 ++++ tests/test_bootstrap_new_repo.bats | 51 +++++++++++ tests/test_seed_repo_template.bats | 14 +++ 4 files changed, 211 insertions(+) create mode 100644 docs/bootstrap/new-repo-validation.md diff --git a/docs/bootstrap/new-repo-validation.md b/docs/bootstrap/new-repo-validation.md new file mode 100644 index 000000000..52ad6c786 --- /dev/null +++ b/docs/bootstrap/new-repo-validation.md @@ -0,0 +1,127 @@ +# New-repo onboarding — primary path & end-to-end DRY_RUN validation + +_Epic #964 · Phase 3 (#970). Validation recorded 2026-06-28._ + +This is the recorded, end-to-end validation that the documented onboarding path — +**"Use this template" → `scripts/bootstrap-new-repo.sh` → ring confirmation** — +produces a fully org-compliant repo, exercised under `DRY_RUN` with no drift. + +## Primary onboarding path (one-click + one-command) + +1. **Use this template.** Create the repo from `petry-projects/repo-template` + (`seed-repo-template.sh` keeps that template in sync with `petry-projects/.github` + `standards/`). This seeds day-0 only: the thin-caller workflow stubs pinned to + their published `@/stable` channel tags plus the root/baseline files + (CODEOWNERS, AGENTS.md/CLAUDE.md pointers, LICENSE, SECURITY.md, `.gitignore`, + `BOOTSTRAP.md`). Then follow `BOOTSTRAP.md` for the two per-stack picks + (Dependabot stack, `ci.yml`). +2. **Run the bootstrap.** `bash scripts/bootstrap-new-repo.sh owner/new-repo` + brings repo settings, security/GHAS + secret-scanning push protection, the two + sanctioned rulesets (with required checks + bypass actors), the standard label + set, and CODEOWNERS verification to org compliance by orchestrating the existing + `apply-*` scripts. It reimplements no policy. +3. **Confirm the release ring.** The ring is an auditable choice (default + `stable`, record-only). A non-stable ring is registered in both central files + and the caller stub is repinned to the matching `@/` channel tag. + +Preview any run with `DRY_RUN=true` first — it prints the full intended state and +makes zero write API calls. + +> **Template seeds day-0 only.** Ongoing standards updates to an existing repo +> still flow through the PR-based sync (`deploy-standard-workflows.sh` / +> `aw-standards-sync.sh`), not by re-templating. The legacy manual runbook is the +> **existing-repo fallback**, not the primary path. + +## Recorded DRY_RUN walkthrough + +Captured with `gh` stubbed to a no-op reader (`echo '{}'`) so the real sub-scripts +(`apply-repo-settings.sh`, `apply-rulesets.sh`) run end-to-end without network or +writes — the same seam the bats suite uses. Executable form: +`tests/test_bootstrap_new_repo.bats` → _"e2e DRY_RUN: covers the whole +intended-state surface with no write calls (#970 AC #2/#3)"_. + +### Default ring (`stable`) + +``` +$ DRY_RUN=true GITHUB_ACTOR=octocat bash scripts/bootstrap-new-repo.sh petry-projects/acme-service +[bootstrap] repo=petry-projects/acme-service dry_run=true ring=dev-lead/stable +[bootstrap] (1/5) release ring confirmation (dev-lead/stable) + [ring-audit] repo=petry-projects/acme-service agent=dev-lead ring=stable operator=octocat at=... decision=recorded + ring=stable — record-only; no central-file change required (covered by the '*' catch-all) +[bootstrap] (2/5) repo settings + security/GHAS + push protection +[dry-run] would patch security_and_analysis on petry-projects/acme-service: secret_scanning secret_scanning_push_protection secret_scanning_ai_detection secret_scanning_non_provider_patterns dependabot_security_updates +[dry-run] would disable auto-trigger for apps 1236702 347564 on petry-projects/acme-service +[bootstrap] (3/5) sanctioned rulesets (pr-quality + code-quality + …) +[apply-rulesets] repo=petry-projects/acme-service dir=.../.github/rulesets dry_run=true + create ruleset 'code-quality' on petry-projects/acme-service + [dry-run] POST repos/petry-projects/acme-service/rulesets + create ruleset 'pr-quality' on petry-projects/acme-service + [dry-run] POST repos/petry-projects/acme-service/rulesets + create ruleset 'release-channel-tags' on petry-projects/acme-service + [dry-run] POST repos/petry-projects/acme-service/rulesets +[apply-rulesets] done (3 ruleset(s)) +[bootstrap] (4/5) standard label set + [dry-run] would ensure label 'needs-human-review' on petry-projects/acme-service + [dry-run] would ensure label 'ack-test-deletion' on petry-projects/acme-service + [dry-run] would ensure label 'dependencies' on petry-projects/acme-service + [dry-run] would ensure label 'automerge' on petry-projects/acme-service +[bootstrap] (5/5) verify CODEOWNERS team (@petry-projects/org-leads first owner) + [dry-run] would verify @petry-projects/org-leads is the first CODEOWNERS owner on petry-projects/acme-service + +[bootstrap] PASS — petry-projects/acme-service bootstrapped to org compliance (dry-run) +``` + +### Non-stable ring (`--ring ring1`) + +The ring step additionally registers the repo in both central files and repins the +caller stub — and asserts no drift before writing: + +``` +[bootstrap] (1/5) release ring confirmation (dev-lead/ring1) + [ring-audit] repo=petry-projects/acme-service agent=dev-lead ring=ring1 operator=octocat at=... decision=registered + [dry-run] would add petry-projects/acme-service to dev-lead 'ring1' members in canary-rings.json + [ring] would register petry-projects/acme-service in petry-projects/.github:scripts/lib/ring-pins.sh for channel dev-lead/ring1 (cross-repo PR, keeps central files in sync) + [ring] would repin petry-projects/acme-service caller stub .github/workflows/dev-lead.yml to @dev-lead/ring1 + ring consistency OK — petry-projects/acme-service sits in 'ring1' across both central files and its stub pins @dev-lead/ring1 +``` + +## Intended state covered (no drift) + +| Surface | Source of truth | Verified in walkthrough | +|---|---|---| +| Repo settings + security/GHAS | `apply-repo-settings.sh` | `security_and_analysis` patch intent | +| Secret-scanning push protection | `lib/push-protection.sh` | `secret_scanning_push_protection` in the patch set | +| Check-suite auto-trigger (Claude/CodeRabbit) | `apply-repo-settings.sh` | `would disable auto-trigger for apps 1236702 347564` | +| `pr-quality` ruleset + bypass actors | `.github/rulesets/pr-quality.json` | created; bypass = OrganizationAdmin + Integration (`bypass_mode: always`) | +| `code-quality` ruleset + required checks + bypass | `.github/rulesets/code-quality.json` | created; required checks SonarCloud, CodeQL, agent-shield, dependency-audit; same bypass actors | +| Required status checks | carried in the ruleset JSONs | not wired by bootstrap — live in `code-quality.json` | +| Standard labels | `bootstrap-new-repo.sh` `BOOTSTRAP_LABELS` | needs-human-review, ack-test-deletion, dependencies, automerge | +| CODEOWNERS team | new repo's `.github/CODEOWNERS` | first owner verified = `@petry-projects/org-leads` | +| Recorded ring | `standards/canary-rings.json` (+ cross-repo `ring-pins.sh`) | audited; stable record-only / non-stable registered with no drift | +| No drift | — | `DRY_RUN` emits **zero** write API calls | + +## AC #1 — cross-repo checklist cutover (petry-projects/.github) + +The two onboarding checklists live in **`petry-projects/.github`**, not in this +repo, so they are landed via the standards cross-repo PR pattern +(`STANDARDS_REPO=petry-projects/.github`, as `aw-standards-sync.sh` does), not on +this branch. The cutover content to apply: + +- **`standards/github-settings.md` — "Applying to a New Repository":** lead with + **"Use this template" + `scripts/bootstrap-new-repo.sh`** as the primary, + one-click + one-command path (it applies repo settings, both rulesets with bypass + actors + required checks, labels, CODEOWNERS, and secret-scanning push + protection). Demote the existing 9-step manual settings runbook to an + **existing-repo fallback**. +- **`standards/ci-standards.md` — "Applying to a New Repository":** same cutover — + template + bootstrap first; the 13-step CI checklist becomes the existing-repo + fallback. State explicitly that the template seeds **day-0 only** and ongoing + CI/standards updates continue to flow through the PR-based sync + (`deploy-standard-workflows.sh`), which remains the ongoing-sync path for + existing repos and is out of scope to replace. + +## See also + +- `scripts/bootstrap-new-repo.sh` — the orchestrator (issues #967, #968) +- `scripts/seed-repo-template.sh` — day-0 template seeding + the generated `BOOTSTRAP.md` (#966) +- `tests/test_bootstrap_new_repo.bats`, `tests/test_seed_repo_template.bats` — the executable validation diff --git a/scripts/seed-repo-template.sh b/scripts/seed-repo-template.sh index 6e1ca78a2..13cc602cc 100755 --- a/scripts/seed-repo-template.sh +++ b/scripts/seed-repo-template.sh @@ -269,6 +269,25 @@ If you enable the `sonarcloud` workflow, set `sonar.projectKey` / --- +## What this template does NOT do + +Two onboarding questions are resolved here so they are not left implicit: + +- **Framework subtrees (`frameworks/`) are opt-in — they are not seeded by this + template.** The agentic frameworks (`bmad-method`, `spec-kit`, `gsd`) are + `git subtree` development tooling maintained in `petry-projects/.github-private`; + a product repo does not need them on day 0. If your project uses one, add it on + demand with `git subtree add` against its upstream — see that repo's `AGENTS.md`. + +- **App installs (Claude, CodeRabbit) are a manual org-admin step — the bootstrap + does not install them.** `bootstrap-new-repo.sh` runs as `GITHUB_TOKEN` and + cannot install org GitHub Apps; installation is a one-time org-level action by an + org admin. The bootstrap only *disables those apps' check-suite auto-trigger* on + the new repo (so queued-but-never-completed suites never block auto-merge), + assuming the apps are already installed org-wide. + +--- + The rest — CODEOWNERS, AGENTS.md/CLAUDE.md pointers, LICENSE, SECURITY.md, .gitignore — is ready as shipped. Repo settings, rulesets, labels, and security configuration are applied separately by the org `bootstrap-new-repo.sh` flow. diff --git a/tests/test_bootstrap_new_repo.bats b/tests/test_bootstrap_new_repo.bats index f9714bed7..de55e4852 100644 --- a/tests/test_bootstrap_new_repo.bats +++ b/tests/test_bootstrap_new_repo.bats @@ -234,3 +234,54 @@ _ring_sot_copy() { [ "$status" -eq 0 ] [[ "$output" == *"ring consistency OK"* ]] } + +# ── end-to-end DRY_RUN validation: the full policy surface in one run (#970) ──── +# A single DRY_RUN walkthrough with the REAL sub-scripts (only `gh` stubbed) must +# describe the entire intended state — recorded ring, repo settings + GHAS + push +# protection, both sanctioned rulesets, the standard labels, CODEOWNERS team — and +# emit zero write API calls (no drift). This is the executable form of the +# end-to-end validation recorded in docs/bootstrap/new-repo-validation.md. +@test "e2e DRY_RUN: covers the whole intended-state surface with no write calls (#970 AC #2/#3)" { + _stub_gh + _ring_sot_copy + run env DRY_RUN=true GITHUB_ACTOR=octocat CANARY_RINGS="$RING_SOT" \ + bash "$BOOTSTRAP" petry-projects/acme-service + [ "$status" -eq 0 ] + + # (1/5) ring — auditable record, default stable, record-only. + [[ "$output" == *"[ring-audit]"* ]] + [[ "$output" == *"ring=stable"* ]] + [[ "$output" == *"decision=recorded"* ]] + + # (2/5) repo settings — security_and_analysis + secret-scanning push protection, + # and the Claude/CodeRabbit check-suite auto-trigger disable. + [[ "$output" == *"security_and_analysis"* ]] + [[ "$output" == *"secret_scanning_push_protection"* ]] + [[ "$output" == *"would disable auto-trigger"* ]] + + # (3/5) both sanctioned rulesets are applied. + [[ "$output" == *"pr-quality"* ]] + [[ "$output" == *"code-quality"* ]] + + # (4/5) the standard label set. + [[ "$output" == *"needs-human-review"* ]] + [[ "$output" == *"ack-test-deletion"* ]] + + # (5/5) CODEOWNERS team verification. + [[ "$output" == *"CODEOWNERS"* ]] + [[ "$output" == *"org-leads"* ]] + + # PASS summary + the no-drift invariant: a pure DRY_RUN makes no write API calls. + [[ "$output" == *"PASS"* ]] + [ ! -f "$CALLS" ] + + # Each sanctioned ruleset carries its bypass actors + required checks in the JSON + # (not wired by bootstrap). These `run jq` calls overwrite $output, so they run + # last, after every transcript assertion above. + run jq -e '[.bypass_actors[].actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/code-quality.json" + [ "$status" -eq 0 ] + run jq -e '[.bypass_actors[].actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/pr-quality.json" + [ "$status" -eq 0 ] + run jq -e '[.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | length > 0' "$RULESETS_DIR/code-quality.json" + [ "$status" -eq 0 ] +} diff --git a/tests/test_seed_repo_template.bats b/tests/test_seed_repo_template.bats index a1c0266ff..2ab171cd4 100644 --- a/tests/test_seed_repo_template.bats +++ b/tests/test_seed_repo_template.bats @@ -175,6 +175,20 @@ _fixture_workflow() { # [[ "$output" == *"stack"* ]] } +@test "baseline: BOOTSTRAP.md resolves the two open onboarding questions — frameworks opt-in, app installs manual (#970 AC #4)" { + run bash "$SEED" --emit-baseline BOOTSTRAP.md + [ "$status" -eq 0 ] + # Q1: framework subtrees (frameworks/) are opt-in, NOT seeded by the template. + [[ "$output" == *"frameworks/"* ]] + [[ "$output" == *"opt-in"* ]] + [[ "$output" == *"not"*"seed"* || "$output" == *"not seeded"* || "$output" == *"are not"* ]] + # Q2: app installs (Claude/CodeRabbit) are a manual org-admin step, not done by bootstrap. + [[ "$output" == *"Claude"* ]] + [[ "$output" == *"CodeRabbit"* ]] + [[ "$output" == *"manual"* ]] + [[ "$output" == *"auto-trigger"* ]] +} + # ── dependabot baseline is sourced from the chosen standards/ stack template ─── @test "baseline: dependabot.yml comes from the chosen standards/dependabot stack" { printf 'version: 2\nupdates:\n - package-ecosystem: npm\n' \ From d152e11916688fb75dd05fac13fe6e78eaf29a11 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:02:50 +0000 Subject: [PATCH 2/2] chore: dev-lead update (review-changes) [skip ci-relay] --- tests/test_bootstrap_new_repo.bats | 6 +++--- tests/test_seed_repo_template.bats | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_bootstrap_new_repo.bats b/tests/test_bootstrap_new_repo.bats index de55e4852..c791bf113 100644 --- a/tests/test_bootstrap_new_repo.bats +++ b/tests/test_bootstrap_new_repo.bats @@ -278,10 +278,10 @@ _ring_sot_copy() { # Each sanctioned ruleset carries its bypass actors + required checks in the JSON # (not wired by bootstrap). These `run jq` calls overwrite $output, so they run # last, after every transcript assertion above. - run jq -e '[.bypass_actors[].actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/code-quality.json" + run jq -e '[.bypass_actors?[]?.actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/code-quality.json" [ "$status" -eq 0 ] - run jq -e '[.bypass_actors[].actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/pr-quality.json" + run jq -e '[.bypass_actors?[]?.actor_type] | (index("OrganizationAdmin") and index("Integration"))' "$RULESETS_DIR/pr-quality.json" [ "$status" -eq 0 ] - run jq -e '[.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | length > 0' "$RULESETS_DIR/code-quality.json" + run jq -e '[.rules[]? | select(.type=="required_status_checks") | .parameters?.required_status_checks?[]?.context] | length > 0' "$RULESETS_DIR/code-quality.json" [ "$status" -eq 0 ] } diff --git a/tests/test_seed_repo_template.bats b/tests/test_seed_repo_template.bats index 2ab171cd4..d94b28ec0 100644 --- a/tests/test_seed_repo_template.bats +++ b/tests/test_seed_repo_template.bats @@ -181,7 +181,7 @@ _fixture_workflow() { # # Q1: framework subtrees (frameworks/) are opt-in, NOT seeded by the template. [[ "$output" == *"frameworks/"* ]] [[ "$output" == *"opt-in"* ]] - [[ "$output" == *"not"*"seed"* || "$output" == *"not seeded"* || "$output" == *"are not"* ]] + [[ "$output" == *"not"*"seed"* || "$output" == *"not seeded"* ]] # Q2: app installs (Claude/CodeRabbit) are a manual org-admin step, not done by bootstrap. [[ "$output" == *"Claude"* ]] [[ "$output" == *"CodeRabbit"* ]]