gitfloe
Keep your forks in sync with their upstreams by calling GitHub's merge-upstream API — the
exact same thing the web "Sync fork" button and gh repo sync do. No git, no cloning. When a
fork has a conflict, it hands off to a pluggable handler (default: email). After each run —
and periodically — it also emails an overall digest of what happened.
What it does
One repo ("the manager") holds a list of your forks and, on a schedule, for each one:
- calls
POST /repos/{owner}/{repo}/merge-upstreamwith{"branch": "<branch>"}, - reads the result:
- 200 → merged (synced),
- 204 → already up to date,
- 409 → conflict → dispatch a handler (default: email),
- 403 / 404 / other → error → dispatch a handler,
- records each result to
state/events.jsonland emails a digest.
It only ever writes to your forks (via the API), never to upstream.
Layout
assets/ # logo (logo.svg, logo-light.svg, logo-lockup.svg, favicon.ico)
examples/ # templates: repos.example.yaml, .env.example
gitfloe/
cli.py # python -m gitfloe.cli
core.py # calls merge-upstream; classifies result; dispatches
config.py # load repos.yaml
plugins.py # handler discovery + dispatch
smtp.py # shared SMTP sender (env-driven)
digest.py # run + interval digest
handlers/
email.py # default: email on conflict/error
ai.py # example LLM handler (shows the hook)
tests/ # pytest tests
.github/workflows/
sync.yml # scheduled sync + per-run digest + state commit
digest.yml # periodic aggregate digest
ci.yml # CI + pytest
state/ # committed back so history persists across runs
Adding your forks
Copy the template examples/repos.example.yaml to repos.yaml, then edit it:
default:
branch: main
handler: email
digest:
on_run: true # email a summary after every sync run
on_interval: true # + weekly digest via digest.yml
repos:
- name: example-fork
fork: you/example-fork
upstream: someorg/example
branch: main
upstreamis for reference/labels (the API derives it from the fork's GitHub parent);branchis the branchmerge-upstreammerges into.
The handler hook
A handler is a Python module exposing HANDLER and handle(event):
# handlers/mine.py
HANDLER = "my-name"
def handle(event: dict) -> dict:
# repo, fork, upstream, branch, kind, api_status, error, summary
return {"handled": True, "summary": "did something"}
The event kind is synced / up_to_date / conflict / error (plus preview during dry-run).
Return {"handled": False} to leave it for another handler / log only. External handlers: drop
.py in a folder and point the run at it via GITFLOE_HANDLER_PATH (colon-separated); external
ones override by name.
Running locally
uv sync # install into .venv (PyYAML only)
uv run gitfloe --config repos.yaml --dry-run # config/validation check (read-only)
uv run gitfloe --config repos.yaml # actually sync (needs GITFLOE_TOKEN)
uv run gitfloe --config repos.yaml --digest-interval # send periodic digest only
Installing from PyPI
uv tool install gitfloe # or: pip install gitfloe
gitfloe --config repos.yaml --dry-run
You can also trigger a single fork manually with the official CLI:
gh repo sync luoyueyuguang/vllm --branch main
Where the secrets live (the key point)
No secret is ever committed. Credentials come from the environment — GitHub Actions Secrets
in CI, a local .env for development.
| Secret | Used by | Purpose |
|---|---|---|
GITFLOE_TOKEN |
core | PAT/fine-grained token on your forks with Contents: write (the API needs it to push the merge). |
GITFLOE_SMTP_HOST/PORT/USER/PASS |
smtp | SMTP for the email handler + digest. |
GITFLOE_MAIL_TO |
smtp | Where attention + digest emails go. |
GITFLOE_AI_ENDPOINT |
ai handler | LLM endpoint (OpenAI-compatible). |
GITFLOE_AI_KEY |
ai handler | LLM bearer token. |
GITFLOE_AI_MODEL |
ai handler | Model id (default gpt-4o-mini). |
LLM token — same rule as the sync token: put GITFLOE_AI_KEY (+ END/MODEL) in Secrets, injected
by the workflow's env:. Local runs read them from .env. If unset, the ai handler degrades to a no-op.
Tip: for Gmail use an App Password +
smtp.gmail.com:587.
State persistence
state/ is tracked so the digest can aggregate across workflow runs; both workflows commit it
back. Peek at the event log: tail -f state/events.jsonl.
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 gitfloe-0.1.0.tar.gz.
File metadata
- Download URL: gitfloe-0.1.0.tar.gz
- Upload date:
- Size: 29.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b0c0ec0c98bb3726f56b86fc082aaab972b2f6b21dd9f5b4c794ed97a401f6ea
|
|
| MD5 |
0660d44b2a74a9f1a4cee053d58147a2
|
|
| BLAKE2b-256 |
622ca72594d2cd4a808b9f0c4ac29e5003410acf6e2fb8c09921a19c5768385e
|
File details
Details for the file gitfloe-0.1.0-py3-none-any.whl.
File metadata
- Download URL: gitfloe-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5bda394e5bb3370039860f644755e2e11f37216f5abca6fe286fe70dce98c119
|
|
| MD5 |
17244583b322b60109b4e9a5b21ab16c
|
|
| BLAKE2b-256 |
6c98686d9f2fc1bb0e5b140b1c7b30cd787c8503f4e57493c698f3649dbf594e
|