A command-line tool for migrating repositories, teams, and members from one GitHub organisation to another. The tool first produces a human-readable plan you can review before anything is changed, then applies the migration with a single confirmation prompt.
- Transfers all repositories from a source organisation to a destination organisation
- Invites organisation members (skips members who already belong to the destination)
- Recreates teams, including their members, repository permissions, and optional parent-team nesting
- Preserves direct collaborator permissions on each repository
- Two conflict-resolution strategies for resources that already exist in the destination
- Dry-run
plancommand — review exactly what will happen before committing - Optional
--outputflag to save the plan to a file - Repository blacklist to exclude specific repos from migration
- Colourised terminal output via Mordant
| Dependency | Version |
|---|---|
| JDK | 21 |
| Kotlin | 2.2.0 |
| Gradle | 8.7 |
A GitHub Personal Access Token (classic) with the following scopes is required:
repo— read/transfer repositoriesadmin:org— read/write organization members and teams
When migrating to an organisation that uses Enterprise Managed Users (EMU) or other provisioned accounts, a single token may not have access to both organisations. In that case provide separate tokens with --source-token and --target-token instead of --token.
Download the latest release archive for your platform from the Releases page, unzip it, and place the github-migration binary on your PATH.
./gradlew nativeCompile
# Output: build/native/nativeCompile/github-migrationThe package task wraps the binary in a versioned zip:
./gradlew package
# Output: build/distributions/github-migration-<version>-<os>.zipNote: GraalVM Native Image must be installed for
nativeCompile. The standard JVM distribution is built with./gradlew installDist.
The tool authenticates against the GitHub API using a token. You can provide it via the --token flag or store it in ~/.gradle/gradle.properties for local development:
gpr.user=your-github-username
gpr.key=your-github-tokenThe library dependency is published to GitHub Packages and resolved at build time using the same credentials:
# ~/.gradle/gradle.properties
gpr.user=<github-username>
gpr.key=<github-token>All commands share a common set of options:
| Option | Short | Required | Description |
|---|---|---|---|
--token |
-t |
✅ * | GitHub Personal Access Token for both organisations |
--source-token |
✅ * | Token for the source organisation (use instead of --token when tokens differ) |
|
--target-token |
✅ * | Token for the target organisation (use instead of --token when tokens differ) |
|
--source |
-s |
✅ | Source organisation login |
--destination |
-d |
✅ | Destination organisation login |
--strategy |
Conflict strategy: Merge (default) or Prefix |
||
--blacklist |
Repository name to exclude (repeatable) | ||
--parent-team |
Slug of an existing team in the destination to nest all migrated teams under |
* Provide either --token or both --source-token and --target-token.
Generates and prints the migration plan without making any changes.
github-migration plan \
--token ghp_... \
--source my-source-org \
--destination my-destination-orgSave the plan to a file for review or archiving:
github-migration plan \
--token ghp_... \
--source my-source-org \
--destination my-destination-org \
--output migration-plan.txtGenerates the plan, displays it, and prompts for confirmation before proceeding.
github-migration apply \
--token ghp_... \
--source my-source-org \
--destination my-destination-orgYou will be asked:
Do you wish to apply these changes (yes/no):
Type yes to proceed; anything else cancels the migration safely.
When a resource (team or repository) with the same name already exists in the destination organisation, the --strategy option controls what happens:
| Strategy | Behaviour |
|---|---|
Merge (default) |
Reuses the existing resource — new members/repos are added to the existing team; a repository keeps its original name |
Prefix |
Creates a new resource prefixed with the source org login, e.g. source-org-my-repo |
Migrate from acme-corp to acme-global, excluding legacy-monolith, and nest all teams under an existing migrated parent team:
github-migration apply \
--token ghp_... \
--source acme-corp \
--destination acme-global \
--blacklist legacy-monolith \
--parent-team migrated \
--strategy PrefixThis project follows Conventional Commits. JReleaser uses the commit history to auto-generate a grouped changelog on each release.
| Prefix | When to use |
|---|---|
feat: |
New user-facing feature (triggers a minor version bump) |
fix: |
Bug fix (triggers a patch version bump) |
chore: |
Build, tooling, or dependency changes |
ci: |
CI/CD pipeline changes |
test: |
Adding or updating tests |
docs: |
Documentation only |
feat!: / fix!: |
Breaking change (triggers a major version bump) |
./gradlew testTests are written with Kotest and MockK.
./gradlew ktlintCheck # check style
./gradlew ktlintFormat # auto-fix
./gradlew detekt # static analysiscommand/
MainCommand # Root Clikt command (no-op, owns subcommands)
PlanCommand # plan subcommand
ApplyCommand # apply subcommand
MigrationOptions # Shared Clikt OptionGroup
domain/
Plan # Pure data class — the migration plan
PlanFactory # createPlan() — builds Plan from raw GitHub data
PlanRenderer # Formats a Plan for terminal output
Repository # Domain repo with originalName + planned name
Team # Domain team with members and repo memberships
Member # Domain member
MigrationError # Sealed error hierarchy (Arrow Either)
service/
PlanService # Fetches GitHub state, returns Either<MigrationError, Plan>
ApplyService # Executes the plan, collects and reports partial failures
Error handling uses Arrow Either throughout — PlanService short-circuits on the first fatal error (missing organisation), while ApplyService collects all partial failures so as much of the migration completes as possible.
GNU General Public License v3.0 — see LICENSE.