Skip to content

feat(org-migration): snapshot/transfer/verify tooling, runbook, and token-rotation doc - #81

Merged
twistedmelonman merged 8 commits into
mainfrom
claude/feat-org-migration-tooling-01RUgidK
Sep 3, 2026
Merged

feat(org-migration): snapshot/transfer/verify tooling, runbook, and token-rotation doc#81
twistedmelonman merged 8 commits into
mainfrom
claude/feat-org-migration-tooling-01RUgidK

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

Summary

Tooling for the org migration (Tasks 3–5 of the plan in #80): moving 31 active repos from user smartwatermelon (to be renamed twistedmelonman) into a new Free org named smartwatermelon, with cleanroom going to nightowlstudiollc. No URL, uses:, tap, or remote changes anywhere — the org takes the old name.

  • scripts/org-migration/move-list.txt — the 31 repo target pairs (alphabetical; transfer.sh --only selects by name).
  • scripts/org-migration/lib.sh — one lookup path for every GitHub state (repos/smartwatermelon/<repo>, following redirects), move-list reader.
  • scripts/org-migration/snapshot.sh — per-repo JSON of settings (visibility, default branch, protection, rulesets, pages, topics, secret names). Only HTTP 404 maps to null; any other gh failure fails that repo. The assembly step is guarded so a failure never leaves a partial file behind a success line.
  • scripts/org-migration/transfer.sh — idempotent, --only, --dry-run; pre-flights that every target resolves as an Organization before any POST; polls the new path until owner.type == Organization.
  • scripts/org-migration/verify.sh — base/after snapshot diff, owner assertions, --clones scan (depth 1 and 2) with git ls-remote; refuses a non-empty after-dir; aborts before comparing if the snapshot fails; a jq failure is a failure, not "ok".
  • Hermetic stub-gh tests (34 assertions) under scripts/org-migration/tests/, shellcheck -S info clean.
  • docs/runbooks/org-migration-rename.md (human-executed), docs/token-rotation.md (no token material), CLAUDE.md structure update.

Independent of #80; both merge to main. transfer.sh has not been run against real GitHub — first live use is --dry-run, then --only github-workflows (the runbook already orders it that way).

Advances #54.

Test plan

  • bash scripts/org-migration/tests/run-tests.sh → 34 PASS, 0 FAIL
  • shellcheck -S info on all eight scripts, no disable directives
  • Every false-OK fix has a test that was observed failing on the pre-fix code
  • Codebase-review dry-run: PASS

https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6

Claude Code Bot added 8 commits September 3, 2026 13:18
move-list.txt is the checked-in source of truth (31 repos; cleanroom
targets nightowlstudiollc). snapshot.sh records owner, default branch,
visibility, archived, topics, pages, secret names, branch protection,
and rulesets per repo, looking every repo up through
repos/smartwatermelon/<repo> so the same path works before the rename,
after it (redirect), and after the transfer.

The test stubs gh on PATH and unsets BASH_ENV: this machine's profile
defines a gh shell function there, and a function beats PATH, so
without that the stub is bypassed and the suite hits the network.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
The header claimed "github-workflows goes first, alone", but the rows are
alphabetized and github-workflows sits mid-file. A checked-in source of
truth must not contradict itself.

Rows stay alphabetical and order still carries no meaning; transfer.sh
selects by name. The comment now says so and points at the actual
mechanism, `transfer.sh --only github-workflows`, rather than implying a
magic first-row dependency.

Comments only: the 31 data rows are unchanged.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
_optional treated any gh failure as "not configured", so a transient
error or an expired token recorded protection: null. verify.sh compares
the after-snapshot against the baseline, so that null would read as
expected state and a real loss of branch protection could pass
verification silently.

Now a 404 still means absent; every other failure marks the repo's
snapshot as failed through the path snapshot.sh already uses, with gh's
message on stderr.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
transfer.sh POSTs /repos/{owner}/{repo}/transfer for each move-list repo
not already under its target org, polls until it resolves as an
Organization, reports and skips failures, and supports --only and
--dry-run. Re-running is safe: a repo already under its target is
skipped, so a partial run is resumed by running it again.

verify.sh re-snapshots into a second directory, allows owner as the only
diff against the baseline, checks each owner is the target org, and
ls-remotes every local clone whose origin still points at
smartwatermelon, so a broken redirect is caught before it bites.

Both tests are hermetic: they stub gh on PATH, unset BASH_ENV (this
machine's profile defines a gh shell function that would otherwise beat
the stub and hit the network), and sandbox HOME.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
Three defects found in review.

verify.sh compared whatever the after-dir held. If it already held JSON
from an earlier run and this run's snapshot failed for some repos, the
loop compared stale files and printed "verify: <repo>: ok" beside a
snapshot error. It now refuses a non-empty after-dir, and exits on a
failed snapshot before the comparison loop rather than after it.

transfer.sh split the owner lookup with ${current% *} / ${current#* },
which silently yields an empty login on unexpected gh output -- the test
now shows the old code POSTing repos//malformed/transfer. It parses with
read and fails the repo unless the result is exactly two space-free
fields.

~/Developer is not flat: clients/<repo> and netlify/crazy-larry sit one
level deeper, so the clone scan missed them entirely. It now scans both
depths and names each clone relative to the clones root so a nested one
is unambiguous.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
The runbook is the exact click path for renaming the user and creating
the org in one sitting, plus the keyring re-login and shell checks. The
rotation doc holds dates only, never token material.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
…ransfer targets

Four defects of the same class: the tooling reported success while doing
nothing, or while recording a failure as expected state.

- verify.sh swallowed a jq failure with `|| true`. An unparseable baseline
  left ${changed} empty, which the loop read as "no fields changed besides
  owner" and printed `verify: <repo>: ok`. Check jq's status on the diff and
  on both owner reads; print `verify: <repo>: cannot diff snapshots` and fail
  the repo instead.

- snapshot.sh left the final `jq -n` assembly unchecked. When it failed the
  script wrote an empty <repo>.json and printed the success line anyway.
  Remove the partial file, print `snapshot: FAILED to assemble <repo>`, count
  the failure, continue.

- snapshot.sh read topics and secrets with `2>/dev/null || echo '[]'`, mapping
  every failure to an empty list. A 403 on the secrets endpoint is a
  permission problem, not a repo without secrets, and verify.sh would read the
  recorded [] as the expected state. Both now go through _optional (404 ->
  null only; any other failure fails the repo) with the projection applied in
  the assembly step.

- transfer.sh POSTed without checking the target existed. During the rename
  window the name `smartwatermelon` is claimable by a stranger, and a
  transfer to a *user* is an invitation we cannot take back. Add a pre-flight
  that requires `gh api orgs/<target> --jq .type` to print `Organization` for
  every distinct target before any POST. It is read-only, so --dry-run runs
  it too.

Tests: 23 -> 34 cases, each written to fail against the old code first. The
transfer stub now serves orgs/<name> from a state file, and --only with a repo
that is not on the move list is covered.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
…p caveats

CLAUDE.md claimed there are no build, test, or lint commands. There is no
build, but the org-migration tooling has a hermetic test suite and shellcheck
applies to every shell script here. Name both.

The rename runbook had no step for running verify.sh. Add one, with the two
things that otherwise read as failures:

- verify.sh must always get a fresh, empty after-dir. It refuses a non-empty
  one, because a leftover JSON would be compared as though this run wrote it.
- cleanroom is the one repo moving to a different owner name, so its
  URL-bearing fields (protection.url, ruleset source/_links, pages.html_url)
  legitimately differ. Inspect such a diff with jq -S rather than "restoring"
  it; the same report for any other repo is real drift.

Claude-Session: https://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review of PR 81: Org migration tooling adds snapshot/transfer/verify scripts with comprehensive hermetic tests. Shell scripts use set -uo pipefail, validate inputs properly, distinguish 404s from transient errors. transfer.sh has preflight org validation, snapshot.sh has proper cleanup on jq failures, verify.sh refuses stale after-dirs. 587 lines of test coverage. No hardcoded credentials. No existing code modified. No reliability regression possible.

VERDICT: PASS

@twistedmelonman
twistedmelonman merged commit 7a984d8 into main Sep 3, 2026
2 checks passed
@twistedmelonman
twistedmelonman deleted the claude/feat-org-migration-tooling-01RUgidK branch September 3, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant