@arungeorgesaji/assembly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/LICENSE +674 -0
- package/README.md +455 -0
- package/ngrok-example.yml +9 -0
- package/package.json +23 -0
- package/src/agent-profiles.js +167 -0
- package/src/agent-runner.js +37 -0
- package/src/approval.js +45 -0
- package/src/cli.js +381 -0
- package/src/config.js +90 -0
- package/src/context-builder.js +91 -0
- package/src/doctor.js +151 -0
- package/src/file-updates.js +89 -0
- package/src/follow-up.js +33 -0
- package/src/git-worktree.js +35 -0
- package/src/github-webhooks.js +159 -0
- package/src/github.js +261 -0
- package/src/init.js +91 -0
- package/src/job-store.js +97 -0
- package/src/job-worker.js +390 -0
- package/src/models.js +55 -0
- package/src/openai-agent-runner.js +141 -0
- package/src/orchestrator.js +221 -0
- package/src/patch.js +78 -0
- package/src/planner.js +279 -0
- package/src/repo-inspector.js +88 -0
- package/src/report.js +46 -0
- package/src/result-validation.js +55 -0
- package/src/review-agent-runner.js +127 -0
- package/src/root.js +51 -0
- package/src/run-store.js +104 -0
- package/src/scope.js +107 -0
- package/src/slack-thread-store.js +34 -0
- package/src/slack-webhooks.js +120 -0
- package/src/slack.js +31 -0
- package/src/validation.js +33 -0
- package/src/verification-runner.js +51 -0
- package/src/webhook-server.js +64 -0
package/src/doctor.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const VALID_AGENT_PROVIDERS = new Set(["stub", "openai"]);
|
|
7
|
+
const VALID_APPROVAL_MODES = new Set(["auto", "manual", "never"]);
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export async function runDoctor({ rootDir = process.cwd(), env = process.env, exec = execFileAsync } = {}) {
|
|
11
|
+
const checks = [];
|
|
12
|
+
const envPath = path.join(rootDir, ".env");
|
|
13
|
+
|
|
14
|
+
checks.push(await checkTargetRepository(rootDir, exec));
|
|
15
|
+
checks.push(await checkEnvFile(envPath));
|
|
16
|
+
checks.push(checkAgentProvider(env));
|
|
17
|
+
checks.push(checkOpenAI(env));
|
|
18
|
+
checks.push(checkApprovalMode(env));
|
|
19
|
+
checks.push(checkRequiredEnv("GITHUB_WEBHOOK_SECRET", env, "Required for GitHub webhook signature verification."));
|
|
20
|
+
checks.push(await checkGitHubAuth(env, exec));
|
|
21
|
+
checks.push(checkRequiredEnv("SLACK_SIGNING_SECRET", env, "Required for Slack request signature verification."));
|
|
22
|
+
checks.push(checkRequiredEnv("SLACK_BOT_TOKEN", env, "Required so Assembly can reply in Slack threads."));
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
ok: !checks.some((check) => check.status === "fail"),
|
|
26
|
+
checks,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatDoctorReport(report) {
|
|
31
|
+
const rows = report.checks.map((check) => [
|
|
32
|
+
check.status.toUpperCase(),
|
|
33
|
+
check.name,
|
|
34
|
+
check.message,
|
|
35
|
+
]);
|
|
36
|
+
const headers = ["STATUS", "CHECK", "DETAIL"];
|
|
37
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
|
|
38
|
+
const formatRow = (row) => row.map((value, index) => value.padEnd(widths[index])).join(" ");
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
"Assembly doctor",
|
|
42
|
+
"",
|
|
43
|
+
formatRow(headers),
|
|
44
|
+
formatRow(widths.map((width) => "-".repeat(width))),
|
|
45
|
+
...rows.map(formatRow),
|
|
46
|
+
"",
|
|
47
|
+
report.ok ? "Ready: required setup is present." : "Not ready: fix the failed checks above.",
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function checkEnvFile(envPath) {
|
|
53
|
+
try {
|
|
54
|
+
await access(envPath);
|
|
55
|
+
return pass(".env", "Found local .env file.");
|
|
56
|
+
} catch {
|
|
57
|
+
return warn(".env", "No .env file found; using process environment only.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function checkTargetRepository(rootDir, exec) {
|
|
62
|
+
try {
|
|
63
|
+
const { stdout } = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: rootDir });
|
|
64
|
+
const gitRoot = path.resolve(stdout.trim());
|
|
65
|
+
const targetRoot = path.resolve(rootDir);
|
|
66
|
+
if (gitRoot !== targetRoot) {
|
|
67
|
+
return warn("Target repository", `Using ${targetRoot}, but Git root is ${gitRoot}. Run from the repo root or pass --repo.`);
|
|
68
|
+
}
|
|
69
|
+
return pass("Target repository", `Git repository at ${targetRoot}.`);
|
|
70
|
+
} catch {
|
|
71
|
+
return fail("Target repository", "Run from a Git repository or pass --repo <path>.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkAgentProvider(env) {
|
|
76
|
+
const provider = env.ASSEMBLY_AGENT_PROVIDER || "stub";
|
|
77
|
+
if (!VALID_AGENT_PROVIDERS.has(provider)) {
|
|
78
|
+
return fail("ASSEMBLY_AGENT_PROVIDER", `Unsupported provider "${provider}". Use stub or openai.`);
|
|
79
|
+
}
|
|
80
|
+
if (provider === "stub") {
|
|
81
|
+
return warn("ASSEMBLY_AGENT_PROVIDER", "Using stub provider; real code editing needs ASSEMBLY_AGENT_PROVIDER=openai.");
|
|
82
|
+
}
|
|
83
|
+
return pass("ASSEMBLY_AGENT_PROVIDER", "OpenAI provider enabled.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkOpenAI(env) {
|
|
87
|
+
const provider = env.ASSEMBLY_AGENT_PROVIDER || "stub";
|
|
88
|
+
if (provider !== "openai") {
|
|
89
|
+
return warn("OPENAI_API_KEY", "Skipped because ASSEMBLY_AGENT_PROVIDER is not openai.");
|
|
90
|
+
}
|
|
91
|
+
if (!env.OPENAI_API_KEY) {
|
|
92
|
+
return fail("OPENAI_API_KEY", "Required when ASSEMBLY_AGENT_PROVIDER=openai.");
|
|
93
|
+
}
|
|
94
|
+
if (!env.OPENAI_API_KEY.startsWith("sk-")) {
|
|
95
|
+
return warn("OPENAI_API_KEY", "Present, but it does not look like an OpenAI secret key.");
|
|
96
|
+
}
|
|
97
|
+
return pass("OPENAI_API_KEY", `Present. Model: ${env.OPENAI_MODEL || "gpt-4.1-mini"}.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkApprovalMode(env) {
|
|
101
|
+
const mode = env.ASSEMBLY_APPROVAL_MODE || "auto";
|
|
102
|
+
if (!VALID_APPROVAL_MODES.has(mode)) {
|
|
103
|
+
return fail("ASSEMBLY_APPROVAL_MODE", `Unsupported mode "${mode}". Use auto, manual, or never.`);
|
|
104
|
+
}
|
|
105
|
+
return pass("ASSEMBLY_APPROVAL_MODE", `Using ${mode}.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkRequiredEnv(name, env, missingMessage) {
|
|
109
|
+
if (!env[name]) {
|
|
110
|
+
return fail(name, missingMessage);
|
|
111
|
+
}
|
|
112
|
+
return pass(name, "Present.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function checkGitHubAuth(env, exec) {
|
|
116
|
+
if (env.GH_TOKEN || env.GITHUB_TOKEN) {
|
|
117
|
+
return pass("GitHub auth", "Token environment variable present.");
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await exec("gh", ["auth", "status"]);
|
|
121
|
+
return pass("GitHub auth", "`gh auth status` is authenticated.");
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code === "ENOENT") {
|
|
124
|
+
return fail("GitHub auth", "GitHub CLI not found; install `gh` or set GH_TOKEN/GITHUB_TOKEN.");
|
|
125
|
+
}
|
|
126
|
+
return fail("GitHub auth", summarizeGitHubAuthError(error));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function summarizeGitHubAuthError(error) {
|
|
131
|
+
const output = `${error.stdout ?? ""}\n${error.stderr ?? ""}`;
|
|
132
|
+
if (/gh auth refresh/i.test(output)) {
|
|
133
|
+
return "`gh auth status` reports an invalid token. Run `gh auth refresh -h github.com` or set GH_TOKEN/GITHUB_TOKEN.";
|
|
134
|
+
}
|
|
135
|
+
if (/not logged into|not logged in|not authenticated/i.test(output)) {
|
|
136
|
+
return "`gh auth status` is not authenticated. Run `gh auth login` or set GH_TOKEN/GITHUB_TOKEN.";
|
|
137
|
+
}
|
|
138
|
+
return "`gh auth status` failed. Run `gh auth status` for details or set GH_TOKEN/GITHUB_TOKEN.";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pass(name, message) {
|
|
142
|
+
return { status: "pass", name, message };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function warn(name, message) {
|
|
146
|
+
return { status: "warn", name, message };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fail(name, message) {
|
|
150
|
+
return { status: "fail", name, message };
|
|
151
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { validateChangedFilesWithinScope } from "./scope.js";
|
|
5
|
+
|
|
6
|
+
export function validateFileUpdatesForTask(task, result) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
|
|
9
|
+
if (result.fileUpdates === undefined) {
|
|
10
|
+
return errors;
|
|
11
|
+
}
|
|
12
|
+
if (!Array.isArray(result.fileUpdates)) {
|
|
13
|
+
return [`task ${task.id} result fileUpdates must be an array when provided`];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const updatePaths = [];
|
|
17
|
+
for (const update of result.fileUpdates) {
|
|
18
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) {
|
|
19
|
+
errors.push(`task ${task.id} fileUpdates entries must be objects`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (typeof update.path !== "string" || update.path.trim() === "") {
|
|
23
|
+
errors.push(`task ${task.id} fileUpdates entry must include a path`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (typeof update.content !== "string") {
|
|
27
|
+
errors.push(`task ${task.id} fileUpdates entry for ${update.path} must include string content`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
updatePaths.push(update.path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
errors.push(...validateChangedFilesWithinScope(task, updatePaths));
|
|
34
|
+
|
|
35
|
+
const declaredFiles = new Set(result.changedFiles);
|
|
36
|
+
for (const updatePath of updatePaths) {
|
|
37
|
+
if (!declaredFiles.has(updatePath)) {
|
|
38
|
+
errors.push(`task ${task.id} updates ${updatePath} but result.changedFiles does not list it`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return errors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function applyFileUpdates(fileUpdates, rootDir = process.cwd()) {
|
|
46
|
+
for (const update of fileUpdates) {
|
|
47
|
+
const destination = path.join(rootDir, update.path);
|
|
48
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
49
|
+
await writeFile(destination, update.content);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function validateAdditiveFileUpdates(task, result, rootDir = process.cwd()) {
|
|
54
|
+
if (task.changePolicy !== "additive" || !Array.isArray(result.fileUpdates)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const errors = [];
|
|
59
|
+
for (const update of result.fileUpdates) {
|
|
60
|
+
const original = await readFile(path.join(rootDir, update.path), "utf8").catch(() => null);
|
|
61
|
+
if (original === null) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isLineSubsequence(original, update.content)) {
|
|
66
|
+
errors.push(`task ${task.id} additive update for ${update.path} removes or rewrites existing lines`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isLineSubsequence(originalContent, updatedContent) {
|
|
74
|
+
const originalLines = originalContent.split("\n");
|
|
75
|
+
const updatedLines = updatedContent.split("\n");
|
|
76
|
+
let updatedIndex = 0;
|
|
77
|
+
|
|
78
|
+
for (const originalLine of originalLines) {
|
|
79
|
+
while (updatedIndex < updatedLines.length && updatedLines[updatedIndex] !== originalLine) {
|
|
80
|
+
updatedIndex += 1;
|
|
81
|
+
}
|
|
82
|
+
if (updatedIndex >= updatedLines.length) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
updatedIndex += 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
package/src/follow-up.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRun } from "./orchestrator.js";
|
|
2
|
+
import { readRun } from "./run-store.js";
|
|
3
|
+
|
|
4
|
+
export async function createFollowUpRun(parentRunId, feedback, options = {}) {
|
|
5
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
6
|
+
const parentRun = await readRun(parentRunId, rootDir);
|
|
7
|
+
const normalizedFeedback = normalizeFeedback(feedback);
|
|
8
|
+
const request = [
|
|
9
|
+
`Follow up on run ${parentRunId}.`,
|
|
10
|
+
`Original request: ${parentRun.request.request}`,
|
|
11
|
+
`Feedback: ${normalizedFeedback}`,
|
|
12
|
+
].join(" ");
|
|
13
|
+
|
|
14
|
+
return createRun(request, {
|
|
15
|
+
...options,
|
|
16
|
+
rootDir,
|
|
17
|
+
metadata: {
|
|
18
|
+
parentRunId,
|
|
19
|
+
parentRequest: parentRun.request.request,
|
|
20
|
+
followUpFeedback: normalizedFeedback,
|
|
21
|
+
source: options.source ?? { provider: "local", kind: "follow_up" },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeFeedback(feedback) {
|
|
27
|
+
const normalized = String(feedback ?? "").trim().replace(/\s+/g, " ");
|
|
28
|
+
if (!normalized) {
|
|
29
|
+
throw new Error("follow-up feedback cannot be empty");
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getRunDir } from "./run-store.js";
|
|
6
|
+
|
|
7
|
+
export async function withTemporaryGitWorktree(rootDir, exec, callback) {
|
|
8
|
+
const worktreeDir = await mkdtemp(path.join(tmpdir(), "assembly-worktree-"));
|
|
9
|
+
let created = false;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await exec("git", ["worktree", "add", "--detach", worktreeDir, "HEAD"], { cwd: rootDir });
|
|
13
|
+
created = true;
|
|
14
|
+
return await callback(worktreeDir);
|
|
15
|
+
} finally {
|
|
16
|
+
if (created) {
|
|
17
|
+
try {
|
|
18
|
+
await exec("git", ["worktree", "remove", "--force", worktreeDir], { cwd: rootDir });
|
|
19
|
+
} catch {
|
|
20
|
+
await rm(worktreeDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
await rm(worktreeDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function copyRunRecord(runId, fromRootDir, toRootDir) {
|
|
29
|
+
const toRunDir = getRunDir(runId, toRootDir);
|
|
30
|
+
await mkdir(path.dirname(toRunDir), { recursive: true });
|
|
31
|
+
await cp(getRunDir(runId, fromRootDir), toRunDir, {
|
|
32
|
+
recursive: true,
|
|
33
|
+
force: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { getGitHubWebhookSecret } from "./config.js";
|
|
4
|
+
import { enqueueJob, findJobByDelivery } from "./job-store.js";
|
|
5
|
+
|
|
6
|
+
export function verifyGitHubSignature(rawBody, signature, secret = getGitHubWebhookSecret()) {
|
|
7
|
+
if (!secret) {
|
|
8
|
+
throw new Error("GITHUB_WEBHOOK_SECRET is required for webhook verification");
|
|
9
|
+
}
|
|
10
|
+
if (!signature?.startsWith("sha256=")) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
|
|
15
|
+
const expectedBuffer = Buffer.from(expected);
|
|
16
|
+
const actualBuffer = Buffer.from(signature);
|
|
17
|
+
return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleGitHubWebhook({ event, delivery, signature, rawBody }, rootDir = process.cwd()) {
|
|
21
|
+
if (!verifyGitHubSignature(rawBody, signature)) {
|
|
22
|
+
return { status: 401, body: { error: "invalid signature" } };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let payload;
|
|
26
|
+
try {
|
|
27
|
+
payload = JSON.parse(rawBody);
|
|
28
|
+
} catch {
|
|
29
|
+
return { status: 400, body: { error: "invalid JSON payload" } };
|
|
30
|
+
}
|
|
31
|
+
const normalized = normalizeGitHubWebhook(event, payload);
|
|
32
|
+
if (normalized.ignored) {
|
|
33
|
+
return { status: 202, body: normalized };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const existingJob = await findJobByDelivery(delivery, rootDir);
|
|
37
|
+
if (existingJob) {
|
|
38
|
+
return { status: 202, body: { queued: false, duplicate: true, jobId: existingJob.id } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const job = await enqueueJob({
|
|
42
|
+
type: normalized.type,
|
|
43
|
+
delivery,
|
|
44
|
+
payload: normalized.payload,
|
|
45
|
+
}, rootDir);
|
|
46
|
+
|
|
47
|
+
return { status: 202, body: { queued: true, jobId: job.id } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeGitHubWebhook(event, payload) {
|
|
51
|
+
if (event === "issue_comment" && payload.action === "created") {
|
|
52
|
+
const body = payload.comment?.body ?? "";
|
|
53
|
+
if (!mentionsAssembly(body)) {
|
|
54
|
+
return { ignored: true, reason: "comment does not mention @assembly" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (payload.issue?.pull_request) {
|
|
58
|
+
return {
|
|
59
|
+
type: "github.pr_feedback",
|
|
60
|
+
payload: {
|
|
61
|
+
kind: "issue_comment",
|
|
62
|
+
commentId: String(payload.comment.id),
|
|
63
|
+
feedback: body,
|
|
64
|
+
commentUrl: payload.comment.html_url,
|
|
65
|
+
prNumber: payload.issue.number,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
type: "github.issue_request",
|
|
72
|
+
payload: {
|
|
73
|
+
kind: "issue_comment",
|
|
74
|
+
issueNumber: payload.issue.number,
|
|
75
|
+
issueTitle: payload.issue.title ?? "",
|
|
76
|
+
issueBody: payload.issue.body ?? "",
|
|
77
|
+
feedback: body,
|
|
78
|
+
url: payload.comment.html_url,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (event === "pull_request_review_comment" && payload.action === "created") {
|
|
84
|
+
const body = payload.comment?.body ?? "";
|
|
85
|
+
if (!mentionsAssembly(body)) {
|
|
86
|
+
return { ignored: true, reason: "comment does not mention @assembly" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
type: "github.pr_feedback",
|
|
91
|
+
payload: {
|
|
92
|
+
kind: "pull_request_review_comment",
|
|
93
|
+
commentId: String(payload.comment.id),
|
|
94
|
+
feedback: formatInlineCommentFeedback(payload.comment),
|
|
95
|
+
commentUrl: payload.comment.html_url,
|
|
96
|
+
prNumber: payload.pull_request.number,
|
|
97
|
+
path: payload.comment.path,
|
|
98
|
+
line: payload.comment.line ?? payload.comment.original_line,
|
|
99
|
+
diffHunk: payload.comment.diff_hunk,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (event === "pull_request_review" && payload.action === "submitted") {
|
|
105
|
+
const body = payload.review?.body ?? "";
|
|
106
|
+
if (!mentionsAssembly(body)) {
|
|
107
|
+
return { ignored: true, reason: "review does not mention @assembly" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
type: "github.pr_feedback",
|
|
112
|
+
payload: {
|
|
113
|
+
kind: "pull_request_review",
|
|
114
|
+
reviewId: String(payload.review.id),
|
|
115
|
+
feedback: body,
|
|
116
|
+
commentUrl: payload.review.html_url,
|
|
117
|
+
prNumber: payload.pull_request.number,
|
|
118
|
+
reviewState: payload.review.state,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (event === "issues" && ["opened", "edited"].includes(payload.action)) {
|
|
124
|
+
if (payload.issue?.pull_request) {
|
|
125
|
+
return { ignored: true, reason: "issue event is for a pull request" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const requestText = [payload.issue?.title, payload.issue?.body].filter(Boolean).join("\n\n");
|
|
129
|
+
if (!mentionsAssembly(requestText)) {
|
|
130
|
+
return { ignored: true, reason: "issue does not mention @assembly" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
type: "github.issue_request",
|
|
135
|
+
payload: {
|
|
136
|
+
kind: `issues.${payload.action}`,
|
|
137
|
+
issueNumber: payload.issue.number,
|
|
138
|
+
issueTitle: payload.issue.title ?? "",
|
|
139
|
+
issueBody: payload.issue.body ?? "",
|
|
140
|
+
feedback: requestText,
|
|
141
|
+
url: payload.issue.html_url,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { ignored: true, reason: "unsupported event" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mentionsAssembly(text) {
|
|
150
|
+
return /(^|\s)@assembly\b/i.test(text ?? "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatInlineCommentFeedback(comment) {
|
|
154
|
+
return [
|
|
155
|
+
`Inline review comment on ${comment.path}${comment.line ? `:${comment.line}` : ""}.`,
|
|
156
|
+
comment.diff_hunk ? `Diff hunk:\n${comment.diff_hunk}` : "",
|
|
157
|
+
`Feedback: ${comment.body}`,
|
|
158
|
+
].filter(Boolean).join("\n\n");
|
|
159
|
+
}
|