Git Providers & Related Repos
Forge is provider-native: it resolves which git host a repository lives on and adapts its behavior — issue creation, change-request terminology, CLI — to that host. GitLab works natively, and GitHub and other hosts plug in through the same adapter layer rather than through special cases.
Why this exists
Before this layer, forge's behavior was split: issue export was GitLab-only (and hard-refused a GitHub remote), while the branch/PR skills assumed GitHub. The adapter layer unifies both behind one resolution path.
Supported providers
| Provider | CLI | Change request | Issues path |
|---|---|---|---|
| GitLab | glab | merge request | /-/issues |
| GitHub | gh | pull request | /issues |
| Gitea | tea | pull request | /issues |
| Bitbucket | — | pull request | /issues |
| Generic | — | change request | — |
Generic is the safe fallback for an unrecognized or missing remote: forge still works, it just won't attempt host-specific actions.
How the provider is resolved
repo.providerin.forge/config.yaml, if set — an explicit override.- Otherwise auto-detected from the git remote. The remote URL is parsed in all common forms — SCP-style (
git@gitlab.com:group/sub/repo.git),ssh://(including a port), andhttps://(including embedded credentials) — with GitLab subgroups and a trailing.githandled. - Self-hosted hosts are matched via the
repo.hostsmap, then by a host-name heuristic (a host containinggitlabresolves to GitLab), then Generic.
Configuration
# .forge/config.yaml
repo:
provider: "" # force gitlab|github|gitea|bitbucket|generic; empty = auto-detect
hosts:
gitlab.example.com: gitlab # self-hosted host → provider kind
related:
- name: api
path: ../api # a local sibling checkout
- name: web
url: https://github.com/acme/web
# provider: github # optional; else derived from the url/pathEvery field is optional — with no repo: section at all, forge auto-detects from the remote.
forge provider
Read-only. Shows the resolved provider for this repo and every related repo:
$ forge provider
This repository:
provider: GitLab
location: gitlab.com/acme/platform/api
cli: glab (merge requests)
Related repos:
- api GitHub git@github.com:acme/api.git
- infra GitLab https://git.acme.internal/ops/infra.gitNote infra above: a self-hosted host resolved to GitLab through repo.hosts, and api resolved to GitHub from its URL — three different hosts, one command.
forge issues create — provider-native issue export
Turns a change's outstanding tasks into issues on whichever host this repo uses:
# preview the exact commands — writes nothing
forge issues create --change add-billing --dry-run
# create them
forge issues create --change add-billing --label forgeIt reads .forge/changes/<change>/tasks.md, skips tasks already done, and creates one issue per remaining task with its task ID, status, and dependencies in the body. Omit --change and the single active change is used.
The argv comes from the resolved provider's adapter, so the same command emits:
# GitLab
glab issue create --title "Add the parser" --description "…" --label forge
# GitHub
gh issue create --title "Add the parser" --body "…" --label forgeWARNING
Forge never creates issues outside the repository's own remote. If the resolved provider has no supported CLI (Bitbucket, Generic), the command stops and points you at the host's issues URL instead of guessing.
forge deliver — open the merge/pull request
Once a change's tasks are done, forge deliver opens the change request on the same resolved host — using that host's terminology and flags:
forge deliver --dry-run # preview; writes nothing
forge deliver --label forge --base main # open it
forge deliver --draft --allow-incomplete # early draft# GitLab
glab mr create --title "Add billing" --description "…" --target-branch main
# GitHub
gh pr create --title "Add billing" --body "…" --base main- Title defaults to the change name humanized (
add-billing→ "Add billing"); override with--title. - Body is assembled from the change's
proposal.mdplus a checked task list, so a reviewer sees intent and scope without opening the repo. - Readiness gate — delivery refuses while any task is not done, listing the blockers and exiting non-zero (mirroring
forge archive).--allow-incompleteis the deliberate override, e.g. to open a draft early.
TIP
--draft is honored where the host supports it (GitLab and GitHub). Gitea has no draft flag, so it is ignored there rather than failing.
--require-ci — don't deliver on red
forge deliver --require-ci
# CI is failing on branch "main"; refusing to deliver (drop --require-ci to override)Off by default. When set, forge queries the branch's pipeline through the provider (gh run list --json status,conclusion / glab ci status) before opening anything — including under --dry-run, so the preview is honest.
It fails open by design. Reading CI status means parsing provider CLI output, which is brittle, so the gate blocks only on a confident failed:
| Verdict | Behavior |
|---|---|
| failed | refuses, non-zero exit |
| success | proceeds (CI: success on branch …) |
| pending / still running | warns, proceeds |
| unparseable, no CI support, branch unresolvable, query failed | warns, proceeds |
That asymmetry is deliberate: a harness gate that blocks on uncertainty trains people to bypass it. This one only stops you when CI is definitively red.
Related repos
repo.related makes forge aware of siblings in a multi-repo project — a frontend, a backend, an infra repo. Today each is resolved to its own provider (shown by forge provider and forge profile), which is the foundation for coordinating a single logical change across repositories.
See also
- Repo Adaptivity — the stack/provider profile forge adapts to
- Multi-Repo Workspace — siblings, and the changes that span them
- Configuration — the full
repo:reference - Team Collaboration — sharing
.forge/across a team