A reusable GitHub Actions workflow that automatically synchronizes private repositories to their public counterparts. This workflow helps maintain public mirrors of private repositories, automatically syncing all content and branch structures while excluding sensitive information.
This workflow allows you to maintain a private repository containing sensitive information alongside a public version of the same codebase. When changes are pushed to the private repository, this workflow automatically updates the corresponding public repository.
- Automatic synchronization on push or PR merge events
- Manual synchronization via workflow dispatch
- Support for custom public repository naming
- Branch-aware synchronization (maintains all branches from the private repo)
- SSH key-based secure authentication
- A private GitHub repository (typically with a name containing "-private")
- A public GitHub repository to serve as the mirror
- An SSH deploy key with write access to the public repository
# Generate a new SSH key pair
ssh-keygen -t ed25519 -C "github-action-sync-key" -f deploy_key -N ""
# deploy_key (private key) - will be used as a secret
# deploy_key.pub (public key) - will be added to the public repository- Go to your public repository on GitHub
- Navigate to Settings > Deploy keys
- Click "Add deploy key"
- Give it a title (e.g., "Sync from Private Repository")
- Paste the content of
deploy_key.pub - Check "Allow write access"
- Click "Add key"
- Go to your private repository on GitHub
- Navigate to Settings > Secrets and variables > Actions
- Click "New repository secret"
- Set the name to
SSH_DEPLOY_KEY - Paste the content of the private
deploy_keyfile - Click "Add secret"
Create a file in your private repository at .github/workflows/sync-mirror.yml with the following content:
name: Sync Repository to Public Mirror
on:
push:
branches:
- '**'
pull_request:
types: [closed]
workflow_dispatch:
inputs:
force_sync:
description: 'Force sync all branches'
required: false
default: 'true'
type: boolean
public_repo_name:
description: 'Public repository name (leave empty to auto-derive by removing "-private")'
required: false
type: string
jobs:
call-sync-workflow:
uses: your-org/workflows/.github/workflows/sync-to-public-mirror.yml@main
with:
force_sync: ${{ github.event.inputs.force_sync || 'true' }}
public_repo_name: ${{ github.event.inputs.public_repo_name || '' }}
secrets:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}Be sure to replace your-org/workflows with the actual path to your centralized workflows repository.
The workflow runs on:
- Push events: Whenever code is pushed to any branch
- Pull request closures: When PRs are merged or closed
- Manual dispatch: For on-demand synchronization
This job determines if the repository is a private one that should be synced:
-
Determine repository names:
- Extracts the private repository name from
GITHUB_REPOSITORY - Extracts the organization/username
- Determines the public repository name by either:
- Using the name provided through manual input
- Automatically deriving it by removing the "-private" suffix
- Extracts the private repository name from
-
Check if repository is private:
- Verifies if the repository name contains "-private"
- Sets the
is_privateoutput flag used by subsequent jobs
This job handles the actual synchronization process:
-
Check out private repository:
- Clones the private repository with all branches (
fetch-depth: 0)
- Clones the private repository with all branches (
-
Set up SSH:
- Configures the SSH agent with the deploy key for secure authentication
- Adds GitHub to known hosts for SSH connections
-
Set Git identity:
- Configures Git with identity information for commits
-
Get all branch names:
- Extracts a list of all branches from the private repository
-
Create workspace for public mirror:
- Sets up a separate workspace for the public repository
- Initializes Git and configures the remote for the public mirror
- Verifies the public repository exists and is accessible
-
Create and update all branches:
- For each branch in the private repository:
- Checks out the branch
- Creates a temporary directory for the branch content
- Copies all files except the
.gitdirectory - Switches to the public mirror workspace
- Creates or updates the branch in the public mirror
- Commits changes
- For each branch in the private repository:
-
Push all branches to public mirror:
- Pushes each branch to the public repository
By default, the workflow derives the public repository name by removing the "-private" suffix from the private repository name. For custom naming:
- In the GitHub UI: When manually triggering the workflow, enter the desired name in the "Public repository name" field.
- In the caller workflow: Add a value for the
public_repo_nameparameter.
The "Force sync all branches" option ensures all branches are synchronized, even if there are no detected changes.
-
SSH Key Access Denied:
- Verify the SSH deploy key is correctly added to the public repository
- Ensure "Allow write access" is checked for the deploy key
- Confirm the private key is correctly stored as a secret
-
Public Repository Not Found:
- Check that the public repository exists
- Verify the naming convention or custom name is correct
-
Workflow Not Running:
- Ensure your private repository's name contains "-private" or modify the check as needed
Contributions to improve this workflow are welcome! Please submit issues or pull requests to the central workflows repository.