* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
7.7 KiB
Adding a Git Forge to Paseo
Paseo's forge layer is a registry/manifest system. A forge is a runtime concern: shared protocol messages carry neutral/open facts, the server adapter owns behavior, and the app owns bundled presentation/runtime interpretation.
The maintainer litmus test is the rule of thumb:
Adding a new forge means adding files in a new directory/module that implement an interface, plus one entry in the centralized registry/manifest for that package.
The Three Registrations
For forge acme, the expected end state is:
-
Protocol manifest - optional, only when the forge should be presented by shared manifest data. Add one
ForgeDefinitiontopackages/protocol/src/forge-manifest.ts. -
Server adapter - add
packages/server/src/services/acme-service.tsimplementingForgeService, any adapter-owned fact types/guards/constants beside it, and onedefaultForgeRegistryentry inpackages/server/src/services/forge-registry.ts. -
App modules - a forge splits into a pure logic half and a view half so logic consumers (URL builders, merge-capability, native checks, and the Node-based e2e harness) never pull the client rendering stack:
packages/app/src/git/forges/acme.ts- logic:id, optionalurlGrammar, optionalfacts(schema, merge-capability, native-check fallbacks). No React/React-Native imports. Register inCLIENT_FORGE_LOGIC_MODULESinpackages/app/src/git/forges/index.ts.packages/app/src/git/forges/acme.view.tsx- view:icon(SVG component underpackages/app/src/components/icons/), optionalbrandColor, optionalpaneContributions. Register inCLIENT_FORGE_VIEW_MODULESinpackages/app/src/git/forges/view.ts.
There should be no protocol typed-union arm, no central app icon/color/url/facts map, and no central server union of known forge facts.
Protocol
forgeSpecific on PR status is an open envelope:
z.object({ forge: z.string() }).passthrough();
The forgeSpecific.forge field is a facts-family tag, not the workspace
brand id. Gitea, Forgejo, and Codeberg can all emit forgeSpecific.forge === "gitea" when they share the same facts shape, while top-level status.forge
keeps the brand id ("gitea", "forgejo", "codeberg").
Protocol does not validate per-forge fact fields. Consumers that understand a facts family validate at runtime with their own schema/guard. Unknown or schema-mismatched facts render neutrally instead of failing the whole message parse. This is the version-skew win: an old client can receive facts from a newer forge and still show the PR/MR in a neutral state.
Shipped GitHub compatibility stays separate:
status.githubremains accepted for released peers.- The server keeps the
COMPAT(forgeSpecific)mirror that copies GitHub facts intostatus.githubfor older clients. - Do not add a compatibility shim unless a released peer (<= 0.1.102) can actually produce the state.
Server
The server-wide status type only promises:
type ForgeSpecificStatusFacts = { forge: string } & Record<string, unknown>;
Adapter-owned files define the typed shapes and guards, for example
github-facts.ts, gitlab-facts.ts, and gitea-facts.ts. The adapter can keep
strong internal types for construction and command guards, but shared server
code must not grow a central list of forge fact arms.
Register the adapter in defaultForgeRegistry with:
createServicematchesHostfrom manifestcloudHostsprobeHostwhen self-hosted/Enterprise detection is supported
Cloud hosts in the manifest are a bounded public-host list, not a self-host allowlist. Self-hosted detection is a trust gate: Paseo only talks to a forge host that is either a known cloud host or one the CLI is already authenticated to. Adapter probes must not make anonymous HTTP requests to remote-derived hosts, and adapters must not route credentials to an unauthenticated host.
App
Each app forge splits into two modules so pure logic never imports the client rendering stack:
acme.ts exports a ClientForgeLogicModule:
id- optional
urlGrammar - optional
factsregistration (schema, merge-capability, native-check fallbacks)
acme.view.tsx exports a ClientForgeViewModule:
idiconbrandColor(nullfor neutral; GitHub intentionally usesnull)- optional
paneContributions
Two registries live under packages/app/src/git/forges/:
CLIENT_FORGE_LOGIC_MODULES (index.ts) drives URL grammar, merge-capability
derivation, and native fallback checks; CLIENT_FORGE_VIEW_MODULES (view.ts)
drives icon/color lookup and PR-pane contributions. Logic consumers must import
the logic registry only — importing the view registry (or a .view.tsx module)
from a logic path pulls react-native and breaks the Node-based e2e harness.
Per-forge brand colors live on the module, not in styles/theme.ts. Use the
Unistyles-safe pattern from docs/unistyles.md: no useUnistyles(). Brand icon
call sites use withUnistyles and a uniProps mapping such as:
(theme) => ({ color: theme.colorScheme === "light" ? colors.light : colors.dark });
Facts modules use one source of truth: a Zod schema. Helpers like
defineForgeFacts, defineNativeFallbackCheck, and definePaneContribution
derive guards from schema.safeParse and re-parse before invoking typed
derivers/renderers. That keeps typed derivers away from the open wire envelope.
Checklist
To add acme:
- Add
acmetoFORGE_DEFINITIONSif the shared manifest should know its label, nouns, icon kind, sign-in CLI, or cloud hosts. - Add
acme-service.tsimplementingForgeService. - Add
acme-facts.tsbeside the adapter if it reports native facts. - Add one
defaultForgeRegistryentry. - Add
packages/app/src/git/forges/acme.ts(logic) andpackages/app/src/git/forges/acme.view.tsx(view). - Add one
CLIENT_FORGE_LOGIC_MODULESentry (index.ts) and oneCLIENT_FORGE_VIEW_MODULESentry (view.ts). - Add/update the icon component only if the client bundle should show a brand mark.
- If the forge's CI/data model does not fit an existing required
ForgeServicefield, widen the shared interface (plus the protocol schema and its guards) instead of faking a value — e.g. Gitea Actions runs carry no check-run id, soGetCheckDetailsOptions.checkRunIdbecame optional withworkflowRunIdas the alternative address. Expect this step to touchforge-service.ts,messages.ts, and the call-site guards of the other adapters. Widening a shared field is not forge-local: it also affects the already-shipped forges/GitHub call sites and the capability-gated RPC (e.g.forgeCheckDetails), so verify every consumer rather than assuming the change only reaches the new adapter. - Run targeted tests: manifest/registry/resolver, the adapter test, protocol checkout PR schema, app forge URL/presentation tests, app merge capability, and any PR-pane native data tests touched.
Run npm run typecheck after each implementation slice. If protocol or client
declarations are stale, run npm run build:client; if server/CLI declarations
are stale, run npm run build:server.
Gotchas
- GitHub is a normal registry entry plus released compatibility shims. Keep all
real shims tagged with
COMPAT(name). - Gitea-family facts use
forgeSpecific.forge === "gitea"even when the top-level brand is Forgejo or Codeberg. - Brand icons are bundled React components, so they cannot come from protocol manifest data.
- Source URL grammars are app-side because blob/tree path syntax is forge-specific. If a forge has no grammar, omit the "Open on ..." source link rather than constructing a wrong URL.
- GitLab pipeline status constants belong to the GitLab adapter/client module, not protocol.