A GitHub CLI extension to migrate GitHub repository secrets from a source repository to a target repository using GitHub Actions workflows. Written in Python and compiled to native binaries using PyInstaller.
- ✨ Migrates secrets from one GitHub repository to another
- 🌍 Recreates repository environments in target repository
- 🔐 Automatically encrypts secrets using GitHub's public key
- 🤖 Uses GitHub Actions workflow for automated migration
- 🔄 Supports both explicit PATs or GITHUB_TOKEN environment variable
- 🌐 Supports custom endpoints for GHEC Data Residency, GHES, and EMU
- 📝 Comprehensive logging with verbose mode
- ✅ Validates PAT permissions before starting migration
- 🧹 Automatic cleanup of temporary secrets
- 🚀 Available as a GitHub CLI extension with precompiled binaries
Install as a GitHub CLI extension for the easiest setup:
# Install from GitHub
gh extension install renan-alm/gh-secrets-migrator
# Use the extension
gh secrets-migrator --source-org myorg --source-repo myrepo --target-org targetorg --target-repo targetrepoThe extension comes with precompiled binaries for Linux, macOS, and Windows, so no Python installation is required.
Download the latest precompiled binary for your platform from the Releases page:
# Linux AMD64
curl -L https://github.com/renan-alm/gh-secrets-migrator/releases/latest/download/gh-secrets-migrator_v<version>_linux-amd64 -o gh-secrets-migrator
chmod +x gh-secrets-migrator
./gh-secrets-migrator --help
# macOS AMD64 (Intel)
curl -L https://github.com/renan-alm/gh-secrets-migrator/releases/latest/download/gh-secrets-migrator_v<version>_darwin-amd64 -o gh-secrets-migrator
chmod +x gh-secrets-migrator
./gh-secrets-migrator --help
# macOS ARM64 (Apple Silicon)
curl -L https://github.com/renan-alm/gh-secrets-migrator/releases/latest/download/gh-secrets-migrator_v<version>_darwin-arm64 -o gh-secrets-migrator
chmod +x gh-secrets-migrator
./gh-secrets-migrator --help
# Windows (PowerShell)
Invoke-WebRequest -Uri "https://github.com/renan-alm/gh-secrets-migrator/releases/latest/download/gh-secrets-migrator_v<version>_windows-amd64.exe" -OutFile "gh-secrets-migrator.exe"
.\gh-secrets-migrator.exe --helpNote: Replace <version> with the actual version number (e.g., 1.0.0). The filenames include the v prefix.
If you prefer to run from source or need to make modifications:
- Python 3.10+
- GitHub Personal Access Tokens (PAT) with appropriate scopes (see Permissions section)
# Clone the repository
git clone https://github.com/renan-alm/gh-secrets-migrator.git
cd gh-secrets-migrator
# Install dependencies
pip install -r requirements.txt
# Run from source
python main.py --help
# Or build a local binary
make build
./bin/gh-secrets-migrator --helpRun the application in a Docker container without installing dependencies locally:
Build the image:
docker build -t gh-secrets-migrator .Run with Docker:
docker run --rm \
-e GITHUB_TOKEN=<your-token> \
gh-secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--org-to-org \
--verboseOr with explicit PATs:
docker run --rm \
gh-secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--source-pat <source-pat> \
--target-pat <target-pat> \
--org-to-orgUsing Docker Compose:
# Set your token in environment
export GITHUB_TOKEN=<your-token>
# Run the migration
docker-compose run --rm secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--org-to-orgImage Size: ~200MB (lightweight Python 3.11 slim base)
Authenticate with GHCR:
echo $GITHUB_TOKEN | docker login ghcr.io -u <username> --password-stdinTag and push the image:
# Build with GHCR tag
docker build -t ghcr.io/renan-alm/gh-secrets-migrator:latest .
# Push to GHCR
docker push ghcr.io/renan-alm/gh-secrets-migrator:latestRun from GHCR:
docker run --rm \
-e GITHUB_TOKEN=<your-token> \
ghcr.io/renan-alm/gh-secrets-migrator:latest \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--org-to-orgUpdate docker-compose.yml to use GHCR:
services:
secrets-migrator:
image: ghcr.io/renan-alm/gh-secrets-migrator:latest
# ... rest of configThe repository includes GitHub Actions workflows that automatically publish Docker images to GHCR:
- On successful release: When the Release workflow completes successfully (triggered by pushing a
v*tag), the Docker image is automatically built and pushed - Pull requests to master: Docker images are built (but not pushed) to validate the Dockerfile
Release workflow:
- Push a version tag:
git tag v1.2.3 && git push origin v1.2.3 - Release workflow validates changelog entry exists and tests passed
- Builds binaries for Windows, macOS, and Linux
- Creates GitHub Release with artifacts and SHA256 checksums
- On success, triggers Docker publish to
ghcr.io/renan-alm/gh-secrets-migrator:v1.2.3
No manual steps needed—just push a tag and the release + Docker image are published!
Both source and target PATs must have the following scopes:
For reading source repo and managing temporary secrets:
repo- Full control of private repositoriesworkflow- Update GitHub Action workflows (for branch/workflow management)
For creating secrets in target repository:
repo- Full control of private repositories
Source PAT:
- ✅ Read repository secrets
- ✅ Create/update repository secrets (temporary PAT storage)
- ✅ Delete repository secrets (cleanup)
- ✅ Create/delete branches
- ✅ Push to repository
Target PAT:
- ✅ Create/update repository secrets
- Go to GitHub Settings → Developer settings → Personal access tokens (classic)
- Click "Generate new token (classic)"
- Give it a descriptive name (e.g., "Secrets Migrator Source")
- Select the required scopes (see above)
- Click "Generate token"
- Copy the token immediately (you won't see it again)
You can use this tool in three ways:
- As a GitHub CLI extension (recommended):
gh secrets-migrator [OPTIONS] - As a standalone binary:
./gh-secrets-migrator [OPTIONS] - From Python source:
python main.py [OPTIONS]
All examples below work with any of these methods - just replace the command accordingly.
# As GitHub CLI extension
gh secrets-migrator \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--target-repo <target-repo> \
--source-pat <source-pat> \
--target-pat <target-pat>
# As standalone binary
./gh-secrets-migrator \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--target-repo <target-repo> \
--source-pat <source-pat> \
--target-pat <target-pat>
# From source
python main.py \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--target-repo <target-repo> \
--source-pat <source-pat> \
--target-pat <target-pat>If you have a single token with permissions for both source and target:
export GITHUB_TOKEN=<your-token>
# Any of these will work
gh secrets-migrator \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--target-repo <target-repo>To migrate only organization-level secrets (ignoring repository and environment secrets):
gh secrets-migrator \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--source-pat <source-pat> \
--target-pat <target-pat> \
--org-to-orgNote:
- Source repository is required to host the migration workflow
- Target repository is optional; if not provided, defaults to the same name as source repo
- Only organization-level secrets are migrated; repository and environment secrets are ignored
Example:
gh secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--org-to-org \
--verboseWith explicit target repository:
gh secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--target-repo .github \
--org-to-org \
--verbosegh secrets-migrator \
--source-org myorg \
--source-repo source-repo \
--target-org targetorg \
--target-repo target-repo \
--source-pat <source-pat> \
--target-pat <target-pat> \
--verboseBy default, environments from the source repository are recreated in the target repository. To skip this:
gh secrets-migrator \
--source-org <source-org> \
--source-repo <source-repo> \
--target-org <target-org> \
--target-repo <target-repo> \
--source-pat <source-pat> \
--target-pat <target-pat> \
--skip-envsgh secrets-migrator \
--source-org renan-org \
--source-repo .github \
--target-org demo-org-renan \
--target-repo migration-sample \
--verbose- Validates PAT permissions - Checks both PATs have necessary scopes before proceeding
- Recreates environments (unless
--skip-envsis set) - Creates environments from source repo in target repo:- Lists all environments from source repository
- Creates each environment in target repository
- Gracefully skips if environment already exists (idempotent)
- Lists secrets - Gets all secrets from source repo (for logging)
- Creates temporary secrets - Stores both PATs in source repo:
SECRETS_MIGRATOR_TARGET_PAT(encrypted) - Used by workflow to access target repoSECRETS_MIGRATOR_SOURCE_PAT(encrypted) - Used by workflow cleanup to delete temporary secrets
- Creates migration branch - Creates a new branch called
migrate-secrets - Pushes workflow - Commits GitHub Actions workflow to migration branch
- Workflow runs - Triggered by push to
migrate-secretsbranch:- Reads all secrets from source repo
- Filters out system secrets (
SECRETS_MIGRATOR_*,github_token) - For each remaining secret: creates it in target repo using target PAT
- Cleanup (always runs):
- Deletes
SECRETS_MIGRATOR_TARGET_PATfrom source repo - Deletes
SECRETS_MIGRATOR_SOURCE_PATfrom source repo - Deletes the migration branch
- Deletes
make install # Install dependencies
make dev # Install with dev dependencies (includes linters/testing)
make lint # Run linting checks (flake8 + pylint)
make format # Format code with black
make test # Run tests with pytest
make clean # Clean build artifacts, cache, .pyc files
make help # Show all available commands--source-org: Source organization name--source-repo: Source repository name (always required - migration workflow runs in this repository)--target-org: Target organization name
--target-repo: Target repository name (required for repo-to-repo migration; optional for org-to-org, defaults to source-repo name if not provided)
--source-pat: Source PAT (required if GITHUB_TOKEN not set)--target-pat: Target PAT (required if GITHUB_TOKEN not set)--verbose: Enable verbose logging (shows debug messages)--skip-envs: Skip environment recreation (by default environments are recreated)--org-to-org: Migrate only organization-level secrets (requires--org-to-orgflag, ignores repo and env secrets)--source-endpoint: GitHub API endpoint for source (default:https://api.github.com)--target-endpoint: GitHub API endpoint for target (default:https://api.github.com)
GITHUB_TOKEN: If set, uses this token for both source and target authentication (must have permissions for both repos)SOURCE_ENDPOINT: GitHub API endpoint for source organization/repositoryTARGET_ENDPOINT: GitHub API endpoint for target organization/repository
The tool supports custom GitHub API endpoints for:
- GHEC Data Residency: GitHub Enterprise Cloud with data residency requirements
- GHEC EMU: GitHub Enterprise Cloud with Enterprise Managed Users
- GHES: GitHub Enterprise Server (self-hosted)
- Standard GitHub.com:
https://api.github.com(default) - GHEC Data Residency (US):
https://us.api.github.com - GHEC Data Residency (EU):
https://eu.api.github.com - GitHub Enterprise Server:
https://github.example.com/api/v3
Migrate from GitHub.com to GHEC US Data Residency:
gh secrets-migrator \
--source-org myorg \
--source-repo myrepo \
--target-org targetorg \
--target-repo targetrepo \
--target-endpoint https://us.api.github.comMigrate from GHEC EU to GitHub.com:
gh secrets-migrator \
--source-org myorg \
--source-repo myrepo \
--target-org targetorg \
--target-repo targetrepo \
--source-endpoint https://eu.api.github.comMigrate between different GHEC Data Residency regions:
gh secrets-migrator \
--source-org myorg \
--source-repo myrepo \
--target-org targetorg \
--target-repo targetrepo \
--source-endpoint https://us.api.github.com \
--target-endpoint https://eu.api.github.comMigrate from GitHub Enterprise Server:
gh secrets-migrator \
--source-org myorg \
--source-repo myrepo \
--target-org targetorg \
--target-repo targetrepo \
--source-endpoint https://github.example.com/api/v3Using environment variables:
export SOURCE_ENDPOINT=https://us.api.github.com
export TARGET_ENDPOINT=https://eu.api.github.com
gh secrets-migrator \
--source-org myorg \
--source-repo myrepo \
--target-org targetorg \
--target-repo targetrepoOrganization-to-Organization migration with custom endpoints:
gh secrets-migrator \
--source-org myorg \
--source-repo .github \
--target-org targetorg \
--source-endpoint https://us.api.github.com \
--target-endpoint https://eu.api.github.com \
--org-to-orgNote: GHEC EMU uses the same endpoint as standard GitHub.com (https://api.github.com) but with organization-specific authentication and access patterns.
- Secrets are encrypted at rest in GitHub using libsodium sealed boxes
- Only available to workflows via
${{ secrets.* }}context - Secrets are masked in GitHub Actions logs (redacted automatically)
- Temporary
SECRETS_MIGRATOR_TARGET_PATandSECRETS_MIGRATOR_SOURCE_PATare always cleaned up after workflow completes - Cleanup runs even if migration fails (
if: always()condition) - Workflow cleanup deletes the migration branch automatically
- PATs should be treated like passwords - keep them secret
- Use separate PATs for source and target for better access control
- Consider using organization-level secrets to rotate credentials
- Review the generated workflow before running (it's visible in the Actions tab)
- Tokens are visible to anyone with write access to the source repository (they can read the workflow file)
The tool automatically recreates all environments from the source repository in the target repository. This is useful for maintaining environment parity between repositories.
- Default: Environments are automatically recreated
- Graceful: If an environment already exists in the target (HTTP 409), it is silently skipped
- Idempotent: Safe to run multiple times; existing environments won't cause failures
- Optional: Use
--skip-envsflag to skip environment recreation
ℹ️ Recreating environments...
ℹ️ Environments to recreate (3 total):
- production
- staging
- development
✅ Environment recreation completed!Environment-specific secrets are now migrated! The tool generates one workflow step per environment-secret combination:
- Lists all environment secrets from the source repository
- Creates dynamic workflow steps for each secret
- Each step migrates that specific secret to the target environment
- Secrets are created using the values already available in the workflow context
- Both source and target PATs must have appropriate scopes
- Workflow runs on source repository (not target)
- Cannot migrate action secrets from Dependabot or Codespaces scopes
- Source and target repositories must be accessible to their respective PATs
- For org-to-org migration: only organization-level secrets are migrated (repo and environment secrets are excluded)
- Verify your PATs are valid:
curl -H "Authorization: token <PAT>" https://api.github.com/user - Check scopes: Go to GitHub Settings → Developer settings → Personal access tokens (classic) → Select token → View scopes
- Ensure PATs have
repoandworkflowscopes
- Verify organization/repository names are correct
- Check that PATs haven't expired
- Ensure you have access to both organizations
- Check that the migration branch was created:
Settings > Branches - Verify GitHub Actions is enabled in the source repository
- Check the Actions tab for any workflow errors
- Ensure the workflow file
.github/workflows/migrate-secrets.ymlwas created
- Verify target PAT has permission to create secrets in target repo
- Check that secret names don't start with
SECRETS_MIGRATOR_(filtered out) - Review workflow logs in the Actions tab
- Verify target repository is accessible to target PAT
- This typically means the PAT doesn't have the
repoorworkflowscope - Update your source PAT to include these scopes
- Regenerate the PAT if needed
- Check workflow cleanup logs in Actions tab
- Manually delete
SECRETS_MIGRATOR_TARGET_PATandSECRETS_MIGRATOR_SOURCE_PATfrom source repo - Verify source PAT has delete permissions
# Clone the repository
git clone https://github.com/renan-alm/gh-secrets-migrator.git
cd gh-secrets-migrator
# Set up development environment
make dev
# Run tests
make test
# Run linting
make lint
# Format code
make format# Build for current platform
make build
# The binary will be in bin/gh-secrets-migrator
./gh-secrets-migrator --help
# Clean build artifacts
make clean# Run with verbose logging from source
python main.py \
--source-org myorg \
--source-repo repo \
--target-org targetorg \
--target-repo target \
--verbose
# Or test the built binary
make build
./gh-secrets-migrator --verbose --helpThe project uses GitHub Actions to automatically build and release binaries for multiple platforms:
- Update
CHANGELOG.mdwith the new version entry - Ensure all tests pass:
make test - Create and push a version tag:
git tag v1.0.0 git push origin v1.0.0
- GitHub Actions will automatically:
- Validate the changelog entry exists
- Verify tests passed on master
- Build binaries for Linux, macOS, and Windows
- Create a GitHub release with binaries
- Make the extension installable via
gh extension install
make help # Show all available commands
make install # Install dependencies
make dev # Install with dev dependencies
make lint # Run linting checks
make format # Format code with black
make test # Run tests with pytest
make build # Build for current platform
make clean # Clean build artifactsgh secrets-migrator [OPTIONS]
# or: ./gh-secrets-migrator [OPTIONS]
# or: python main.py [OPTIONS]
Options:
--source-org TEXT Source organization name [required]
--source-repo TEXT Source repository name [required]
--target-org TEXT Target organization name [required]
--target-repo TEXT Target repository name [conditionally required]
--source-pat TEXT Source Personal Access Token (defaults to GITHUB_TOKEN)
--target-pat TEXT Target Personal Access Token (defaults to GITHUB_TOKEN)
--source-endpoint TEXT GitHub API endpoint for source (default: https://api.github.com)
--target-endpoint TEXT GitHub API endpoint for target (default: https://api.github.com)
--verbose Enable verbose logging
--skip-envs Skip environment recreation
--org-to-org Migrate only organization-level secrets
--help Show help message