小橘 57c740cdde Revert "chore(workflow): auto-generated commit"
This reverts commit 75f2768a8c7713879bb2ab564f42f24bc609338e.
2026-04-28 15:49:22 +00:00

174 lines
5.8 KiB
TypeScript

import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
import { spawnSafe } from "@uncaged/nerve-workflow-utils";
import { PUBLISHER_MAX_ATTEMPTS } from "../../lib/constants.js";
import { lastMetaForRole } from "../../lib/meta-helpers.js";
import { runWithRetries } from "../../lib/run-with-retries.js";
import { classifyPublisherFailure, formatSpawnFailure } from "../../lib/spawn-utils.js";
import { extractPrInfo } from "../../lib/text-utils.js";
import type { ImplementerMeta } from "../implementer/index.js";
import type { IntakeMeta } from "../intake/index.js";
import type { IssueReaderMeta } from "../issue-reader/index.js";
import type { PlannerMeta } from "../planner/index.js";
import type { TesterMeta } from "../tester/index.js";
export type PrPublisherMeta = {
prUrl: string | null;
prNumber: number | null;
linkedIssue: string | null;
published: boolean;
attempt: number;
failureKind: "none" | "auth_or_network" | "permanent";
failureReason: string | null;
};
export type BuildPrPublisherDeps = {
nerveRoot: string;
};
export function buildPrPublisherRole({ nerveRoot }: BuildPrPublisherDeps): Role<PrPublisherMeta> {
return async (_start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PrPublisherMeta>> => {
const attempt = messages.filter((message) => message.role === "pr-publisher").length + 1;
const intakeMeta = lastMetaForRole<IntakeMeta>(messages, "intake");
const issueMeta = lastMetaForRole<IssueReaderMeta>(messages, "issue-reader");
const plannerMeta = lastMetaForRole<PlannerMeta>(messages, "planner");
const implementerMeta = lastMetaForRole<ImplementerMeta>(messages, "implementer");
const testerMeta = lastMetaForRole<TesterMeta>(messages, "tester");
if (testerMeta === null || !testerMeta.passed) {
return {
content: "skip PR publishing: tester.passed is not true",
meta: {
prUrl: null,
prNumber: null,
linkedIssue: intakeMeta?.issueUrl ?? null,
published: false,
attempt,
failureKind: "permanent",
failureReason: "tester did not pass",
},
};
}
if (
intakeMeta === null ||
intakeMeta.owner === null ||
intakeMeta.repo === null ||
implementerMeta === null ||
implementerMeta.branchName === null
) {
return {
content: "pr-publisher cannot continue: missing required context",
meta: {
prUrl: null,
prNumber: null,
linkedIssue: intakeMeta?.issueUrl ?? null,
published: false,
attempt,
failureKind: "permanent",
failureReason: "missing context",
},
};
}
const pushRun = await runWithRetries(async () => {
const run = await spawnSafe("git", ["push", "-u", "origin", implementerMeta.branchName as string], {
cwd: nerveRoot,
env: null,
timeoutMs: 180_000,
});
if (run.ok) {
return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr };
}
return {
ok: false,
error: run.error,
lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout,
lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr,
};
}, PUBLISHER_MAX_ATTEMPTS);
if (!pushRun.ok) {
const failureKind = classifyPublisherFailure(pushRun.error);
return {
content: `failed to push branch before PR: ${formatSpawnFailure(pushRun.error)}`,
meta: {
prUrl: null,
prNumber: null,
linkedIssue: intakeMeta.issueUrl,
published: false,
attempt,
failureKind,
failureReason: formatSpawnFailure(pushRun.error),
},
};
}
const shortTitle = (issueMeta?.issue?.title ?? "issue fix").slice(0, 72);
const title = `fix: ${shortTitle}`;
const body = [
`Issue: ${intakeMeta.issueUrl}`,
"",
"## Summary",
...(implementerMeta.changedFiles ?? []).map((file) => `- updated \`${file}\``),
"",
"## Plan",
plannerMeta?.plan ?? "(no planner output)",
"",
"## Test Results",
testerMeta.testCommandResults?.map((r) => `- ${r.ok ? "PASS" : "FAIL"} \`${r.command}\``).join("\n") ??
"- no test logs",
].join("\n");
const repoSpec = `${intakeMeta.owner}/${intakeMeta.repo}`;
const prRun = await runWithRetries(async () => {
const run = await spawnSafe(
"tea",
["pr", "create", "--repo", repoSpec, "--title", title, "--body", body, "--head", implementerMeta.branchName as string],
{
cwd: nerveRoot,
env: null,
timeoutMs: 120_000,
},
);
if (run.ok) {
return { ok: true, stdout: run.value.stdout, stderr: run.value.stderr };
}
return {
ok: false,
error: run.error,
lastStdout: run.error.kind === "spawn_failed" ? "" : run.error.stdout,
lastStderr: run.error.kind === "spawn_failed" ? run.error.message : run.error.stderr,
};
}, PUBLISHER_MAX_ATTEMPTS);
if (!prRun.ok) {
const failureKind = classifyPublisherFailure(prRun.error);
return {
content: `PR creation failed: ${formatSpawnFailure(prRun.error)}`,
meta: {
prUrl: null,
prNumber: null,
linkedIssue: intakeMeta.issueUrl,
published: false,
attempt,
failureKind,
failureReason: formatSpawnFailure(prRun.error),
},
};
}
const info = extractPrInfo(`${prRun.stdout}\n${prRun.stderr}`);
return {
content: `PR created: ${title}\n${prRun.stdout}`,
meta: {
prUrl: info.prUrl,
prNumber: info.prNumber,
linkedIssue: intakeMeta.issueUrl,
published: true,
attempt,
failureKind: "none",
failureReason: null,
},
};
};
}