feat(org-migration): snapshot/transfer/verify tooling, runbook, and token-rotation doc - #81
Merged
Conversation
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
|
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
deleted the
claude/feat-org-migration-tooling-01RUgidK
branch
September 3, 2026 21:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tooling for the org migration (Tasks 3–5 of the plan in #80): moving 31 active repos from user
smartwatermelon(to be renamedtwistedmelonman) into a new Free org namedsmartwatermelon, withcleanroomgoing tonightowlstudiollc. No URL,uses:, tap, or remote changes anywhere — the org takes the old name.scripts/org-migration/move-list.txt— the 31repo targetpairs (alphabetical;transfer.sh --onlyselects 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 untilowner.type == Organization.scripts/org-migration/verify.sh— base/after snapshot diff, owner assertions,--clonesscan (depth 1 and 2) withgit ls-remote; refuses a non-empty after-dir; aborts before comparing if the snapshot fails; a jq failure is a failure, not "ok".ghtests (34 assertions) underscripts/org-migration/tests/,shellcheck -S infoclean.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.shhas 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 FAILshellcheck -S infoon all eight scripts, no disable directiveshttps://claude.ai/code/session_01RUgidKkV54aNnH1rRNfUq6