Scaffold Matrix + GitOps + Claude automation stack
build-agent / build-and-push (push) Failing after 18s
build-agent / build-and-push (push) Failing after 18s
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import { authenticatedCloneUrl, createBranch, createPullRequest } from "./gitea.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const WORKSPACE_ROOT = "/workspace";
|
||||
|
||||
async function run(cmd, args, opts = {}) {
|
||||
const { stdout } = await execFileAsync(cmd, args, {
|
||||
maxBuffer: 1024 * 1024 * 32,
|
||||
...opts,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
async function withWorkspace(fn) {
|
||||
await mkdir(WORKSPACE_ROOT, { recursive: true });
|
||||
const dir = await mkdtemp(path.join(WORKSPACE_ROOT, "job-"));
|
||||
try {
|
||||
return await fn(dir);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Runs Claude Code headless. Unattended containers have no TTY to answer permission
|
||||
// prompts, so this trusts the sandboxing of the throwaway clone dir instead:
|
||||
// bypassPermissions to avoid hanging, plus --disallowedTools as defense in depth so
|
||||
// Claude can never push/commit/checkout itself — this script owns those steps.
|
||||
async function runClaude(cwd, prompt, { allowEdits }) {
|
||||
const disallowed = ["Bash(git push:*)", "Bash(git commit:*)", "Bash(git checkout:*)"];
|
||||
if (!allowEdits) disallowed.push("Edit", "Write", "NotebookEdit");
|
||||
|
||||
const args = [
|
||||
"-p", prompt,
|
||||
"--output-format", "text",
|
||||
"--permission-mode", "bypassPermissions",
|
||||
"--disallowedTools", disallowed.join(","),
|
||||
];
|
||||
|
||||
return run("claude", args, { cwd });
|
||||
}
|
||||
|
||||
export async function reviewPullRequest({ owner, repo, ref, cloneUrl, prTitle, prBody }) {
|
||||
return withWorkspace(async (dir) => {
|
||||
const authedUrl = authenticatedCloneUrl(cloneUrl);
|
||||
await run("git", ["clone", "--quiet", authedUrl, dir]);
|
||||
await run("git", ["fetch", "--quiet", "origin", ref], { cwd: dir });
|
||||
await run("git", ["checkout", "--quiet", ref], { cwd: dir });
|
||||
|
||||
const prompt = [
|
||||
"You are reviewing a pull request. Diff HEAD against the base branch (origin/main)",
|
||||
"and give a concise, specific code review: correctness bugs first, then",
|
||||
"simplification/reuse/efficiency notes. Read-only — do not edit any files.",
|
||||
`PR title: ${prTitle}`,
|
||||
`PR description:\n${prBody}`,
|
||||
].join("\n");
|
||||
|
||||
return runClaude(dir, prompt, { allowEdits: false });
|
||||
});
|
||||
}
|
||||
|
||||
export async function runChatTask({ owner, repo, cloneUrl, instruction }) {
|
||||
return withWorkspace(async (dir) => {
|
||||
const authedUrl = authenticatedCloneUrl(cloneUrl);
|
||||
await run("git", ["clone", "--quiet", authedUrl, dir]);
|
||||
|
||||
const branch = `claude/${crypto.randomBytes(4).toString("hex")}`;
|
||||
await createBranch(owner, repo, branch);
|
||||
await run("git", ["fetch", "--quiet", "origin", branch], { cwd: dir });
|
||||
await run("git", ["checkout", "--quiet", branch], { cwd: dir });
|
||||
|
||||
const prompt = [
|
||||
"Implement the change described below in this repository. Make the smallest",
|
||||
"correct change that satisfies it. Do not run git commit, git push, or git",
|
||||
"checkout yourself — just edit files; committing and pushing happens separately.",
|
||||
`Instruction: ${instruction}`,
|
||||
].join("\n");
|
||||
|
||||
await runClaude(dir, prompt, { allowEdits: true });
|
||||
|
||||
await run("git", ["add", "-A"], { cwd: dir });
|
||||
const status = await run("git", ["status", "--porcelain"], { cwd: dir });
|
||||
if (!status.trim()) {
|
||||
throw new Error("Claude made no changes for this instruction");
|
||||
}
|
||||
await run("git", ["commit", "-m", `claude: ${instruction}`.slice(0, 200)], { cwd: dir });
|
||||
await run("git", ["push", "--quiet", "origin", branch], { cwd: dir });
|
||||
|
||||
return createPullRequest(owner, repo, {
|
||||
head: branch,
|
||||
title: `claude: ${instruction}`.slice(0, 200),
|
||||
body: `Requested via Matrix:\n\n> ${instruction}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user