Merge branch 'puter/issue-5-gitea-lifecycle' into puter/issue-14-integration
# Conflicts: # bun.lock # packages/agents/package.json # packages/agents/src/agents/project-manager.ts # packages/backend/convex/agentWorkspace.ts
This commit is contained in:
132
packages/agents/src/actions/finalize-gitea-lifecycle.ts
Normal file
132
packages/agents/src/actions/finalize-gitea-lifecycle.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||
import { parseAgentEnv } from "@code/env/agent";
|
||||
import { defineAction } from "@flue/runtime";
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
import * as v from "valibot";
|
||||
|
||||
import {
|
||||
createGiteaHttpTransport,
|
||||
runPostRunGiteaLifecycle,
|
||||
} from "../git/gitea";
|
||||
|
||||
const pullRequestOutput = v.object({
|
||||
baseBranch: v.string(),
|
||||
branch: v.string(),
|
||||
number: v.number(),
|
||||
status: v.picklist(["open", "closed", "merged"]),
|
||||
url: v.string(),
|
||||
});
|
||||
|
||||
const output = v.object({
|
||||
baseBranch: v.string(),
|
||||
branch: v.string(),
|
||||
commitSha: v.optional(v.string()),
|
||||
pullRequest: v.optional(pullRequestOutput),
|
||||
status: v.picklist([
|
||||
"no_changes",
|
||||
"committed",
|
||||
"pushed",
|
||||
"pull_request_open",
|
||||
"failed",
|
||||
]),
|
||||
});
|
||||
|
||||
export const createFinalizeGiteaLifecycle = (
|
||||
issueAgentId: string,
|
||||
runtimeEnv: Record<string, string | undefined>
|
||||
) => {
|
||||
const env = parseAgentEnv(runtimeEnv);
|
||||
const client = new ConvexHttpClient(env.CONVEX_URL);
|
||||
const issueId = issueAgentId as Id<"projectIssues">;
|
||||
|
||||
return defineAction({
|
||||
description:
|
||||
"After verification, inspect the issue workspace, commit and push its work branch, and create an open Gitea pull request. Never merge.",
|
||||
input: v.object({
|
||||
commitMessage: v.optional(v.pipe(v.string(), v.maxLength(120))),
|
||||
verified: v.boolean(),
|
||||
}),
|
||||
name: "finalize_gitea_lifecycle",
|
||||
output,
|
||||
async run({ harness, input, log }) {
|
||||
const context = await client.query(api.agentWorkspace.get, {
|
||||
issueId,
|
||||
token: env.FLUE_DB_TOKEN,
|
||||
});
|
||||
if (!context.source) {
|
||||
throw new Error("Project has no Git source configured");
|
||||
}
|
||||
if (!context.source.defaultBranch) {
|
||||
throw new Error("Project Git source has no default branch");
|
||||
}
|
||||
if (!env.GITEA_TOKEN) {
|
||||
throw new Error(
|
||||
"GITEA_TOKEN is required for Gitea pull request creation"
|
||||
);
|
||||
}
|
||||
|
||||
const runner = {
|
||||
async run(
|
||||
command: string,
|
||||
options?: { cwd?: string; env?: Record<string, string> }
|
||||
) {
|
||||
return await harness.shell(command, options);
|
||||
},
|
||||
};
|
||||
const transport = createGiteaHttpTransport({
|
||||
baseUrl: env.GITEA_URL,
|
||||
token: env.GITEA_TOKEN,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runPostRunGiteaLifecycle({
|
||||
baseBranch: context.source.defaultBranch,
|
||||
body: `Verified changes for project issue #${context.issue.number}. Merge remains a manual review action.`,
|
||||
commitMessage: input.commitMessage,
|
||||
issueNumber: context.issue.number,
|
||||
issueTitle: context.issue.title,
|
||||
repositoryPath: context.source.repositoryPath,
|
||||
runner,
|
||||
title: `Issue #${context.issue.number}: ${context.issue.title}`,
|
||||
transport,
|
||||
verification: input.verified ? "passed" : "failed",
|
||||
workspace: "/workspace/repository",
|
||||
});
|
||||
await client.mutation(api.agentWorkspace.recordGiteaLifecycle, {
|
||||
baseBranch: result.baseBranch,
|
||||
branch: result.branch,
|
||||
...(result.commitSha === undefined
|
||||
? {}
|
||||
: { commitSha: result.commitSha }),
|
||||
issueId,
|
||||
...(result.pullRequest === undefined
|
||||
? {}
|
||||
: { pullRequest: result.pullRequest }),
|
||||
status: result.status,
|
||||
token: env.FLUE_DB_TOKEN,
|
||||
});
|
||||
log.info("Gitea lifecycle completed", {
|
||||
branch: result.branch,
|
||||
status: result.status,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const branchResult = await runner.run("git branch --show-current", {
|
||||
cwd: "/workspace/repository",
|
||||
});
|
||||
const branch = branchResult.stdout.trim() || "unknown";
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await client.mutation(api.agentWorkspace.recordGiteaLifecycle, {
|
||||
baseBranch: context.source.defaultBranch,
|
||||
branch,
|
||||
error: message,
|
||||
issueId,
|
||||
status: "failed",
|
||||
token: env.FLUE_DB_TOKEN,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { parseAgentEnv } from "@code/env/agent";
|
||||
import { defineAgent } from "@flue/runtime";
|
||||
import type { AgentRouteHandler } from "@flue/runtime";
|
||||
|
||||
import { createFinalizeGiteaLifecycle } from "../actions/finalize-gitea-lifecycle";
|
||||
import { agentOs } from "../sandboxes/agent-os";
|
||||
import { createProjectTools } from "../tools/project";
|
||||
|
||||
@@ -14,6 +15,7 @@ export default defineAgent(({ env, id }) => {
|
||||
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
|
||||
|
||||
return {
|
||||
actions: [createFinalizeGiteaLifecycle(id, env)],
|
||||
cwd: "/workspace/repository",
|
||||
description,
|
||||
instructions: `You are the issue-scoped project manager and coding agent.
|
||||
@@ -22,7 +24,7 @@ Start every run by calling lookup_issue_context, reading /workspace/control/issu
|
||||
|
||||
Work only on the bound issue. The repository checkout is the current working directory; inspect existing source files before changing them. If a missing product decision makes safe progress impossible, publish the current work.md and steps.md, report needs-input, then ask one focused question. Otherwise implement the complete issue, run the relevant install, edit, test, or scenario command in AgentOS, and preserve command evidence.
|
||||
|
||||
Before finishing, update the operational files under /workspace/control/artifacts and publish each changed canonical artifact with publish_project_artifact. Report completed only after verification. On an unrecoverable error, report failed with the exact blocker. Never claim a repository change or command result you did not observe.`,
|
||||
Before finishing, update the operational files under /workspace/control/artifacts and publish each changed canonical artifact with publish_project_artifact. After verification, call finalize_gitea_lifecycle with verified=true; it owns the post-run Git/Gitea lifecycle and never merges. Report completed only after that action succeeds or reports no_changes. On an unrecoverable error, report failed with the exact blocker. Never claim a repository change or command result you did not observe.`,
|
||||
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
||||
sandbox: agentOs(env),
|
||||
tools: createProjectTools(id, env),
|
||||
|
||||
155
packages/agents/src/git/gitea.test.ts
Normal file
155
packages/agents/src/git/gitea.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createGiteaHttpTransport, runPostRunGiteaLifecycle } from "./gitea";
|
||||
import type { GitCommandRunner, GiteaTransport } from "./gitea";
|
||||
|
||||
const repository = {
|
||||
cloneUrl: "https://git.openputer.com/puter/zopu-code.git",
|
||||
defaultBranch: "main",
|
||||
htmlUrl: "https://git.openputer.com/puter/zopu-code",
|
||||
name: "zopu-code",
|
||||
sshUrl: "ssh://git@git.openputer.com:2222/puter/zopu-code.git",
|
||||
};
|
||||
|
||||
const makeRunner = (): GitCommandRunner & { readonly commands: string[] } => {
|
||||
const commands: string[] = [];
|
||||
let statusCalls = 0;
|
||||
return {
|
||||
commands,
|
||||
run(command) {
|
||||
commands.push(command);
|
||||
if (command === "git branch --show-current") {
|
||||
return Promise.resolve({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: "work/issue-5\n",
|
||||
});
|
||||
}
|
||||
if (command === "git status --porcelain=v1") {
|
||||
statusCalls += 1;
|
||||
return Promise.resolve({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: statusCalls === 1 ? " M src/gitea.ts\n" : "",
|
||||
});
|
||||
}
|
||||
if (command === "git diff --no-ext-diff --binary") {
|
||||
return Promise.resolve({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: "diff --git ...",
|
||||
});
|
||||
}
|
||||
if (command === "git rev-parse HEAD") {
|
||||
return Promise.resolve({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stdout: "abc123\n",
|
||||
});
|
||||
}
|
||||
if (command.includes("git status --porcelain=v1")) {
|
||||
return Promise.resolve({ exitCode: 0, stderr: "", stdout: "" });
|
||||
}
|
||||
return Promise.resolve({ exitCode: 0, stderr: "", stdout: "" });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("Gitea lifecycle adapter", () => {
|
||||
test("inspects, commits, pushes, and creates an open PR through mocked boundaries", async () => {
|
||||
const runner = makeRunner();
|
||||
const pullRequests: unknown[] = [];
|
||||
const transport: GiteaTransport = {
|
||||
createPullRequest(input) {
|
||||
pullRequests.push(input);
|
||||
return Promise.resolve({
|
||||
base: { ref: input.base },
|
||||
head: { ref: input.head },
|
||||
htmlUrl: "https://git.openputer.com/puter/zopu-code/pulls/5",
|
||||
number: 5,
|
||||
state: "open",
|
||||
});
|
||||
},
|
||||
getRepository() {
|
||||
return Promise.resolve(repository);
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runPostRunGiteaLifecycle({
|
||||
baseBranch: "main",
|
||||
body: "Generated by the verified project run.",
|
||||
issueNumber: 5,
|
||||
issueTitle: "Publish verified changes",
|
||||
repositoryPath: "puter/zopu-code",
|
||||
runner,
|
||||
transport,
|
||||
verification: "passed",
|
||||
workspace: "/workspace",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
branch: "work/issue-5",
|
||||
commitSha: "abc123",
|
||||
pullRequest: {
|
||||
baseBranch: "main",
|
||||
branch: "work/issue-5",
|
||||
number: 5,
|
||||
status: "open",
|
||||
},
|
||||
status: "pull_request_open",
|
||||
});
|
||||
expect(runner.commands).toEqual([
|
||||
"git branch --show-current",
|
||||
"git status --porcelain=v1",
|
||||
"git diff --no-ext-diff --binary",
|
||||
"git add --all && git commit -m 'feat(issue-5): Publish verified changes'",
|
||||
"git rev-parse HEAD",
|
||||
"git status --porcelain=v1",
|
||||
"git push 'ssh://git@git.openputer.com:2222/puter/zopu-code.git' HEAD:'work/issue-5'",
|
||||
]);
|
||||
expect(pullRequests).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("mocks the HTTP Gitea boundary without calling the network", async () => {
|
||||
const requests: Request[] = [];
|
||||
const transport = createGiteaHttpTransport({
|
||||
baseUrl: "https://git.openputer.com",
|
||||
fetch: (input, init) => {
|
||||
const request = new Request(String(input), init);
|
||||
requests.push(request);
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
request.method === "GET"
|
||||
? JSON.stringify(repository)
|
||||
: JSON.stringify({
|
||||
base: { ref: "main" },
|
||||
head: { ref: "work/issue-5" },
|
||||
html_url: "https://git.openputer.com/puter/zopu-code/pulls/5",
|
||||
number: 5,
|
||||
state: "open",
|
||||
}),
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
)
|
||||
);
|
||||
},
|
||||
token: "scoped-test-token",
|
||||
});
|
||||
|
||||
await transport.getRepository("puter/zopu-code");
|
||||
await transport.createPullRequest({
|
||||
base: "main",
|
||||
body: "body",
|
||||
head: "work/issue-5",
|
||||
repositoryPath: "puter/zopu-code",
|
||||
title: "title",
|
||||
});
|
||||
|
||||
expect(requests).toHaveLength(2);
|
||||
expect(requests[0]?.url).toBe(
|
||||
"https://git.openputer.com/api/v1/repos/puter/zopu-code"
|
||||
);
|
||||
expect(requests[1]?.headers.get("authorization")).toBe(
|
||||
"token scoped-test-token"
|
||||
);
|
||||
});
|
||||
});
|
||||
307
packages/agents/src/git/gitea.ts
Normal file
307
packages/agents/src/git/gitea.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import {
|
||||
decideGitLifecycle,
|
||||
GitLifecycleError,
|
||||
inspectGitWorkspace,
|
||||
makeCommitMessage,
|
||||
validatePullRequestMetadata,
|
||||
} from "@code/primitives/git";
|
||||
import type {
|
||||
GitLifecycleDecisionResult,
|
||||
GitLifecycleResult,
|
||||
GitWorkspaceInspection,
|
||||
} from "@code/primitives/git";
|
||||
import { Effect } from "effect";
|
||||
|
||||
export interface GitCommandResult {
|
||||
readonly exitCode: number;
|
||||
readonly stderr: string;
|
||||
readonly stdout: string;
|
||||
}
|
||||
|
||||
export interface GitCommandRunner {
|
||||
readonly run: (
|
||||
command: string,
|
||||
options?: {
|
||||
readonly cwd?: string;
|
||||
readonly env?: Record<string, string>;
|
||||
}
|
||||
) => Promise<GitCommandResult>;
|
||||
}
|
||||
|
||||
export interface GiteaRepository {
|
||||
readonly cloneUrl: string;
|
||||
readonly defaultBranch: string;
|
||||
readonly htmlUrl: string;
|
||||
readonly name: string;
|
||||
readonly sshUrl: string;
|
||||
}
|
||||
|
||||
export interface GiteaPullRequest {
|
||||
readonly base: { readonly ref: string };
|
||||
readonly head: { readonly ref: string };
|
||||
readonly htmlUrl: string;
|
||||
readonly number: number;
|
||||
readonly state: "open" | "closed" | "merged";
|
||||
}
|
||||
|
||||
export interface GiteaTransport {
|
||||
readonly createPullRequest: (input: {
|
||||
readonly base: string;
|
||||
readonly body: string;
|
||||
readonly head: string;
|
||||
readonly repositoryPath: string;
|
||||
readonly title: string;
|
||||
}) => Promise<GiteaPullRequest>;
|
||||
readonly getRepository: (repositoryPath: string) => Promise<GiteaRepository>;
|
||||
}
|
||||
|
||||
export interface GiteaHttpTransportOptions {
|
||||
readonly baseUrl: string;
|
||||
readonly fetch?: (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit
|
||||
) => Promise<Response>;
|
||||
readonly token: string;
|
||||
}
|
||||
|
||||
const shellQuote = (value: string): string =>
|
||||
`'${value.replaceAll("'", `'"'"'`)}'`;
|
||||
|
||||
const providerFailure = (
|
||||
message: string,
|
||||
reason: "CommandFailed" | "RemoteRejected"
|
||||
) => new GitLifecycleError({ message, reason });
|
||||
|
||||
const runChecked = async (
|
||||
runner: GitCommandRunner,
|
||||
command: string,
|
||||
options?: Parameters<GitCommandRunner["run"]>[1]
|
||||
): Promise<string> => {
|
||||
let result: GitCommandResult;
|
||||
try {
|
||||
result = await runner.run(command, options);
|
||||
} catch (error) {
|
||||
throw new GitLifecycleError({
|
||||
message: `Git command could not run: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
reason: "CommandFailed",
|
||||
});
|
||||
}
|
||||
if (result.exitCode !== 0) {
|
||||
throw providerFailure(
|
||||
result.stderr.trim() || `Git command failed: ${command}`,
|
||||
command.startsWith("git push") ? "RemoteRejected" : "CommandFailed"
|
||||
);
|
||||
}
|
||||
return result.stdout;
|
||||
};
|
||||
|
||||
const toHttpPath = (repositoryPath: string): string =>
|
||||
repositoryPath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
|
||||
const requestJson = async <T>(
|
||||
options: GiteaHttpTransportOptions,
|
||||
path: string,
|
||||
init?: RequestInit
|
||||
): Promise<T> => {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await (options.fetch ?? globalThis.fetch)(
|
||||
`${options.baseUrl.replace(/\/$/u, "")}${path}`,
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${options.token}`,
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new GitLifecycleError({
|
||||
message: `Gitea request could not run: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
reason: "RequestFailed",
|
||||
});
|
||||
}
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new GitLifecycleError({
|
||||
message: `Gitea request failed (${response.status}): ${detail}`,
|
||||
reason:
|
||||
response.status === 401 || response.status === 403
|
||||
? "Authentication"
|
||||
: "RequestFailed",
|
||||
});
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
};
|
||||
|
||||
export const createGiteaHttpTransport = (
|
||||
options: GiteaHttpTransportOptions
|
||||
): GiteaTransport => ({
|
||||
async createPullRequest(input) {
|
||||
const response = await requestJson<{
|
||||
base: { ref: string };
|
||||
head: { ref: string };
|
||||
html_url: string;
|
||||
number: number;
|
||||
state: "open" | "closed" | "merged";
|
||||
}>(options, `/api/v1/repos/${toHttpPath(input.repositoryPath)}/pulls`, {
|
||||
body: JSON.stringify({
|
||||
base: input.base,
|
||||
body: input.body,
|
||||
head: input.head,
|
||||
title: input.title,
|
||||
}),
|
||||
method: "POST",
|
||||
});
|
||||
return {
|
||||
base: response.base,
|
||||
head: response.head,
|
||||
htmlUrl: response.html_url,
|
||||
number: response.number,
|
||||
state: response.state,
|
||||
};
|
||||
},
|
||||
async getRepository(repositoryPath) {
|
||||
const response = await requestJson<{
|
||||
clone_url: string;
|
||||
default_branch: string;
|
||||
html_url: string;
|
||||
name: string;
|
||||
ssh_url: string;
|
||||
}>(options, `/api/v1/repos/${toHttpPath(repositoryPath)}`);
|
||||
return {
|
||||
cloneUrl: response.clone_url,
|
||||
defaultBranch: response.default_branch,
|
||||
htmlUrl: response.html_url,
|
||||
name: response.name,
|
||||
sshUrl: response.ssh_url,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export interface RunPostRunGiteaLifecycleInput {
|
||||
readonly baseBranch: string;
|
||||
readonly body: string;
|
||||
readonly commitMessage?: string;
|
||||
readonly issueNumber: number;
|
||||
readonly issueTitle: string;
|
||||
readonly repositoryPath: string;
|
||||
readonly runner: GitCommandRunner;
|
||||
readonly title?: string;
|
||||
readonly transport: GiteaTransport;
|
||||
readonly verification: "passed" | "failed" | "not-run";
|
||||
readonly workspace: string;
|
||||
}
|
||||
|
||||
const inspect = async (
|
||||
input: RunPostRunGiteaLifecycleInput
|
||||
): Promise<GitWorkspaceInspection> => {
|
||||
const [branch, status, diff] = await Promise.all([
|
||||
runChecked(input.runner, "git branch --show-current", {
|
||||
cwd: input.workspace,
|
||||
}),
|
||||
runChecked(input.runner, "git status --porcelain=v1", {
|
||||
cwd: input.workspace,
|
||||
}),
|
||||
runChecked(input.runner, "git diff --no-ext-diff --binary", {
|
||||
cwd: input.workspace,
|
||||
}),
|
||||
]);
|
||||
return await Effect.runPromise(
|
||||
inspectGitWorkspace({
|
||||
branch: branch.trim(),
|
||||
diff,
|
||||
status,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const runPostRunGiteaLifecycle = async (
|
||||
input: RunPostRunGiteaLifecycleInput
|
||||
): Promise<GitLifecycleResult> => {
|
||||
const inspection = await inspect(input);
|
||||
const initialDecision = (await Effect.runPromise(
|
||||
decideGitLifecycle({
|
||||
baseBranch: input.baseBranch,
|
||||
inspection,
|
||||
pushed: false,
|
||||
verification: input.verification,
|
||||
})
|
||||
)) as GitLifecycleDecisionResult;
|
||||
|
||||
if (initialDecision.decision === "finish") {
|
||||
return {
|
||||
baseBranch: input.baseBranch as GitLifecycleResult["baseBranch"],
|
||||
branch: inspection.branch,
|
||||
commitSha: undefined,
|
||||
pullRequest: undefined,
|
||||
status: "no_changes",
|
||||
};
|
||||
}
|
||||
|
||||
const repository = await input.transport.getRepository(input.repositoryPath);
|
||||
|
||||
await runChecked(
|
||||
input.runner,
|
||||
`git add --all && git commit -m ${shellQuote(
|
||||
input.commitMessage ??
|
||||
makeCommitMessage(input.issueNumber, input.issueTitle)
|
||||
)}`,
|
||||
{ cwd: input.workspace }
|
||||
);
|
||||
const commitOutput = await runChecked(input.runner, "git rev-parse HEAD", {
|
||||
cwd: input.workspace,
|
||||
});
|
||||
const commitSha = commitOutput.trim();
|
||||
const cleanStatus = await runChecked(
|
||||
input.runner,
|
||||
"git status --porcelain=v1",
|
||||
{ cwd: input.workspace }
|
||||
);
|
||||
if (cleanStatus.trim().length > 0) {
|
||||
throw providerFailure(
|
||||
"Git workspace remained dirty after commit",
|
||||
"CommandFailed"
|
||||
);
|
||||
}
|
||||
|
||||
await runChecked(
|
||||
input.runner,
|
||||
`git push ${shellQuote(repository.sshUrl)} HEAD:${shellQuote(inspection.branch)}`,
|
||||
{ cwd: input.workspace }
|
||||
);
|
||||
|
||||
const pullRequest = await input.transport.createPullRequest({
|
||||
base: input.baseBranch,
|
||||
body: input.body,
|
||||
head: inspection.branch,
|
||||
repositoryPath: input.repositoryPath,
|
||||
title: input.title ?? `Issue #${input.issueNumber}: ${input.issueTitle}`,
|
||||
});
|
||||
const metadata = await Effect.runPromise(
|
||||
validatePullRequestMetadata({
|
||||
baseBranch: pullRequest.base.ref,
|
||||
branch: pullRequest.head.ref,
|
||||
number: pullRequest.number,
|
||||
status: pullRequest.state,
|
||||
url: pullRequest.htmlUrl,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
baseBranch: input.baseBranch as GitLifecycleResult["baseBranch"],
|
||||
branch: inspection.branch,
|
||||
commitSha,
|
||||
pullRequest: metadata,
|
||||
status: "pull_request_open",
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user