1import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl";
2import {
3 type CommandId,
4 pullRequestHostOf,
5 type GitRunStackedActionResult,
6 type OrchestrationProjectShell,
7 type SourceControlProviderKind,
8 type ThreadId,
9} from "@t3tools/contracts";
10import { parseChangeRequestUrl } from "@t3tools/shared/changeRequestUrl";
11import * as Cause from "effect/Cause";
12import * as Effect from "effect/Effect";
13import * as Option from "effect/Option";
14
15import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
16import * as ProjectService from "../project/ProjectService.ts";
17
18export interface CreatedPullRequestKey {
19 readonly host: string;
20 readonly repository: string;
21 readonly number: number;
22 readonly url: string;
23}
24
25/**
26 * The identity a stacked action's pull request should be linked under, or
27 * null when the action did not leave one behind. Reading the host and
28 * repository from the URL keeps the link host-level even when the checkout's
29 * remote differs from where the PR was opened (a fork, say); the project is
30 * only consulted when the URL is one this cannot read.
31 */
32export function createdPullRequestKey(
33 result: Pick<GitRunStackedActionResult, "pr">,
34 project: OrchestrationProjectShell | undefined,
35): CreatedPullRequestKey | null {
36 const { status, number, url } = result.pr;
37 if ((status !== "created" && status !== "opened_existing") || number === undefined || !url) {
38 return null;
39 }
40 const parsed = parseChangeRequestUrl(url);
41 if (parsed !== null) return { ...parsed, url };
42 const identity = project?.repositoryIdentity;
43 const kind = identity?.provider as SourceControlProviderKind | undefined;
44 const repository = sourceControlRepositorySelector(identity);
45 if (!identity || kind === undefined || repository === null) return null;
46 return {
47 host: pullRequestHostOf(identity, kind),
48 repository: repository.toLowerCase(),
49 number,
50 url,
51 };
52}
53
54/**
55 * Links the pull request a `create_pr`-shaped action produced to the thread it
56 * ran beside. Never fails: the git action already succeeded and its result is
57 * on its way to the client, so a link that cannot be made is logged and
58 * dropped. A duplicate link is the decider saying the thread already knew.
59 */
60export const linkCreatedPullRequest = <E>(input: {
61 readonly threadId: ThreadId;
62 readonly result: Pick<GitRunStackedActionResult, "pr">;
63 readonly commandId: Effect.Effect<CommandId, E>;
64}): Effect.Effect<void, never, Orchestrator.OrchestratorV2 | ProjectService.ProjectService> =>
65 Effect.gen(function* () {
66 const engine = yield* Orchestrator.OrchestratorV2;
67 const projects = yield* ProjectService.ProjectService;
68 const thread = yield* engine
69 .getThreadShell(input.threadId)
70 .pipe(Effect.map(Option.fromNullishOr));
71 if (Option.isNone(thread)) return;
72 const project = Option.getOrUndefined(yield* projects.getShell(thread.value.projectId));
73 const key = createdPullRequestKey(input.result, project);
74 if (key === null) return;
75 const commandId = yield* input.commandId;
76 yield* engine
77 .dispatch({
78 type: "thread.pull-request.link",
79 commandId,
80 threadId: input.threadId,
81 ...key,
82 source: "created",
83 })
84 .pipe(Effect.asVoid);
85 }).pipe(
86 Effect.withSpan("linkCreatedPullRequest"),
87 Effect.catchCause((cause) =>
88 Cause.hasInterruptsOnly(cause)
89 ? Effect.failCause(cause as Cause.Cause<never>)
90 : Effect.logWarning("failed to link created pull request to thread", {
91 threadId: input.threadId,
92 }),
93 ),
94 );