From 09ff2413e4b3a685bb44cfed33235b140a50cebe Mon Sep 17 00:00:00 2001 From: Johnathan Falk Date: Sat, 25 Apr 2026 06:38:38 -0400 Subject: [PATCH 1/2] fix(bootstrap-repo): correct sync script invocations after dry-run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live dry-run on jdfalk/bootstrap-test-DELETE-ME surfaced three integration bugs: 1. ghcommon's sync-repo-setup.py CLI only exposes sync_all_repositories() (auto-discovers targets) — not the single-target sync_repository() method we need. Add a small Python shim (_sync_one_repo.py) that imports the existing class and calls the existing per-repo method, without modifying ghcommon's script. 2. ghcommon's sync-github-labels.py uses positional args (owner, repo), not flags, and reads PAT_TOKEN/GITHUB_TOKEN env vars (not GH_TOKEN). Fix bootstrap_repo.sh to match the actual signature. 3. verify_bootstrap.sh false-positive when required_status_checks is null (no PR-triggering workflows). Skip the .strict check in that case rather than treating null != "true" as drift. After these fixes, end-to-end dry-run is fully idempotent: 4th run produces zero file changes and verify_bootstrap.sh exits 0. Label sync hits GitHub's secondary rate limit around ~190/242 labels but is non-fatal and resumes on re-run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bootstrap-repo/scripts/_sync_one_repo.py | 51 +++++ .../bootstrap-repo/scripts/bootstrap_repo.sh | 11 +- .../scripts/verify_bootstrap.sh | 7 +- PLAN.md | 187 ++++++++++++++++++ 4 files changed, 250 insertions(+), 6 deletions(-) create mode 100755 .claude/skills/bootstrap-repo/scripts/_sync_one_repo.py create mode 100644 PLAN.md diff --git a/.claude/skills/bootstrap-repo/scripts/_sync_one_repo.py b/.claude/skills/bootstrap-repo/scripts/_sync_one_repo.py new file mode 100755 index 00000000..9fd4a9f3 --- /dev/null +++ b/.claude/skills/bootstrap-repo/scripts/_sync_one_repo.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Run ghcommon's RepoSetupSyncer.sync_repository() against a single target. + +ghcommon's `sync-repo-setup.py` only exposes `sync_all_repositories()` via its +CLI, which auto-discovers all repos. We need a single-target flow for bootstrap. +This shim imports the existing class and calls the existing per-repo method, +without modifying ghcommon's script. + +Usage: + _sync_one_repo.py +""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +def load_syncer_module(ghcommon: Path): + script = ghcommon / "scripts" / "sync-repo-setup.py" + spec = importlib.util.spec_from_file_location("sync_repo_setup", script) + if spec is None or spec.loader is None: + raise RuntimeError(f"could not load {script}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(argv: list[str]) -> int: + if len(argv) != 3: + print(f"usage: {argv[0]} ", file=sys.stderr) + return 2 + ghcommon = Path(argv[1]).resolve() + target = Path(argv[2]).resolve() + if not (ghcommon / "scripts" / "sync-repo-setup.py").is_file(): + print(f"error: ghcommon sync-repo-setup.py not found under {ghcommon}", file=sys.stderr) + return 1 + if not target.is_dir(): + print(f"error: target not a directory: {target}", file=sys.stderr) + return 1 + + module = load_syncer_module(ghcommon) + syncer = module.RepoSetupSyncer(source_repo=ghcommon, dry_run=False) + result = syncer.sync_repository(target_repo=target) + print(f"sync result: {result.get('repo_name')}: {result.get('status', 'ok')}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh b/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh index f65f6c19..8fe0be9c 100755 --- a/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh +++ b/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh @@ -170,8 +170,8 @@ fi echo "→ Syncing instruction/dependabot/AGENTS files from ghcommon" SYNC_SCRIPT="${GHCOMMON}/scripts/sync-repo-setup.py" if [[ -f ${SYNC_SCRIPT} ]]; then - python3 "${SYNC_SCRIPT}" --target "${REPO_PATH}" || - echo " (sync-repo-setup.py failed; continuing — may need flag adjustment)" + python3 "${SCRIPT_DIR}/_sync_one_repo.py" "${GHCOMMON}" "${REPO_PATH}" || + echo " (sync shim failed; continuing)" else echo " (sync-repo-setup.py not found at ${SYNC_SCRIPT}; skipping)" fi @@ -182,9 +182,10 @@ if [[ ${SKIP_LABELS} -eq 0 ]]; then echo "→ Syncing labels from ghcommon/labels.json" LABEL_SCRIPT="${GHCOMMON}/scripts/sync-github-labels.py" if [[ -f ${LABEL_SCRIPT} ]]; then - GH_TOKEN="${GH_TOKEN:-$(gh auth token)}" \ - python3 "${LABEL_SCRIPT}" --owner "${OWNER}" --repo "${NAME}" \ - --labels "${GHCOMMON}/labels.json" || + # ghcommon's sync-github-labels.py reads PAT_TOKEN or GITHUB_TOKEN, not GH_TOKEN + GITHUB_TOKEN="${PAT_TOKEN:-${GITHUB_TOKEN:-$(gh auth token)}}" \ + python3 "${LABEL_SCRIPT}" "${OWNER}" "${NAME}" \ + --labels-file "${GHCOMMON}/labels.json" || echo " (sync-github-labels.py failed; continuing)" fi fi diff --git a/.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh b/.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh index b5530fac..557eb920 100755 --- a/.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh +++ b/.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh @@ -102,7 +102,12 @@ else DRIFT=1 fi } - check '.required_status_checks.strict' 'true' + # required_status_checks may be null (no PR-triggering workflows). Only check + # .strict when the object exists. + HAS_RSC=$(echo "${PROTECTION}" | jq -r '.required_status_checks != null') + if [[ ${HAS_RSC} == "true" ]]; then + check '.required_status_checks.strict' 'true' + fi check '.enforce_admins.enabled' 'false' check '.required_pull_request_reviews.dismiss_stale_reviews' 'true' check '.required_pull_request_reviews.required_approving_review_count' '0' diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..31af2e88 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,187 @@ +# bootstrap-repo skill + +## Goal + +Add a `bootstrap-repo` skill at `.claude/skills/bootstrap-repo/` that creates a +new GitHub repo (or adopts an existing one) and applies the full ghcommon house +standard: merge settings, branch protection, labels, dependabot, instruction +files, and flavor-specific overlays for action / library / service repos. The +skill orchestrates existing ghcommon scripts where they exist +(`sync-repo-setup.py`, `sync-github-labels.py`) and fills the one real gap — +repo settings + branch protection — with new shell scripts calling `gh api` +directly. + +In parallel, a Haiku subagent splits `ACTION_REPO_STANDARDS.md` into three +flavor docs, audits `labels.json` against live repos, and updates `AGENTS.md` / +`CLAUDE.md` to point at the new structure. + +## Affected files + +### New (this PR) + +- `.claude/skills/bootstrap-repo/SKILL.md` — thin trigger doc + flavor decision + tree, <100 lines +- `.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh` — main entry; + orchestrates 1–9 below +- `.claude/skills/bootstrap-repo/scripts/apply_repo_settings.sh` — + `gh api PATCH /repos/:o/:r` with the agreed JSON +- `.claude/skills/bootstrap-repo/scripts/apply_branch_protection.sh` — + `gh api PUT /repos/:o/:r/branches/main/protection`; autodiscovers status + checks from `.github/workflows/*.yml` job IDs (only workflows with + `pull_request` trigger) +- `.claude/skills/bootstrap-repo/scripts/discover_status_checks.py` — YAML + parser; outputs newline-separated job IDs +- `.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh` — reads back + settings + protection, diffs against expected, exits non-zero on drift +- `.claude/skills/bootstrap-repo/references/repo-settings.md` — exact JSON + + rationale per setting +- `.claude/skills/bootstrap-repo/references/branch-protection.md` — protection + payload + matrix-job caveat +- `.claude/skills/bootstrap-repo/references/flavors.md` — action vs library vs + service file lists, derived from new `docs/standards/` + +### Touched (this PR) + +- `AGENTS.md` — add bootstrap-repo skill to skills index (only if subagent + hasn't already) + +### Out of scope (separate PR by Haiku subagent, parallel) + +- `docs/standards/action-repo.md`, `library-repo.md`, `service-repo.md`, + `README.md` — split from `ACTION_REPO_STANDARDS.md` +- `docs/label-audit-2026-04-24.md` — diff of `labels.json` vs live + `gh api repos/:o/:r/labels` across all active jdfalk repos, with proposed + colors/descriptions +- `AGENTS.md`, `CLAUDE.md` — pointers to new standards docs + +### Out of scope (future, tracked separately) + +- Secret provisioning automation — issue + [#264](https://github.com/jdfalk/ghcommon/issues/264) +- Creating `jft-library-template` and `jft-service-template` — done after skill + stabilizes by running the skill against empty repos and flipping + `isTemplate: true` + +## Confirmed design decisions + +| Decision | Value | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Owner default | hardcoded `jdfalk`, override via `--owner` | +| Status check discovery | parse YAML `jobs.` keys from workflows triggering on `pull_request`; matrix expansions accepted as visible-but-not-required | +| `web_commit_signoff_required` | **off** | +| `required_conversation_resolution` | **on** | +| `enforce_admins` | **off** | +| `has_issues` / `has_wiki` / `has_projects` | **on / off / off** | +| Merge settings | rebase-only (`allow_merge_commit: false`, `allow_squash_merge: false`, `allow_rebase_merge: true`), `allow_auto_merge: true`, `delete_branch_on_merge: false`, `allow_update_branch: true`, `allow_deletions: false`, `allow_force_pushes: false` | +| Required approvals | 0 (with `dismiss_stale_reviews: true`) | +| Secrets | manual checklist printed; not seeded | +| Subagent dispatch | parallel, file-scope split | + +## Steps + +Each step is a single conventional commit on `feat/bootstrap-repo-skill`. + +1. **Scaffold skill via init_skill.py.** + `python ~/.claude/skills/skill-creator/scripts/init_skill.py bootstrap-repo --path .claude/skills/`. + Delete the example files we won't use. +2. **Write `references/repo-settings.md` and + `references/branch-protection.md`.** These are the source of truth — script + payloads will mirror them. Documenting first prevents drift. +3. **Write `scripts/discover_status_checks.py`.** Pure function: dir → list of + job IDs. Unit-testable in isolation. +4. **Write `scripts/apply_repo_settings.sh` and + `scripts/apply_branch_protection.sh`.** Each is a single `gh api` call with + payload from step 2's docs. Idempotent (PATCH/PUT semantics). +5. **Write `scripts/bootstrap_repo.sh`.** The orchestrator. Flavor flag, mode + flag, owner flag, ghcommon-path resolution, calls 3–4 in order, then shells + out to ghcommon's `sync-repo-setup.py` and `sync-github-labels.py`, then + flavor overlay copy, then commit/push, then `verify_bootstrap.sh`. +6. **Write `scripts/verify_bootstrap.sh`.** Reads back via `gh api GET`, diffs + against expected, prints a colored summary. Used both inline at end of + bootstrap and standalone for adoption auditing. +7. **Write `references/flavors.md`.** Enumerates per-flavor file lists; must + align with what subagent puts in `docs/standards/`. If subagent's PR isn't + merged yet, link to the docs by anticipated path with a note. +8. **Write `SKILL.md`.** Thin: trigger description, flavor decision tree, + call-out to `scripts/bootstrap_repo.sh`. Cite references for "why". +9. **Dry-run on a throwaway repo.** Create `jdfalk/bootstrap-test-DELETE-ME` + (private), run skill in adopt mode, verify settings stuck, run again to + confirm idempotency, delete repo. +10. **Open PR.** Title `feat: add bootstrap-repo skill`. Reference issue #264. + +## Parallel work (Haiku subagent, dispatched at step 1) + +Self-contained prompt; no shared writes with the skill PR. + +- **Audit labels.json** against live labels in all non-archived `jdfalk/*` repos + (use `gh api`). Output `docs/label-audit-2026-04-24.md` with: missing labels + per repo, labels in `labels.json` not used anywhere, suggested + colors/descriptions for the 242 grey entries grouped by prefix (`tech:`, + `priority:`, `size:`, `type:`). +- **Split** `ACTION_REPO_STANDARDS.md` into `docs/standards/action-repo.md`, + `library-repo.md`, `service-repo.md`, plus `docs/standards/README.md` index. + Library and service variants extrapolate from the action variant by removing + action-specific files (`action.yml`, `ruff.toml` for non-Python) and adding + service-specific ones (Dockerfile, deployment section). +- **Update** `AGENTS.md` and `CLAUDE.md` to link new docs. +- **Open separate PR** titled + `docs: split repo standards by flavor and audit labels`. +- **Hard constraint**: write only under `docs/standards/`, + `docs/label-audit-*.md`, `AGENTS.md`, `CLAUDE.md`. Do not touch `.claude/`, + `scripts/`, `labels.json` (audit only — actual changes proposed in the audit + doc for human review), `.github/`. + +## Test strategy + +**Unit-ish** (run from worktree): + +``` +# YAML parsing produces expected job IDs against ghcommon's own workflows +python .claude/skills/bootstrap-repo/scripts/discover_status_checks.py .github/workflows/ + +# Shellcheck on every shell script +shellcheck .claude/skills/bootstrap-repo/scripts/*.sh +``` + +**Integration** (against throwaway repo, step 9): + +``` +# Create private throwaway +gh repo create jdfalk/bootstrap-test-DELETE-ME --private --description "DELETE ME" --add-readme +# Run skill in adopt mode +.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh \ + --flavor library --mode adopt --name bootstrap-test-DELETE-ME + +# Verify expected state +.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh \ + --owner jdfalk --repo bootstrap-test-DELETE-ME --flavor library + +# Idempotency: run bootstrap again, expect zero diffs from verify +.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh \ + --flavor library --mode adopt --name bootstrap-test-DELETE-ME +.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh \ + --owner jdfalk --repo bootstrap-test-DELETE-ME --flavor library + +# Clean up +gh repo delete jdfalk/bootstrap-test-DELETE-ME --yes +``` + +**Success criteria:** + +- `verify_bootstrap.sh` exits 0 on first run after bootstrap. +- Second bootstrap run produces zero file diffs in the worktree (`git status` + clean) and `verify_bootstrap.sh` still exits 0. +- Branch protection on `main` is visible in GitHub UI with the agreed settings. +- All 242 labels are present on the throwaway repo. +- `shellcheck` passes on all scripts. + +## Rollback + +- Worktree: `git worktree remove ../ghcommon-bootstrap-repo-skill --force` from + main repo. +- Throwaway repo: `gh repo delete jdfalk/bootstrap-test-DELETE-ME --yes`. +- Subagent's PR: close without merge if its proposed label colors are wrong; the + audit doc itself is non-destructive. +- This PR: revert single commit on `main` if the skill ever ships and turns out + to be net-negative; the skill is additive and doesn't modify any existing + scripts. From 6479c5e455432135773e255feab60378d8319cc0 Mon Sep 17 00:00:00 2001 From: Johnathan Falk Date: Sat, 25 Apr 2026 06:51:16 -0400 Subject: [PATCH 2/2] feat(bootstrap-repo): add cli flavor and adopted-repos registry Two related additions: 1. New 'cli' flavor for distributable binaries (goreleaser/brew/winget) that don't fit 'service' (no Dockerfile) or 'library' (not imported). Seeds CHANGELOG.md only; same content as library overlay but with repository.type: cli for downstream tooling. 2. Per-bootstrap registry at ghcommon/.github/bootstrapped-repos.json. Schema {version, repos: [{owner, name, flavor, mode, first_bootstrapped, last_bootstrapped}]}. Idempotent upserts via _update_registry.py. Skill writes the file but doesn't commit it; manual commit from ghcommon enables future "bootstrap-all" sweeps and sharing across machines. Drops PLAN.md re-introduced by the cherry-picked recovery commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/bootstrap-repo/SKILL.md | 15 +- .../bootstrap-repo/references/flavors.md | 24 +++ .../scripts/_update_registry.py | 81 ++++++++ .../bootstrap-repo/scripts/bootstrap_repo.sh | 34 +++- PLAN.md | 187 ------------------ 5 files changed, 148 insertions(+), 193 deletions(-) create mode 100755 .claude/skills/bootstrap-repo/scripts/_update_registry.py delete mode 100644 PLAN.md diff --git a/.claude/skills/bootstrap-repo/SKILL.md b/.claude/skills/bootstrap-repo/SKILL.md index 3e9e7918..6d1c6052 100644 --- a/.claude/skills/bootstrap-repo/SKILL.md +++ b/.claude/skills/bootstrap-repo/SKILL.md @@ -1,6 +1,6 @@ --- name: bootstrap-repo -description: Create a new GitHub repo or apply ghcommon house standards to an existing one. Use when the user says "create a new repo", "bootstrap a repo", "set up a new repo to our standards", "apply our standards to repo X", "make this repo compliant", "new action/library/service repo", or wants merge settings, branch protection, labels, dependabot, and instruction files configured in one shot. Wraps gh repo create, applies repo-level settings (rebase-only, auto-merge, no delete) and branch protection on main, runs ghcommon's label and instruction-file sync scripts, and seeds flavor-specific overlay files. +description: Create a new GitHub repo or apply ghcommon house standards to an existing one. Use when the user says "create a new repo", "bootstrap a repo", "set up a new repo to our standards", "apply our standards to repo X", "make this repo compliant", "new action/library/service/cli repo", or wants merge settings, branch protection, labels, dependabot, and instruction files configured in one shot. Wraps gh repo create, applies repo-level settings (rebase-only, auto-merge, no delete) and branch protection on main, runs ghcommon's label and instruction-file sync scripts, seeds flavor-specific overlay files, and records the bootstrap in ghcommon/.github/bootstrapped-repos.json. --- # bootstrap-repo @@ -42,7 +42,8 @@ Steps 2 and 8 are split because branch protection requires `main` to exist. - Building a GitHub Action → `--flavor action` - Building a reusable library/SDK/package → `--flavor library` -- Building a deployable service or app → `--flavor service` +- Building a deployable service or app (containerized) → `--flavor service` +- Building a distributable binary/CLI (goreleaser, brew, etc) → `--flavor cli` ## Usage @@ -75,6 +76,16 @@ Standalone use: --repo-path ~/repos/github.com/jdfalk/my-thing ``` +## Registry + +Every successful bootstrap appends/updates `ghcommon/.github/bootstrapped-repos.json`. +Schema: `{version, repos: [{owner, name, flavor, mode, first_bootstrapped, last_bootstrapped}]}`. +Idempotent (existing entries get `last_bootstrapped` refreshed; `first_bootstrapped` +is preserved). + +The skill writes the file but does not commit it. Commit it from ghcommon to share +across machines and to enable future "bootstrap-all" sweeps. + ## What this skill does NOT do - **Secrets** — does not seed `CI_APP_ID` or any other secret. Run diff --git a/.claude/skills/bootstrap-repo/references/flavors.md b/.claude/skills/bootstrap-repo/references/flavors.md index ec97832e..1fc8cc47 100644 --- a/.claude/skills/bootstrap-repo/references/flavors.md +++ b/.claude/skills/bootstrap-repo/references/flavors.md @@ -60,6 +60,30 @@ Deployable application or service. - `.github/workflows/publish-docker.yml` (recommended) - `.github/dependabot.yml`, `.github/copilot-instructions.md`, `.github/instructions/` +## cli + +Distributable binary or CLI tool, shipped via goreleaser / brew / winget / +direct install scripts — **not** a containerized service. + +**Overlay files seeded if missing** + +- `CHANGELOG.md` — Keep-a-Changelog template (same as library) + +**Template repo**: `jdfalk/jft-cli-template` (does not yet exist; falls back to empty) + +**Notable required files** + +- `README.md` with **Installation** section (per-platform install steps), `LICENSE` +- `CHANGELOG.md` +- `.goreleaser.yml` or equivalent release config (tool-specific; not seeded by skill) +- Language-appropriate manifest (`go.mod`, `Cargo.toml`, etc) +- `.github/dependabot.yml`, `.github/copilot-instructions.md`, `.github/instructions/` + +**Differs from `service`**: no `Dockerfile`. CLIs are distributed as native binaries +to user machines, not deployed as containers. Differs from `library`: produces a +binary artifact, not an importable package; CHANGELOG drives the release tag, not +a SemVer of an exported API. + ## Common to all flavors These come from `ghcommon/scripts/sync-repo-setup.py`, regardless of flavor: diff --git a/.claude/skills/bootstrap-repo/scripts/_update_registry.py b/.claude/skills/bootstrap-repo/scripts/_update_registry.py new file mode 100755 index 00000000..72ebbcfa --- /dev/null +++ b/.claude/skills/bootstrap-repo/scripts/_update_registry.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Upsert an entry into the bootstrapped-repos registry. + +Lives at `${GHCOMMON}/.github/bootstrapped-repos.json`. Records every repo +that has been through `bootstrap_repo.sh` so we can iterate the set later +(drift sweeps, mass standards updates, etc). + +Schema: + {"version": 1, "repos": [{owner, name, flavor, mode, first_bootstrapped, last_bootstrapped}, ...]} + +Idempotent: an existing (owner, name) pair has its `flavor`, `mode`, and +`last_bootstrapped` updated; `first_bootstrapped` is preserved. + +Usage: + _update_registry.py --registry PATH --owner OWNER --name NAME \\ + --flavor FLAVOR --mode MODE +""" + +from __future__ import annotations + +import argparse +import datetime +import json +import sys +from pathlib import Path + +SCHEMA_VERSION = 1 + + +def load_registry(path: Path) -> dict: + if not path.exists(): + return {"version": SCHEMA_VERSION, "repos": []} + with path.open() as f: + data = json.load(f) + data.setdefault("version", SCHEMA_VERSION) + data.setdefault("repos", []) + return data + + +def upsert(data: dict, owner: str, name: str, flavor: str, mode: str) -> dict: + now = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + for entry in data["repos"]: + if entry.get("owner") == owner and entry.get("name") == name: + entry["flavor"] = flavor + entry["mode"] = mode + entry["last_bootstrapped"] = now + return data + data["repos"].append( + { + "owner": owner, + "name": name, + "flavor": flavor, + "mode": mode, + "first_bootstrapped": now, + "last_bootstrapped": now, + } + ) + data["repos"].sort(key=lambda e: (e.get("owner", ""), e.get("name", ""))) + return data + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--registry", type=Path, required=True) + parser.add_argument("--owner", required=True) + parser.add_argument("--name", required=True) + parser.add_argument("--flavor", required=True) + parser.add_argument("--mode", required=True) + args = parser.parse_args(argv[1:]) + + args.registry.parent.mkdir(parents=True, exist_ok=True) + data = load_registry(args.registry) + data = upsert(data, args.owner, args.name, args.flavor, args.mode) + with args.registry.open("w") as f: + json.dump(data, f, indent=2) + f.write("\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh b/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh index 8fe0be9c..fc192f47 100755 --- a/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh +++ b/.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh @@ -5,10 +5,10 @@ # create — gh repo create + apply standards from empty # adopt — apply standards to an existing repo # -# Flavors: action | library | service +# Flavors: action | library | service | cli # # Usage: -# bootstrap_repo.sh --flavor {action|library|service} \ +# bootstrap_repo.sh --flavor {action|library|service|cli} \ # --mode {create|adopt} \ # --name REPO \ # [--owner jdfalk] [--private|--public] \ @@ -101,8 +101,8 @@ done exit 2 } -case "${FLAVOR}" in action | library | service) ;; *) - echo "error: --flavor must be action, library, or service" >&2 +case "${FLAVOR}" in action | library | service | cli) ;; *) + echo "error: --flavor must be action, library, service, or cli" >&2 exit 2 ;; esac @@ -229,6 +229,22 @@ service) # TODO: choose a base image FROM scratch # TODO: COPY artifacts and set ENTRYPOINT +EOF + fi + ;; +cli) + # CLI / distributable binary. No Dockerfile (binary is shipped via goreleaser + # / brew / winget, not containers). Same CHANGELOG seed as library. + if [[ ! -f "${REPO_PATH}/CHANGELOG.md" ]]; then + cat >"${REPO_PATH}/CHANGELOG.md" <<'EOF' +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] EOF fi ;; @@ -277,6 +293,16 @@ fi "${SCRIPT_DIR}/verify_bootstrap.sh" --owner "${OWNER}" --repo "${NAME}" \ --repo-path "${REPO_PATH}" --flavor "${FLAVOR}" +# ---------- update registry ---------- + +REGISTRY="${GHCOMMON}/.github/bootstrapped-repos.json" +python3 "${SCRIPT_DIR}/_update_registry.py" \ + --registry "${REGISTRY}" \ + --owner "${OWNER}" --name "${NAME}" \ + --flavor "${FLAVOR}" --mode "${MODE}" +echo "→ Registry updated: ${REGISTRY}" +echo " (commit ghcommon/.github/bootstrapped-repos.json to record this bootstrap)" + # ---------- next steps ---------- cat <` keys from workflows triggering on `pull_request`; matrix expansions accepted as visible-but-not-required | -| `web_commit_signoff_required` | **off** | -| `required_conversation_resolution` | **on** | -| `enforce_admins` | **off** | -| `has_issues` / `has_wiki` / `has_projects` | **on / off / off** | -| Merge settings | rebase-only (`allow_merge_commit: false`, `allow_squash_merge: false`, `allow_rebase_merge: true`), `allow_auto_merge: true`, `delete_branch_on_merge: false`, `allow_update_branch: true`, `allow_deletions: false`, `allow_force_pushes: false` | -| Required approvals | 0 (with `dismiss_stale_reviews: true`) | -| Secrets | manual checklist printed; not seeded | -| Subagent dispatch | parallel, file-scope split | - -## Steps - -Each step is a single conventional commit on `feat/bootstrap-repo-skill`. - -1. **Scaffold skill via init_skill.py.** - `python ~/.claude/skills/skill-creator/scripts/init_skill.py bootstrap-repo --path .claude/skills/`. - Delete the example files we won't use. -2. **Write `references/repo-settings.md` and - `references/branch-protection.md`.** These are the source of truth — script - payloads will mirror them. Documenting first prevents drift. -3. **Write `scripts/discover_status_checks.py`.** Pure function: dir → list of - job IDs. Unit-testable in isolation. -4. **Write `scripts/apply_repo_settings.sh` and - `scripts/apply_branch_protection.sh`.** Each is a single `gh api` call with - payload from step 2's docs. Idempotent (PATCH/PUT semantics). -5. **Write `scripts/bootstrap_repo.sh`.** The orchestrator. Flavor flag, mode - flag, owner flag, ghcommon-path resolution, calls 3–4 in order, then shells - out to ghcommon's `sync-repo-setup.py` and `sync-github-labels.py`, then - flavor overlay copy, then commit/push, then `verify_bootstrap.sh`. -6. **Write `scripts/verify_bootstrap.sh`.** Reads back via `gh api GET`, diffs - against expected, prints a colored summary. Used both inline at end of - bootstrap and standalone for adoption auditing. -7. **Write `references/flavors.md`.** Enumerates per-flavor file lists; must - align with what subagent puts in `docs/standards/`. If subagent's PR isn't - merged yet, link to the docs by anticipated path with a note. -8. **Write `SKILL.md`.** Thin: trigger description, flavor decision tree, - call-out to `scripts/bootstrap_repo.sh`. Cite references for "why". -9. **Dry-run on a throwaway repo.** Create `jdfalk/bootstrap-test-DELETE-ME` - (private), run skill in adopt mode, verify settings stuck, run again to - confirm idempotency, delete repo. -10. **Open PR.** Title `feat: add bootstrap-repo skill`. Reference issue #264. - -## Parallel work (Haiku subagent, dispatched at step 1) - -Self-contained prompt; no shared writes with the skill PR. - -- **Audit labels.json** against live labels in all non-archived `jdfalk/*` repos - (use `gh api`). Output `docs/label-audit-2026-04-24.md` with: missing labels - per repo, labels in `labels.json` not used anywhere, suggested - colors/descriptions for the 242 grey entries grouped by prefix (`tech:`, - `priority:`, `size:`, `type:`). -- **Split** `ACTION_REPO_STANDARDS.md` into `docs/standards/action-repo.md`, - `library-repo.md`, `service-repo.md`, plus `docs/standards/README.md` index. - Library and service variants extrapolate from the action variant by removing - action-specific files (`action.yml`, `ruff.toml` for non-Python) and adding - service-specific ones (Dockerfile, deployment section). -- **Update** `AGENTS.md` and `CLAUDE.md` to link new docs. -- **Open separate PR** titled - `docs: split repo standards by flavor and audit labels`. -- **Hard constraint**: write only under `docs/standards/`, - `docs/label-audit-*.md`, `AGENTS.md`, `CLAUDE.md`. Do not touch `.claude/`, - `scripts/`, `labels.json` (audit only — actual changes proposed in the audit - doc for human review), `.github/`. - -## Test strategy - -**Unit-ish** (run from worktree): - -``` -# YAML parsing produces expected job IDs against ghcommon's own workflows -python .claude/skills/bootstrap-repo/scripts/discover_status_checks.py .github/workflows/ - -# Shellcheck on every shell script -shellcheck .claude/skills/bootstrap-repo/scripts/*.sh -``` - -**Integration** (against throwaway repo, step 9): - -``` -# Create private throwaway -gh repo create jdfalk/bootstrap-test-DELETE-ME --private --description "DELETE ME" --add-readme -# Run skill in adopt mode -.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh \ - --flavor library --mode adopt --name bootstrap-test-DELETE-ME - -# Verify expected state -.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh \ - --owner jdfalk --repo bootstrap-test-DELETE-ME --flavor library - -# Idempotency: run bootstrap again, expect zero diffs from verify -.claude/skills/bootstrap-repo/scripts/bootstrap_repo.sh \ - --flavor library --mode adopt --name bootstrap-test-DELETE-ME -.claude/skills/bootstrap-repo/scripts/verify_bootstrap.sh \ - --owner jdfalk --repo bootstrap-test-DELETE-ME --flavor library - -# Clean up -gh repo delete jdfalk/bootstrap-test-DELETE-ME --yes -``` - -**Success criteria:** - -- `verify_bootstrap.sh` exits 0 on first run after bootstrap. -- Second bootstrap run produces zero file diffs in the worktree (`git status` - clean) and `verify_bootstrap.sh` still exits 0. -- Branch protection on `main` is visible in GitHub UI with the agreed settings. -- All 242 labels are present on the throwaway repo. -- `shellcheck` passes on all scripts. - -## Rollback - -- Worktree: `git worktree remove ../ghcommon-bootstrap-repo-skill --force` from - main repo. -- Throwaway repo: `gh repo delete jdfalk/bootstrap-test-DELETE-ME --yes`. -- Subagent's PR: close without merge if its proposed label colors are wrong; the - audit doc itself is non-destructive. -- This PR: revert single commit on `main` if the skill ever ships and turns out - to be net-negative; the skill is additive and doesn't modify any existing - scripts.