Skip to content

Commit d0f0c27

Browse files
committed
feat(code): unified PR creation workflow
1 parent 350c434 commit d0f0c27

19 files changed

Lines changed: 1224 additions & 139 deletions
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { getGitOperationManager } from "@posthog/git/operation-manager";
2+
import { getHeadSha } from "@posthog/git/queries";
3+
import { Saga, type SagaLogger } from "@posthog/shared";
4+
import type { LlmCredentials } from "../llm-gateway/schemas";
5+
import type {
6+
ChangedFile,
7+
CommitOutput,
8+
CreatePrFlowProgressPayload,
9+
CreatePrOutput,
10+
GitSyncStatus,
11+
PublishOutput,
12+
PushOutput,
13+
} from "./schemas";
14+
15+
export interface CreatePrFlowSagaInput {
16+
directoryPath: string;
17+
branchName?: string;
18+
commitMessage?: string;
19+
prTitle?: string;
20+
prBody?: string;
21+
draft?: boolean;
22+
credentials?: LlmCredentials;
23+
}
24+
25+
export interface CreatePrFlowSagaOutput {
26+
prUrl: string | null;
27+
}
28+
29+
export interface CreatePrFlowDeps {
30+
getCurrentBranch(dir: string): Promise<string | null>;
31+
createBranch(dir: string, name: string): Promise<void>;
32+
checkoutBranch(
33+
dir: string,
34+
name: string,
35+
): Promise<{ previousBranch: string; currentBranch: string }>;
36+
getChangedFilesHead(dir: string): Promise<ChangedFile[]>;
37+
generateCommitMessage(
38+
dir: string,
39+
credentials: LlmCredentials,
40+
): Promise<{ message: string }>;
41+
commit(dir: string, message: string): Promise<CommitOutput>;
42+
getSyncStatus(dir: string): Promise<GitSyncStatus>;
43+
push(dir: string): Promise<PushOutput>;
44+
publish(dir: string): Promise<PublishOutput>;
45+
generatePrTitleAndBody(
46+
dir: string,
47+
credentials: LlmCredentials,
48+
): Promise<{ title: string; body: string }>;
49+
createPr(
50+
dir: string,
51+
title?: string,
52+
body?: string,
53+
draft?: boolean,
54+
): Promise<CreatePrOutput>;
55+
onProgress(
56+
step: CreatePrFlowProgressPayload["step"],
57+
message: string,
58+
prUrl?: string,
59+
): void;
60+
}
61+
62+
export class CreatePrFlowSaga extends Saga<
63+
CreatePrFlowSagaInput,
64+
CreatePrFlowSagaOutput
65+
> {
66+
readonly sagaName = "CreatePrFlowSaga";
67+
private deps: CreatePrFlowDeps;
68+
69+
constructor(deps: CreatePrFlowDeps, logger?: SagaLogger) {
70+
super(logger);
71+
this.deps = deps;
72+
}
73+
74+
protected async execute(
75+
input: CreatePrFlowSagaInput,
76+
): Promise<CreatePrFlowSagaOutput> {
77+
const { directoryPath, draft, credentials } = input;
78+
let { commitMessage, prTitle, prBody } = input;
79+
80+
// Step 1: Create branch (if needed)
81+
if (input.branchName) {
82+
this.deps.onProgress(
83+
"creating-branch",
84+
`Creating branch ${input.branchName}...`,
85+
);
86+
87+
const originalBranch = await this.readOnlyStep(
88+
"get-original-branch",
89+
() => this.deps.getCurrentBranch(directoryPath),
90+
);
91+
92+
await this.step({
93+
name: "creating-branch",
94+
execute: () => this.deps.createBranch(directoryPath, input.branchName!),
95+
rollback: async () => {
96+
if (originalBranch) {
97+
await this.deps.checkoutBranch(directoryPath, originalBranch);
98+
}
99+
},
100+
});
101+
}
102+
103+
// Step 2: Commit (if there are uncommitted changes)
104+
const changedFiles = await this.readOnlyStep("check-changes", () =>
105+
this.deps.getChangedFilesHead(directoryPath),
106+
);
107+
108+
if (changedFiles.length > 0) {
109+
// Generate commit message if not provided
110+
if (!commitMessage && credentials) {
111+
this.deps.onProgress("committing", "Generating commit message...");
112+
const generated = await this.readOnlyStep(
113+
"generate-commit-message",
114+
async () => {
115+
try {
116+
return await this.deps.generateCommitMessage(
117+
directoryPath,
118+
credentials,
119+
);
120+
} catch {
121+
return null;
122+
}
123+
},
124+
);
125+
if (generated) commitMessage = generated.message;
126+
}
127+
128+
if (!commitMessage) {
129+
throw new Error("Commit message is required.");
130+
}
131+
132+
this.deps.onProgress("committing", "Committing changes...");
133+
134+
const preCommitSha = await this.readOnlyStep("get-pre-commit-sha", () =>
135+
getHeadSha(directoryPath),
136+
);
137+
138+
await this.step({
139+
name: "committing",
140+
execute: async () => {
141+
const result = await this.deps.commit(directoryPath, commitMessage!);
142+
if (!result.success) throw new Error(result.message);
143+
return result;
144+
},
145+
rollback: async () => {
146+
const manager = getGitOperationManager();
147+
await manager.executeWrite(directoryPath, (git) =>
148+
git.reset(["--soft", preCommitSha]),
149+
);
150+
},
151+
});
152+
}
153+
154+
// Step 3: Push
155+
this.deps.onProgress("pushing", "Pushing to remote...");
156+
157+
const syncStatus = await this.readOnlyStep("check-sync-status", () =>
158+
this.deps.getSyncStatus(directoryPath),
159+
);
160+
161+
await this.step({
162+
name: "pushing",
163+
execute: async () => {
164+
const result = syncStatus.hasRemote
165+
? await this.deps.push(directoryPath)
166+
: await this.deps.publish(directoryPath);
167+
if (!result.success) throw new Error(result.message);
168+
return result;
169+
},
170+
// Push can't be meaningfully rolled back without force-push
171+
rollback: async () => {},
172+
});
173+
174+
// Step 4: Generate PR title/body if not provided
175+
if ((!prTitle || !prBody) && credentials) {
176+
this.deps.onProgress("creating-pr", "Generating PR description...");
177+
const generated = await this.readOnlyStep(
178+
"generate-pr-description",
179+
async () => {
180+
try {
181+
return await this.deps.generatePrTitleAndBody(
182+
directoryPath,
183+
credentials,
184+
);
185+
} catch {
186+
return null;
187+
}
188+
},
189+
);
190+
if (generated) {
191+
if (!prTitle) prTitle = generated.title;
192+
if (!prBody) prBody = generated.body;
193+
}
194+
}
195+
196+
// Step 5: Create PR
197+
this.deps.onProgress("creating-pr", "Creating pull request...");
198+
199+
const prResult = await this.step({
200+
name: "creating-pr",
201+
execute: async () => {
202+
const result = await this.deps.createPr(
203+
directoryPath,
204+
prTitle || undefined,
205+
prBody || undefined,
206+
draft,
207+
);
208+
if (!result.success) throw new Error(result.message);
209+
return result;
210+
},
211+
// PR creation doesn't need rollback — the branch/commit are already pushed
212+
rollback: async () => {},
213+
});
214+
215+
return { prUrl: prResult.prUrl };
216+
}
217+
}

