feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)

* 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>
This commit is contained in:
nllptrx
2026-07-17 15:03:26 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 04d1ebdce0
commit a8ebd390fa
187 changed files with 20988 additions and 2907 deletions
@@ -42,7 +42,7 @@ import { WorktreeRequestError } from "./worktree-errors.js";
import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js";
import type { GeneratedWorkspaceName } from "./worktree-branch-name-generator.js";
import { WorkspaceAutoName } from "./workspace-auto-name.js";
import type { GitHubService } from "../services/github-service.js";
import type { ForgeService } from "../services/forge-service.js";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import {
asSessionLogger,
@@ -370,7 +370,7 @@ function createWorkspaceRuntimeSnapshot(
cwd: string,
overrides?: {
git?: Partial<WorkspaceGitRuntimeSnapshot["git"]>;
github?: Partial<WorkspaceGitRuntimeSnapshot["github"]>;
forge?: Partial<WorkspaceGitRuntimeSnapshot["forge"]>;
},
): WorkspaceGitRuntimeSnapshot {
const base: WorkspaceGitRuntimeSnapshot = {
@@ -390,7 +390,7 @@ function createWorkspaceRuntimeSnapshot(
hasRemote: true,
diffStat: { additions: 1, deletions: 0 },
},
github: {
forge: {
featuresEnabled: true,
pullRequest: {
url: "https://github.com/acme/repo/pull/123",
@@ -410,17 +410,17 @@ function createWorkspaceRuntimeSnapshot(
...base.git,
...overrides?.git,
},
github: {
...base.github,
...overrides?.github,
forge: {
...base.forge,
...overrides?.forge,
pullRequest:
overrides?.github && "pullRequest" in overrides.github
? (overrides.github.pullRequest ?? null)
: base.github.pullRequest,
overrides?.forge && "pullRequest" in overrides.forge
? (overrides.forge.pullRequest ?? null)
: base.forge.pullRequest,
error:
overrides?.github && "error" in overrides.github
? (overrides.github.error ?? null)
: base.github.error,
overrides?.forge && "error" in overrides.forge
? (overrides.forge.error ?? null)
: base.forge.error,
},
};
}
@@ -532,7 +532,7 @@ function createSessionForWorkspaceTests(
terminalManager?: TerminalManager | null;
projectRegistry?: SessionOptions["projectRegistry"];
workspaceRegistry?: SessionOptions["workspaceRegistry"];
github?: GitHubService;
github?: ForgeService;
paseoHome?: string;
worktreesRoot?: string;
renameCurrentBranch?: (
@@ -668,6 +668,7 @@ function createSessionForWorkspaceTests(
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -822,6 +823,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -945,6 +947,7 @@ test("create_agent_request does not title an existing workspace from the agent p
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -1327,6 +1330,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients"
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -1687,6 +1691,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -1872,6 +1877,7 @@ test("close_items_request archives stored agents that are not currently loaded",
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -2019,6 +2025,7 @@ test("close_items_request continues after an archive failure", async () => {
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -3013,6 +3020,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
}),
scheduleRefreshForCwd: () => {},
onWorkspaceStateMayHaveChanged: () => {},
invalidateForge: () => {},
getMetrics: () => ({
checkoutDiffTargetCount: 0,
checkoutDiffSubscriptionCount: 0,
@@ -5338,7 +5346,7 @@ test("archive_workspace_request archives a worktree-kind workspace and removes t
hasRemote: false,
diffStat: null,
},
github: {
forge: {
featuresEnabled: false,
pullRequest: null,
error: null,
@@ -6190,7 +6198,7 @@ test("fetch_workspaces_response reads runtime fields from passive workspace git
aheadOfOrigin: 3,
behindOfOrigin: 1,
},
github: {
forge: {
pullRequest: {
url: "https://github.com/acme/repo/pull/456",
title: "Ship runtime payloads",
@@ -6357,7 +6365,7 @@ test("workspace_update includes updated runtime fields", async () => {
currentBranch: "feature/runtime-payloads",
isDirty: true,
},
github: {
forge: {
pullRequest: {
url: "https://github.com/acme/repo/pull/789",
title: "Updated runtime payloads",
@@ -7350,7 +7358,7 @@ function createWorkspaceCreatePrRepo(): WorkspaceCreatePrRepoFixture {
return { tempDir, repoDir, paseoHome, headRef, prFileName, prNumber };
}
function createPrCheckoutGitHubService(params: { headRef: string }): GitHubService {
function createPrCheckoutGitHubService(params: { headRef: string }): ForgeService {
return {
listPullRequests: async () => [],
listIssues: async () => [],
@@ -7375,6 +7383,10 @@ function createPrCheckoutGitHubService(params: { headRef: string }): GitHubServi
headRepositoryUrl: null,
isCrossRepository: false,
}),
defaultCheckoutRefs: ({ changeRequestNumber }) => [
{ remoteName: "origin", remoteRef: `refs/pull/${changeRequestNumber}/head` },
],
buildPrLocalBranchName: ({ headRef }) => headRef,
getCurrentPullRequestStatus: async () => null,
getPullRequestTimeline: async ({ prNumber }) => ({
prNumber,
@@ -7384,7 +7396,7 @@ function createPrCheckoutGitHubService(params: { headRef: string }): GitHubServi
truncated: false,
error: null,
}),
getGitHubCheckDetails: async ({ checkRunId, workflowRunId }) => ({
getCheckDetails: async ({ checkRunId, workflowRunId }) => ({
checkRunId,
workflowRunId: workflowRunId ?? null,
name: "test",
@@ -7397,7 +7409,11 @@ function createPrCheckoutGitHubService(params: { headRef: string }): GitHubServi
failedJobs: [],
truncated: false,
}),
searchIssuesAndPrs: async () => ({ items: [], githubFeaturesEnabled: true }),
searchIssuesAndPrs: async () => ({
items: [],
featuresEnabled: true,
githubFeaturesEnabled: true,
}),
createPullRequest: async () => ({ number: 1, url: "https://github.com/acme/repo/pull/1" }),
mergePullRequest: async () => ({ success: true }),
enablePullRequestAutoMerge: async () => ({ success: true }),