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
+75 -3
View File
@@ -40,7 +40,9 @@ import type {
CheckoutPrCreateResponse,
CheckoutPrMergeResponse,
CheckoutPrMergeMethod,
CheckoutForgeSetAutoMergeResponse,
CheckoutGithubSetAutoMergeResponse,
CheckoutForgeGetCheckDetailsResponse,
CheckoutGithubGetCheckDetailsResponse,
CheckoutPrStatusResponse,
PullRequestTimelineResponse,
@@ -50,6 +52,8 @@ import type {
StashListResponse,
ValidateBranchResponse,
BranchSuggestionsResponse,
ForgeSearchResponse,
ForgeSearchRequest,
GitHubSearchResponse,
GitHubSearchRequest,
DirectorySuggestionsResponse,
@@ -342,6 +346,7 @@ export interface CreatePaseoWorktreeInput extends Pick<
| "firstAgentContext"
| "refName"
| "action"
| "checkoutSource"
| "githubPrNumber"
> {}
@@ -359,7 +364,9 @@ type CheckoutPushPayload = CheckoutPushResponse["payload"];
type CheckoutRefreshPayload = CheckoutRefreshResponse["payload"];
type CheckoutPrCreatePayload = CheckoutPrCreateResponse["payload"];
type CheckoutPrMergePayload = CheckoutPrMergeResponse["payload"];
type CheckoutForgeSetAutoMergePayload = CheckoutForgeSetAutoMergeResponse["payload"];
type CheckoutGithubSetAutoMergePayload = CheckoutGithubSetAutoMergeResponse["payload"];
type CheckoutForgeGetCheckDetailsPayload = CheckoutForgeGetCheckDetailsResponse["payload"];
type CheckoutGithubGetCheckDetailsPayload = CheckoutGithubGetCheckDetailsResponse["payload"];
type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"];
type PullRequestTimelinePayload = PullRequestTimelineResponse["payload"];
@@ -370,6 +377,7 @@ type StashPopPayload = StashPopResponse["payload"];
type StashListPayload = StashListResponse["payload"];
type ValidateBranchPayload = ValidateBranchResponse["payload"];
type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"];
type ForgeSearchPayload = ForgeSearchResponse["payload"];
type GitHubSearchPayload = GitHubSearchResponse["payload"];
type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"];
type PaseoWorktreeListPayload = PaseoWorktreeListResponse["payload"];
@@ -3579,6 +3587,23 @@ export class DaemonClient {
});
}
async checkoutForgeSetAutoMerge(
cwd: string,
input: { enabled: true; method: CheckoutPrMergeMethod } | { enabled: false },
requestId?: string,
): Promise<CheckoutForgeSetAutoMergePayload> {
return this.sendNamespacedCorrelatedSessionRequest<"checkout.forge.set_auto_merge.response">({
requestId,
message: {
type: "checkout.forge.set_auto_merge.request",
cwd,
enabled: input.enabled,
...(input.enabled ? { mergeMethod: input.method } : {}),
},
timeout: 60000,
});
}
async checkoutGithubSetAutoMerge(
cwd: string,
input: { enabled: true; method: CheckoutPrMergeMethod } | { enabled: false },
@@ -3595,12 +3620,40 @@ export class DaemonClient {
});
}
async checkoutForgeGetCheckDetails(
input: {
cwd: string;
repoOwner?: string;
repoName?: string;
checkRunId?: number;
workflowRunId?: number;
changeRequestNumber?: number;
},
requestId?: string,
): Promise<CheckoutForgeGetCheckDetailsPayload> {
return this.sendNamespacedCorrelatedSessionRequest<"checkout.forge.get_check_details.response">(
{
requestId,
message: {
type: "checkout.forge.get_check_details.request",
cwd: input.cwd,
repoOwner: input.repoOwner,
repoName: input.repoName,
checkRunId: input.checkRunId,
workflowRunId: input.workflowRunId,
changeRequestNumber: input.changeRequestNumber,
},
timeout: 60000,
},
);
}
async checkoutGithubGetCheckDetails(
input: {
cwd: string;
repoOwner: string;
repoName: string;
checkRunId: number;
repoOwner?: string;
repoName?: string;
checkRunId?: number;
workflowRunId?: number;
},
requestId?: string,
@@ -3775,6 +3828,7 @@ export class DaemonClient {
: {}),
...(input.refName !== undefined ? { refName: input.refName } : {}),
...(input.action !== undefined ? { action: input.action } : {}),
...(input.checkoutSource !== undefined ? { checkoutSource: input.checkoutSource } : {}),
...(input.githubPrNumber !== undefined ? { githubPrNumber: input.githubPrNumber } : {}),
},
responseType: "create_paseo_worktree_response",
@@ -3834,6 +3888,24 @@ export class DaemonClient {
});
}
async searchForge(
options: { cwd: string; query: string; limit?: number; kinds?: ForgeSearchRequest["kinds"] },
requestId?: string,
): Promise<ForgeSearchPayload> {
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "forge.search.request",
cwd: options.cwd,
query: options.query,
limit: options.limit,
kinds: options.kinds,
},
responseType: "forge.search.response",
timeout: 15000,
});
}
async searchGitHub(
options: { cwd: string; query: string; limit?: number; kinds?: GitHubSearchRequest["kinds"] },
requestId?: string,