Bidirectional mirror sync between GitHub and Codeberg. Every push to either platform triggers a full mirror to the other, with a scheduled fallback every 4 hours.
Two CI workflows keep the repositories in sync:
| Direction | Trigger file | Target |
|---|---|---|
| GitHub → Codeberg | .github/workflows/codeberg-sync.yml |
opendesk-edu/opendesk-edu on Codeberg |
| Codeberg → GitHub | .forgejo/workflows/github-sync.yml |
tobias-weiss-ai-xr/opendesk-edu on GitHub |
Both workflows share the same logic:
fetch-depth: 0)git push --mirror to push all refs (branches, tags, notes)Each workflow runs on three triggers:
0 */4 * * * (every 4 hours) as a safety net for missed pushesworkflow_dispatch for on-demand sync via the UI or CLIgit push --mirror pushes everything: all branches, all tags, all refs. This means deleting a branch on one side will delete it on the other after the next sync.
| Secret | Description | Required scopes |
|---|---|---|
CODEBERG_TOKEN |
Personal access token from Codeberg | repo, write:packages |
| Secret | Description | Required scopes |
|---|---|---|
GH_TOKEN |
Personal access token (classic) from GitHub | repo, workflow |
curl -X POST 'https://codeberg.org/api/v1/users/-/tokens' \
-H "Authorization: Basic $(echo -n 'YOUR_CODEBERG_USERNAME:YOUR_PASSWORD' | base64)" \
-H 'Content-Type: application/json' \
-d '{"name": "github-sync","scopes": ["repo","write:packages"]}'
Save the sha1 field from the response. This is your CODEBERG_TOKEN.
Go to GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) and generate a token with repo and workflow scopes. Alternatively via the API:
curl -X POST 'https://api.github.com/authorizations' \
-H "Authorization: token YOUR_EXISTING_GITHUB_TOKEN" \
-H 'Accept: application/vnd.github+json' \
-d '{"scopes":["repo","workflow"],"note":"codeberg-sync"}'
Save the token field from the response. This is your GH_TOKEN.
On GitHub:
gh secret set CODEBERG_TOKEN --repo tobias-weiss-ai-xr/opendesk-edu --body 'YOUR_CODEBERG_TOKEN'
On Codeberg:
curl -X POST 'https://codeberg.org/api/v1/repos/opendesk-edu/opendesk-edu/actions/secrets' \
-H "Authorization: token YOUR_CODEBERG_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"GH_TOKEN","data":"YOUR_GITHUB_TOKEN"}'
# List workflows and their last status
gh workflow list --repo tobias-weiss-ai-xr/opendesk-edu
# View recent runs
gh run list --workflow=codeberg-sync.yml --repo tobias-weiss-ai-xr/opendesk-edu --limit 5
# Watch a specific run
gh run view --repo tobias-weiss-ai-xr/opendesk-edu --log
# Trigger manually
gh workflow run codeberg-sync.yml --repo tobias-weiss-ai-xr/opendesk-edu
Both platforms support a “Run workflow” button on the Actions page. On GitHub, go to Actions → Sync to Codeberg → Run workflow. On Codeberg, go to Actions → Sync to GitHub → Run workflow.
The verify step compares local and remote ref counts after a successful push. A mismatch is a warning, not an error. Common causes:
If the counts consistently diverge, check whether someone is pushing directly to the remote without going through the sync workflow.
If a push lands on GitHub and Codeberg within seconds of each other, the --mirror flag on the second sync will overwrite the first. This is expected behavior for a mirror setup. The scheduled 4-hour cron acts as a reconciliation layer, so any divergence self-heals within 4 hours at most.
Both workflows have a 10-minute timeout. A full mirror of a large repository with deep history can be slow. If you hit this limit:
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20)git gc on the repositorytimeout-minutes: 10)| File | Platform | Purpose |
|---|---|---|
.github/workflows/codeberg-sync.yml |
GitHub Actions | Pushes mirror to Codeberg |
.forgejo/workflows/github-sync.yml |
Codeberg CI (Forgejo) | Pushes mirror to GitHub |
Both workflows use identical retry logic:
git push --mirrorEach workflow has timeout-minutes: 10. If the job doesn’t complete within 10 minutes, GitHub/Codeberg kills it.
After a successful push, both workflows count local refs (git show-ref) and remote refs (git ls-remote) and compare them. A mismatch prints a warning but does not fail the workflow.
Both tokens are scoped to the minimum permissions needed. The repo scope allows reading and writing repository contents. The write:packages scope (Codeberg) and workflow scope (GitHub) are needed for the CI integration.
Neither token has admin, delete_repo, or organization-wide permissions.
Tokens are passed via $ and $. Both GitHub Actions and Forgejo automatically mask secrets in logs. The token appears in the git remote URL, but the runner scrubs it before logging.
Rotate tokens periodically:
GitHub classic tokens don’t expire by default. Set an explicit expiration date when creating them. Codeberg tokens also don’t expire automatically, so track rotation in a calendar or secrets manager.
Only repository admins should be able to create and manage sync tokens. On GitHub, restrict Actions permissions under Settings → Actions → General → “Workflow permissions” to limit who can trigger workflows.