Skip to content
Download PDF

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 ​

ProviderCLIChange requestIssues path
GitLabglabmerge request/-/issues
GitHubghpull request/issues
Giteateapull 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 ​

  1. repo.provider in .forge/config.yaml, if set — an explicit override.
  2. 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), and https:// (including embedded credentials) — with GitLab subgroups and a trailing .git handled.
  3. Self-hosted hosts are matched via the repo.hosts map, then by a host-name heuristic (a host containing gitlab resolves to GitLab), then Generic.

Configuration ​

yaml
# .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/path

Every 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:

sh
$ 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.git

Note 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:

sh
# preview the exact commands — writes nothing
forge issues create --change add-billing --dry-run

# create them
forge issues create --change add-billing --label forge

It 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:

sh
# GitLab
glab issue create --title "Add the parser" --description "…" --label forge
# GitHub
gh issue create   --title "Add the parser" --body "…"        --label forge

WARNING

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:

sh
forge deliver --dry-run                    # preview; writes nothing
forge deliver --label forge --base main    # open it
forge deliver --draft --allow-incomplete   # early draft
sh
# 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.md plus 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-incomplete is 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 ​

sh
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:

VerdictBehavior
failedrefuses, non-zero exit
successproceeds (CI: success on branch …)
pending / still runningwarns, proceeds
unparseable, no CI support, branch unresolvable, query failedwarns, 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.

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 ​