mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
* feat(protocol): add forge check presentation metadata Expose optional rawStatus, isManual, and requiresAction fields while keeping older check payloads backward-compatible. * fix(gitea): model Actions jobs as tasks Gitea's /actions/tasks endpoint returns individual jobs rather than workflow runs. Preserve sibling tasks from the latest execution, avoid collapsing same-name jobs, and enrich blocked tasks with run-level approval metadata. * feat(app): distinguish forge check presentation states * fix(gitlab): preserve pipeline job semantics * refactor(app): centralize forge check presentation policy Use one canonical category order, count shape, visual policy, and translated copy mapping across sidebar, hover-card, PR-pane, and GitLab pipeline surfaces. * refactor: consolidate forge check handling * feat(forge): auto-attach pasted forge references * docs(compat): correct forge rollout metadata * refactor(forge): model check nuances as traits * fix(forge): harden checks from branch review Structural Gitea Actions shadow dedup by run id (verified upstream), rerun grouping without workflow_id, shared trait/status constants and Gitea state mapping in the protocol package, all cloud host aliases in pasted-link matching, logged glab job-detail failures, concurrent approvals/pipeline fetch, checks aggregate from the same pipeline as the job list, and removal of the unread check-level rawStatus. The released pipeline-level rawStatus is staged for removal under COMPAT(pipelineRawStatus). * fix(gitea): detect fork-PR Actions approval hold as action_required gitea.com represents a fork PR awaiting Actions approval as a run in status "waiting" with no tasks and pending shadow commit statuses; it does not expose need_approval. The previous mapping only set action_required from a task with blocked+need_approval, so the gate never surfaced (0 tasks -> early return; need_approval absent). Detect the hold from /actions/runs: a waiting/blocked run whose PR is cross-repo (head.repo.id != base.repo.id), or one that does expose need_approval (self-hosted). Tag the run's pending shadow statuses action_required. Runs are only fetched when a pending shadow has no covering task, keeping the common path at two requests. Verified live against gitea.com fork PR (run 715170, waiting, cross-repo). * fix(gitea): resolve cross-fork PRs by head sha The current-PR matcher gated candidates on the checkout owner (giteaHeadOwner === expectedHeadOwner), which excludes every fork PR: its head carries the fork owner while the checkout is the base repo, so a fork PR - the only shape that can be approval-held on Gitea - never resolved and its checks never showed in-app. GitLab has no such gate (it resolves fork MRs from the target project by source branch). Accept a recent PR whose head sha equals the workspace head sha regardless of owner; the sha is an unambiguous identity, so the anti-collision owner gate still applies when no sha match is available. Verified live against gitea.com fork PR #6 (resolves + renders action_required). * fix: resolve forge rebase drift * fix: type forge auto-attach boundary * fix(i18n): add Korean check status copy --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
218 lines
7.7 KiB
TypeScript
218 lines
7.7 KiB
TypeScript
import type { ComponentType } from "react";
|
|
import type { ReactNode } from "react";
|
|
import type { z } from "zod";
|
|
import type { CheckoutPrMergeMethod, CheckoutPrStatusResponse } from "@getpaseo/protocol/messages";
|
|
import type { Theme } from "@/styles/theme";
|
|
import type { Forge } from "@/git/forge";
|
|
import type { PrPaneCheck } from "@/git/pull-request-panel/data";
|
|
|
|
export interface ForgeIconProps {
|
|
size?: number;
|
|
color?: string;
|
|
}
|
|
|
|
export type ForgeIconComponent = ComponentType<ForgeIconProps>;
|
|
|
|
export interface ForgeBrandColor {
|
|
light: string;
|
|
dark: string;
|
|
}
|
|
|
|
export type ForgeIconColorMapping = (theme: Theme) => { color: string };
|
|
|
|
export interface ForgeUrlGrammar {
|
|
/** Path infix before the branch in a tree URL, e.g. "/tree/", "/-/tree/". */
|
|
treeInfix: string;
|
|
/** Path infix before the branch in a blob URL, e.g. "/blob/", "/-/blob/". */
|
|
blobInfix: string;
|
|
/** Line/range anchor, e.g. github "#L1-L5" vs gitlab "#L1-5". */
|
|
lineAnchor: (start: number, end?: number) => string;
|
|
/** Suffix for the change request's checks/pipelines overview. */
|
|
changeRequestChecksSuffix?: string;
|
|
/**
|
|
* Web-path infixes used when recognizing pasted links for this forge. The
|
|
* parser anchors each infix after the current remote's full repository path,
|
|
* so links for another repository are never auto-attached.
|
|
*/
|
|
referencePaths?: readonly ForgeReferencePath[];
|
|
}
|
|
|
|
export interface ForgeReferencePath {
|
|
kind: "change_request" | "issue";
|
|
/** Path between the repository path and the numeric local id. */
|
|
infix: string;
|
|
}
|
|
|
|
/** Line/range anchor shared by every GitHub-family forge (github, gitea, forgejo, codeberg). */
|
|
export function GITHUB_LINE_ANCHOR(start: number, end?: number): string {
|
|
return end && end > start ? `#L${start}-L${end}` : `#L${start}`;
|
|
}
|
|
|
|
/** URL grammar shared by the gitea-family forges (gitea, forgejo, codeberg). */
|
|
export const GITEA_FAMILY_URL_GRAMMAR: ForgeUrlGrammar = {
|
|
treeInfix: "/src/branch/",
|
|
blobInfix: "/src/branch/",
|
|
lineAnchor: GITHUB_LINE_ANCHOR,
|
|
referencePaths: [
|
|
{ kind: "change_request", infix: "/pulls/" },
|
|
{ kind: "issue", infix: "/issues/" },
|
|
],
|
|
};
|
|
|
|
export type ForgeSpecificEnvelope = { forge: string } & Record<string, unknown>;
|
|
|
|
type CheckoutPrStatus = NonNullable<CheckoutPrStatusResponse["payload"]["status"]>;
|
|
|
|
export type LegacyGithubMergeFacts = NonNullable<CheckoutPrStatus["github"]>;
|
|
|
|
export interface MergeCapability {
|
|
/** The change request can be merged directly right now. */
|
|
directMergeReady: boolean;
|
|
/** Auto-merge can be enabled right now. */
|
|
canEnableAutoMerge: boolean;
|
|
/** Auto-merge is already enabled on the change request. */
|
|
autoMergeEnabled: boolean;
|
|
/** The viewer is allowed to disable the active auto-merge. */
|
|
canDisableAutoMerge: boolean;
|
|
/** A merge queue is blocking both direct merge and auto-merge. */
|
|
mergeBlockedByQueue: boolean;
|
|
/** Merge methods the forge permits for this change request. */
|
|
allowedMethods: CheckoutPrMergeMethod[];
|
|
/** The forge's preferred/default merge method, if it reports one. */
|
|
preferredMethod: CheckoutPrMergeMethod | null;
|
|
}
|
|
|
|
export interface PaneChecksSlotContext {
|
|
serverId: string;
|
|
cwd: string;
|
|
/** Change request (PR/MR) number, so a section can address its head pipeline. */
|
|
changeRequestNumber: number;
|
|
open: boolean;
|
|
onToggle: () => void;
|
|
/** Whether the daemon advertises pluggable forge support (gates rich sections). */
|
|
enabled: boolean;
|
|
/**
|
|
* Whether the daemon can serve forge check/pipeline details over the
|
|
* forge-routed RPC. Single source of truth for gating on-demand detail
|
|
* fetches so a section never reaches an RPC the daemon lacks.
|
|
*/
|
|
canFetchCheckDetails: boolean;
|
|
}
|
|
|
|
export interface PaneNativeContribution {
|
|
guard: (facts: unknown) => boolean;
|
|
renderHeaderMeta: (facts: unknown) => ReactNode;
|
|
renderChecksSection: (facts: unknown, ctx: PaneChecksSlotContext) => ReactNode;
|
|
}
|
|
|
|
export interface NativeFallbackCheckEntry {
|
|
contribute: (facts: unknown, status: CheckoutPrStatus, forge: Forge) => PrPaneCheck | null;
|
|
}
|
|
|
|
interface TypedPaneContribution<TFacts extends ForgeSpecificEnvelope> {
|
|
renderHeaderMeta?: (facts: TFacts) => ReactNode;
|
|
renderChecksSection?: (facts: TFacts, ctx: PaneChecksSlotContext) => ReactNode;
|
|
}
|
|
|
|
interface TypedNativeFallbackCheck<TFacts extends ForgeSpecificEnvelope> {
|
|
contribute: (facts: TFacts, status: CheckoutPrStatus, forge: Forge) => PrPaneCheck | null;
|
|
}
|
|
|
|
export interface ClientForgeFactsEntry<TFacts extends ForgeSpecificEnvelope> {
|
|
readonly family: TFacts["forge"];
|
|
parse: (facts: unknown) => TFacts | null;
|
|
deriveMergeCapability: (facts: unknown) => MergeCapability | null;
|
|
readonly nativeFallbackChecks: readonly NativeFallbackCheckEntry[];
|
|
}
|
|
|
|
export interface ClientForgeFactsRegistration<TFacts extends ForgeSpecificEnvelope> {
|
|
readonly family: TFacts["forge"];
|
|
readonly schema: z.ZodType<TFacts>;
|
|
readonly deriveMergeCapability?: (facts: TFacts) => MergeCapability;
|
|
readonly nativeFallbackChecks?: readonly NativeFallbackCheckEntry[];
|
|
}
|
|
|
|
function parseFacts<TFacts extends ForgeSpecificEnvelope>(
|
|
schema: z.ZodType<TFacts>,
|
|
facts: unknown,
|
|
): TFacts | null {
|
|
if (!facts) {
|
|
return null;
|
|
}
|
|
const result = schema.safeParse(facts);
|
|
return result.success ? result.data : null;
|
|
}
|
|
|
|
export function defineNativeFallbackCheck<TFacts extends ForgeSpecificEnvelope>(
|
|
schema: z.ZodType<TFacts>,
|
|
contribution: TypedNativeFallbackCheck<TFacts>,
|
|
): NativeFallbackCheckEntry {
|
|
return {
|
|
contribute: (facts, status, forge) => {
|
|
const parsed = parseFacts(schema, facts);
|
|
return parsed ? contribution.contribute(parsed, status, forge) : null;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function definePaneContribution<TFacts extends ForgeSpecificEnvelope>(
|
|
schema: z.ZodType<TFacts>,
|
|
contribution: TypedPaneContribution<TFacts>,
|
|
): PaneNativeContribution {
|
|
return {
|
|
guard: (facts) => parseFacts(schema, facts) !== null,
|
|
renderHeaderMeta: (facts) => {
|
|
const parsed = parseFacts(schema, facts);
|
|
return parsed && contribution.renderHeaderMeta ? contribution.renderHeaderMeta(parsed) : null;
|
|
},
|
|
renderChecksSection: (facts, ctx) => {
|
|
const parsed = parseFacts(schema, facts);
|
|
return parsed && contribution.renderChecksSection
|
|
? contribution.renderChecksSection(parsed, ctx)
|
|
: null;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function defineForgeFacts<TFacts extends ForgeSpecificEnvelope>(
|
|
registration: ClientForgeFactsRegistration<TFacts>,
|
|
): ClientForgeFactsEntry<TFacts> {
|
|
return {
|
|
family: registration.family,
|
|
parse: (facts) => parseFacts(registration.schema, facts),
|
|
deriveMergeCapability: (facts) => {
|
|
if (!registration.deriveMergeCapability) {
|
|
return null;
|
|
}
|
|
const parsed = parseFacts(registration.schema, facts);
|
|
return parsed ? registration.deriveMergeCapability(parsed) : null;
|
|
},
|
|
nativeFallbackChecks: registration.nativeFallbackChecks ?? [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Pure logic half of a forge: URL grammar and runtime-facts derivations. Kept
|
|
* free of any React/React-Native imports so logic consumers (URL builders,
|
|
* merge-capability, native-check fallbacks) — and the Node-based e2e harness
|
|
* that transitively imports them — never pull the client's rendering stack.
|
|
*/
|
|
export interface ClientForgeLogicModule<
|
|
TFacts extends ForgeSpecificEnvelope = ForgeSpecificEnvelope,
|
|
> {
|
|
readonly id: string;
|
|
readonly urlGrammar?: ForgeUrlGrammar;
|
|
readonly facts?: ClientForgeFactsEntry<TFacts>;
|
|
}
|
|
|
|
/**
|
|
* View half of a forge: the brand mark, brand color, and PR-pane render
|
|
* contributions. Imported only by rendering code (icon lookup, the PR pane).
|
|
*/
|
|
export interface ClientForgeViewModule {
|
|
readonly id: string;
|
|
readonly icon: ForgeIconComponent;
|
|
readonly brandColor?: ForgeBrandColor | null;
|
|
readonly paneContributions?: readonly PaneNativeContribution[];
|
|
}
|