Post

Syncing GitHub Repositories Between Environments in Bulk

A GitHub Action to mirror clone repositories between GitHub environments (GitHub.com, EMU, and GitHub Enterprise Server) with support for visibility control, Actions disabling, and archiving

Syncing GitHub Repositories Between Environments in Bulk

Overview

If you work across multiple GitHub environments - like GitHub.com, GitHub Enterprise Server, Enterprise Managed User (EMU), or Data Residency (DR) - you’ve probably had to figure out how to keep repositories in sync between them. Maybe you’re migrating to EMU and need to keep repos mirrored during the transition. Or your company wants to mirror public actions repositories internally so teams can control the update cadence. Or you simply have a private working-copy repository that syncs to a public read-only repository for distribution and deployment.

I built the bulk-github-repo-sync-action (GitHub Marketplace) to handle this. It’s a GitHub Action that mirror clones repositories from a source to a target organization, automatically creating target repos if they don’t exist. It works across GitHub.com, GitHub Enterprise Server, EMU, and DR environments.

See my GitHub Migration Tools Collection post for more migration-related tools and scripts.

Use Cases

There are a couple of scenarios where I’ve found this useful:

Syncing Between GitHub Environments

This is the primary use case. If you’re running GitHub Enterprise Server alongside GitHub.com, or migrating from a regular org to an EMU org, you may need to keep certain repos mirrored between environments during the transition period. This action lets you automate that on a schedule so the repos stay in sync.

Mirroring Public Actions Internally

Some companies want to mirror public GitHub Actions repositories into their own internal organization. This gives them control over when updates are pulled in - teams can pin to specific versions and update on their own cadence rather than depending on external availability. The action’s disable-github-actions option is handy here since you probably don’t want CI running on the mirrored copies.

Post-Migration Archiving

After a migration, you might want to keep the old repos around for reference but prevent anyone from committing to them. The archive-after-sync option handles this - it does the final sync and archives the target repo, and it’s smart enough to unarchive/re-archive if you need to run it again.

Features

  • Mirror cloning - Full repository sync including all branches and tags
  • Automatic repo creation - Creates target repos if they don’t exist
  • Visibility control - Set repository visibility per repo (private, public, or internal)
  • GitHub Actions management - Disable Actions on target repositories (useful for mirrored copies)
  • Repository archiving - Archive repositories after sync (with smart unarchive/re-archive)
  • Multi-server support - Sync between GitHub.com, EMU, and GitHub Enterprise Server
  • Post-run summary - Detailed sync summary with statistics

Setting Up the Action

Prerequisites

  1. GitHub tokens (PAT or GitHub App) for both source and target environments
  2. A repository list YAML file defining what to sync
  3. GitHub Actions enabled on the repo where the workflow runs

Authentication

GitHub Apps are recommended for both source and target:

If you are new to GitHub Apps, check out my post on creating and using GitHub Apps! It’s really much easier than you think. 🚀

  • Source App: Repository Read access to contents
  • Target App: Repository Read and Write access to administration, contents, and workflows

Repository List

Define what to sync in a YAML file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
repos:
  - source: source-org/repo-1
    target: target-org/repo-1
    visibility: private
    disable-github-actions: true
    archive-after-sync: false
  - source: source-org/repo-2
    target: target-org/repo-2
    visibility: internal
    disable-github-actions: true
    archive-after-sync: false
  - source: source-org/old-repo
    target: target-org/old-repo
    visibility: private
    disable-github-actions: true
    archive-after-sync: true # archive after final sync

Each repo entry supports:

SettingDescriptionDefault
sourceSource repository in owner/repo format-
targetTarget repository in owner/repo format-
visibilityRepository visibility (private, public, or internal)private
disable-github-actionsDisable GitHub Actions on the target repotrue
archive-after-syncArchive the target repo after syncfalse

Workflow Example

Here’s a workflow that syncs repos on a schedule and on demand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: sync-repos

on:
  schedule:
    - cron: '0 6 * * *' # daily at 6am UTC
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v6

      # source
      - uses: actions/create-github-app-token@v2
        id: source-app-token
        with:
          app-id: ${{ vars.SOURCE_APP_ID }}
          private-key: ${{ secrets.SOURCE_APP_PRIVATE_KEY }}
          owner: ${{ github.repository_owner }}

      # target
      - uses: actions/create-github-app-token@v2
        id: target-app-token
        with:
          app-id: ${{ vars.TARGET_APP_ID }}
          private-key: ${{ secrets.TARGET_APP_PRIVATE_KEY }}
          owner: target-org-name

      - name: Bulk GitHub Repository Sync
        uses: joshjohanning/bulk-github-repo-sync-action@v1
        with:
          repo-list-file: repos.yml
          source-github-token: ${{ steps.source-app-token.outputs.token }}
          target-github-token: ${{ steps.target-app-token.outputs.token }}
          overwrite-repo-visibility: true

For cross-environment syncs (e.g., GitHub.com to GitHub Enterprise Server), add the API URL inputs:

1
2
3
4
5
6
7
8
- name: Bulk GitHub Repository Sync
  uses: joshjohanning/bulk-github-repo-sync-action@v1
  with:
    repo-list-file: repos.yml
    source-github-token: ${{ steps.source-app-token.outputs.token }}
    target-github-token: ${{ steps.target-app-token.outputs.token }}
    source-github-api-url: https://api.github.com
    target-github-api-url: https://ghes.company.com/api/v3

For a real-world example, check out my sync-github-repos-to-emu-and-ghe repo with multiple repo list configs and the workflow file.

Action Inputs

InputDescriptionRequiredDefault
repo-list-fileYAML file with repository configurationsYes-
source-github-tokenGitHub token for source repositoriesYes-
target-github-tokenGitHub token for target repositoriesNo(uses source token)
source-github-api-urlSource GitHub API URLNo${{ github.api_url }}
target-github-api-urlTarget GitHub API URLNo${{ github.api_url }}
overwrite-repo-visibilityForce update visibility of existing reposNofalse
force-pushForce push to target repositories (overwrites history)Nofalse

Sample Output

After each run, the action generates a summary:

1
2
3
4
5
6
7
8
9
=== SYNC SUMMARY ===
Total repositories: 6
✅ Successful: 6
❌ Failed: 0
🆕 Created: 0
🔄 Updated: 6
👁️  Visibility updated: 0
📝 Description updated: 0
📦 Archived: 0

Things to Keep in Mind

  • Mirror cloning syncs all branches and tags - it’s a full copy of the repository
  • force-push: true will overwrite the target repo’s history - use with caution ⚠️
  • disable-github-actions: true is the default as to not run CI on mirrored copies, but you can set it to false if you want to keep Actions enabled
  • Archiving is smart about unarchive/re-archive if you need to re-run the sync
  • If target-github-token is not provided, the action uses the source token (useful when syncing within the same GitHub instance)
  • The action auto-derives the instance URL from the API URL, so you don’t need to specify both

Summary

If you need to keep repositories in sync between GitHub environments - whether during a migration, for internal mirroring, or for controlling your dependency update cadence - the bulk-github-repo-sync-action can help. Set up a YAML file with your repo mappings, point it at your source and target, and let it run on a schedule.

Check out the action on the GitHub Marketplace and drop a comment here or open an issue if you have questions or feedback! Happy syncing! 🚀

This post is licensed under CC BY 4.0 by the author.