Mirror all hosts setup — one-time token generation + repo pre-creation for the 9 popular hosts
Mirror all hosts setup — one-time
Complete setup for the 9-host weekly git mirror. Run once. After this, the
Friday cron in .github/workflows/mirror-all.yml runs hands-free.
Where the workflow reads from. Mirror secrets and ENABLE_MIRROR_*
variables live at the chirag127 GitHub org level — that's the org
that owns the umbrella chirag127/workspace repo running the cron. Local
mirror in c:/D/oriz/.env (gitignored) is the authoring copy; Doppler
may also hold it as a personal vault but the workflow path is GitHub
org secrets only.
Prerequisites
ghCLI authenticated as admin ofchirag127org- Browser access for token generation
jqandcurlinstalledradCLI (for Radicle keypair only):curl -sSf https://radicle.xyz/install | sh
Step 1: Generate 9 host credentials (browser + local)
Fill the value into c:/D/oriz/.env next to the matching MIRROR_<HOST>_*
key as you go. Push to org secrets in Step 2.
1A. GitLab.com — Personal Access Token
- Log in: https://gitlab.com
- Token page: https://gitlab.com/-/user_settings/personal_access_tokens
- Add new token → name
oriz-mirror-bot→ expiry 1 year → scopes ✅api+ ✅write_repository - Copy → paste into
.env:MIRROR_GITLAB_TOKEN,MIRROR_GITLAB_USERNAME
1B. Codeberg.org — Access Token
Re-check status first: https://status.codeberg.eu. Skip if site is down.
- Sign up: https://codeberg.org/user/sign_up
- Token page: https://codeberg.org/user/settings/applications
- Under "Manage Access Tokens" → Generate Token → name
oriz-mirror-bot→ scope ✅write:repository - Copy →
.env:MIRROR_CODEBERG_TOKEN,MIRROR_CODEBERG_USERNAME
1C. Bitbucket Cloud — Workspace Access Token (NOT App Password)
⚠️ App Passwords retired 2026-07-28. Use Workspace Access Tokens.
- Sign up: https://bitbucket.org/account/signup
- Token page: https://bitbucket.org/account/settings/access-tokens/ (if 404: Workspace → Settings → Security → Access tokens)
- Create access token → name
oriz-mirror-bot→ permissions ✅ Repositories: Write + ✅ Projects: Read → expiry 1 year - Copy →
.env:MIRROR_BITBUCKET_API_TOKEN,MIRROR_BITBUCKET_USERNAME(workspace slug)
1D. GitFlic.ru — Personal Token
- Sign up: https://gitflic.ru/auth/signup/first-step
- Token page: https://gitflic.ru/user/settings/tokens
- Create token → name
oriz-mirror-bot→ scoperepo:write - Copy →
.env:MIRROR_GITFLIC_TOKEN,MIRROR_GITFLIC_USERNAME
1E. Azure DevOps — Org-scoped PAT
⚠️ Use org-scoped PAT, not "All accessible organizations".
- Sign up: https://dev.azure.com
- Create org (e.g.
chirag127) → create project (e.g.mirrors) - Token page:
https://dev.azure.com/{org}/_usersSettings/tokens - + New Token → name
oriz-mirror-bot→ org: pick your specific org → expiry 1 year → custom scope Code → Manage - Copy →
.env:MIRROR_AZURE_DEVOPS_TOKEN,MIRROR_AZURE_DEVOPS_ORG,MIRROR_AZURE_DEVOPS_PROJECT
1F. NotABug.org — Gogs Access Token
- Sign up: https://notabug.org/user/sign_up
- Token page: https://notabug.org/user/settings/applications
- Manage Access Tokens → Generate New Token → name
oriz-mirror-bot - ⚠️ COPY IMMEDIATELY — Gogs shows the token once
- Paste →
.env:MIRROR_NOTABUG_TOKEN,MIRROR_NOTABUG_USERNAME
Workflow uses continue-on-error: true for NotABug so flakes don't break the cron.
1G. GitGud.io — GitLab PAT
- Sign up: https://gitgud.io/users/sign_up
- Token page: https://gitgud.io/-/user_settings/personal_access_tokens
- Add new token → name
oriz-mirror-bot→ expiry 1 year → scopes ✅api+ ✅write_repository - Copy →
.env:MIRROR_GITGUD_TOKEN,MIRROR_GITGUD_USERNAME
1H. RocketGit.com — API Token
- Sign up: https://rocketgit.com/op/register
- Dashboard → Settings → API tokens → Create
- Copy →
.env:MIRROR_ROCKETGIT_TOKEN,MIRROR_ROCKETGIT_USERNAME
⚠️ RocketGit has no public REST repo-create API. Pre-create each repos/own/* repo via the web UI before the first cron, or push will 404. Idempotent: repeat only for new submodules.
1I. Radicle — Identity bootstrap (local one-time)
Radicle is P2P. Generate a keypair locally and ship to the runner.
curl -sSf https://radicle.xyz/install | sh
rad auth # pick an alias + a strong passphrase
# Encode the keypair directory as one line:
tar czf - -C ~/.radicle keys | base64 -w0 > /tmp/rad-keypair-b64.txt
Then paste:
- contents of
/tmp/rad-keypair-b64.txt→.env:MIRROR_RADICLE_KEYPAIR_TAR_B64 - the passphrase you typed during
rad auth→.env:MIRROR_RADICLE_PASSPHRASE
Public seed node radicle.garden is used. No self-hosted Radicle node needed.
Step 2: Push all secrets to chirag127 org level
Mirror secrets live at chirag127 (the org that owns the workflow repo).
Doppler may also hold them; the workflow only reads org-level GH secrets.
Reads from your local .env:
#!/bin/bash
# Run from c:/D/oriz with .env already populated
set -e
set -a; . ./.env; set +a # load .env into shell
SECRETS=(
MIRROR_GITLAB_TOKEN
MIRROR_GITLAB_USERNAME
MIRROR_CODEBERG_TOKEN
MIRROR_CODEBERG_USERNAME
MIRROR_BITBUCKET_API_TOKEN
MIRROR_BITBUCKET_USERNAME
MIRROR_GITFLIC_TOKEN
MIRROR_GITFLIC_USERNAME
MIRROR_AZURE_DEVOPS_TOKEN
MIRROR_AZURE_DEVOPS_ORG
MIRROR_AZURE_DEVOPS_PROJECT
MIRROR_NOTABUG_TOKEN
MIRROR_NOTABUG_USERNAME
MIRROR_GITGUD_TOKEN
MIRROR_GITGUD_USERNAME
MIRROR_ROCKETGIT_TOKEN
MIRROR_ROCKETGIT_USERNAME
MIRROR_RADICLE_KEYPAIR_TAR_B64
MIRROR_RADICLE_PASSPHRASE
)
for NAME in "${SECRETS[@]}"; do
VAL="${!NAME}"
if [ -z "$VAL" ]; then
echo "⊘ skip $NAME (empty in .env)"
continue
fi
printf '%s' "$VAL" | gh secret set "$NAME" --org chirag127 --visibility all
echo "✓ set $NAME"
done
echo ""
gh secret list --org chirag127 | grep -E '^MIRROR_'
Each empty value is skipped so partial setups (e.g. Codeberg still down) don't blow up the loop.
Per-host ENABLE flags as org-level Variables
Flags are Variables, not Secrets — they're 0/1 toggles, not credentials:
#!/bin/bash
# Set / reset the 9 ENABLE flags from .env values
set -a; . ./.env; set +a
FLAGS=(
ENABLE_MIRROR_GITLAB
ENABLE_MIRROR_CODEBERG
ENABLE_MIRROR_BITBUCKET
ENABLE_MIRROR_GITFLIC
ENABLE_MIRROR_AZURE_DEVOPS
ENABLE_MIRROR_NOTABUG
ENABLE_MIRROR_GITGUD
ENABLE_MIRROR_ROCKETGIT
ENABLE_MIRROR_RADICLE
)
for NAME in "${FLAGS[@]}"; do
VAL="${!NAME:-0}"
gh variable set "$NAME" --org chirag127 --visibility all --body "$VAL"
echo "✓ var $NAME=$VAL"
done
gh variable list --org chirag127 | grep -E '^ENABLE_MIRROR_'
To toggle a single host later: gh variable set ENABLE_MIRROR_CODEBERG --org chirag127 --visibility all --body 1.
Step 3: Pre-create mirror repos on each host
This script reads repos/own/* submodules from .gitmodules and creates
empty target repos on the 6 HTTPS hosts (Radicle creates on first
rad init, no pre-creation step needed). Idempotent — 409/4xx errors on
existing repos are ignored.
#!/bin/bash
# pre-create-mirror-repos.sh — requires curl, jq
set -e
set -a; . ./.env; set +a
# Collect repos/own/* submodule names from .gitmodules
REPOS=$(awk '
/^\[submodule/ { path="" }
/^[[:space:]]*path[[:space:]]*=/ { sub(/^[^=]*=[[:space:]]*/, ""); path=$0
if (path ~ /^repos\/own\//) { n=split(path, p, "/"); print p[n] }
}
' .gitmodules)
echo "$REPOS" | while read -r REPO_NAME; do
[ -z "$REPO_NAME" ] && continue
echo "--- $REPO_NAME ---"
# GitLab
if [ "${ENABLE_MIRROR_GITLAB:-0}" = "1" ] && [ -n "$MIRROR_GITLAB_TOKEN" ]; then
curl -s -o /dev/null -X POST "https://gitlab.com/api/v4/projects" \
-H "PRIVATE-TOKEN: ${MIRROR_GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"visibility\":\"public\"}" || true
sleep 0.3
fi
# Codeberg
if [ "${ENABLE_MIRROR_CODEBERG:-0}" = "1" ] && [ -n "$MIRROR_CODEBERG_TOKEN" ]; then
curl -s -o /dev/null -X POST "https://codeberg.org/api/v1/user/repos" \
-H "Authorization: token ${MIRROR_CODEBERG_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"private\":false,\"auto_init\":false}" || true
sleep 0.3
fi
# Bitbucket
if [ "${ENABLE_MIRROR_BITBUCKET:-0}" = "1" ] && [ -n "$MIRROR_BITBUCKET_API_TOKEN" ]; then
curl -s -o /dev/null -X POST \
-H "Authorization: Bearer ${MIRROR_BITBUCKET_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"scm\":\"git\",\"is_private\":false}" \
"https://api.bitbucket.org/2.0/repositories/${MIRROR_BITBUCKET_USERNAME}/${REPO_NAME}" || true
sleep 0.3
fi
# GitFlic
if [ "${ENABLE_MIRROR_GITFLIC:-0}" = "1" ] && [ -n "$MIRROR_GITFLIC_TOKEN" ]; then
curl -s -o /dev/null -X POST "https://api.gitflic.ru/project" \
-H "Authorization: token ${MIRROR_GITFLIC_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"title\":\"${REPO_NAME}\",\"alias\":\"${REPO_NAME}\",\"private\":false}" || true
sleep 0.3
fi
# Azure DevOps (cache project ID across iterations)
if [ "${ENABLE_MIRROR_AZURE_DEVOPS:-0}" = "1" ] && [ -n "$MIRROR_AZURE_DEVOPS_TOKEN" ]; then
if [ -z "$ADO_PROJECT_ID" ]; then
ADO_PROJECT_ID=$(curl -s -u ":${MIRROR_AZURE_DEVOPS_TOKEN}" \
"https://dev.azure.com/${MIRROR_AZURE_DEVOPS_ORG}/_apis/projects/${MIRROR_AZURE_DEVOPS_PROJECT}?api-version=7.1" \
| jq -r '.id')
export ADO_PROJECT_ID
fi
curl -s -o /dev/null -X POST -u ":${MIRROR_AZURE_DEVOPS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"project\":{\"id\":\"${ADO_PROJECT_ID}\"}}" \
"https://dev.azure.com/${MIRROR_AZURE_DEVOPS_ORG}/${MIRROR_AZURE_DEVOPS_PROJECT}/_apis/git/repositories?api-version=7.1" || true
sleep 0.3
fi
# NotABug (Gogs API)
if [ "${ENABLE_MIRROR_NOTABUG:-0}" = "1" ] && [ -n "$MIRROR_NOTABUG_TOKEN" ]; then
curl -s -o /dev/null -X POST "https://notabug.org/api/v1/user/repos" \
-H "Authorization: token ${MIRROR_NOTABUG_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"private\":false,\"auto_init\":false}" || true
sleep 0.3
fi
echo "✓ $REPO_NAME pre-created (enabled hosts only; Radicle inits on first sync)"
done
Step 4: Dry-run the workflow
gh workflow run mirror-all.yml --repo chirag127/workspace
gh run watch --repo chirag127/workspace
Disabled hosts emit ::notice::Mirror <host> disabled and skip. Enabled
hosts should show ✓ per repo. Any ✗ = missing repo or wrong token — fix
before the next Friday cron.
Step 5: First real run
The cron runs every Friday 22:00 UTC. To force one now:
gh workflow run mirror-all.yml --repo chirag127/workspace
Spot-check each enabled host's web UI for a fresh commit history matching GitHub HEAD.
Adding a new repo (recurring task)
When a new submodule is added under repos/own/*:
- Re-run Step 3's repo-creation script (idempotent — safe to re-run).
- The next Friday cron picks up the new repo automatically.
Token rotation
When a token expires or is compromised:
- Regenerate on the host (see Step 1 for the exact URL).
- Overwrite the value in
c:/D/oriz/.env. - Re-run Step 2's
gh secret setloop — it overwrites existing secrets idempotently. - Per
runbooks/security/rotate-leaked-secret.md.
Re-enabling a previously disabled host
When a downed host (Codeberg / Bitbucket / GitFlic) comes back:
- Verify signup + token page work end-to-end (Step 1).
- Paste credentials into
c:/D/oriz/.env. - Flip
ENABLE_MIRROR_<HOST>=1in.env. - Re-run Step 2 (secrets loop) and Step 2's flag loop.
- Re-run Step 3 to create any missing target repos.
See also
- Mirror decision →
../../decisions/ops/mirror-to-9-popular-alternatives-2026-06-28.md - Org secrets rule →
../../rules/security/github-org-level-secrets.md - Set org secrets runbook →
../security/set-github-org-level-secrets.md - Rotate leaked secret →
../security/rotate-leaked-secret.md - Service files per host →
../../services/infra/hosting/ - Workflow file →
.github/workflows/mirror-all.yml