apps/code/src/main/services/git/schemas.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,54 @@ export const searchGithubIssuesInput = z.object({
414414
});
415415

416416
export const searchGithubIssuesOutput = z.array(githubIssueSchema);
417+
418+
// Create PR flow (composite operation)
419+
export const createPrFlowStep = z.enum([
420+
"creating-branch",
421+
"committing",
422+
"pushing",
423+
"creating-pr",
424+
"complete",
425+
"error",
426+
]);
427+
428+
export type CreatePrFlowStep = z.infer<typeof createPrFlowStep>;
429+
430+
export const createPrFlowInput = z.object({
431+
directoryPath: z.string(),
432+
flowId: z.string(),
433+
branchName: z.string().optional(),
434+
commitMessage: z.string().optional(),
435+
prTitle: z.string().optional(),
436+
prBody: z.string().optional(),
437+
draft: z.boolean().optional(),
438+
credentials: z
439+
.object({
440+
apiKey: z.string(),
441+
apiHost: z.string(),
442+
})
443+
.optional(),
444+
});
445+
446+
export type CreatePrFlowInput = z.infer<typeof createPrFlowInput>;
447+
448+
export const createPrFlowProgressPayload = z.object({
449+
flowId: z.string(),
450+
step: createPrFlowStep,
451+
message: z.string(),
452+
prUrl: z.string().optional(),
453+
});
454+
455+
export type CreatePrFlowProgressPayload = z.infer<
456+
typeof createPrFlowProgressPayload
457+
>;
458+
459+
export const createPrFlowOutput = z.object({
460+
success: z.boolean(),
461+
message: z.string(),
462+
prUrl: z.string().nullable(),
463+
failedStep: createPrFlowStep.nullable(),
464+
state: gitStateSnapshotSchema.optional(),
465+
});
466+
467+
export type CreatePrFlowOutput = z.infer<typeof createPrFlowOutput>;

0 commit comments

Comments
 (0)