Every forge behind one contract: PR enrichment (open/closed/merged state
per branch) and report-issue (find/create/update one issue with a
fixed title). Merge detection itself is fully offline — forge state is a
best-effort enrichment on top.
| Forge | Detected remote | PR backend | report-issue |
|---|---|---|---|
| GitHub | github.com | gh CLI, else REST + GITHUB_TOKEN | ✅ |
| GitLab | gitlab.com, *.gitlab.* self-hosted | REST + GITLAB_TOKEN (or CI_JOB_TOKEN in GitLab CI) | ✅ |
| Gitea / Codeberg / Forgejo | gitea.com, codeberg.org, forgejo.org | REST + GITEA_TOKEN (GitHub-compatible API) | ✅ |
| Bitbucket Cloud | bitbucket.org | REST + BITBUCKET_TOKEN | ✅ |
| Bitbucket Server / Data Center | host containing bitbucket, not bitbucket.org | REST (/rest/api/ dialect) + BITBUCKET_TOKEN | ❌ (issues live in Jira — loud error, never silent) |
GitLab self-hosted domains (*.gitlab.*) and Bitbucket Server hosts
are recognized by hostname. For anything else — a Gitea at
git.internal, a custom GitLab domain — claim the host explicitly;
an explicit mapping always wins:
// .gitcleanup.json or ~/.config/git-cleanup/config.json
{
"forge": {
"hosts": {
"git.example.com": "gitlab",
"git.internal": "gitea",
"stash.internal": "bitbucket-server"
}
}
}
Override API bases per forge when the instance lives behind a proxy or
custom path: GITLAB_API_BASE, BITBUCKET_API_BASE,
GITEA_API_BASE, GITHUB_API_BASE.
gh CLI is used when present (no token needed);
otherwise set GITHUB_TOKEN.GITLAB_TOKEN with api scope; inside
GitLab CI, CI_JOB_TOKEN is used as a fallback so the scheduled
report job needs no extra secret.GITEA_TOKEN.BITBUCKET_TOKEN — one token
covers both.
Missing tokens never break scans: PR enrichment degrades to pure-Git
detection, and git-cleanup doctor tells you exactly what to set.
A provider implements one contract (see src/forge.mjs): PR
shape { number, title, url, headRef, isDraft, state, updatedAt, mergedAt,
ageDays } plus an optional issues capability. All consumers
(analyze, classify, report, CLI, the GitHub Action) read only that shape,
so a new forge is one provider module + one registration line.