Provision a repo across self-hosted GitLab, GitLab.com, and GitHub with push mirroring.
Project description
One source, many mirrors. Provision a single repo on self-hosted GitLab, GitLab.com, and GitHub in one shot — with push mirroring wired up so every push fans out automatically.
Hydra is a small Python CLI for teams who keep code on a self-hosted GitLab but also need it on GitLab.com and/or GitHub — for open-source releases, customer access, vendor integrations, or backup. You run one command and Hydra creates the project on all three hosts, then configures GitLab's built-in push mirrors so the self-hosted copy is the only place you ever push.
┌──────────────────┐
┌──▶ │ GitLab.com │
│ └──────────────────┘
┌────────────────────────┐ push
│ Self-hosted GitLab │ ──┤
│ (source of truth) │ push
└────────────────────────┘ │ ┌──────────────────┐
▲ └──▶ │ GitHub │
│ └──────────────────┘
git push (you)
Requirements
- Python 3.9 or newer
- Permission to create projects/repos on each host you want to use (self-hosted GitLab, GitLab.com, GitHub)
- A personal access token for each host (Hydra tells you exactly which scopes during setup — see Token scopes)
Install
From PyPI:
pip install hydra-repo-syncer
hydra --version
From source (if you'd rather pin to a checkout, or want to hack on Hydra itself):
git clone <this-repo-url>
cd hydra
python -m venv venv && source venv/bin/activate
pip install -e .
hydra --version
After installing, the hydra command is on your PATH.
Quickstart
# 1. One-time setup — pick hosts, defaults, and store tokens
hydra configure
# (optional) shell tab-completion for bash/zsh/fish
hydra --install-completion
# 2. See what *would* happen, without making any API calls
hydra create my-first-repo --dry-run
# 3. Do it for real
hydra create my-first-repo
That's it. The repo now exists on all three hosts, and any future git push to the self-hosted GitLab will mirror automatically to the other two.
Configure (one-time)
hydra configure
A four-step wizard walks you through:
| Step | What you provide |
|---|---|
| 1. Hosts | URLs for self-hosted GitLab, GitLab.com, and GitHub |
| 2. GitHub account | Your GitHub user, or an organisation name |
| 3. Defaults | Default group path; default visibility (private/public) |
| 4. Tokens | API tokens for each host, plus where to store them |
Non-secret settings are saved to ~/.config/hydra/config.yaml. Tokens go to your OS keyring (macOS Keychain, Linux Secret Service) — never to the YAML.
Token scopes
When you mint personal access tokens, use these scopes:
| Host | Required scope | Mint a token at |
|---|---|---|
| Self-hosted GitLab | api |
<your-host>/-/user_settings/personal_access_tokens |
| GitLab.com | api |
https://gitlab.com/-/user_settings/personal_access_tokens |
| GitHub | repo (plus admin:org if creating under an organisation). To delete GitHub repos with hydra destroy, also grant delete_repo for classic PATs or Administration: Read and write for fine-grained PATs. |
https://github.com/settings/tokens |
Token resolution order
For each host, Hydra looks up the token in this order and stops at the first hit:
- OS keyring — set via
hydra configure, or directly:keyring set hydra <github|gitlab|self_hosted_gitlab> - Environment variable —
HYDRA_GITHUB_TOKEN,HYDRA_GITLAB_TOKEN,HYDRA_SELF_HOSTED_GITLAB_TOKEN .envfile in the current working directory (see.env.example)- Interactive prompt (only if attached to a TTY)
This lets you use the keyring on your laptop and env vars in CI without changing anything else.
Creating repos
Two modes — interactive wizard (good for one-offs), or flag-driven (good for scripts).
Interactive
hydra create
The wizard collects the repo name, description, group, visibility, GitHub destination, and mirror toggle, shows a review summary, then asks you to create now, dry-run, or cancel.
Flags
# Dry-run — recommended for the first try; renders the plan, no API calls
hydra create my-repo -d "demo" -g platform/services --dry-run
# Real run — renders the plan first, then prompts y/N before any mutation
hydra create my-repo -d "demo" -g platform/services
# Skip the prompt (useful in CI / scripts)
hydra create my-repo -d "demo" -g platform/services --yes
# Public repo, under a GitHub org, skip mirror setup
hydra create my-repo --public --host-option github.org=acme --no-mirror
Omit the name to launch the wizard; pass a name to stay in flag mode.
Every mutating run starts by printing the plan — the ordered list of
namespaces / repos / mirrors / journal entries that would be created. With
--dry-run it stops there. Without it, you get one confirmation prompt
before any provider call. --yes skips the prompt.
| Flag | Meaning |
|---|---|
-d, --description |
Repo description |
-g, --group |
Group path on self-hosted GitLab |
--public |
Create as public (default is private) |
--host-option <id.k=v> |
Per-host override, e.g. github.org=acme |
--no-mirror |
Skip push-mirror setup |
--dry-run |
Print the plan and exit; no API calls |
-y, --yes |
Skip the confirmation prompt |
--config <path> |
Use a non-default config file |
-v, --verbose |
Print extra detail (group IDs, etc.) |
Destroying repos
# Preview the repo/fork cleanup plan, then confirm
hydra destroy my-repo
# Skip the confirmation prompt
hydra destroy my-repo --yes
# Also delete inferred GitLab groups/namespaces after repos are deleted
hydra destroy my-repo --delete-group
hydra destroy <name> reads the local journal, deletes fork repos first, then
the primary repo, and removes the journal row after successful cleanup. If the
journal is incomplete because an earlier hydra create failed before mirror
setup, Hydra probes configured fork hosts for orphaned repos by name and includes
anything it finds in the plan.
Group deletion is deliberately opt-in. --delete-group (alias:
--delete-namespace) infers GitLab namespaces from repo URLs and deletes those
namespaces after repo deletion. Use it only for groups Hydra created or groups
you know are safe to remove.
GitLab project deletion is asynchronous. If a retry sees that GitLab has already marked a project for deletion, Hydra treats that as success and continues. If a delete fails because of permissions, the journal row is preserved so you can fix the token and rerun the same command.
Inspecting mirrors
hydra status my-repo # offline — reads the journal cache
hydra status my-repo --refresh # re-query the primary, then show
Shows per-mirror last status and last error inline for one repo, straight from the journal — no network unless you pass --refresh. Exits non-zero if any mirror is unhealthy, so it doubles as a CI health gate. When a mirror is broken, hydra repair re-establishes it without a full scan.
Commands
| Command | Description |
|---|---|
hydra create [name] |
Create the repo across all three hosts. Without name, runs the wizard. Renders a plan + prompts before applying (skip with --yes). |
hydra destroy <name> |
Delete a journaled repo and its forks. Probes for orphaned forks, deletes forks before the primary, and can also remove inferred GitLab groups with --delete-group. |
hydra configure |
Onboarding wizard — config + tokens. |
hydra status <name> |
Per-mirror health for one repo from the journal (offline). --refresh re-queries the primary first. Exits non-zero if any mirror is unhealthy. |
hydra list |
List journaled repos and last-known mirror status. --refresh re-queries the primary (uses --max-workers, default 8). |
hydra scan |
Diff the journal against the primary. --apply adopts unknowns and resyncs drifted ids (renders a plan + prompts; skip with --yes). --interactive filters the plan per-repo first. --max-workers <N> controls concurrent HTTP calls (default 8, env HYDRA_SCAN_WORKERS). |
hydra repair [name] |
Re-establish mirrors the journal marks unhealthy (broken/missing/failed/error): re-adds gone mirrors, replaces failing ones. Renders a plan + prompts (skip with --yes); supports --dry-run and --host <id>. |
hydra rotate-token |
Rotate a host PAT in the keyring and rewrite every push-mirror that embeds the old token. |
hydra doctor |
Diagnose configuration, tokens, and topology. --fix runs safe migrations. |
hydra config-path |
Print the resolved config-file path. |
hydra journal-path |
Print the resolved journal database path. |
Run hydra <cmd> --help for full flags.
Error handling
Hydra translates HTTP failures into actionable messages:
✗ GitLab.com authentication failed (401) while searching for group 'platform/services'
The GitLab.com token was rejected. Rotate it at
https://gitlab.com/-/user_settings/personal_access_tokens
and re-run `hydra configure`, or set HYDRA_GITLAB_TOKEN in your environment.
If a failure happens after some resources have been created, the partial state is reported and Hydra offers to roll those resources back immediately:
⚠ Partial progress before the failure:
• self-hosted GitLab repo: https://gitlab.example.com/sandbox/demo
• gitlab.com group: https://gitlab.com/repo-syncer-managed-groups/sandbox-20260508131245
These resources exist now.
Roll back the created resources? [y/N]:
If you decline rollback or the process is interrupted, rerun cleanup later with
hydra destroy <name>. Add --delete-group if Hydra created GitLab groups that
should be removed too.
Config file
Lives at ~/.config/hydra/config.yaml by default. Override with --config <path> or the HYDRA_CONFIG environment variable. See config.yaml.example for the full schema:
self_hosted_gitlab:
url: https://gitlab.example.com
gitlab:
url: https://gitlab.com
managed_group_prefix: repo-syncer-managed-groups
github:
url: https://api.github.com
org: null # null = create under your user; or set an org name
defaults:
private: true
group: "" # optional default group path on the self-hosted GitLab
Security notes
- Tokens are never written to the YAML config.
- Tokens injected into mirror URLs (
https://oauth2:<token>@host/...) are stored on the self-hosted GitLab'sremote_mirrorstable. Anyone with project admin access can read them back via the GitLab API — use scoped tokens. - Keep
.envgitignored. It already is in this repo.
Development
Clone the repo and install with the dev extras:
git clone <this-repo-url>
cd hydra
python -m venv venv && source venv/bin/activate
pip install -e '.[dev]'
pytest
Unit tests cover error translation, slug generation, wizard validators, and credential injection. CI runs the same suite plus a hydra --help smoke test on every push (.gitlab-ci.yml).
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hydra_repo_syncer-0.5.1.tar.gz.
File metadata
- Download URL: hydra_repo_syncer-0.5.1.tar.gz
- Upload date:
- Size: 130.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
517d0880d38602dd39ce8eed93ee06157cbe3668c7374173eeb4f9c33fdd40a5
|
|
| MD5 |
55e97a543755138e1d565b59b591022e
|
|
| BLAKE2b-256 |
042dcedf235457e762fec37a31a1ad9296eaea0fd594addb26d95050d23f0e4f
|
File details
Details for the file hydra_repo_syncer-0.5.1-py3-none-any.whl.
File metadata
- Download URL: hydra_repo_syncer-0.5.1-py3-none-any.whl
- Upload date:
- Size: 98.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fbfc6c462977a54f3048707e39c2613f6557fbdfdddfbd007fa46e4b85df2c6
|
|
| MD5 |
31f38bffd4c343bde012dd80ad473363
|
|
| BLAKE2b-256 |
c42b270997cda5fd1fc0cc5358017cc91e554504b98aa183b5a63a272cf10a17
|