GitHub Action to mirror a repo to any git forge
This action mirrors all branches and tags, pruning any that no longer exist in the source.
Warning
Mirrors should be treated as read-only; there is no two-way sync. Issues and pull requests are also not mirrored.
-
Create a destination: an empty repository on your desired forge(s) (e.g. GitLab, Tangled, Codeberg)
-
Prepare authentication: either a personal access token (e.g. for GitLab) or SSH key (e.g. for Tangled)
-
Add this action to your repo: after checking out with
fetch-depth: 0, call the action once per destination:- uses: actions/checkout@v6 with: repository: your-org/your-repo fetch-depth: 0 - name: Mirror to GitLab uses: roostorg/mirror@main with: destination: https://gitlab.com/your-org/your-repo.git token: ${{ secrets.GITLAB_TOKEN }} - name: Mirror to Tangled uses: roostorg/mirror@main with: destination: git@tangled.org:your-handle/your-repo ssh_private_key: ${{ secrets.TANGLED_SSH_PRIVATE_KEY }} ssh_known_hosts: ${{ secrets.TANGLED_KNOWN_HOSTS }}
See the examples below for specific forge configuration, or see how ROOST uses this.
| Input | Required | Default | Description |
|---|---|---|---|
destination |
yes | — | Remote URL to push to (HTTPS or SSH). Do not embed credentials here. |
token |
for HTTPS | — | OAuth2 or personal access token. Injected into the URL; never echoed. |
token_username |
no | oauth2 |
Username in the HTTPS URL. oauth2 works for GitLab; use the account username for Gitea, Forgejo, and Codeberg. |
ssh_private_key |
for SSH | — | PEM private key (ED25519 or RSA). Written to a temp file with mode 0600 and deleted after the push. |
ssh_known_hosts |
recommended | — | Known-hosts entries for the destination host (see below). If omitted, falls back to ssh-keyscan with a TOFU warning. |
GitLab: HTTPS with OAuth2 token (default token_username of oauth2 is correct):
- uses: roostorg/mirror@main
with:
destination: https://gitlab.com/your-org/your-repo.git
token: ${{ secrets.GITLAB_TOKEN }}Tangled: SSH with ED25519 key:
- uses: roostorg/mirror@main
with:
destination: git@tangled.org:your-handle/your-repo
ssh_private_key: ${{ secrets.TANGLED_SSH_PRIVATE_KEY }}
ssh_known_hosts: ${{ secrets.TANGLED_KNOWN_HOSTS }}Codeberg: HTTPS with a Codeberg username and token:
- uses: roostorg/mirror@main
with:
destination: https://codeberg.org/your-username/your-repo.git
token: ${{ secrets.CODEBERG_TOKEN }}
token_username: your-usernameGitea / Forgejo: HTTPS with account username and token:
- uses: roostorg/mirror@main
with:
destination: https://your-forgejo-instance.example.com/your-username/your-repo.git
token: ${{ secrets.FORGEJO_TOKEN }}
token_username: your-usernameRun this once and store the output as a repository secret:
ssh-keyscan -t ed25519,rsa,ecdsa tangled.orgProviding this value means the action verifies the host's identity at push time. Without it, the action falls back to ssh-keyscan on each run, which does not verify host identity (trust-on-first-use).
- Tokens are injected into a shell variable and never echoed; GitHub Actions also masks the raw secret value in all log output.
- SSH private keys are written to
$RUNNER_TEMP(not~/.ssh) withchmod 600and deleted immediately after the push. IdentitiesOnly=yesprevents the SSH agent from offering other keys during the push.StrictHostKeyChecking=yesensures the action hard-fails if the host key doesn't match, rather than silently connecting.- Only the git repository (branches, tags, and commits) is mirrored; pull requests and issues are not.
- Do not mirror private repositories. The action does not check repo visibility.
ROOST maintains a mirror workflow in this repo that runs hourly and on push to main. It checks out each repo in a matrix, then calls this action once for each remote.
To add a ROOST repo to our mirrors:
- Create an empty repo of the same name on each forge (e.g. gitlab.com/roostorg)
- Add the repo name to the
repomatrix in .github/workflows/mirror.yml - Wait up to an hour, or trigger the workflow manually from the Actions tab