Enterprise‑grade migration toolkit for GitHub → GitHub organization transfers
This suite provides a complete, scriptable, menu‑driven, and automated solution for migrating repositories and all associated work items between GitHub organizations.
It supports:
- Repository mirroring (all branches, tags, refs)
- Issues, labels, milestones, PR metadata (via
gh-migrate) - Classic Projects (Projects v1) — full backlog:
- Projects
- Columns
- Cards (issue cards + note cards)
- Ordering
- Projects v2 (GraphQL-based)
- Projects v1 → v2 cross-migration (Classic → Projects v2)
- Discussions (export + import)
- Secure credential storage using Windows DPAPI
- Python and PowerShell orchestration
- GitHub Actions workflows for CI/CD migration
- Modular architecture (PSM1 modules + Python modules)
- Auto-detection of source / target project versions
- Interactive project selection and confirmation prompts
- Non-interactive (auto-yes) mode for CI/automation
github-migration-suite/
├── python/
│ ├── migrate_python.py
│ ├── config.json
│ ├── requirements.txt
│ ├── modules/
│ │ ├── repo.py
│ │ ├── issues.py
│ │ ├── projects.py
│ │ ├── projects_v1.py
│ │ └── discussions.py
│ └── utils/
│ ├── logger.py
│ └── shell.py
├── powershell/
│ ├── menu.ps1
│ ├── migrate-github.ps1
│ ├── config.json
│ ├── readme.md
│ └── modules/
│ ├── repo.psm1
│ ├── issues.psm1
│ ├── projects.psm1
│ ├── projectsV1.psm1
│ └── discussions.psm1
├── templates/
│ ├── project-migrator.json
│ ├── discussions-export.json
│ └── discussions-import.json
├── examples/
│ ├── sample-log.txt
│ ├── sample-export.json
│ └── sample-import.json
└── .github/
└── workflows/
├── python-migration.yml
└── powershell-migration.yml
- Full mirror (
--mirror) - All branches, tags, refs
- Recreates repo in target org automatically
Migrated via:
gh migrate repoSupports:
- Issue bodies
- Comments
- Labels
- Milestones
- PR metadata
- Releases
Full backlog migration:
- Project metadata
- Columns
- Cards (issue cards + note cards)
- Ordering preserved
- Backlog views reconstructed
Migrated via project-migrator:
- Fields
- Iterations
- Statuses
- Items
- Views (implicitly reconstructed)
When your source org uses Classic Projects (v1) and the target uses Projects v2:
- Reads v1 columns, cards, and issue/PR links via REST API
- Creates equivalent V2 projects in the target org via GraphQL API
- Maps v1 column names to V2
Statusfield options - Links issues/PRs as V2 items
- Converts note cards to V2 draft issues
When projects_auto_detect is enabled (default), the suite will:
- Query the source org to see if it uses v1 (Classic), v2, or both projects
- Query the target org the same way
- Decide the appropriate migration route:
| Source | Target | Action |
|---|---|---|
| v1 only | v1 only | v1 → v1 migration |
| v2 only | v2 only | v2 → v2 migration |
| v1 | v2 | Cross-migration v1 → v2 (with prompts) |
| v2 | v1 | Skipped (downgrade not supported) |
The migration script will:
- Show the user a numbered list of all detected projects
- Let the user pick: a single number, comma-separated numbers, or
Afor all - Ask for a final
y/nconfirmation before any mutation occurs
To skip the prompts in CI/automation contexts, set
projects_v1_to_v2.auto_yes / ProjectsV1ToV2.AutoYes to true in the
config file.
Migrated via discussions-migration:
- Categories
- Threads
- Comments
- Authors (mapped)
Tokens stored at:
%LOCALAPPDATA%\GitHubMigrationSuite\creds.dat
Encrypted with:
ProtectedData.Protect(..., CurrentUser)Interactive menu:
1. Mirror Repository
2. Migrate Issues / PRs / Labels / Milestones / Releases
3. Migrate Projects v2 (source v2 -> target v2)
4. Migrate Discussions
5. Run FULL Migration (all enabled)
6. Update Stored Credentials
7. Migrate Projects v1 -> v1 (Classic, full backlog)
8. Migrate Projects v1 -> v2 (Classic -> Projects v2)
9. Detect and migrate Projects (auto v1/v2, with prompts)
0. Exit
Two workflows included:
- Python migration
- PowerShell migration
gh auth login
gh extension install github/gh-migratenpm install -g @github/migration-toolkit-projectsnpm install -g @github/discussions-migrationpip install -r python/requirements.txt- PowerShell 7+
- Windows DPAPI (Windows only)
A successful migration requires the following credentials to be configured. Read this section carefully — the wrong setup is the most common cause of silent or failed migrations.
The source token is used to:
- Mirror the source repository (
git clone --mirrorover HTTPS with token authentication) - Read Classic Projects (v1) and their columns / cards (REST +
inertia-previewheader) - Read Projects v2 (GraphQL)
- Read issues, PRs, milestones, labels, releases (via
gh-migrate)
Required scopes (fine-grained PAT recommended):
| Resource | Access |
|---|---|
| Source org → repositories | Read |
| Source org → projects (v1 & v2) | Read |
The target token is used to:
- Create the target repository (
gh repo create) - Push the mirror to the target (
git push --mirror) - Create V2 projects and items (GraphQL mutations)
- Create issues / PRs / labels / milestones / releases (via
gh-migrate)
Required scopes (fine-grained PAT recommended):
| Resource | Access |
|---|---|
| Target org → repositories | Read and Write |
| Target org → projects (v1 & v2) | Read and Write |
| Target org → members / teams (optional) | Read |
- PowerShell menu: tokens are encrypted with DPAPI (Windows-only) and
stored at
%LOCALAPPDATA%\GitHubMigrationSuite\creds.dat. The CurrentUser scope means the file is only readable by your Windows account on that machine. - Python pipeline: tokens are read from environment variables. The pipeline does not persist them to disk.
- Tokens in config: the config file contains only the names of the
environment variables (e.g.
GH_SOURCE_TOKEN), never the tokens themselves.
When you run the suite from a GitHub Actions workflow, the tokens must be stored as encrypted repository or organisation secrets:
-
Open the repository (or org) on GitHub
-
Settings→Secrets and variables→Actions -
Click
New repository secret(orNew organization secret) -
Create two secrets:
Secret name Value GH_SOURCE_TOKENThe PAT (or installation token) for the source org GH_TARGET_TOKENThe PAT (or installation token) for the target org -
In your workflow file (
.github/workflows/python-migration.ymlor.github/workflows/powershell-migration.yml), reference the secrets asenv:values:jobs: migrate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies working-directory: ./python run: pip install -r requirements.txt - name: Run migration working-directory: ./python env: GH_SOURCE_TOKEN: ${{ secrets.GH_SOURCE_TOKEN }} GH_TARGET_TOKEN: ${{ secrets.GH_TARGET_TOKEN }} run: python migrate_python.py
-
The workflow input
repo_namelets you choose which repo to migrate at dispatch time; the runner substitutes it intoconfig.jsonbefore executing the script.
Security notes for GitHub Actions:
- Never echo the secrets in
run:blocks - Mask the tokens in any debug output (the workflows above do not log them)
- Prefer fine-grained PATs over classic tokens so that each migration only has the minimum required scopes
- Rotate the tokens after the migration is complete
Before running a migration, verify:
- The source token can read the source org's projects
- The target token can create repositories, projects, and issues in the target org
- For v1 → v2 cross-migration, both Classic Projects and Projects v2 are enabled in the target org's settings
- The source repo's default branch name matches the local default
- The work directory (default
../work) has enough disk space for the mirror
Edit:
python/config.jsonpowershell/config.json
Example:
{
"Source": {
"TokenEnv": "GH_SOURCE_TOKEN",
"Org": "old-org"
},
"Target": {
"TokenEnv": "GH_TARGET_TOKEN",
"Org": "new-org"
},
"Repository": {
"Name": "example-repo",
"Mirror": true
},
"Projects": {
"Enabled": true,
"ProjectNumbers": [1, 2]
},
"ProjectsV1": {
"Enabled": false
},
"ProjectsV1ToV2": {
"Enabled": true,
"AutoYes": false
},
"ProjectsAutoDetect": true,
"Discussions": {
"Enabled": true,
"Repo": "example-repo"
}
}| Key | Purpose |
|---|---|
Source.TokenEnv |
Name of the env var that holds the source token |
Source.Org |
Source organisation / user name |
Target.TokenEnv |
Name of the env var that holds the target token |
Target.Org |
Target organisation / user name |
Repository.Name |
Repository to migrate |
Repository.Mirror |
If true, push a full git mirror first |
Projects.Enabled |
Enable Projects v2 migration (when applicable) |
ProjectsV1.Enabled |
Enable Classic Projects v1 → v1 migration |
ProjectsV1ToV2.Enabled |
Enable Classic Projects v1 → v2 cross-migration |
ProjectsV1ToV2.AutoYes |
Skip interactive prompts (use in CI) |
ProjectsAutoDetect |
Auto-detect v1/v2 and pick the right route |
Discussions.Enabled |
Enable discussions migration |
GhMigrate.* |
gh-migrate flags to pass through |
cd powershell
pwsh ./menu.ps1The menu has 9 options. Option 9 is the smart entry point — it auto-detects the project versions and routes you to the right migration.
cd python
python migrate_python.pyThe included workflows (.github/workflows/python-migration.yml and
.github/workflows/powershell-migration.yml) can be triggered manually
via workflow_dispatch. Provide the repository name when prompted.
The workflow:
- Checks out the migration suite
- Sets up Python / PowerShell
- Installs dependencies
- Reads the two secrets (
GH_SOURCE_TOKEN,GH_TARGET_TOKEN) - Overrides the
repository.nameinconfig.jsonwith the supplied input - Runs the migration
- Tokens never stored in plaintext (DPAPI encryption on Windows; env vars only elsewhere)
- No external transmission of credentials
- Logs do not contain tokens
- Secrets are only ever read at the moment of an API call and never echoed back
Use the examples/ folder for:
- Sample logs
- Sample exports
- Sample imports
- GitHub Projects v1 API is in "inertia-preview" mode; headers are included automatically.
- Views in Projects v1 are reconstructed implicitly by preserving column/card order.
- Cross‑repo project cards are supported if issues exist in target org.
- Auto-detection is non-destructive: it only queries the source and target
orgs to learn which project versions they use, then chooses the right
migration path. The actual migration still asks for confirmation (unless
auto_yesis enabled).