@opentag/github 0.1.0 → 0.3.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/LICENSE +21 -0
- package/dist/apply.d.ts +91 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +950 -58
- package/dist/index.js.map +1 -1
- package/dist/ingress.d.ts +100 -0
- package/dist/ingress.d.ts.map +1 -0
- package/dist/normalize.d.ts.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,542 @@
|
|
|
1
|
+
// src/pull-request.ts
|
|
2
|
+
function buildPullRequestBody(result) {
|
|
3
|
+
const lines = ["## Summary", "", result.summary];
|
|
4
|
+
if (result.changedFiles?.length) {
|
|
5
|
+
lines.push("", "## Changed Files");
|
|
6
|
+
for (const file of result.changedFiles) {
|
|
7
|
+
lines.push(`- \`${file}\``);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
if (result.verification?.length) {
|
|
11
|
+
lines.push("", "## Verification");
|
|
12
|
+
for (const check of result.verification) {
|
|
13
|
+
lines.push(`- \`${check.command}\`: ${check.outcome}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return lines.join("\n");
|
|
17
|
+
}
|
|
18
|
+
async function createPullRequestViaFetch(input, fetchImpl = fetch) {
|
|
19
|
+
const response = await fetchImpl(`https://api.github.com/repos/${input.owner}/${input.repo}/pulls`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
accept: "application/vnd.github+json",
|
|
23
|
+
authorization: `Bearer ${input.token}`,
|
|
24
|
+
"content-type": "application/json",
|
|
25
|
+
"x-github-api-version": "2022-11-28"
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
title: input.title,
|
|
29
|
+
body: input.body,
|
|
30
|
+
head: input.head,
|
|
31
|
+
base: input.base
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`create pull request failed: ${response.status} ${await response.text()}`);
|
|
36
|
+
}
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
if (!body.html_url) {
|
|
39
|
+
throw new Error("create pull request response did not include html_url");
|
|
40
|
+
}
|
|
41
|
+
return body.html_url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/apply.ts
|
|
45
|
+
function labelFromIntent(intent) {
|
|
46
|
+
const value = intent.params?.["label"];
|
|
47
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
48
|
+
}
|
|
49
|
+
function labelsFromIntent(intent) {
|
|
50
|
+
const value = intent.params?.["labels"];
|
|
51
|
+
if (!Array.isArray(value)) return void 0;
|
|
52
|
+
const labels = value.filter((label) => typeof label === "string" && label.length > 0);
|
|
53
|
+
return labels.length > 0 ? labels : void 0;
|
|
54
|
+
}
|
|
55
|
+
function assigneeFromIntent(intent) {
|
|
56
|
+
const value = intent.params?.["assignee"];
|
|
57
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
58
|
+
}
|
|
59
|
+
function assigneesFromIntent(intent) {
|
|
60
|
+
const value = intent.params?.["assignees"];
|
|
61
|
+
if (!Array.isArray(value)) return void 0;
|
|
62
|
+
const assignees = value.filter((assignee) => typeof assignee === "string" && assignee.length > 0);
|
|
63
|
+
return assignees.length > 0 ? assignees : void 0;
|
|
64
|
+
}
|
|
65
|
+
function reviewersFromIntent(intent) {
|
|
66
|
+
const reviewer = intent.params?.["reviewer"];
|
|
67
|
+
const reviewers = intent.params?.["reviewers"];
|
|
68
|
+
const values = [
|
|
69
|
+
...typeof reviewer === "string" ? [reviewer] : [],
|
|
70
|
+
...Array.isArray(reviewers) ? reviewers : []
|
|
71
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
72
|
+
return values.length > 0 ? [...new Set(values)] : void 0;
|
|
73
|
+
}
|
|
74
|
+
function teamReviewersFromIntent(intent) {
|
|
75
|
+
const reviewer = intent.params?.["teamReviewer"];
|
|
76
|
+
const reviewers = intent.params?.["teamReviewers"] ?? intent.params?.["team_reviewers"];
|
|
77
|
+
const values = [
|
|
78
|
+
...typeof reviewer === "string" ? [reviewer] : [],
|
|
79
|
+
...Array.isArray(reviewers) ? reviewers : []
|
|
80
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
81
|
+
return values.length > 0 ? [...new Set(values)] : void 0;
|
|
82
|
+
}
|
|
83
|
+
function stringParam(intent, ...keys) {
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const value = intent.params?.[key];
|
|
86
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function stringArrayParam(intent, key) {
|
|
91
|
+
const value = intent.params?.[key];
|
|
92
|
+
if (!Array.isArray(value)) return [];
|
|
93
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
94
|
+
}
|
|
95
|
+
function verificationLinesFromIntent(intent) {
|
|
96
|
+
const value = intent.params?.["verification"];
|
|
97
|
+
if (!Array.isArray(value)) return [];
|
|
98
|
+
return value.map((item) => {
|
|
99
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
100
|
+
const command = item["command"];
|
|
101
|
+
const outcome = item["outcome"];
|
|
102
|
+
return typeof command === "string" && typeof outcome === "string" ? `- \`${command}\`: ${outcome}` : void 0;
|
|
103
|
+
}).filter((line) => Boolean(line));
|
|
104
|
+
}
|
|
105
|
+
function pullRequestBodyFromIntent(intent) {
|
|
106
|
+
const explicitBody = stringParam(intent, "body");
|
|
107
|
+
const changedFiles = stringArrayParam(intent, "changedFiles");
|
|
108
|
+
const risks = stringArrayParam(intent, "risks");
|
|
109
|
+
const verification = verificationLinesFromIntent(intent);
|
|
110
|
+
const executorConditions = stringArrayParam(intent, "executorConditions");
|
|
111
|
+
const lines = explicitBody ? [explicitBody] : ["## Summary", "", intent.summary];
|
|
112
|
+
if (changedFiles.length > 0) {
|
|
113
|
+
lines.push("", "## Changed Files", ...changedFiles.map((file) => `- \`${file}\``));
|
|
114
|
+
}
|
|
115
|
+
if (risks.length > 0) {
|
|
116
|
+
lines.push("", "## Risks", ...risks.map((risk) => `- ${risk}`));
|
|
117
|
+
}
|
|
118
|
+
if (verification.length > 0) {
|
|
119
|
+
lines.push("", "## Verification", ...verification);
|
|
120
|
+
}
|
|
121
|
+
if (executorConditions.length > 0) {
|
|
122
|
+
lines.push("", "## Executor Conditions", ...executorConditions.map((condition) => `- ${condition}`));
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
function mappedValueFromIntent(intent) {
|
|
127
|
+
const key = intent.domain === "status" ? "status" : "priority";
|
|
128
|
+
const value = intent.params?.[key] ?? intent.params?.["value"];
|
|
129
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
130
|
+
}
|
|
131
|
+
function labelMappingForIntent(input) {
|
|
132
|
+
const semanticValue = mappedValueFromIntent(input.intent);
|
|
133
|
+
if (!semanticValue) return void 0;
|
|
134
|
+
const mapping = input.mappings.find(
|
|
135
|
+
(candidate) => candidate.adapter === "github" && candidate.domain === input.intent.domain && candidate.strategy === "label"
|
|
136
|
+
);
|
|
137
|
+
const label = mapping?.values[semanticValue];
|
|
138
|
+
if (!label || !mapping) return void 0;
|
|
139
|
+
return {
|
|
140
|
+
label,
|
|
141
|
+
removeLabels: Object.values(mapping.values).filter((mappedLabel) => mappedLabel !== label)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function githubJson(input) {
|
|
145
|
+
const response = await input.fetchImpl(`https://api.github.com/repos/${input.target.owner}/${input.target.repo}${input.path}`, {
|
|
146
|
+
method: input.method,
|
|
147
|
+
headers: {
|
|
148
|
+
accept: "application/vnd.github+json",
|
|
149
|
+
authorization: `Bearer ${input.target.token}`,
|
|
150
|
+
"content-type": "application/json",
|
|
151
|
+
"x-github-api-version": "2022-11-28"
|
|
152
|
+
},
|
|
153
|
+
...input.body ? { body: JSON.stringify(input.body) } : {}
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok && !(input.okStatuses ?? []).includes(response.status)) {
|
|
156
|
+
throw new Error(`${input.method} ${input.path} failed: ${response.status} ${await response.text()}`);
|
|
157
|
+
}
|
|
158
|
+
return `https://github.com/${input.target.owner}/${input.target.repo}/issues/${input.target.issueNumber}`;
|
|
159
|
+
}
|
|
160
|
+
async function githubJsonBody(input) {
|
|
161
|
+
const response = await input.fetchImpl(`https://api.github.com/repos/${input.target.owner}/${input.target.repo}${input.path}`, {
|
|
162
|
+
method: input.method,
|
|
163
|
+
headers: {
|
|
164
|
+
accept: "application/vnd.github+json",
|
|
165
|
+
authorization: `Bearer ${input.target.token}`,
|
|
166
|
+
"content-type": "application/json",
|
|
167
|
+
"x-github-api-version": "2022-11-28"
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
if (!response.ok && !(input.okStatuses ?? []).includes(response.status)) {
|
|
171
|
+
throw new Error(`${input.method} ${input.path} failed: ${response.status} ${await response.text()}`);
|
|
172
|
+
}
|
|
173
|
+
return await response.json();
|
|
174
|
+
}
|
|
175
|
+
function requestedReviewerLogins(response) {
|
|
176
|
+
return new Set((response.users ?? []).map((user) => user.login).filter((login) => typeof login === "string"));
|
|
177
|
+
}
|
|
178
|
+
function requestedTeamReviewerNames(response) {
|
|
179
|
+
return new Set(
|
|
180
|
+
(response.teams ?? []).flatMap((team) => [team.slug, team.name]).filter((value) => typeof value === "string" && value.length > 0)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function compileGitHubIssueMutationIntent(intent, options = {}) {
|
|
184
|
+
if (intent.action === "create_pull_request") {
|
|
185
|
+
const head = stringParam(intent, "head", "branch");
|
|
186
|
+
if (!head) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
outcome: {
|
|
190
|
+
intentId: intent.intentId,
|
|
191
|
+
outcome: "failed",
|
|
192
|
+
message: "create_pull_request requires params.head or params.branch."
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
intentId: intent.intentId,
|
|
199
|
+
operation: {
|
|
200
|
+
kind: "create_pull_request",
|
|
201
|
+
intentId: intent.intentId,
|
|
202
|
+
title: stringParam(intent, "title") ?? intent.summary,
|
|
203
|
+
body: pullRequestBodyFromIntent(intent),
|
|
204
|
+
head,
|
|
205
|
+
base: stringParam(intent, "base", "baseBranch") ?? "main"
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (intent.action === "request_review" || intent.domain === "review") {
|
|
210
|
+
if (options.targetKind !== "pull_request") {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
outcome: {
|
|
214
|
+
intentId: intent.intentId,
|
|
215
|
+
outcome: "unsupported",
|
|
216
|
+
message: "GitHub review requests require a pull request target."
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const reviewers = reviewersFromIntent(intent);
|
|
221
|
+
const teamReviewers = teamReviewersFromIntent(intent);
|
|
222
|
+
if (!reviewers?.length && !teamReviewers?.length) {
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
outcome: {
|
|
226
|
+
intentId: intent.intentId,
|
|
227
|
+
outcome: "failed",
|
|
228
|
+
message: "request_review requires params.reviewer, params.reviewers, or params.teamReviewers."
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
intentId: intent.intentId,
|
|
235
|
+
operation: {
|
|
236
|
+
kind: "request_review",
|
|
237
|
+
intentId: intent.intentId,
|
|
238
|
+
reviewers: reviewers ?? [],
|
|
239
|
+
...teamReviewers?.length ? { teamReviewers } : {}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (intent.domain === "status") {
|
|
244
|
+
const mapped = labelMappingForIntent({ intent, mappings: options.mappings ?? [] });
|
|
245
|
+
if (mapped) {
|
|
246
|
+
return { ok: true, intentId: intent.intentId, operation: { kind: "replace_mapped_label", intentId: intent.intentId, ...mapped } };
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
outcome: {
|
|
251
|
+
intentId: intent.intentId,
|
|
252
|
+
outcome: "unsupported",
|
|
253
|
+
message: "GitHub status writes require an explicit Project field or label mapping policy."
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (intent.domain === "priority") {
|
|
258
|
+
const mapped = labelMappingForIntent({ intent, mappings: options.mappings ?? [] });
|
|
259
|
+
if (mapped) {
|
|
260
|
+
return { ok: true, intentId: intent.intentId, operation: { kind: "replace_mapped_label", intentId: intent.intentId, ...mapped } };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
outcome: {
|
|
265
|
+
intentId: intent.intentId,
|
|
266
|
+
outcome: "unsupported",
|
|
267
|
+
message: "GitHub priority writes require an explicit label or Project field mapping policy."
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (intent.domain !== "labels" && intent.domain !== "assignee") {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
outcome: {
|
|
275
|
+
intentId: intent.intentId,
|
|
276
|
+
outcome: "unsupported",
|
|
277
|
+
message: `GitHub apply supports labels and assignee only, not ${intent.domain}.`
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (intent.domain === "assignee") {
|
|
282
|
+
if (intent.action === "set_assignee") {
|
|
283
|
+
const assignee = assigneeFromIntent(intent);
|
|
284
|
+
return assignee ? { ok: true, intentId: intent.intentId, operation: { kind: "set_assignees", intentId: intent.intentId, assignees: [assignee] } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "set_assignee requires params.assignee." } };
|
|
285
|
+
}
|
|
286
|
+
if (intent.action === "set_assignees") {
|
|
287
|
+
const assignees = assigneesFromIntent(intent);
|
|
288
|
+
return assignees ? { ok: true, intentId: intent.intentId, operation: { kind: "set_assignees", intentId: intent.intentId, assignees } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "set_assignees requires params.assignees." } };
|
|
289
|
+
}
|
|
290
|
+
if (intent.action === "add_assignee") {
|
|
291
|
+
const assignee = assigneeFromIntent(intent);
|
|
292
|
+
return assignee ? { ok: true, intentId: intent.intentId, operation: { kind: "add_assignee", intentId: intent.intentId, assignee } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "add_assignee requires params.assignee." } };
|
|
293
|
+
}
|
|
294
|
+
if (intent.action === "remove_assignee") {
|
|
295
|
+
const assignee = assigneeFromIntent(intent);
|
|
296
|
+
return assignee ? { ok: true, intentId: intent.intentId, operation: { kind: "remove_assignee", intentId: intent.intentId, assignee } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "remove_assignee requires params.assignee." } };
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
outcome: {
|
|
301
|
+
intentId: intent.intentId,
|
|
302
|
+
outcome: "unsupported",
|
|
303
|
+
message: `GitHub apply does not support assignee action ${intent.action}.`
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (intent.action === "add_label") {
|
|
308
|
+
const label = labelFromIntent(intent);
|
|
309
|
+
return label ? { ok: true, intentId: intent.intentId, operation: { kind: "add_label", intentId: intent.intentId, label } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "add_label requires params.label." } };
|
|
310
|
+
}
|
|
311
|
+
if (intent.action === "remove_label") {
|
|
312
|
+
const label = labelFromIntent(intent);
|
|
313
|
+
return label ? { ok: true, intentId: intent.intentId, operation: { kind: "remove_label", intentId: intent.intentId, label } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "remove_label requires params.label." } };
|
|
314
|
+
}
|
|
315
|
+
if (intent.action === "set_labels") {
|
|
316
|
+
const labels = labelsFromIntent(intent);
|
|
317
|
+
return labels ? { ok: true, intentId: intent.intentId, operation: { kind: "set_labels", intentId: intent.intentId, labels } } : { ok: false, outcome: { intentId: intent.intentId, outcome: "failed", message: "set_labels requires params.labels." } };
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
outcome: {
|
|
322
|
+
intentId: intent.intentId,
|
|
323
|
+
outcome: "unsupported",
|
|
324
|
+
message: `GitHub apply does not support labels action ${intent.action}.`
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function compileGitHubIssueMutationIntents(intents, options = {}) {
|
|
329
|
+
return intents.map((intent) => compileGitHubIssueMutationIntent(intent, options));
|
|
330
|
+
}
|
|
331
|
+
function createGitHubIssueMutationCompiler(options = {}) {
|
|
332
|
+
return {
|
|
333
|
+
adapter: "github",
|
|
334
|
+
compile(intent) {
|
|
335
|
+
const compilation = compileGitHubIssueMutationIntent(intent, options);
|
|
336
|
+
if (!compilation.ok) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
adapter: "github",
|
|
340
|
+
outcome: compilation.outcome
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
adapter: "github",
|
|
346
|
+
intentId: compilation.intentId,
|
|
347
|
+
operation: compilation.operation
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async function applyGitHubIssueMutationOperation(input) {
|
|
353
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
354
|
+
try {
|
|
355
|
+
if (input.operation.kind === "create_pull_request") {
|
|
356
|
+
const externalUri2 = await createPullRequestViaFetch(
|
|
357
|
+
{
|
|
358
|
+
token: input.target.token,
|
|
359
|
+
owner: input.target.owner,
|
|
360
|
+
repo: input.target.repo,
|
|
361
|
+
title: input.operation.title,
|
|
362
|
+
body: input.operation.body,
|
|
363
|
+
head: input.operation.head,
|
|
364
|
+
base: input.operation.base
|
|
365
|
+
},
|
|
366
|
+
fetchImpl
|
|
367
|
+
);
|
|
368
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
369
|
+
}
|
|
370
|
+
if (input.operation.kind === "request_review") {
|
|
371
|
+
const pullRequestNumber = input.target.pullRequestNumber;
|
|
372
|
+
if (typeof pullRequestNumber !== "number") {
|
|
373
|
+
return {
|
|
374
|
+
intentId: input.operation.intentId,
|
|
375
|
+
outcome: "failed",
|
|
376
|
+
message: "request_review requires target.pullRequestNumber."
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const requested = await githubJsonBody({
|
|
380
|
+
target: input.target,
|
|
381
|
+
fetchImpl,
|
|
382
|
+
method: "GET",
|
|
383
|
+
path: `/pulls/${pullRequestNumber}/requested_reviewers`
|
|
384
|
+
});
|
|
385
|
+
const existingReviewers = requestedReviewerLogins(requested);
|
|
386
|
+
const existingTeamReviewers = requestedTeamReviewerNames(requested);
|
|
387
|
+
const reviewers = input.operation.reviewers.filter((reviewer) => !existingReviewers.has(reviewer));
|
|
388
|
+
const teamReviewers = input.operation.teamReviewers?.filter((reviewer) => !existingTeamReviewers.has(reviewer));
|
|
389
|
+
if (reviewers.length === 0 && (!teamReviewers || teamReviewers.length === 0)) {
|
|
390
|
+
return {
|
|
391
|
+
intentId: input.operation.intentId,
|
|
392
|
+
outcome: "applied",
|
|
393
|
+
externalUri: `https://github.com/${input.target.owner}/${input.target.repo}/pull/${pullRequestNumber}`,
|
|
394
|
+
message: "Requested reviewers were already present; skipped GitHub notification retry."
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
await githubJson({
|
|
398
|
+
target: input.target,
|
|
399
|
+
fetchImpl,
|
|
400
|
+
method: "POST",
|
|
401
|
+
path: `/pulls/${pullRequestNumber}/requested_reviewers`,
|
|
402
|
+
body: {
|
|
403
|
+
...reviewers.length ? { reviewers } : {},
|
|
404
|
+
...teamReviewers?.length ? { team_reviewers: teamReviewers } : {}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
intentId: input.operation.intentId,
|
|
409
|
+
outcome: "applied",
|
|
410
|
+
externalUri: `https://github.com/${input.target.owner}/${input.target.repo}/pull/${pullRequestNumber}`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const issueNumber = input.target.issueNumber;
|
|
414
|
+
if (typeof issueNumber !== "number") {
|
|
415
|
+
return {
|
|
416
|
+
intentId: input.operation.intentId,
|
|
417
|
+
outcome: "failed",
|
|
418
|
+
message: "GitHub issue mutation requires target.issueNumber."
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (input.operation.kind === "set_assignees") {
|
|
422
|
+
const externalUri2 = await githubJson({
|
|
423
|
+
target: input.target,
|
|
424
|
+
fetchImpl,
|
|
425
|
+
method: "PATCH",
|
|
426
|
+
path: `/issues/${issueNumber}`,
|
|
427
|
+
body: { assignees: input.operation.assignees }
|
|
428
|
+
});
|
|
429
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
430
|
+
}
|
|
431
|
+
if (input.operation.kind === "add_assignee") {
|
|
432
|
+
const externalUri2 = await githubJson({
|
|
433
|
+
target: input.target,
|
|
434
|
+
fetchImpl,
|
|
435
|
+
method: "POST",
|
|
436
|
+
path: `/issues/${issueNumber}/assignees`,
|
|
437
|
+
body: { assignees: [input.operation.assignee] }
|
|
438
|
+
});
|
|
439
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
440
|
+
}
|
|
441
|
+
if (input.operation.kind === "remove_assignee") {
|
|
442
|
+
const externalUri2 = await githubJson({
|
|
443
|
+
target: input.target,
|
|
444
|
+
fetchImpl,
|
|
445
|
+
method: "DELETE",
|
|
446
|
+
path: `/issues/${issueNumber}/assignees`,
|
|
447
|
+
body: { assignees: [input.operation.assignee] }
|
|
448
|
+
});
|
|
449
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
450
|
+
}
|
|
451
|
+
if (input.operation.kind === "replace_mapped_label") {
|
|
452
|
+
for (const label of input.operation.removeLabels) {
|
|
453
|
+
await githubJson({
|
|
454
|
+
target: input.target,
|
|
455
|
+
fetchImpl,
|
|
456
|
+
method: "DELETE",
|
|
457
|
+
path: `/issues/${issueNumber}/labels/${encodeURIComponent(label)}`,
|
|
458
|
+
okStatuses: [200, 404]
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const externalUri2 = await githubJson({
|
|
462
|
+
target: input.target,
|
|
463
|
+
fetchImpl,
|
|
464
|
+
method: "POST",
|
|
465
|
+
path: `/issues/${issueNumber}/labels`,
|
|
466
|
+
body: { labels: [input.operation.label] }
|
|
467
|
+
});
|
|
468
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
469
|
+
}
|
|
470
|
+
if (input.operation.kind === "add_label") {
|
|
471
|
+
const externalUri2 = await githubJson({
|
|
472
|
+
target: input.target,
|
|
473
|
+
fetchImpl,
|
|
474
|
+
method: "POST",
|
|
475
|
+
path: `/issues/${issueNumber}/labels`,
|
|
476
|
+
body: { labels: [input.operation.label] }
|
|
477
|
+
});
|
|
478
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
479
|
+
}
|
|
480
|
+
if (input.operation.kind === "remove_label") {
|
|
481
|
+
const externalUri2 = await githubJson({
|
|
482
|
+
target: input.target,
|
|
483
|
+
fetchImpl,
|
|
484
|
+
method: "DELETE",
|
|
485
|
+
path: `/issues/${issueNumber}/labels/${encodeURIComponent(input.operation.label)}`
|
|
486
|
+
});
|
|
487
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri: externalUri2 };
|
|
488
|
+
}
|
|
489
|
+
const externalUri = await githubJson({
|
|
490
|
+
target: input.target,
|
|
491
|
+
fetchImpl,
|
|
492
|
+
method: "PUT",
|
|
493
|
+
path: `/issues/${issueNumber}/labels`,
|
|
494
|
+
body: { labels: input.operation.labels }
|
|
495
|
+
});
|
|
496
|
+
return { intentId: input.operation.intentId, outcome: "applied", externalUri };
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
intentId: input.operation.intentId,
|
|
500
|
+
outcome: "failed",
|
|
501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function applyGitHubIssueMutationIntent(input) {
|
|
506
|
+
const compiled = compileGitHubIssueMutationIntent(input.intent, {
|
|
507
|
+
...input.mappings ? { mappings: input.mappings } : {},
|
|
508
|
+
...input.targetKind ? { targetKind: input.targetKind } : {}
|
|
509
|
+
});
|
|
510
|
+
if (!compiled.ok) return compiled.outcome;
|
|
511
|
+
return applyGitHubIssueMutationOperation({
|
|
512
|
+
target: input.target,
|
|
513
|
+
operation: compiled.operation,
|
|
514
|
+
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
async function applyGitHubIssueMutationIntents(input) {
|
|
518
|
+
const outcomes = [];
|
|
519
|
+
for (const intent of input.intents) {
|
|
520
|
+
outcomes.push(
|
|
521
|
+
await applyGitHubIssueMutationIntent({
|
|
522
|
+
target: input.target,
|
|
523
|
+
intent,
|
|
524
|
+
...input.mappings ? { mappings: input.mappings } : {},
|
|
525
|
+
...input.targetKind ? { targetKind: input.targetKind } : {},
|
|
526
|
+
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return outcomes;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/ingress.ts
|
|
534
|
+
import { createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
535
|
+
import { serve } from "@hono/node-server";
|
|
536
|
+
import { createOpenTagClient } from "@opentag/client";
|
|
537
|
+
import { parseThreadActionCommand } from "@opentag/core";
|
|
538
|
+
import { Hono } from "hono";
|
|
539
|
+
|
|
1
540
|
// src/normalize.ts
|
|
2
541
|
import { parseOpenTagMention } from "@opentag/core";
|
|
3
542
|
function permissionsForIntent(intent) {
|
|
@@ -29,9 +568,77 @@ function permissionsForIntent(intent) {
|
|
|
29
568
|
}
|
|
30
569
|
return permissions;
|
|
31
570
|
}
|
|
571
|
+
function permissionsForPullRequestReviewCommentIntent(intent) {
|
|
572
|
+
const permissions = permissionsForIntent(intent);
|
|
573
|
+
if (intent === "review") {
|
|
574
|
+
permissions.push({
|
|
575
|
+
scope: "pr:update",
|
|
576
|
+
reason: "request reviewers on the source pull request after explicit approval"
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return permissions;
|
|
580
|
+
}
|
|
581
|
+
function contextPointersForCommand(command, privateRepo) {
|
|
582
|
+
const visibility = privateRepo ? "private" : "public";
|
|
583
|
+
const context = [];
|
|
584
|
+
for (const reference of command.parsed?.references ?? []) {
|
|
585
|
+
if (reference.kind === "url") {
|
|
586
|
+
context.push({
|
|
587
|
+
kind: "url",
|
|
588
|
+
uri: reference.uri,
|
|
589
|
+
visibility,
|
|
590
|
+
title: reference.title ?? "Command URL reference"
|
|
591
|
+
});
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (reference.kind === "file" || reference.kind === "path" || reference.kind === "line" || reference.kind === "range") {
|
|
595
|
+
context.push({
|
|
596
|
+
kind: "file",
|
|
597
|
+
uri: reference.uri,
|
|
598
|
+
...reference.line ? { line: reference.line } : {},
|
|
599
|
+
...reference.startLine ? { startLine: reference.startLine } : {},
|
|
600
|
+
...reference.endLine ? { endLine: reference.endLine } : {},
|
|
601
|
+
visibility,
|
|
602
|
+
title: referenceTitle(reference)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return context;
|
|
607
|
+
}
|
|
608
|
+
function referenceTitle(reference) {
|
|
609
|
+
return reference.title ?? "Command file reference";
|
|
610
|
+
}
|
|
611
|
+
function githubWorkItem(input) {
|
|
612
|
+
return {
|
|
613
|
+
provider: "github",
|
|
614
|
+
kind: input.kind,
|
|
615
|
+
externalId: `${input.owner}/${input.repo}#${input.number}`,
|
|
616
|
+
uri: input.uri,
|
|
617
|
+
ownerContainer: {
|
|
618
|
+
provider: "github",
|
|
619
|
+
id: `${input.owner}/${input.repo}`,
|
|
620
|
+
uri: `https://github.com/${input.owner}/${input.repo}`
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function commandMetadata(command) {
|
|
625
|
+
if (!command.parsed) return {};
|
|
626
|
+
return {
|
|
627
|
+
commandParser: command.parsed.version,
|
|
628
|
+
commandDiagnostics: command.parsed.diagnostics,
|
|
629
|
+
...command.parsed.approval ? { approval: command.parsed.approval } : {},
|
|
630
|
+
...command.parsed.network ? { network: command.parsed.network } : {}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
32
633
|
function normalizeGitHubIssueComment(input) {
|
|
33
634
|
const mention = parseOpenTagMention(input.commentBody);
|
|
34
635
|
if (!mention.matched) return null;
|
|
636
|
+
const command = {
|
|
637
|
+
rawText: mention.rawText,
|
|
638
|
+
intent: mention.intent,
|
|
639
|
+
args: mention.args,
|
|
640
|
+
...mention.parsed ? { parsed: mention.parsed } : {}
|
|
641
|
+
};
|
|
35
642
|
return {
|
|
36
643
|
id: `evt_github_comment_${input.id}`,
|
|
37
644
|
source: "github",
|
|
@@ -44,35 +651,44 @@ function normalizeGitHubIssueComment(input) {
|
|
|
44
651
|
},
|
|
45
652
|
target: {
|
|
46
653
|
mention: "@opentag",
|
|
47
|
-
agentId: "opentag"
|
|
48
|
-
|
|
49
|
-
command: {
|
|
50
|
-
rawText: mention.rawText,
|
|
51
|
-
intent: mention.intent,
|
|
52
|
-
args: mention.args
|
|
654
|
+
agentId: "opentag",
|
|
655
|
+
...mention.parsed?.executorHint ? { executorHint: mention.parsed.executorHint } : {}
|
|
53
656
|
},
|
|
657
|
+
command,
|
|
54
658
|
context: [
|
|
55
659
|
{
|
|
56
|
-
|
|
660
|
+
provider: "github",
|
|
661
|
+
kind: "issue",
|
|
57
662
|
uri: input.issueUrl,
|
|
58
663
|
visibility: input.private ? "private" : "public"
|
|
59
664
|
},
|
|
60
665
|
{
|
|
61
|
-
|
|
666
|
+
provider: "github",
|
|
667
|
+
kind: "comment",
|
|
62
668
|
uri: input.commentUrl,
|
|
63
669
|
visibility: input.private ? "private" : "public"
|
|
64
|
-
}
|
|
670
|
+
},
|
|
671
|
+
...contextPointersForCommand(command, input.private)
|
|
65
672
|
],
|
|
673
|
+
workItem: githubWorkItem({
|
|
674
|
+
owner: input.owner,
|
|
675
|
+
repo: input.repo,
|
|
676
|
+
kind: "issue",
|
|
677
|
+
number: input.issueNumber,
|
|
678
|
+
uri: input.issueUrl
|
|
679
|
+
}),
|
|
66
680
|
permissions: permissionsForIntent(mention.intent),
|
|
67
681
|
callback: {
|
|
68
682
|
provider: "github",
|
|
69
683
|
uri: input.apiCommentsUrl,
|
|
70
|
-
threadKey: `${input.owner}/${input.repo}`
|
|
684
|
+
threadKey: `${input.owner}/${input.repo}#${input.issueNumber}`
|
|
71
685
|
},
|
|
72
686
|
metadata: {
|
|
687
|
+
repoProvider: "github",
|
|
73
688
|
owner: input.owner,
|
|
74
689
|
repo: input.repo,
|
|
75
690
|
issueNumber: input.issueNumber,
|
|
691
|
+
...commandMetadata(command),
|
|
76
692
|
...typeof input.installationId === "number" ? { installationId: input.installationId } : {}
|
|
77
693
|
}
|
|
78
694
|
};
|
|
@@ -80,6 +696,12 @@ function normalizeGitHubIssueComment(input) {
|
|
|
80
696
|
function normalizeGitHubPullRequestReviewComment(input) {
|
|
81
697
|
const mention = parseOpenTagMention(input.commentBody);
|
|
82
698
|
if (!mention.matched) return null;
|
|
699
|
+
const command = {
|
|
700
|
+
rawText: mention.rawText,
|
|
701
|
+
intent: mention.intent,
|
|
702
|
+
args: mention.args,
|
|
703
|
+
...mention.parsed ? { parsed: mention.parsed } : {}
|
|
704
|
+
};
|
|
83
705
|
return {
|
|
84
706
|
id: `evt_github_pr_review_comment_${input.id}`,
|
|
85
707
|
source: "github",
|
|
@@ -92,84 +714,339 @@ function normalizeGitHubPullRequestReviewComment(input) {
|
|
|
92
714
|
},
|
|
93
715
|
target: {
|
|
94
716
|
mention: "@opentag",
|
|
95
|
-
agentId: "opentag"
|
|
96
|
-
|
|
97
|
-
command: {
|
|
98
|
-
rawText: mention.rawText,
|
|
99
|
-
intent: mention.intent,
|
|
100
|
-
args: mention.args
|
|
717
|
+
agentId: "opentag",
|
|
718
|
+
...mention.parsed?.executorHint ? { executorHint: mention.parsed.executorHint } : {}
|
|
101
719
|
},
|
|
720
|
+
command,
|
|
102
721
|
context: [
|
|
103
722
|
{
|
|
104
|
-
|
|
723
|
+
provider: "github",
|
|
724
|
+
kind: "pull_request",
|
|
105
725
|
uri: input.pullRequestUrl,
|
|
106
726
|
visibility: input.private ? "private" : "public"
|
|
107
727
|
},
|
|
108
728
|
{
|
|
109
|
-
|
|
729
|
+
provider: "github",
|
|
730
|
+
kind: "comment",
|
|
110
731
|
uri: input.commentUrl,
|
|
111
732
|
visibility: input.private ? "private" : "public"
|
|
112
|
-
}
|
|
733
|
+
},
|
|
734
|
+
...contextPointersForCommand(command, input.private)
|
|
113
735
|
],
|
|
114
|
-
|
|
736
|
+
workItem: githubWorkItem({
|
|
737
|
+
owner: input.owner,
|
|
738
|
+
repo: input.repo,
|
|
739
|
+
kind: "pull_request",
|
|
740
|
+
number: input.pullRequestNumber,
|
|
741
|
+
uri: input.pullRequestUrl
|
|
742
|
+
}),
|
|
743
|
+
permissions: permissionsForPullRequestReviewCommentIntent(mention.intent),
|
|
115
744
|
callback: {
|
|
116
745
|
provider: "github",
|
|
117
746
|
uri: input.apiCommentsUrl,
|
|
118
747
|
threadKey: `${input.owner}/${input.repo}#${input.pullRequestNumber}`
|
|
119
748
|
},
|
|
120
749
|
metadata: {
|
|
750
|
+
repoProvider: "github",
|
|
121
751
|
owner: input.owner,
|
|
122
752
|
repo: input.repo,
|
|
123
753
|
pullRequestNumber: input.pullRequestNumber,
|
|
754
|
+
...commandMetadata(command),
|
|
124
755
|
...typeof input.installationId === "number" ? { installationId: input.installationId } : {}
|
|
125
756
|
}
|
|
126
757
|
};
|
|
127
758
|
}
|
|
128
759
|
|
|
129
|
-
// src/
|
|
130
|
-
function
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
760
|
+
// src/ingress.ts
|
|
761
|
+
function computeGitHubSignature(input) {
|
|
762
|
+
const digest = createHmac("sha256", input.webhookSecret).update(input.rawBody).digest("hex");
|
|
763
|
+
return `sha256=${digest}`;
|
|
764
|
+
}
|
|
765
|
+
function verifyGitHubSignature(input) {
|
|
766
|
+
const expected = computeGitHubSignature(input);
|
|
767
|
+
const expectedBuffer = Buffer.from(expected);
|
|
768
|
+
const actualBuffer = Buffer.from(input.signature);
|
|
769
|
+
return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
|
|
770
|
+
}
|
|
771
|
+
function parseJsonPayload(rawBody) {
|
|
772
|
+
try {
|
|
773
|
+
return JSON.parse(rawBody);
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
137
776
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
777
|
+
}
|
|
778
|
+
async function handleIssueCommentCreated(input) {
|
|
779
|
+
if (input.payload.action && input.payload.action !== "created") return;
|
|
780
|
+
if (parseThreadActionCommand(input.payload.comment.body) && input.submitThreadAction) {
|
|
781
|
+
await input.submitThreadAction({
|
|
782
|
+
id: `approval_github_comment_${input.payload.comment.id}`,
|
|
783
|
+
rawText: input.payload.comment.body,
|
|
784
|
+
actor: {
|
|
785
|
+
provider: "github",
|
|
786
|
+
providerUserId: String(input.payload.sender.id),
|
|
787
|
+
handle: input.payload.sender.login
|
|
788
|
+
},
|
|
789
|
+
callback: {
|
|
790
|
+
provider: "github",
|
|
791
|
+
uri: input.payload.issue.comments_url,
|
|
792
|
+
threadKey: `${input.payload.repository.owner.login}/${input.payload.repository.name}#${input.payload.issue.number}`
|
|
793
|
+
},
|
|
794
|
+
metadata: {
|
|
795
|
+
repoProvider: "github",
|
|
796
|
+
owner: input.payload.repository.owner.login,
|
|
797
|
+
repo: input.payload.repository.name,
|
|
798
|
+
issueNumber: input.payload.issue.number,
|
|
799
|
+
commentUrl: input.payload.comment.html_url
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const event = normalizeGitHubIssueComment({
|
|
805
|
+
id: String(input.payload.comment.id),
|
|
806
|
+
commentBody: input.payload.comment.body,
|
|
807
|
+
commentUrl: input.payload.comment.html_url,
|
|
808
|
+
apiCommentsUrl: input.payload.issue.comments_url,
|
|
809
|
+
issueUrl: input.payload.issue.html_url,
|
|
810
|
+
issueNumber: input.payload.issue.number,
|
|
811
|
+
owner: input.payload.repository.owner.login,
|
|
812
|
+
repo: input.payload.repository.name,
|
|
813
|
+
actorId: input.payload.sender.id,
|
|
814
|
+
actorLogin: input.payload.sender.login,
|
|
815
|
+
private: input.payload.repository.private,
|
|
816
|
+
receivedAt: input.now(),
|
|
817
|
+
...input.payload.installation ? { installationId: input.payload.installation.id } : {}
|
|
818
|
+
});
|
|
819
|
+
if (event) {
|
|
820
|
+
await input.createRun(event);
|
|
143
821
|
}
|
|
144
|
-
return lines.join("\n");
|
|
145
822
|
}
|
|
146
|
-
async function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
823
|
+
async function handlePullRequestReviewCommentCreated(input) {
|
|
824
|
+
if (input.payload.action && input.payload.action !== "created") return;
|
|
825
|
+
const owner = input.payload.repository.owner.login;
|
|
826
|
+
const repo = input.payload.repository.name;
|
|
827
|
+
if (parseThreadActionCommand(input.payload.comment.body) && input.submitThreadAction) {
|
|
828
|
+
await input.submitThreadAction({
|
|
829
|
+
id: `approval_github_pr_review_comment_${input.payload.comment.id}`,
|
|
830
|
+
rawText: input.payload.comment.body,
|
|
831
|
+
actor: {
|
|
832
|
+
provider: "github",
|
|
833
|
+
providerUserId: String(input.payload.sender.id),
|
|
834
|
+
handle: input.payload.sender.login
|
|
835
|
+
},
|
|
836
|
+
callback: {
|
|
837
|
+
provider: "github",
|
|
838
|
+
uri: `https://api.github.com/repos/${owner}/${repo}/issues/${input.payload.pull_request.number}/comments`,
|
|
839
|
+
threadKey: `${owner}/${repo}#${input.payload.pull_request.number}`
|
|
840
|
+
},
|
|
841
|
+
metadata: {
|
|
842
|
+
repoProvider: "github",
|
|
843
|
+
owner,
|
|
844
|
+
repo,
|
|
845
|
+
pullRequestNumber: input.payload.pull_request.number,
|
|
846
|
+
commentUrl: input.payload.comment.html_url
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const event = normalizeGitHubPullRequestReviewComment({
|
|
852
|
+
id: String(input.payload.comment.id),
|
|
853
|
+
commentBody: input.payload.comment.body,
|
|
854
|
+
commentUrl: input.payload.comment.html_url,
|
|
855
|
+
pullRequestUrl: input.payload.pull_request.html_url,
|
|
856
|
+
apiCommentsUrl: `https://api.github.com/repos/${owner}/${repo}/issues/${input.payload.pull_request.number}/comments`,
|
|
857
|
+
owner,
|
|
858
|
+
repo,
|
|
859
|
+
pullRequestNumber: input.payload.pull_request.number,
|
|
860
|
+
actorId: input.payload.sender.id,
|
|
861
|
+
actorLogin: input.payload.sender.login,
|
|
862
|
+
private: input.payload.repository.private,
|
|
863
|
+
receivedAt: input.now(),
|
|
864
|
+
...input.payload.installation ? { installationId: input.payload.installation.id } : {}
|
|
161
865
|
});
|
|
162
|
-
if (
|
|
163
|
-
|
|
866
|
+
if (event) {
|
|
867
|
+
await input.createRun(event);
|
|
164
868
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
869
|
+
}
|
|
870
|
+
function createGitHubWebhookApp(input) {
|
|
871
|
+
const app = new Hono();
|
|
872
|
+
const webhookPath = input.webhookPath ?? "/github/webhooks";
|
|
873
|
+
if (!webhookPath.startsWith("/")) {
|
|
874
|
+
throw new Error("GitHub webhook path must start with /.");
|
|
168
875
|
}
|
|
169
|
-
|
|
876
|
+
app.post(webhookPath, async (c) => {
|
|
877
|
+
const signature = c.req.header("x-hub-signature-256");
|
|
878
|
+
if (!signature) {
|
|
879
|
+
return c.json({ error: "missing_signature_header" }, 401);
|
|
880
|
+
}
|
|
881
|
+
const rawBody = await c.req.text();
|
|
882
|
+
if (!verifyGitHubSignature({ webhookSecret: input.webhookSecret, rawBody, signature })) {
|
|
883
|
+
return c.json({ error: "invalid_signature" }, 401);
|
|
884
|
+
}
|
|
885
|
+
const eventName = c.req.header("x-github-event");
|
|
886
|
+
const payload = parseJsonPayload(rawBody);
|
|
887
|
+
if (!payload || typeof payload !== "object") {
|
|
888
|
+
return c.json({ error: "invalid_json" }, 400);
|
|
889
|
+
}
|
|
890
|
+
if (eventName === "ping") {
|
|
891
|
+
return c.json({ ok: true });
|
|
892
|
+
}
|
|
893
|
+
if (eventName === "issue_comment") {
|
|
894
|
+
await handleIssueCommentCreated({
|
|
895
|
+
payload,
|
|
896
|
+
createRun: input.createRun,
|
|
897
|
+
...input.submitThreadAction ? { submitThreadAction: input.submitThreadAction } : {},
|
|
898
|
+
now: input.now
|
|
899
|
+
});
|
|
900
|
+
return c.json({ ok: true });
|
|
901
|
+
}
|
|
902
|
+
if (eventName === "pull_request_review_comment") {
|
|
903
|
+
await handlePullRequestReviewCommentCreated({
|
|
904
|
+
payload,
|
|
905
|
+
createRun: input.createRun,
|
|
906
|
+
...input.submitThreadAction ? { submitThreadAction: input.submitThreadAction } : {},
|
|
907
|
+
now: input.now
|
|
908
|
+
});
|
|
909
|
+
return c.json({ ok: true });
|
|
910
|
+
}
|
|
911
|
+
return c.json({ ok: true, ignored: "unsupported_event" });
|
|
912
|
+
});
|
|
913
|
+
return app;
|
|
914
|
+
}
|
|
915
|
+
function startGitHubIngress(config) {
|
|
916
|
+
const dispatcherClient = createOpenTagClient({
|
|
917
|
+
dispatcherUrl: config.dispatcherUrl,
|
|
918
|
+
...config.dispatcherToken ? { pairingToken: config.dispatcherToken } : {}
|
|
919
|
+
});
|
|
920
|
+
const port = config.port ?? 3e3;
|
|
921
|
+
const hostname = config.hostname ?? "127.0.0.1";
|
|
922
|
+
const webhookPath = config.webhookPath ?? "/github/webhooks";
|
|
923
|
+
const server = serve({
|
|
924
|
+
fetch: createGitHubWebhookApp({
|
|
925
|
+
webhookSecret: config.webhookSecret,
|
|
926
|
+
webhookPath,
|
|
927
|
+
async createRun(event) {
|
|
928
|
+
const runId = `run_${randomUUID()}`;
|
|
929
|
+
const created = await dispatcherClient.createRun({ runId, event });
|
|
930
|
+
return created.outcome === "run_created" ? { runId: created.run.id } : {};
|
|
931
|
+
},
|
|
932
|
+
async submitThreadAction(action) {
|
|
933
|
+
await dispatcherClient.submitThreadAction(action);
|
|
934
|
+
},
|
|
935
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
936
|
+
}).fetch,
|
|
937
|
+
port,
|
|
938
|
+
hostname
|
|
939
|
+
});
|
|
940
|
+
return {
|
|
941
|
+
url: `http://${hostname}:${port}`,
|
|
942
|
+
webhookPath,
|
|
943
|
+
server,
|
|
944
|
+
close() {
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
server.close((error) => {
|
|
947
|
+
if (error) {
|
|
948
|
+
reject(error);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
resolve();
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
};
|
|
170
956
|
}
|
|
171
957
|
|
|
172
958
|
// src/render.ts
|
|
959
|
+
import { suggestedActionCandidatesFromResult } from "@opentag/core";
|
|
960
|
+
function nextActionSummary(result) {
|
|
961
|
+
if (!result.nextAction) return void 0;
|
|
962
|
+
if (typeof result.nextAction === "string") return result.nextAction;
|
|
963
|
+
return result.nextAction.summary;
|
|
964
|
+
}
|
|
965
|
+
function stringParam2(params, key) {
|
|
966
|
+
const value = params?.[key];
|
|
967
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
968
|
+
}
|
|
969
|
+
function stringArrayParam2(params, key) {
|
|
970
|
+
const value = params?.[key];
|
|
971
|
+
if (!Array.isArray(value)) return [];
|
|
972
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
973
|
+
}
|
|
974
|
+
function renderVerificationParams(params) {
|
|
975
|
+
const value = params?.["verification"];
|
|
976
|
+
if (!Array.isArray(value)) return [];
|
|
977
|
+
return value.map((item) => {
|
|
978
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
979
|
+
const command = item["command"];
|
|
980
|
+
const outcome = item["outcome"];
|
|
981
|
+
const summary = item["summary"];
|
|
982
|
+
if (typeof outcome !== "string") return void 0;
|
|
983
|
+
const prefix = typeof command === "string" && command.length > 0 ? `\`${command}\`: ${outcome}` : outcome;
|
|
984
|
+
return typeof summary === "string" && summary.length > 0 ? ` - ${prefix} - ${summary}` : ` - ${prefix}`;
|
|
985
|
+
}).filter((line) => Boolean(line));
|
|
986
|
+
}
|
|
987
|
+
function renderSuggestedActionDetails(params, action) {
|
|
988
|
+
if (action !== "create_pull_request") return [];
|
|
989
|
+
const lines = [];
|
|
990
|
+
const title = stringParam2(params, "title");
|
|
991
|
+
const head = stringParam2(params, "head") ?? stringParam2(params, "branch");
|
|
992
|
+
const base = stringParam2(params, "base") ?? stringParam2(params, "baseBranch");
|
|
993
|
+
const changedFiles = stringArrayParam2(params, "changedFiles");
|
|
994
|
+
const risks = stringArrayParam2(params, "risks");
|
|
995
|
+
const verification = renderVerificationParams(params);
|
|
996
|
+
if (title) lines.push(`- Title: ${title}`);
|
|
997
|
+
if (head || base) lines.push(`- Branch: \`${head ?? "unknown"}\` -> \`${base ?? "main"}\``);
|
|
998
|
+
if (changedFiles.length > 0) lines.push(`- Changed files: ${changedFiles.map((file) => `\`${file}\``).join(", ")}`);
|
|
999
|
+
if (risks.length > 0) {
|
|
1000
|
+
lines.push("- Risks:");
|
|
1001
|
+
for (const risk of risks) {
|
|
1002
|
+
lines.push(` - ${risk}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (verification.length > 0) {
|
|
1006
|
+
lines.push("- Verification:");
|
|
1007
|
+
lines.push(...verification);
|
|
1008
|
+
}
|
|
1009
|
+
return lines;
|
|
1010
|
+
}
|
|
1011
|
+
function renderSuggestedActions(result) {
|
|
1012
|
+
const candidates = suggestedActionCandidatesFromResult(result);
|
|
1013
|
+
if (candidates.length === 0) return [];
|
|
1014
|
+
const lines = [
|
|
1015
|
+
"### Suggested actions:",
|
|
1016
|
+
"",
|
|
1017
|
+
"Source-thread approval: choose one command in this GitHub thread to apply a protocolized mutation or PR action to the system of record."
|
|
1018
|
+
];
|
|
1019
|
+
for (const candidate of candidates) {
|
|
1020
|
+
lines.push(
|
|
1021
|
+
"",
|
|
1022
|
+
`#### Action ${candidate.index}: ${candidate.intent.summary}`,
|
|
1023
|
+
"",
|
|
1024
|
+
`- System-of-record action: \`${candidate.intent.action}\` (\`${candidate.intent.domain}\`)`,
|
|
1025
|
+
`- Proposal: \`${candidate.proposalId}\``,
|
|
1026
|
+
`- Intent ID: \`${candidate.intent.intentId}\``
|
|
1027
|
+
);
|
|
1028
|
+
lines.push(...renderSuggestedActionDetails(candidate.intent.params, candidate.intent.action));
|
|
1029
|
+
if (candidate.proposalPreconditions?.length) {
|
|
1030
|
+
lines.push("- Preconditions:");
|
|
1031
|
+
for (const precondition of candidate.proposalPreconditions) {
|
|
1032
|
+
lines.push(` - ${precondition}`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
lines.push(
|
|
1036
|
+
"",
|
|
1037
|
+
"**Approve in this thread**",
|
|
1038
|
+
"",
|
|
1039
|
+
`| Decision | Comment command | Effect |`,
|
|
1040
|
+
`| --- | --- | --- |`,
|
|
1041
|
+
`| Apply now | \`apply ${candidate.index}\` | Applies this action to the system of record. |`,
|
|
1042
|
+
`| Approve only | \`approve ${candidate.index}\` | Records approval without applying yet. |`,
|
|
1043
|
+
`| Continue | \`continue ${candidate.index}\` | Starts a follow-up run from this proposal. |`,
|
|
1044
|
+
`| Reject | \`reject ${candidate.index}\` | Rejects this action. |`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
lines.push("", "Bulk shortcut: comment `apply all` to apply every supported approved action in this thread.");
|
|
1048
|
+
return lines;
|
|
1049
|
+
}
|
|
173
1050
|
function renderAcknowledgement(runId) {
|
|
174
1051
|
return `OpenTag picked this up. Run: \`${runId}\``;
|
|
175
1052
|
}
|
|
@@ -184,18 +1061,33 @@ function renderFinalResult(result) {
|
|
|
184
1061
|
lines.push(`- \`${check.command}\`: ${check.outcome}`);
|
|
185
1062
|
}
|
|
186
1063
|
}
|
|
187
|
-
|
|
188
|
-
|
|
1064
|
+
const nextAction = nextActionSummary(result);
|
|
1065
|
+
if (nextAction) {
|
|
1066
|
+
lines.push("", `Next action: ${nextAction}`);
|
|
1067
|
+
}
|
|
1068
|
+
const suggestedActions = renderSuggestedActions(result);
|
|
1069
|
+
if (suggestedActions.length > 0) {
|
|
1070
|
+
lines.push("", ...suggestedActions);
|
|
189
1071
|
}
|
|
190
1072
|
return lines.join("\n");
|
|
191
1073
|
}
|
|
192
1074
|
export {
|
|
1075
|
+
applyGitHubIssueMutationIntent,
|
|
1076
|
+
applyGitHubIssueMutationIntents,
|
|
1077
|
+
applyGitHubIssueMutationOperation,
|
|
193
1078
|
buildPullRequestBody,
|
|
1079
|
+
compileGitHubIssueMutationIntent,
|
|
1080
|
+
compileGitHubIssueMutationIntents,
|
|
1081
|
+
computeGitHubSignature,
|
|
1082
|
+
createGitHubIssueMutationCompiler,
|
|
1083
|
+
createGitHubWebhookApp,
|
|
194
1084
|
createPullRequestViaFetch,
|
|
195
1085
|
normalizeGitHubIssueComment,
|
|
196
1086
|
normalizeGitHubPullRequestReviewComment,
|
|
197
1087
|
renderAcknowledgement,
|
|
198
1088
|
renderFinalResult,
|
|
199
|
-
renderProgress
|
|
1089
|
+
renderProgress,
|
|
1090
|
+
startGitHubIngress,
|
|
1091
|
+
verifyGitHubSignature
|
|
200
1092
|
};
|
|
201
1093
|
//# sourceMappingURL=index.js.map
|