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.
Metadata
Release files for gitfloe 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gitfloe-0.1.1.tar.gz | 29.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gitfloe-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.1 kB
Release files / gitfloe-0.1.1.tar.gz
| Download URL | gitfloe-0.1.1.tar.gz |
|---|---|
| Size | 29.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c81d6be08ef0500b01d7639558b38fa3da6d6678458b726d9360ba6f650a9150
|
|
BLAKE2b-256 checksum How to use checksums |
1b01d720266248d55aeeec3c095e4f3179eb23e8254e5b4a527f0c4a1b1a552d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / gitfloe-0.1.1-py3-none-any.whl
| Download URL | gitfloe-0.1.1-py3-none-any.whl |
|---|---|
| Size | 14.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0178e27859bce9e553a7e7f0390c0cfaee496737ad25302a7d1ac50cc905a9d6
|
|
BLAKE2b-256 checksum How to use checksums |
6c7476c4dee46eef9976046f96fcf48a8fce418e874596e7794a44539787331e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|