Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/bootstrap/new-repo-validation.md
Original file line number Diff line number Diff line change
@@ -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 `@<name>/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 `@<agent>/<ring>` 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
19 changes: 19 additions & 0 deletions scripts/seed-repo-template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions tests/test_bootstrap_new_repo.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
}
14 changes: 14 additions & 0 deletions tests/test_seed_repo_template.bats
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ _fixture_workflow() { # <name> <heredoc-content-on-stdin>
[[ "$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"* ]]
# 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' \
Expand Down
Loading