patch.moi/flows/patch-moi-harness-fork/exec/update-fork.ts
matamune 8b1e31df1a
Some checks failed
check / check (push) Failing after 26s
Reshape harness flows like fork maintenance
2026-05-18 22:34:53 +00:00

375 lines
13 KiB
TypeScript

import path from "node:path";
type FlowContext = {
flow: {
config?: Record<string, unknown>;
event: {
id: string;
type: string;
payload?: Record<string, unknown>;
};
};
};
type CommandResult = {
label: string;
cmd: string[];
cwd: string;
code: number;
stdout: string;
stderr: string;
};
type PatchBranch = {
name: string;
sha: string;
subject: string;
};
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
const config = context.flow.config ?? {};
const payload = context.flow.event.payload ?? {};
const commands: CommandResult[] = [];
const workspaceRoot = process.cwd();
const forkRepo = path.resolve(workspaceRoot, stringConfig("fork_repo", "harness/fork"));
const forkRepoFullName = stringConfig("fork_repo_full_name", "matamune-peezy/patch-moi-harness");
const targetBranch = stringConfig("target_branch", "main");
const upstreamBranch = stringConfig("upstream_branch", "upstream");
const patchPrefix = stringConfig("patch_prefix", "patch/");
const upstreamRemote = stringConfig("upstream_remote", "upstream");
const upstreamRepoUrl = stringConfig("upstream_repo_url", "https://github.com/peezy-tech/patch-moi-harness.git");
const verifyCommands = stringArrayConfig("verify_commands", ["npm test", "npm run pack:dry-run"]);
const pushRemotes = stringArrayConfig("push_remotes", ["origin", "jojo"]);
function finish(value: Record<string, unknown>): never {
process.stdout.write(`FLOW_RESULT ${JSON.stringify(value)}\n`);
process.exit(0);
}
try {
const expectedRepo = stringConfig("expected_repo", "peezy-tech/patch-moi-harness");
const repo = stringValue(payload.repo);
if (repo !== expectedRepo) {
finish({ status: "skipped", message: `Harness fork flow ignores ${repo}.` });
}
await requireGitRepo();
await ensureUpstreamRemote();
if (enabled("fetch", true)) {
await run("fetch upstream refs", ["git", "fetch", upstreamRemote, "--tags", "--prune"]);
}
await requireNoRebaseOrCherryPickInProgress();
await requireCleanWorktree("before harness patch rebuild");
const base = await resolveBase();
await run("update local upstream branch", ["git", "branch", "-f", upstreamBranch, base.sha]);
await ensureSeedPatchBranches();
const currentSha = (await runChecked("read harness checkout head", ["git", "rev-parse", "HEAD"])).stdout.trim();
const targetExists = await branchExists(targetBranch);
const beforeSha = targetExists ? await resolveCommit(targetBranch) : currentSha;
const beforeTree = beforeSha ? await resolveTree(beforeSha) : "";
const patchBranches = await listPatchBranches();
if (patchBranches.length === 0) {
finish({
status: "blocked",
message: `Harness fork has no ${patchPrefix} branches.`,
artifacts: baseArtifacts(base),
});
}
await run("switch to upstream rebuild base", ["git", "switch", "--detach", base.sha]);
const applied: PatchBranch[] = [];
for (const patchBranch of patchBranches) {
const pick = await run(`apply ${patchBranch.name}`, ["git", "cherry-pick", patchBranch.sha], { allowFailure: true });
if (pick.code !== 0) {
const status = await run("patch rebuild conflict status", ["git", "status", "--short", "--branch"], { allowFailure: true });
const unmerged = await run("unmerged files", ["git", "diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
finish({
status: "needs_intervention",
message: `Harness patch rebuild stopped while applying ${patchBranch.name}.`,
artifacts: {
...baseArtifacts(base),
beforeSha,
applied,
failedPatch: patchBranch,
statusOutput: status.stdout,
unmergedFiles: lines(unmerged.stdout),
commands: commandArtifacts(),
},
});
}
applied.push(patchBranch);
}
const candidateSha = (await runChecked("read rebuilt harness head", ["git", "rev-parse", "HEAD"])).stdout.trim();
const candidateTree = await resolveTree(candidateSha);
const changed = !beforeSha || candidateTree !== beforeTree;
if (changed) {
await run("update maintained harness branch", ["git", "branch", "-f", targetBranch, candidateSha]);
} else if (!targetExists) {
await run("seed maintained harness branch", ["git", "branch", "-f", targetBranch, beforeSha]);
}
await run("switch maintained harness branch", ["git", "switch", targetBranch]);
for (const command of verifyCommands) {
const result = await run(`verify: ${command}`, ["bash", "-lc", command], { allowFailure: true });
if (result.code !== 0) {
finish({
status: "needs_intervention",
message: `Harness verification failed: ${command}.`,
artifacts: {
...baseArtifacts(base),
beforeSha,
candidateSha,
applied,
failedCommand: command,
commands: commandArtifacts(),
},
});
}
}
await requireCleanWorktree("after harness verification");
const afterSha = (await runChecked("read maintained harness head", ["git", "rev-parse", "HEAD"])).stdout.trim();
if (enabled("push", false)) {
for (const remote of pushRemotes) {
await run(`push ${remote}/${targetBranch}`, [
"git",
"push",
"--force-with-lease",
remote,
`HEAD:${targetBranch}`,
]);
}
}
finish({
status: changed ? "changed" : "completed",
message: changed
? `Harness fork rebuilt ${targetBranch} from ${base.label} plus ${patchBranches.length} patches.`
: `Harness fork already matches ${base.label} plus ${patchBranches.length} patches.`,
artifacts: {
...baseArtifacts(base),
eventId: context.flow.event.id,
forkRepo,
forkRepoFullName,
targetBranch,
upstreamBranch,
patchPrefix,
beforeSha,
afterSha,
applied,
checks: verifyCommands.map((name) => ({ name, status: "passed" })),
candidateRefs: candidateRefsFor(afterSha),
commands: commandArtifacts(),
},
});
} catch (error) {
finish({
status: "failed",
message: error instanceof Error ? error.message : String(error),
artifacts: { commands: commandArtifacts() },
});
}
async function requireGitRepo(): Promise<void> {
await runChecked("verify harness fork checkout", ["git", "rev-parse", "--show-toplevel"]);
}
async function ensureUpstreamRemote(): Promise<void> {
const current = await run("read upstream remote", ["git", "remote", "get-url", upstreamRemote], { allowFailure: true });
if (current.code === 0) return;
if (!upstreamRepoUrl) {
finish({ status: "blocked", message: `Missing ${upstreamRemote} remote and no upstream_repo_url is configured.` });
}
await run("add upstream remote", ["git", "remote", "add", upstreamRemote, upstreamRepoUrl]);
}
async function requireNoRebaseOrCherryPickInProgress(): Promise<void> {
const state = await run(
"check existing replay state",
["bash", "-lc", 'test -d "$(git rev-parse --git-path rebase-merge)" -o -d "$(git rev-parse --git-path rebase-apply)" -o -f "$(git rev-parse --git-path CHERRY_PICK_HEAD)"'],
{ allowFailure: true },
);
if (state.code === 0) {
finish({
status: "blocked",
message: "A rebase or cherry-pick is already in progress in the harness fork checkout.",
artifacts: { forkRepo, commands: commandArtifacts() },
});
}
}
async function requireCleanWorktree(stage: string): Promise<void> {
const status = await run(`dirty worktree check ${stage}`, ["git", "status", "--porcelain=v1"]);
if (status.stdout.trim()) {
finish({
status: "blocked",
message: `Harness fork checkout has local changes ${stage}.`,
artifacts: {
dirtyStatus: status.stdout,
forkRepo,
commands: commandArtifacts(),
},
});
}
}
async function resolveBase(): Promise<{ kind: "release" | "branch"; label: string; sha: string }> {
if (context.flow.event.type === "upstream.release") {
const tag = stringValue(payload.tag) || shortTag(stringValue(payload.ref) ?? "");
if (!tag) {
finish({ status: "failed", message: "upstream.release requires payload.tag." });
}
return { kind: "release", label: tag, sha: await resolveCommit(`refs/tags/${tag}`).catch(() => resolveCommit(tag)) };
}
if (context.flow.event.type === "upstream.branch_update") {
const ref = stringValue(payload.ref) ?? "refs/heads/main";
const sha = stringValue(payload.sha);
if (!sha) {
finish({ status: "failed", message: "upstream.branch_update requires payload.sha." });
}
return { kind: "branch", label: `${ref}@${sha}`, sha: await resolveCommit(sha) };
}
finish({ status: "skipped", message: `Unsupported harness fork event ${context.flow.event.type}.` });
}
async function ensureSeedPatchBranches(): Promise<void> {
const seeds = stringArrayConfig("seed_patch_refs", []);
for (const seed of seeds) {
const [name, ref] = seed.split("=");
if (!name?.startsWith(patchPrefix) || !ref) {
throw new Error(`Invalid seed_patch_refs entry: ${seed}`);
}
const exists = await branchExists(name);
if (exists) continue;
await resolveCommit(ref);
await run(`seed ${name}`, ["git", "branch", "-f", name, ref]);
}
}
async function listPatchBranches(): Promise<PatchBranch[]> {
const refsPath = `refs/heads/${patchPrefix.replace(/\/+$/, "")}`;
const result = await run("list harness patch branches", [
"git",
"for-each-ref",
"--format=%(refname:short)%09%(objectname)%09%(contents:subject)",
refsPath,
], { allowFailure: true });
if (result.code !== 0 || !result.stdout.trim()) return [];
return result.stdout.trim().split(/\r?\n/).map((line) => {
const [name = "", sha = "", subject = ""] = line.split("\t");
return { name, sha, subject };
}).filter((branch) => branch.name.startsWith(patchPrefix)).sort((left, right) => left.name.localeCompare(right.name));
}
async function branchExists(branch: string): Promise<boolean> {
return (await run(`check branch ${branch}`, ["git", "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { allowFailure: true })).code === 0;
}
async function resolveCommit(ref: string): Promise<string> {
return (await runChecked(`resolve ${ref}`, ["git", "rev-parse", "--verify", `${ref}^{commit}`])).stdout.trim();
}
async function resolveTree(ref: string): Promise<string> {
return (await runChecked(`resolve tree ${ref}`, ["git", "rev-parse", "--verify", `${ref}^{tree}`])).stdout.trim();
}
async function runChecked(label: string, cmd: string[]): Promise<CommandResult> {
const result = await run(label, cmd);
if (result.code !== 0) {
throw new Error(`${label} failed with exit ${result.code}:\n${result.stderr || result.stdout}`);
}
return result;
}
async function run(label: string, cmd: string[], options: { allowFailure?: boolean } = {}): Promise<CommandResult> {
process.stderr.write(`+ ${label}: ${cmd.join(" ")}\n`);
const child = Bun.spawn(cmd, {
cwd: forkRepo,
env: process.env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, code] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
]);
if (stdout) process.stderr.write(stdout);
if (stderr) process.stderr.write(stderr);
const result = { label, cmd, cwd: forkRepo, code, stdout, stderr };
commands.push(result);
if (code !== 0 && !options.allowFailure) {
throw new Error(`${label} failed with exit ${code}:\n${stderr || stdout}`);
}
return result;
}
function baseArtifacts(base: { kind: string; label: string; sha: string }): Record<string, unknown> {
return {
upstreamKind: base.kind,
upstreamLabel: base.label,
upstreamSha: base.sha,
};
}
function candidateRefsFor(sha: string): Array<Record<string, unknown>> {
const pushed = enabled("push", false);
const remotes = pushed ? pushRemotes : ["local"];
return remotes.map((remote) => ({
kind: "branch",
repo: forkRepoFullName,
remote,
ref: `refs/heads/${targetBranch}`,
sha,
pushed,
}));
}
function commandArtifacts(): Array<Record<string, unknown>> {
return commands.map((command) => ({
...command,
stdout: truncate(command.stdout),
stderr: truncate(command.stderr),
}));
}
function enabled(name: string, fallback: boolean): boolean {
const envValue = process.env[`CODEX_FLOW_${name.toUpperCase()}`];
if (envValue !== undefined) {
return ["1", "true", "yes", "on"].includes(envValue.trim().toLowerCase());
}
const value = config[name];
return typeof value === "boolean" ? value : fallback;
}
function stringConfig(name: string, fallback: string): string {
const value = config[name];
return typeof value === "string" && value.trim() ? value : fallback;
}
function stringArrayConfig(name: string, fallback: string[]): string[] {
const value = config[name];
if (!Array.isArray(value)) return fallback;
const entries = value.filter((entry): entry is string => typeof entry === "string" && entry.trim());
return entries.length > 0 ? entries : fallback;
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function lines(value: string): string[] {
return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
}
function shortTag(value: string): string {
return value.replace(/^refs\/tags\//, "");
}
function truncate(value: string, max = 4000): string {
return value.length <= max ? value : `${value.slice(0, max)}\n...[truncated ${value.length - max} chars]`;
}