@sentry/junior-github 0.107.1 → 0.109.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/README.md +3 -3
- package/SETUP.md +13 -8
- package/dist/db/database.d.ts +5 -0
- package/dist/db/schema.d.ts +470 -0
- package/dist/index.js +775 -139
- package/dist/issue-outcomes/store.d.ts +30 -0
- package/dist/{pull-request-outcomes → outcomes}/report.d.ts +2 -2
- package/dist/pull-request-outcomes/commit-composition.d.ts +6 -0
- package/dist/pull-request-outcomes/store.d.ts +18 -5
- package/dist/tools/footer.d.ts +2 -0
- package/dist/webhooks/handler.d.ts +8 -2
- package/dist/webhooks/issue-outcome.d.ts +6 -0
- package/dist/webhooks/ownership.d.ts +2 -0
- package/dist/webhooks/pull-request-outcome.d.ts +6 -1
- package/migrations/0001_issue_outcomes.sql +14 -0
- package/migrations/0002_pull_request_commit_composition.sql +1 -0
- package/migrations/0003_pull_request_conversations.sql +1 -0
- package/migrations/0004_marvelous_toad_men.sql +5 -0
- package/migrations/meta/0001_snapshot.json +256 -0
- package/migrations/meta/0002_snapshot.json +262 -0
- package/migrations/meta/0003_snapshot.json +269 -0
- package/migrations/meta/0004_snapshot.json +275 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +2 -2
- package/skills/attach-github-assets/SOURCES.md +2 -2
- package/skills/attach-github-assets/SPEC.md +2 -2
- package/skills/github-code/SKILL.md +3 -14
package/dist/index.js
CHANGED
|
@@ -90,6 +90,7 @@ import { z } from "zod";
|
|
|
90
90
|
import { PluginToolInputError } from "@sentry/junior-plugin-api";
|
|
91
91
|
var GITHUB_SESSION_FOOTER_START = "<!-- junior-session-footer:start -->";
|
|
92
92
|
var GITHUB_SESSION_FOOTER_END = "<!-- junior-session-footer:end -->";
|
|
93
|
+
var GITHUB_CONVERSATION_ID_MARKER = "junior-conversation-id:";
|
|
93
94
|
function nonEmptyString(value, name) {
|
|
94
95
|
if (!value?.trim()) {
|
|
95
96
|
throw new PluginToolInputError(`${name} is required`);
|
|
@@ -132,7 +133,9 @@ function githubConversationFooter(conversationId, dashboardUrl) {
|
|
|
132
133
|
return void 0;
|
|
133
134
|
}
|
|
134
135
|
const label = normalizedDashboardUrl ? "View Junior Session" : "View Junior Session in Sentry";
|
|
136
|
+
const conversationMarker = `<!-- ${GITHUB_CONVERSATION_ID_MARKER}${encodeURIComponent(id)} -->`;
|
|
135
137
|
return `${GITHUB_SESSION_FOOTER_START}
|
|
138
|
+
${conversationMarker}
|
|
136
139
|
|
|
137
140
|
--
|
|
138
141
|
|
|
@@ -140,6 +143,29 @@ function githubConversationFooter(conversationId, dashboardUrl) {
|
|
|
140
143
|
|
|
141
144
|
${GITHUB_SESSION_FOOTER_END}`;
|
|
142
145
|
}
|
|
146
|
+
function githubConversationIds(body) {
|
|
147
|
+
if (!body) return [];
|
|
148
|
+
const ids = /* @__PURE__ */ new Set();
|
|
149
|
+
const footer = new RegExp(
|
|
150
|
+
`${escapeRegExp(GITHUB_SESSION_FOOTER_START)}[\\s\\S]*?${escapeRegExp(GITHUB_SESSION_FOOTER_END)}`,
|
|
151
|
+
"g"
|
|
152
|
+
);
|
|
153
|
+
const marker = new RegExp(
|
|
154
|
+
`<!--\\s*${escapeRegExp(GITHUB_CONVERSATION_ID_MARKER)}([^\\s]+)\\s*-->`,
|
|
155
|
+
"g"
|
|
156
|
+
);
|
|
157
|
+
for (const footerMatch of body.matchAll(footer)) {
|
|
158
|
+
for (const markerMatch of footerMatch[0].matchAll(marker)) {
|
|
159
|
+
try {
|
|
160
|
+
const id = decodeURIComponent(markerMatch[1] ?? "").trim();
|
|
161
|
+
if (id) ids.add(id);
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...ids];
|
|
168
|
+
}
|
|
143
169
|
function appendGitHubFooter(body, conversationId, dashboardUrl) {
|
|
144
170
|
const footer = githubConversationFooter(conversationId, dashboardUrl);
|
|
145
171
|
const normalizedBody = body.trimEnd();
|
|
@@ -1069,7 +1095,7 @@ function createGitHubTools(ctx) {
|
|
|
1069
1095
|
// src/webhooks/handler.ts
|
|
1070
1096
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
1071
1097
|
|
|
1072
|
-
// src/
|
|
1098
|
+
// src/issue-outcomes/store.ts
|
|
1073
1099
|
import { and, eq, lte } from "drizzle-orm";
|
|
1074
1100
|
import { z as z6 } from "zod";
|
|
1075
1101
|
|
|
@@ -1082,6 +1108,36 @@ var githubPullRequestStateSchema = z5.enum([
|
|
|
1082
1108
|
"merged",
|
|
1083
1109
|
"open"
|
|
1084
1110
|
]);
|
|
1111
|
+
var githubPullRequestCommitCompositionSchema = z5.enum([
|
|
1112
|
+
"junior_only",
|
|
1113
|
+
"mixed"
|
|
1114
|
+
]);
|
|
1115
|
+
var githubIssueStateSchema = z5.enum(["closed", "open"]);
|
|
1116
|
+
var githubIssueStateReasonSchema = z5.enum([
|
|
1117
|
+
"completed",
|
|
1118
|
+
"duplicate",
|
|
1119
|
+
"not_planned",
|
|
1120
|
+
"reopened"
|
|
1121
|
+
]);
|
|
1122
|
+
var juniorGitHubIssues = pgTable(
|
|
1123
|
+
"junior_github_issues",
|
|
1124
|
+
{
|
|
1125
|
+
issueId: text("issue_id").primaryKey(),
|
|
1126
|
+
repositoryId: text("repository_id").notNull(),
|
|
1127
|
+
repositoryFullName: text("repository_full_name").notNull(),
|
|
1128
|
+
number: integer("number").notNull(),
|
|
1129
|
+
state: text("state").$type().notNull(),
|
|
1130
|
+
stateReason: text("state_reason").$type(),
|
|
1131
|
+
openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
|
|
1132
|
+
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
1133
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
1134
|
+
},
|
|
1135
|
+
(table) => [
|
|
1136
|
+
index("junior_github_issues_opened_at_idx").on(table.openedAt),
|
|
1137
|
+
index("junior_github_issues_closed_at_idx").on(table.closedAt),
|
|
1138
|
+
index("junior_github_issues_open_idx").on(table.issueId).where(sql`${table.state} = 'open'`)
|
|
1139
|
+
]
|
|
1140
|
+
);
|
|
1085
1141
|
var juniorGitHubPullRequests = pgTable(
|
|
1086
1142
|
"junior_github_pull_requests",
|
|
1087
1143
|
{
|
|
@@ -1090,6 +1146,8 @@ var juniorGitHubPullRequests = pgTable(
|
|
|
1090
1146
|
repositoryFullName: text("repository_full_name").notNull(),
|
|
1091
1147
|
number: integer("number").notNull(),
|
|
1092
1148
|
state: text("state").$type().notNull(),
|
|
1149
|
+
commitComposition: text("commit_composition").$type(),
|
|
1150
|
+
conversationIds: text("conversation_ids").array().notNull().default(sql`ARRAY[]::text[]`),
|
|
1093
1151
|
openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
|
|
1094
1152
|
mergedAt: timestamp("merged_at", { withTimezone: true }),
|
|
1095
1153
|
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
@@ -1103,71 +1161,137 @@ var juniorGitHubPullRequests = pgTable(
|
|
|
1103
1161
|
]
|
|
1104
1162
|
);
|
|
1105
1163
|
|
|
1106
|
-
// src/
|
|
1107
|
-
var
|
|
1164
|
+
// src/issue-outcomes/store.ts
|
|
1165
|
+
var githubIssueOutcomeInputSchema = z6.object({
|
|
1108
1166
|
candidateOwned: z6.boolean(),
|
|
1109
1167
|
closedAt: z6.date().optional(),
|
|
1110
|
-
|
|
1168
|
+
issueId: z6.string().min(1),
|
|
1111
1169
|
number: z6.number().int().positive(),
|
|
1112
1170
|
openedAt: z6.date(),
|
|
1113
|
-
pullRequestId: z6.string().min(1),
|
|
1114
1171
|
repositoryFullName: z6.string().min(1),
|
|
1115
1172
|
repositoryId: z6.string().min(1),
|
|
1116
|
-
state:
|
|
1173
|
+
state: githubIssueStateSchema,
|
|
1174
|
+
stateReason: githubIssueStateReasonSchema.optional(),
|
|
1117
1175
|
updatedAt: z6.date()
|
|
1118
1176
|
}).strict();
|
|
1119
1177
|
function projectionValues(input) {
|
|
1120
1178
|
return {
|
|
1121
1179
|
closedAt: input.closedAt ?? null,
|
|
1122
|
-
mergedAt: input.mergedAt ?? null,
|
|
1123
1180
|
number: input.number,
|
|
1124
1181
|
openedAt: input.openedAt,
|
|
1125
1182
|
repositoryFullName: input.repositoryFullName,
|
|
1126
1183
|
repositoryId: input.repositoryId,
|
|
1127
1184
|
state: input.state,
|
|
1185
|
+
stateReason: input.stateReason ?? null,
|
|
1128
1186
|
updatedAt: input.updatedAt
|
|
1129
1187
|
};
|
|
1130
1188
|
}
|
|
1131
|
-
async function
|
|
1132
|
-
const outcome =
|
|
1189
|
+
async function recordGitHubIssueOutcome(db, input) {
|
|
1190
|
+
const outcome = githubIssueOutcomeInputSchema.parse(input);
|
|
1133
1191
|
const values = projectionValues(outcome);
|
|
1134
1192
|
if (!outcome.candidateOwned) {
|
|
1135
|
-
await db.update(
|
|
1193
|
+
await db.update(juniorGitHubIssues).set(values).where(
|
|
1136
1194
|
and(
|
|
1137
|
-
eq(
|
|
1138
|
-
lte(
|
|
1195
|
+
eq(juniorGitHubIssues.issueId, outcome.issueId),
|
|
1196
|
+
lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
|
|
1139
1197
|
)
|
|
1140
1198
|
);
|
|
1141
1199
|
return;
|
|
1142
1200
|
}
|
|
1143
|
-
await db.insert(
|
|
1144
|
-
target:
|
|
1201
|
+
await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
|
|
1202
|
+
target: juniorGitHubIssues.issueId,
|
|
1145
1203
|
set: values,
|
|
1146
|
-
where: lte(
|
|
1204
|
+
where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
|
|
1147
1205
|
});
|
|
1148
1206
|
}
|
|
1149
1207
|
|
|
1150
|
-
// src/
|
|
1208
|
+
// src/pull-request-outcomes/store.ts
|
|
1209
|
+
import { and as and2, eq as eq2, lte as lte2, sql as sql2 } from "drizzle-orm";
|
|
1151
1210
|
import { z as z7 } from "zod";
|
|
1152
|
-
var
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1211
|
+
var githubPullRequestOutcomeInputSchema = z7.object({
|
|
1212
|
+
candidateOwned: z7.boolean(),
|
|
1213
|
+
closedAt: z7.date().optional(),
|
|
1214
|
+
commitComposition: githubPullRequestCommitCompositionSchema.optional(),
|
|
1215
|
+
mergedAt: z7.date().optional(),
|
|
1216
|
+
number: z7.number().int().positive(),
|
|
1217
|
+
openedAt: z7.date(),
|
|
1218
|
+
pullRequestId: z7.string().min(1),
|
|
1219
|
+
repositoryFullName: z7.string().min(1),
|
|
1220
|
+
repositoryId: z7.string().min(1),
|
|
1221
|
+
state: githubPullRequestStateSchema,
|
|
1222
|
+
updatedAt: z7.date()
|
|
1223
|
+
}).strict();
|
|
1224
|
+
var githubPullRequestConversationsInputSchema = z7.object({
|
|
1225
|
+
conversationIds: z7.array(z7.string().min(1)).min(1),
|
|
1226
|
+
pullRequestId: z7.string().min(1)
|
|
1227
|
+
}).strict();
|
|
1228
|
+
function projectionValues2(input) {
|
|
1229
|
+
return {
|
|
1230
|
+
closedAt: input.closedAt ?? null,
|
|
1231
|
+
...input.commitComposition ? { commitComposition: input.commitComposition } : {},
|
|
1232
|
+
mergedAt: input.mergedAt ?? null,
|
|
1233
|
+
number: input.number,
|
|
1234
|
+
openedAt: input.openedAt,
|
|
1235
|
+
repositoryFullName: input.repositoryFullName,
|
|
1236
|
+
repositoryId: input.repositoryId,
|
|
1237
|
+
state: input.state,
|
|
1238
|
+
updatedAt: input.updatedAt
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
async function recordGitHubPullRequestOutcome(db, input) {
|
|
1242
|
+
const outcome = githubPullRequestOutcomeInputSchema.parse(input);
|
|
1243
|
+
const values = projectionValues2(outcome);
|
|
1244
|
+
if (!outcome.candidateOwned) {
|
|
1245
|
+
const updated = await db.update(juniorGitHubPullRequests).set(values).where(
|
|
1246
|
+
and2(
|
|
1247
|
+
eq2(juniorGitHubPullRequests.pullRequestId, outcome.pullRequestId),
|
|
1248
|
+
lte2(juniorGitHubPullRequests.updatedAt, outcome.updatedAt)
|
|
1249
|
+
)
|
|
1250
|
+
).returning({
|
|
1251
|
+
commitComposition: juniorGitHubPullRequests.commitComposition
|
|
1252
|
+
});
|
|
1253
|
+
return {
|
|
1254
|
+
applied: updated.length > 0,
|
|
1255
|
+
commitComposition: updated[0]?.commitComposition ?? void 0
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const inserted = await db.insert(juniorGitHubPullRequests).values({ pullRequestId: outcome.pullRequestId, ...values }).onConflictDoUpdate({
|
|
1259
|
+
target: juniorGitHubPullRequests.pullRequestId,
|
|
1260
|
+
set: values,
|
|
1261
|
+
where: lte2(juniorGitHubPullRequests.updatedAt, outcome.updatedAt)
|
|
1262
|
+
}).returning({
|
|
1263
|
+
commitComposition: juniorGitHubPullRequests.commitComposition
|
|
1264
|
+
});
|
|
1265
|
+
return {
|
|
1266
|
+
applied: inserted.length > 0,
|
|
1267
|
+
commitComposition: inserted[0]?.commitComposition ?? void 0
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
async function recordGitHubPullRequestConversations(db, input) {
|
|
1271
|
+
const association = githubPullRequestConversationsInputSchema.parse(input);
|
|
1272
|
+
const conversationIds = sql2.join(
|
|
1273
|
+
association.conversationIds.map((id) => sql2`${id}`),
|
|
1274
|
+
sql2`, `
|
|
1275
|
+
);
|
|
1276
|
+
const updated = await db.update(juniorGitHubPullRequests).set({
|
|
1277
|
+
conversationIds: sql2`ARRAY(
|
|
1278
|
+
SELECT DISTINCT value
|
|
1279
|
+
FROM unnest(
|
|
1280
|
+
${juniorGitHubPullRequests.conversationIds}
|
|
1281
|
+
|| ARRAY[${conversationIds}]::text[]
|
|
1282
|
+
) AS value
|
|
1283
|
+
ORDER BY value
|
|
1284
|
+
)`
|
|
1285
|
+
}).where(
|
|
1286
|
+
eq2(juniorGitHubPullRequests.pullRequestId, association.pullRequestId)
|
|
1287
|
+
).returning({ pullRequestId: juniorGitHubPullRequests.pullRequestId });
|
|
1288
|
+
return updated.length > 0;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/webhooks/issue-outcome.ts
|
|
1292
|
+
import { z as z8 } from "zod";
|
|
1293
|
+
|
|
1294
|
+
// src/webhooks/ownership.ts
|
|
1171
1295
|
var GITHUB_NOREPLY_DOMAIN = "users.noreply.github.com";
|
|
1172
1296
|
function botLoginFromEmail(value) {
|
|
1173
1297
|
const email = value?.trim();
|
|
@@ -1180,11 +1304,195 @@ function botLoginFromEmail(value) {
|
|
|
1180
1304
|
const login = localPart.slice(localPart.indexOf("+") + 1).trim();
|
|
1181
1305
|
return login.toLowerCase().endsWith("[bot]") ? login : void 0;
|
|
1182
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
// src/webhooks/issue-outcome.ts
|
|
1309
|
+
var canonicalIssueOutcomeSchema = z8.object({
|
|
1310
|
+
action: z8.enum(["opened", "closed", "reopened"]),
|
|
1311
|
+
issue: z8.object({
|
|
1312
|
+
body: z8.string().nullable().optional(),
|
|
1313
|
+
closed_at: z8.string().nullable().optional(),
|
|
1314
|
+
created_at: z8.string(),
|
|
1315
|
+
id: z8.number().int().positive(),
|
|
1316
|
+
number: z8.number().int().positive(),
|
|
1317
|
+
state_reason: githubIssueStateReasonSchema.nullable().optional(),
|
|
1318
|
+
updated_at: z8.string(),
|
|
1319
|
+
user: z8.object({ login: z8.string().min(1) }).strict()
|
|
1320
|
+
}).strict(),
|
|
1321
|
+
repository: z8.object({
|
|
1322
|
+
full_name: z8.string().min(1),
|
|
1323
|
+
id: z8.number().int().positive()
|
|
1324
|
+
}).strict()
|
|
1325
|
+
}).strict();
|
|
1326
|
+
var issueOutcomeSchema = z8.object({
|
|
1327
|
+
action: z8.enum(["opened", "closed", "reopened"]),
|
|
1328
|
+
issue: z8.object({
|
|
1329
|
+
body: z8.string().nullable().optional(),
|
|
1330
|
+
closed_at: z8.string().nullable().optional(),
|
|
1331
|
+
created_at: z8.string(),
|
|
1332
|
+
id: z8.number().int().positive(),
|
|
1333
|
+
number: z8.number().int().positive(),
|
|
1334
|
+
state_reason: githubIssueStateReasonSchema.nullable().optional(),
|
|
1335
|
+
updated_at: z8.string(),
|
|
1336
|
+
user: z8.object({ login: z8.string().min(1) }).passthrough()
|
|
1337
|
+
}).passthrough(),
|
|
1338
|
+
repository: z8.object({
|
|
1339
|
+
full_name: z8.string().min(1),
|
|
1340
|
+
id: z8.number().int().positive()
|
|
1341
|
+
}).passthrough()
|
|
1342
|
+
}).passthrough().transform(
|
|
1343
|
+
(provider) => canonicalIssueOutcomeSchema.parse({
|
|
1344
|
+
action: provider.action,
|
|
1345
|
+
issue: {
|
|
1346
|
+
body: provider.issue.body,
|
|
1347
|
+
closed_at: provider.issue.closed_at,
|
|
1348
|
+
created_at: provider.issue.created_at,
|
|
1349
|
+
id: provider.issue.id,
|
|
1350
|
+
number: provider.issue.number,
|
|
1351
|
+
state_reason: provider.issue.state_reason,
|
|
1352
|
+
updated_at: provider.issue.updated_at,
|
|
1353
|
+
user: { login: provider.issue.user.login }
|
|
1354
|
+
},
|
|
1355
|
+
repository: {
|
|
1356
|
+
full_name: provider.repository.full_name,
|
|
1357
|
+
id: provider.repository.id
|
|
1358
|
+
}
|
|
1359
|
+
})
|
|
1360
|
+
);
|
|
1361
|
+
var issueLifecycleActionSchema = z8.object({ action: z8.string() }).passthrough();
|
|
1183
1362
|
function timestamp2(value) {
|
|
1184
1363
|
if (!value) return void 0;
|
|
1185
1364
|
const parsed = new Date(value);
|
|
1186
1365
|
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
1187
1366
|
}
|
|
1367
|
+
function normalizeGitHubIssueOutcome(args) {
|
|
1368
|
+
const lifecycle = issueLifecycleActionSchema.safeParse(args.body);
|
|
1369
|
+
if (!lifecycle.success || !["opened", "closed", "reopened"].includes(lifecycle.data.action)) {
|
|
1370
|
+
return void 0;
|
|
1371
|
+
}
|
|
1372
|
+
const parsed = issueOutcomeSchema.parse(args.body);
|
|
1373
|
+
const issue = parsed.issue;
|
|
1374
|
+
const openedAt = timestamp2(issue.created_at);
|
|
1375
|
+
const updatedAt = timestamp2(issue.updated_at);
|
|
1376
|
+
if (!openedAt || !updatedAt) {
|
|
1377
|
+
throw new Error("GitHub issue lifecycle timestamps are invalid");
|
|
1378
|
+
}
|
|
1379
|
+
const authorLogin = issue.user.login.trim().toLowerCase();
|
|
1380
|
+
const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
|
|
1381
|
+
if (parsed.action === "opened" && !botLogin) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
"The configured GitHub App bot email must encode a [bot] login in GitHub's noreply format to classify issue ownership"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
const hasOwnershipMarker = Boolean(
|
|
1387
|
+
botLogin && authorLogin === botLogin && issue.body?.includes(GITHUB_SESSION_FOOTER_START)
|
|
1388
|
+
);
|
|
1389
|
+
const candidateOwned = hasOwnershipMarker && (parsed.action === "opened" || parsed.action === "closed");
|
|
1390
|
+
const closedAt = timestamp2(issue.closed_at);
|
|
1391
|
+
if (parsed.action === "closed" && !closedAt) {
|
|
1392
|
+
throw new Error("GitHub issue terminal timestamp is invalid");
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
candidateOwned,
|
|
1396
|
+
closedAt,
|
|
1397
|
+
issueId: String(issue.id),
|
|
1398
|
+
number: issue.number,
|
|
1399
|
+
openedAt,
|
|
1400
|
+
repositoryFullName: parsed.repository.full_name,
|
|
1401
|
+
repositoryId: String(parsed.repository.id),
|
|
1402
|
+
state: parsed.action === "closed" ? "closed" : "open",
|
|
1403
|
+
stateReason: parsed.action === "closed" ? issue.state_reason ?? void 0 : void 0,
|
|
1404
|
+
updatedAt
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/webhooks/pull-request-outcome.ts
|
|
1409
|
+
import { z as z9 } from "zod";
|
|
1410
|
+
var canonicalPullRequestOutcomeSchema = z9.object({
|
|
1411
|
+
action: z9.enum(["opened", "closed", "reopened"]),
|
|
1412
|
+
pull_request: z9.object({
|
|
1413
|
+
body: z9.string().nullable().optional(),
|
|
1414
|
+
closed_at: z9.string().nullable().optional(),
|
|
1415
|
+
created_at: z9.string(),
|
|
1416
|
+
id: z9.number().int().positive(),
|
|
1417
|
+
merged: z9.boolean(),
|
|
1418
|
+
merged_at: z9.string().nullable().optional(),
|
|
1419
|
+
number: z9.number().int().positive(),
|
|
1420
|
+
updated_at: z9.string(),
|
|
1421
|
+
user: z9.object({ login: z9.string().min(1) }).strict()
|
|
1422
|
+
}).strict(),
|
|
1423
|
+
repository: z9.object({
|
|
1424
|
+
full_name: z9.string().min(1),
|
|
1425
|
+
id: z9.number().int().positive()
|
|
1426
|
+
}).strict()
|
|
1427
|
+
}).strict();
|
|
1428
|
+
var pullRequestOutcomeSchema = z9.object({
|
|
1429
|
+
action: z9.enum(["opened", "closed", "reopened"]),
|
|
1430
|
+
pull_request: z9.object({
|
|
1431
|
+
body: z9.string().nullable().optional(),
|
|
1432
|
+
closed_at: z9.string().nullable().optional(),
|
|
1433
|
+
created_at: z9.string(),
|
|
1434
|
+
id: z9.number().int().positive(),
|
|
1435
|
+
merged: z9.boolean(),
|
|
1436
|
+
merged_at: z9.string().nullable().optional(),
|
|
1437
|
+
number: z9.number().int().positive(),
|
|
1438
|
+
updated_at: z9.string(),
|
|
1439
|
+
user: z9.object({ login: z9.string().min(1) }).passthrough()
|
|
1440
|
+
}).passthrough(),
|
|
1441
|
+
repository: z9.object({
|
|
1442
|
+
full_name: z9.string().min(1),
|
|
1443
|
+
id: z9.number().int().positive()
|
|
1444
|
+
}).passthrough()
|
|
1445
|
+
}).passthrough().transform(
|
|
1446
|
+
(provider) => canonicalPullRequestOutcomeSchema.parse({
|
|
1447
|
+
action: provider.action,
|
|
1448
|
+
pull_request: {
|
|
1449
|
+
body: provider.pull_request.body,
|
|
1450
|
+
closed_at: provider.pull_request.closed_at,
|
|
1451
|
+
created_at: provider.pull_request.created_at,
|
|
1452
|
+
id: provider.pull_request.id,
|
|
1453
|
+
merged: provider.pull_request.merged,
|
|
1454
|
+
merged_at: provider.pull_request.merged_at,
|
|
1455
|
+
number: provider.pull_request.number,
|
|
1456
|
+
updated_at: provider.pull_request.updated_at,
|
|
1457
|
+
user: { login: provider.pull_request.user.login }
|
|
1458
|
+
},
|
|
1459
|
+
repository: {
|
|
1460
|
+
full_name: provider.repository.full_name,
|
|
1461
|
+
id: provider.repository.id
|
|
1462
|
+
}
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1465
|
+
var pullRequestLifecycleActionSchema = z9.object({ action: z9.string() }).passthrough();
|
|
1466
|
+
var canonicalPullRequestConversationSchema = z9.object({
|
|
1467
|
+
pull_request: z9.object({
|
|
1468
|
+
body: z9.string().nullable().optional(),
|
|
1469
|
+
id: z9.number().int().positive(),
|
|
1470
|
+
user: z9.object({ login: z9.string().min(1) }).strict()
|
|
1471
|
+
}).strict(),
|
|
1472
|
+
sender: z9.object({ login: z9.string().min(1) }).strict()
|
|
1473
|
+
}).strict();
|
|
1474
|
+
var pullRequestConversationSchema = z9.object({
|
|
1475
|
+
pull_request: z9.object({
|
|
1476
|
+
body: z9.string().nullable().optional(),
|
|
1477
|
+
id: z9.number().int().positive(),
|
|
1478
|
+
user: z9.object({ login: z9.string().min(1) }).passthrough()
|
|
1479
|
+
}).passthrough(),
|
|
1480
|
+
sender: z9.object({ login: z9.string().min(1) }).passthrough()
|
|
1481
|
+
}).passthrough().transform(
|
|
1482
|
+
(provider) => canonicalPullRequestConversationSchema.parse({
|
|
1483
|
+
pull_request: {
|
|
1484
|
+
body: provider.pull_request.body,
|
|
1485
|
+
id: provider.pull_request.id,
|
|
1486
|
+
user: { login: provider.pull_request.user.login }
|
|
1487
|
+
},
|
|
1488
|
+
sender: { login: provider.sender.login }
|
|
1489
|
+
})
|
|
1490
|
+
);
|
|
1491
|
+
function timestamp3(value) {
|
|
1492
|
+
if (!value) return void 0;
|
|
1493
|
+
const parsed = new Date(value);
|
|
1494
|
+
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
1495
|
+
}
|
|
1188
1496
|
function normalizeGitHubPullRequestOutcome(args) {
|
|
1189
1497
|
const lifecycle = pullRequestLifecycleActionSchema.safeParse(args.body);
|
|
1190
1498
|
if (!lifecycle.success || !["opened", "closed", "reopened"].includes(lifecycle.data.action)) {
|
|
@@ -1192,8 +1500,8 @@ function normalizeGitHubPullRequestOutcome(args) {
|
|
|
1192
1500
|
}
|
|
1193
1501
|
const parsed = pullRequestOutcomeSchema.parse(args.body);
|
|
1194
1502
|
const pullRequest = parsed.pull_request;
|
|
1195
|
-
const openedAt =
|
|
1196
|
-
const updatedAt =
|
|
1503
|
+
const openedAt = timestamp3(pullRequest.created_at);
|
|
1504
|
+
const updatedAt = timestamp3(pullRequest.updated_at);
|
|
1197
1505
|
if (!openedAt || !updatedAt) {
|
|
1198
1506
|
throw new Error("GitHub pull request lifecycle timestamps are invalid");
|
|
1199
1507
|
}
|
|
@@ -1209,8 +1517,8 @@ function normalizeGitHubPullRequestOutcome(args) {
|
|
|
1209
1517
|
);
|
|
1210
1518
|
const candidateOwned = hasOwnershipMarker && (parsed.action === "opened" || parsed.action === "closed");
|
|
1211
1519
|
const state = parsed.action !== "closed" ? "open" : pullRequest.merged ? "merged" : "closed_unmerged";
|
|
1212
|
-
const closedAt =
|
|
1213
|
-
const mergedAt =
|
|
1520
|
+
const closedAt = timestamp3(pullRequest.closed_at);
|
|
1521
|
+
const mergedAt = timestamp3(pullRequest.merged_at);
|
|
1214
1522
|
if (parsed.action === "closed" && (pullRequest.merged ? !mergedAt : !closedAt)) {
|
|
1215
1523
|
throw new Error("GitHub pull request terminal timestamp is invalid");
|
|
1216
1524
|
}
|
|
@@ -1227,6 +1535,19 @@ function normalizeGitHubPullRequestOutcome(args) {
|
|
|
1227
1535
|
updatedAt
|
|
1228
1536
|
};
|
|
1229
1537
|
}
|
|
1538
|
+
function normalizeGitHubPullRequestConversations(args) {
|
|
1539
|
+
const parsed = pullRequestConversationSchema.safeParse(args.body);
|
|
1540
|
+
const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
|
|
1541
|
+
if (!parsed.success || !botLogin) return void 0;
|
|
1542
|
+
if (parsed.data.pull_request.user.login.trim().toLowerCase() !== botLogin || parsed.data.sender.login.trim().toLowerCase() !== botLogin) {
|
|
1543
|
+
return void 0;
|
|
1544
|
+
}
|
|
1545
|
+
const conversationIds = githubConversationIds(parsed.data.pull_request.body);
|
|
1546
|
+
return conversationIds.length > 0 ? {
|
|
1547
|
+
conversationIds,
|
|
1548
|
+
pullRequestId: String(parsed.data.pull_request.id)
|
|
1549
|
+
} : void 0;
|
|
1550
|
+
}
|
|
1230
1551
|
|
|
1231
1552
|
// src/webhooks/handler.ts
|
|
1232
1553
|
function verifyGitHubSignature(body, signature, secret) {
|
|
@@ -1260,13 +1581,45 @@ function createGitHubWebhookRoute(args) {
|
|
|
1260
1581
|
return new Response("Malformed GitHub webhook", { status: 400 });
|
|
1261
1582
|
}
|
|
1262
1583
|
const body = parseJson(rawBody);
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}) : void 0;
|
|
1267
|
-
if (
|
|
1268
|
-
await recordGitHubPullRequestOutcome(
|
|
1584
|
+
const botEmail = args.botEmail();
|
|
1585
|
+
const pullRequestOutcome = eventName === "pull_request" ? normalizeGitHubPullRequestOutcome({ body, botEmail }) : void 0;
|
|
1586
|
+
const issueOutcome = eventName === "issues" ? normalizeGitHubIssueOutcome({ body, botEmail }) : void 0;
|
|
1587
|
+
const pullRequestConversations = eventName === "pull_request" ? normalizeGitHubPullRequestConversations({ body, botEmail }) : void 0;
|
|
1588
|
+
if (pullRequestOutcome) {
|
|
1589
|
+
const recordedOutcome = await recordGitHubPullRequestOutcome(
|
|
1590
|
+
args.db,
|
|
1591
|
+
pullRequestOutcome
|
|
1592
|
+
);
|
|
1593
|
+
if (recordedOutcome.applied && !recordedOutcome.commitComposition && pullRequestOutcome.state === "merged" && args.classifyPullRequestCommits) {
|
|
1594
|
+
let commitComposition;
|
|
1595
|
+
try {
|
|
1596
|
+
commitComposition = await args.classifyPullRequestCommits({
|
|
1597
|
+
number: pullRequestOutcome.number,
|
|
1598
|
+
repositoryFullName: pullRequestOutcome.repositoryFullName
|
|
1599
|
+
});
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
args.log?.error("GitHub PR commit classification failed", {
|
|
1602
|
+
deliveryId,
|
|
1603
|
+
errorType: error instanceof Error ? error.name : "UnknownError",
|
|
1604
|
+
number: pullRequestOutcome.number,
|
|
1605
|
+
repository: pullRequestOutcome.repositoryFullName
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
if (commitComposition) {
|
|
1609
|
+
await recordGitHubPullRequestOutcome(args.db, {
|
|
1610
|
+
...pullRequestOutcome,
|
|
1611
|
+
commitComposition
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (issueOutcome) {
|
|
1617
|
+
await recordGitHubIssueOutcome(args.db, issueOutcome);
|
|
1269
1618
|
}
|
|
1619
|
+
const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
|
|
1620
|
+
args.db,
|
|
1621
|
+
pullRequestConversations
|
|
1622
|
+
) : false;
|
|
1270
1623
|
const resourceEvents = normalizeGitHubResourceEvents({
|
|
1271
1624
|
body,
|
|
1272
1625
|
deliveryId,
|
|
@@ -1275,7 +1628,7 @@ function createGitHubWebhookRoute(args) {
|
|
|
1275
1628
|
for (const event of resourceEvents) {
|
|
1276
1629
|
await args.resourceEvents.publish(event);
|
|
1277
1630
|
}
|
|
1278
|
-
if (!
|
|
1631
|
+
if (!pullRequestOutcome && !issueOutcome && !recordedPullRequestConversations && resourceEvents.length === 0) {
|
|
1279
1632
|
return new Response("Ignored", { status: 202 });
|
|
1280
1633
|
}
|
|
1281
1634
|
return new Response("Accepted", { status: 202 });
|
|
@@ -1283,32 +1636,38 @@ function createGitHubWebhookRoute(args) {
|
|
|
1283
1636
|
};
|
|
1284
1637
|
}
|
|
1285
1638
|
|
|
1286
|
-
// src/
|
|
1287
|
-
import { sql as
|
|
1288
|
-
import { z as
|
|
1639
|
+
// src/outcomes/report.ts
|
|
1640
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
1641
|
+
import { z as z10 } from "zod";
|
|
1289
1642
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1290
1643
|
var WINDOWS = [7, 30, 90];
|
|
1291
|
-
var
|
|
1292
|
-
closed:
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1644
|
+
var pullRequestStatsSchema = z10.object({
|
|
1645
|
+
closed: z10.number().int().nonnegative(),
|
|
1646
|
+
compositionUnknown: z10.number().int().nonnegative(),
|
|
1647
|
+
created: z10.number().int().nonnegative(),
|
|
1648
|
+
days: z10.number().int().positive(),
|
|
1649
|
+
juniorOnly: z10.number().int().nonnegative(),
|
|
1650
|
+
medianMergeTimeMs: z10.number().nonnegative().nullable(),
|
|
1651
|
+
merged: z10.number().int().nonnegative(),
|
|
1652
|
+
mixed: z10.number().int().nonnegative()
|
|
1298
1653
|
}).strict().transform((row) => {
|
|
1299
1654
|
const terminal = row.merged + row.closed;
|
|
1655
|
+
const classified = row.juniorOnly + row.mixed;
|
|
1300
1656
|
return {
|
|
1301
1657
|
...row,
|
|
1302
1658
|
medianMergeTimeMs: row.medianMergeTimeMs ?? void 0,
|
|
1303
|
-
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1659
|
+
mergeRate: terminal > 0 ? row.merged / terminal : void 0,
|
|
1660
|
+
juniorOnlyRate: classified > 0 ? row.juniorOnly / classified : void 0
|
|
1304
1661
|
};
|
|
1305
1662
|
});
|
|
1306
|
-
var
|
|
1307
|
-
closed:
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1663
|
+
var pullRequestRepositoryStatsSchema = z10.object({
|
|
1664
|
+
closed: z10.number().int().nonnegative(),
|
|
1665
|
+
compositionUnknown: z10.number().int().nonnegative(),
|
|
1666
|
+
created: z10.number().int().nonnegative(),
|
|
1667
|
+
juniorOnly: z10.number().int().nonnegative(),
|
|
1668
|
+
merged: z10.number().int().nonnegative(),
|
|
1669
|
+
mixed: z10.number().int().nonnegative(),
|
|
1670
|
+
repository: z10.string().min(1)
|
|
1312
1671
|
}).strict().transform((row) => {
|
|
1313
1672
|
const terminal = row.merged + row.closed;
|
|
1314
1673
|
return {
|
|
@@ -1316,19 +1675,39 @@ var repositoryStatsSchema = z8.object({
|
|
|
1316
1675
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1317
1676
|
};
|
|
1318
1677
|
});
|
|
1678
|
+
var issueStatsSchema = z10.object({
|
|
1679
|
+
closedCompleted: z10.number().int().nonnegative(),
|
|
1680
|
+
closedDuplicate: z10.number().int().nonnegative(),
|
|
1681
|
+
closedNotPlanned: z10.number().int().nonnegative(),
|
|
1682
|
+
closedUnknown: z10.number().int().nonnegative(),
|
|
1683
|
+
created: z10.number().int().nonnegative(),
|
|
1684
|
+
days: z10.number().int().positive(),
|
|
1685
|
+
medianCloseTimeMs: z10.number().nonnegative().nullable()
|
|
1686
|
+
}).strict().transform((row) => ({
|
|
1687
|
+
...row,
|
|
1688
|
+
medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
|
|
1689
|
+
}));
|
|
1690
|
+
var issueRepositoryStatsSchema = z10.object({
|
|
1691
|
+
closedCompleted: z10.number().int().nonnegative(),
|
|
1692
|
+
closedDuplicate: z10.number().int().nonnegative(),
|
|
1693
|
+
closedNotPlanned: z10.number().int().nonnegative(),
|
|
1694
|
+
closedUnknown: z10.number().int().nonnegative(),
|
|
1695
|
+
created: z10.number().int().nonnegative(),
|
|
1696
|
+
repository: z10.string().min(1)
|
|
1697
|
+
}).strict();
|
|
1319
1698
|
function queryRows(result) {
|
|
1320
1699
|
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
1321
1700
|
throw new TypeError("GitHub outcome query did not return rows");
|
|
1322
1701
|
}
|
|
1323
1702
|
return result.rows;
|
|
1324
1703
|
}
|
|
1325
|
-
async function
|
|
1704
|
+
async function aggregatePullRequestWindows(args) {
|
|
1326
1705
|
const starts = WINDOWS.map(
|
|
1327
1706
|
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
1328
1707
|
);
|
|
1329
1708
|
const oldestStart = starts.at(-1)[1];
|
|
1330
1709
|
const table = juniorGitHubPullRequests;
|
|
1331
|
-
const result = await args.db.execute(
|
|
1710
|
+
const result = await args.db.execute(sql3`
|
|
1332
1711
|
WITH windows(days, start_at) AS (
|
|
1333
1712
|
VALUES
|
|
1334
1713
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -1338,12 +1717,12 @@ async function aggregateOutcomeWindows(args) {
|
|
|
1338
1717
|
SELECT
|
|
1339
1718
|
${table.pullRequestId},
|
|
1340
1719
|
${table.state},
|
|
1720
|
+
${table.commitComposition},
|
|
1341
1721
|
${table.openedAt},
|
|
1342
1722
|
${table.mergedAt},
|
|
1343
1723
|
${table.closedAt}
|
|
1344
1724
|
FROM ${table}
|
|
1345
|
-
WHERE ${table.
|
|
1346
|
-
OR ${table.openedAt} >= ${oldestStart}
|
|
1725
|
+
WHERE ${table.openedAt} >= ${oldestStart}
|
|
1347
1726
|
OR ${table.mergedAt} >= ${oldestStart}
|
|
1348
1727
|
OR ${table.closedAt} >= ${oldestStart}
|
|
1349
1728
|
)
|
|
@@ -1363,7 +1742,23 @@ async function aggregateOutcomeWindows(args) {
|
|
|
1363
1742
|
AND recent_pull_requests.closed_at >= windows.start_at
|
|
1364
1743
|
)::integer AS "closed",
|
|
1365
1744
|
count(recent_pull_requests.pull_request_id)
|
|
1366
|
-
FILTER (
|
|
1745
|
+
FILTER (
|
|
1746
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
1747
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1748
|
+
AND recent_pull_requests.commit_composition = 'junior_only'
|
|
1749
|
+
)::integer AS "juniorOnly",
|
|
1750
|
+
count(recent_pull_requests.pull_request_id)
|
|
1751
|
+
FILTER (
|
|
1752
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
1753
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1754
|
+
AND recent_pull_requests.commit_composition = 'mixed'
|
|
1755
|
+
)::integer AS "mixed",
|
|
1756
|
+
count(recent_pull_requests.pull_request_id)
|
|
1757
|
+
FILTER (
|
|
1758
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
1759
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1760
|
+
AND recent_pull_requests.commit_composition IS NULL
|
|
1761
|
+
)::integer AS "compositionUnknown",
|
|
1367
1762
|
(
|
|
1368
1763
|
percentile_cont(0.5) WITHIN GROUP (
|
|
1369
1764
|
ORDER BY extract(
|
|
@@ -1381,12 +1776,12 @@ async function aggregateOutcomeWindows(args) {
|
|
|
1381
1776
|
GROUP BY windows.days
|
|
1382
1777
|
ORDER BY windows.days
|
|
1383
1778
|
`);
|
|
1384
|
-
return
|
|
1779
|
+
return z10.array(pullRequestStatsSchema).parse(queryRows(result));
|
|
1385
1780
|
}
|
|
1386
|
-
async function
|
|
1781
|
+
async function aggregatePullRequestRepositories(args) {
|
|
1387
1782
|
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
1388
1783
|
const table = juniorGitHubPullRequests;
|
|
1389
|
-
const result = await args.db.execute(
|
|
1784
|
+
const result = await args.db.execute(sql3`
|
|
1390
1785
|
SELECT
|
|
1391
1786
|
${table.repositoryFullName} AS "repository",
|
|
1392
1787
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -1398,17 +1793,136 @@ async function aggregateRepositories(args) {
|
|
|
1398
1793
|
WHERE ${table.state} = 'closed_unmerged'
|
|
1399
1794
|
AND ${table.closedAt} >= ${start}
|
|
1400
1795
|
)::integer AS "closed",
|
|
1401
|
-
count(*) FILTER (
|
|
1796
|
+
count(*) FILTER (
|
|
1797
|
+
WHERE ${table.state} = 'merged'
|
|
1798
|
+
AND ${table.mergedAt} >= ${start}
|
|
1799
|
+
AND ${table.commitComposition} = 'junior_only'
|
|
1800
|
+
)::integer AS "juniorOnly",
|
|
1801
|
+
count(*) FILTER (
|
|
1802
|
+
WHERE ${table.state} = 'merged'
|
|
1803
|
+
AND ${table.mergedAt} >= ${start}
|
|
1804
|
+
AND ${table.commitComposition} = 'mixed'
|
|
1805
|
+
)::integer AS "mixed",
|
|
1806
|
+
count(*) FILTER (
|
|
1807
|
+
WHERE ${table.state} = 'merged'
|
|
1808
|
+
AND ${table.mergedAt} >= ${start}
|
|
1809
|
+
AND ${table.commitComposition} IS NULL
|
|
1810
|
+
)::integer AS "compositionUnknown"
|
|
1402
1811
|
FROM ${table}
|
|
1403
|
-
WHERE ${table.
|
|
1404
|
-
OR ${table.openedAt} >= ${start}
|
|
1812
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1405
1813
|
OR ${table.mergedAt} >= ${start}
|
|
1406
1814
|
OR ${table.closedAt} >= ${start}
|
|
1407
1815
|
GROUP BY ${table.repositoryFullName}
|
|
1408
1816
|
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
1409
1817
|
LIMIT 25
|
|
1410
1818
|
`);
|
|
1411
|
-
return
|
|
1819
|
+
return z10.array(pullRequestRepositoryStatsSchema).parse(queryRows(result));
|
|
1820
|
+
}
|
|
1821
|
+
async function aggregateIssueWindows(args) {
|
|
1822
|
+
const starts = WINDOWS.map(
|
|
1823
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
1824
|
+
);
|
|
1825
|
+
const oldestStart = starts.at(-1)[1];
|
|
1826
|
+
const table = juniorGitHubIssues;
|
|
1827
|
+
const result = await args.db.execute(sql3`
|
|
1828
|
+
WITH windows(days, start_at) AS (
|
|
1829
|
+
VALUES
|
|
1830
|
+
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
1831
|
+
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
1832
|
+
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
1833
|
+
), recent_issues AS MATERIALIZED (
|
|
1834
|
+
SELECT
|
|
1835
|
+
${table.issueId},
|
|
1836
|
+
${table.state},
|
|
1837
|
+
${table.stateReason},
|
|
1838
|
+
${table.openedAt},
|
|
1839
|
+
${table.closedAt}
|
|
1840
|
+
FROM ${table}
|
|
1841
|
+
WHERE ${table.openedAt} >= ${oldestStart}
|
|
1842
|
+
OR ${table.closedAt} >= ${oldestStart}
|
|
1843
|
+
)
|
|
1844
|
+
SELECT
|
|
1845
|
+
windows.days AS "days",
|
|
1846
|
+
count(recent_issues.issue_id)
|
|
1847
|
+
FILTER (WHERE recent_issues.opened_at >= windows.start_at)::integer
|
|
1848
|
+
AS "created",
|
|
1849
|
+
count(recent_issues.issue_id)
|
|
1850
|
+
FILTER (
|
|
1851
|
+
WHERE recent_issues.state = 'closed'
|
|
1852
|
+
AND recent_issues.state_reason = 'completed'
|
|
1853
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1854
|
+
)::integer AS "closedCompleted",
|
|
1855
|
+
count(recent_issues.issue_id)
|
|
1856
|
+
FILTER (
|
|
1857
|
+
WHERE recent_issues.state = 'closed'
|
|
1858
|
+
AND recent_issues.state_reason = 'duplicate'
|
|
1859
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1860
|
+
)::integer AS "closedDuplicate",
|
|
1861
|
+
count(recent_issues.issue_id)
|
|
1862
|
+
FILTER (
|
|
1863
|
+
WHERE recent_issues.state = 'closed'
|
|
1864
|
+
AND recent_issues.state_reason = 'not_planned'
|
|
1865
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1866
|
+
)::integer AS "closedNotPlanned",
|
|
1867
|
+
count(recent_issues.issue_id)
|
|
1868
|
+
FILTER (
|
|
1869
|
+
WHERE recent_issues.state = 'closed'
|
|
1870
|
+
AND recent_issues.state_reason IS NULL
|
|
1871
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1872
|
+
)::integer AS "closedUnknown",
|
|
1873
|
+
(
|
|
1874
|
+
percentile_cont(0.5) WITHIN GROUP (
|
|
1875
|
+
ORDER BY extract(
|
|
1876
|
+
epoch FROM (recent_issues.closed_at - recent_issues.opened_at)
|
|
1877
|
+
) * 1000
|
|
1878
|
+
) FILTER (
|
|
1879
|
+
WHERE recent_issues.state = 'closed'
|
|
1880
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1881
|
+
)
|
|
1882
|
+
)::double precision AS "medianCloseTimeMs"
|
|
1883
|
+
FROM windows
|
|
1884
|
+
LEFT JOIN recent_issues ON true
|
|
1885
|
+
GROUP BY windows.days
|
|
1886
|
+
ORDER BY windows.days
|
|
1887
|
+
`);
|
|
1888
|
+
return z10.array(issueStatsSchema).parse(queryRows(result));
|
|
1889
|
+
}
|
|
1890
|
+
async function aggregateIssueRepositories(args) {
|
|
1891
|
+
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
1892
|
+
const table = juniorGitHubIssues;
|
|
1893
|
+
const result = await args.db.execute(sql3`
|
|
1894
|
+
SELECT
|
|
1895
|
+
${table.repositoryFullName} AS "repository",
|
|
1896
|
+
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
1897
|
+
AS "created",
|
|
1898
|
+
count(*) FILTER (
|
|
1899
|
+
WHERE ${table.state} = 'closed'
|
|
1900
|
+
AND ${table.stateReason} = 'completed'
|
|
1901
|
+
AND ${table.closedAt} >= ${start}
|
|
1902
|
+
)::integer AS "closedCompleted",
|
|
1903
|
+
count(*) FILTER (
|
|
1904
|
+
WHERE ${table.state} = 'closed'
|
|
1905
|
+
AND ${table.stateReason} = 'duplicate'
|
|
1906
|
+
AND ${table.closedAt} >= ${start}
|
|
1907
|
+
)::integer AS "closedDuplicate",
|
|
1908
|
+
count(*) FILTER (
|
|
1909
|
+
WHERE ${table.state} = 'closed'
|
|
1910
|
+
AND ${table.stateReason} = 'not_planned'
|
|
1911
|
+
AND ${table.closedAt} >= ${start}
|
|
1912
|
+
)::integer AS "closedNotPlanned",
|
|
1913
|
+
count(*) FILTER (
|
|
1914
|
+
WHERE ${table.state} = 'closed'
|
|
1915
|
+
AND ${table.stateReason} IS NULL
|
|
1916
|
+
AND ${table.closedAt} >= ${start}
|
|
1917
|
+
)::integer AS "closedUnknown"
|
|
1918
|
+
FROM ${table}
|
|
1919
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1920
|
+
OR ${table.closedAt} >= ${start}
|
|
1921
|
+
GROUP BY ${table.repositoryFullName}
|
|
1922
|
+
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
1923
|
+
LIMIT 25
|
|
1924
|
+
`);
|
|
1925
|
+
return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
|
|
1412
1926
|
}
|
|
1413
1927
|
function formatPercent(value) {
|
|
1414
1928
|
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
@@ -1420,38 +1934,68 @@ function formatDuration(value) {
|
|
|
1420
1934
|
if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
|
|
1421
1935
|
return `${Math.round(hours / 24 * 10) / 10}d`;
|
|
1422
1936
|
}
|
|
1937
|
+
function formatCommitComposition(stats) {
|
|
1938
|
+
return `${stats.juniorOnly} Junior-only \xB7 ${stats.mixed} mixed \xB7 ${stats.compositionUnknown} unknown`;
|
|
1939
|
+
}
|
|
1423
1940
|
async function buildGitHubOutcomeReport(args) {
|
|
1424
|
-
const [windows, repositories] = await Promise.all([
|
|
1425
|
-
|
|
1426
|
-
|
|
1941
|
+
const [windows, repositories, issueWindows, issueRepositories] = await Promise.all([
|
|
1942
|
+
aggregatePullRequestWindows(args),
|
|
1943
|
+
aggregatePullRequestRepositories(args),
|
|
1944
|
+
aggregateIssueWindows(args),
|
|
1945
|
+
aggregateIssueRepositories(args)
|
|
1427
1946
|
]);
|
|
1428
1947
|
const thirtyDays = windows.find((window) => window.days === 30);
|
|
1948
|
+
const issueThirtyDays = issueWindows.find((window) => window.days === 30);
|
|
1429
1949
|
return {
|
|
1430
1950
|
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1431
1951
|
title: "GitHub work delivered",
|
|
1432
1952
|
metrics: [
|
|
1433
|
-
{ label: "created \xB7 30d", value: String(thirtyDays.created) },
|
|
1953
|
+
{ label: "PRs created \xB7 30d", value: String(thirtyDays.created) },
|
|
1434
1954
|
{
|
|
1435
|
-
label: "merged \xB7 30d",
|
|
1955
|
+
label: "PRs merged \xB7 30d",
|
|
1436
1956
|
tone: thirtyDays.merged > 0 ? "good" : "neutral",
|
|
1437
1957
|
value: String(thirtyDays.merged)
|
|
1438
1958
|
},
|
|
1439
|
-
{ label: "closed unmerged \xB7 30d", value: String(thirtyDays.closed) },
|
|
1440
|
-
{ label: "open now", value: String(thirtyDays.open) },
|
|
1441
|
-
{ label: "merge rate \xB7 30d", value: formatPercent(thirtyDays.mergeRate) },
|
|
1442
1959
|
{
|
|
1443
|
-
label: "
|
|
1960
|
+
label: "Junior-only merge rate \xB7 30d",
|
|
1961
|
+
value: formatPercent(thirtyDays.juniorOnlyRate)
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
label: "PRs closed unmerged \xB7 30d",
|
|
1965
|
+
value: String(thirtyDays.closed)
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
label: "PR merge rate \xB7 30d",
|
|
1969
|
+
value: formatPercent(thirtyDays.mergeRate)
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
label: "median PR merge time \xB7 30d",
|
|
1444
1973
|
value: formatDuration(thirtyDays.medianMergeTimeMs)
|
|
1974
|
+
},
|
|
1975
|
+
{ label: "issues created \xB7 30d", value: String(issueThirtyDays.created) },
|
|
1976
|
+
{
|
|
1977
|
+
label: "issues completed \xB7 30d",
|
|
1978
|
+
tone: issueThirtyDays.closedCompleted > 0 ? "good" : "neutral",
|
|
1979
|
+
value: String(issueThirtyDays.closedCompleted)
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
label: "issues closed as duplicate \xB7 30d",
|
|
1983
|
+
value: String(issueThirtyDays.closedDuplicate)
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
label: "issues closed as not planned \xB7 30d",
|
|
1987
|
+
value: String(issueThirtyDays.closedNotPlanned)
|
|
1445
1988
|
}
|
|
1446
1989
|
],
|
|
1447
1990
|
recordSets: [
|
|
1448
1991
|
{
|
|
1449
|
-
title: "
|
|
1992
|
+
title: "Pull request outcome windows",
|
|
1450
1993
|
fields: [
|
|
1451
1994
|
{ key: "window", label: "Window" },
|
|
1452
1995
|
{ key: "created", label: "Created" },
|
|
1453
1996
|
{ key: "merged", label: "Merged" },
|
|
1454
1997
|
{ key: "closed", label: "Closed unmerged" },
|
|
1998
|
+
{ key: "commitComposition", label: "Merged commit composition" },
|
|
1455
1999
|
{ key: "mergeRate", label: "Merge rate" },
|
|
1456
2000
|
{ key: "mergeTime", label: "Median merge time" }
|
|
1457
2001
|
],
|
|
@@ -1462,20 +2006,21 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1462
2006
|
created: String(stats.created),
|
|
1463
2007
|
merged: String(stats.merged),
|
|
1464
2008
|
closed: String(stats.closed),
|
|
2009
|
+
commitComposition: formatCommitComposition(stats),
|
|
1465
2010
|
mergeRate: formatPercent(stats.mergeRate),
|
|
1466
2011
|
mergeTime: formatDuration(stats.medianMergeTimeMs)
|
|
1467
2012
|
}
|
|
1468
2013
|
}))
|
|
1469
2014
|
},
|
|
1470
2015
|
{
|
|
1471
|
-
title: "
|
|
2016
|
+
title: "Pull request repositories \xB7 30d",
|
|
1472
2017
|
emptyText: "No Junior-owned pull request activity yet.",
|
|
1473
2018
|
fields: [
|
|
1474
2019
|
{ key: "repository", label: "Repository" },
|
|
1475
2020
|
{ key: "created", label: "Created" },
|
|
1476
2021
|
{ key: "merged", label: "Merged" },
|
|
1477
2022
|
{ key: "closed", label: "Closed unmerged" },
|
|
1478
|
-
{ key: "
|
|
2023
|
+
{ key: "commitComposition", label: "Merged commit composition" },
|
|
1479
2024
|
{ key: "mergeRate", label: "Merge rate" }
|
|
1480
2025
|
],
|
|
1481
2026
|
records: repositories.map(({ repository, ...stats }) => ({
|
|
@@ -1485,15 +2030,115 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1485
2030
|
created: String(stats.created),
|
|
1486
2031
|
merged: String(stats.merged),
|
|
1487
2032
|
closed: String(stats.closed),
|
|
1488
|
-
|
|
2033
|
+
commitComposition: formatCommitComposition(stats),
|
|
1489
2034
|
mergeRate: formatPercent(stats.mergeRate)
|
|
1490
2035
|
}
|
|
1491
2036
|
}))
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
title: "Issue outcome windows",
|
|
2040
|
+
fields: [
|
|
2041
|
+
{ key: "window", label: "Window" },
|
|
2042
|
+
{ key: "created", label: "Created" },
|
|
2043
|
+
{ key: "completed", label: "Completed" },
|
|
2044
|
+
{ key: "duplicate", label: "Duplicate" },
|
|
2045
|
+
{ key: "notPlanned", label: "Not planned" },
|
|
2046
|
+
{ key: "unknown", label: "Unknown reason" },
|
|
2047
|
+
{ key: "closeTime", label: "Median close time" }
|
|
2048
|
+
],
|
|
2049
|
+
records: issueWindows.map((stats) => ({
|
|
2050
|
+
id: `${stats.days}d`,
|
|
2051
|
+
values: {
|
|
2052
|
+
window: `${stats.days} days`,
|
|
2053
|
+
created: String(stats.created),
|
|
2054
|
+
completed: String(stats.closedCompleted),
|
|
2055
|
+
duplicate: String(stats.closedDuplicate),
|
|
2056
|
+
notPlanned: String(stats.closedNotPlanned),
|
|
2057
|
+
unknown: String(stats.closedUnknown),
|
|
2058
|
+
closeTime: formatDuration(stats.medianCloseTimeMs)
|
|
2059
|
+
}
|
|
2060
|
+
}))
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
title: "Issue repositories \xB7 30d",
|
|
2064
|
+
emptyText: "No Junior-owned issue activity yet.",
|
|
2065
|
+
fields: [
|
|
2066
|
+
{ key: "repository", label: "Repository" },
|
|
2067
|
+
{ key: "created", label: "Created" },
|
|
2068
|
+
{ key: "completed", label: "Completed" },
|
|
2069
|
+
{ key: "duplicate", label: "Duplicate" },
|
|
2070
|
+
{ key: "notPlanned", label: "Not planned" },
|
|
2071
|
+
{ key: "unknown", label: "Unknown reason" }
|
|
2072
|
+
],
|
|
2073
|
+
records: issueRepositories.map(({ repository, ...stats }) => ({
|
|
2074
|
+
id: repository,
|
|
2075
|
+
values: {
|
|
2076
|
+
repository,
|
|
2077
|
+
created: String(stats.created),
|
|
2078
|
+
completed: String(stats.closedCompleted),
|
|
2079
|
+
duplicate: String(stats.closedDuplicate),
|
|
2080
|
+
notPlanned: String(stats.closedNotPlanned),
|
|
2081
|
+
unknown: String(stats.closedUnknown)
|
|
2082
|
+
}
|
|
2083
|
+
}))
|
|
1492
2084
|
}
|
|
1493
2085
|
]
|
|
1494
2086
|
};
|
|
1495
2087
|
}
|
|
1496
2088
|
|
|
2089
|
+
// src/pull-request-outcomes/commit-composition.ts
|
|
2090
|
+
import { z as z11 } from "zod";
|
|
2091
|
+
var canonicalCommitSchema = z11.object({
|
|
2092
|
+
authorEmail: z11.string().nullable(),
|
|
2093
|
+
authorLogin: z11.string().nullable()
|
|
2094
|
+
}).strict();
|
|
2095
|
+
var providerCommitSchema = z11.object({
|
|
2096
|
+
author: z11.object({ login: z11.string() }).passthrough().nullable(),
|
|
2097
|
+
commit: z11.object({
|
|
2098
|
+
author: z11.object({ email: z11.string() }).passthrough().nullable()
|
|
2099
|
+
}).passthrough()
|
|
2100
|
+
}).passthrough();
|
|
2101
|
+
var commitPageSchema = z11.array(providerCommitSchema).transform(
|
|
2102
|
+
(commits) => commits.map(
|
|
2103
|
+
(commit) => canonicalCommitSchema.parse({
|
|
2104
|
+
authorEmail: commit.commit.author?.email ?? null,
|
|
2105
|
+
authorLogin: commit.author?.login ?? null
|
|
2106
|
+
})
|
|
2107
|
+
)
|
|
2108
|
+
);
|
|
2109
|
+
var PAGE_SIZE = 100;
|
|
2110
|
+
var MAX_PAGES = 3;
|
|
2111
|
+
var MAX_LISTED_COMMITS = 250;
|
|
2112
|
+
async function classifyGitHubPullRequestCommitComposition(args) {
|
|
2113
|
+
const botEmail = args.botEmail.trim().toLowerCase();
|
|
2114
|
+
const botLogin = botLoginFromEmail(botEmail)?.toLowerCase();
|
|
2115
|
+
if (!botEmail || !botLogin) {
|
|
2116
|
+
throw new Error(
|
|
2117
|
+
"The configured GitHub App bot email cannot classify pull request commits"
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
let foundCommit = false;
|
|
2121
|
+
let inspectedCommits = 0;
|
|
2122
|
+
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
2123
|
+
const commits = commitPageSchema.parse(
|
|
2124
|
+
await args.loadPage(page, PAGE_SIZE)
|
|
2125
|
+
);
|
|
2126
|
+
inspectedCommits += commits.length;
|
|
2127
|
+
for (const commit of commits) {
|
|
2128
|
+
foundCommit = true;
|
|
2129
|
+
const authorLogin = commit.authorLogin?.trim().toLowerCase();
|
|
2130
|
+
const authorEmail = commit.authorEmail?.trim().toLowerCase();
|
|
2131
|
+
if (authorLogin !== botLogin && authorEmail !== botEmail) {
|
|
2132
|
+
return "mixed";
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (inspectedCommits >= MAX_LISTED_COMMITS) return void 0;
|
|
2136
|
+
if (commits.length < PAGE_SIZE) break;
|
|
2137
|
+
if (page === MAX_PAGES) return void 0;
|
|
2138
|
+
}
|
|
2139
|
+
return foundCommit ? "junior_only" : void 0;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1497
2142
|
// src/index.ts
|
|
1498
2143
|
var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
|
|
1499
2144
|
var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
|
|
@@ -2059,28 +2704,6 @@ function githubRepositoryFromLeaseScope(leaseScope) {
|
|
|
2059
2704
|
}
|
|
2060
2705
|
return { owner: match[1], name: match[2] };
|
|
2061
2706
|
}
|
|
2062
|
-
function githubRepositoryIdFromUploadUrl(upstreamUrl) {
|
|
2063
|
-
const repositoryId = Number(upstreamUrl.searchParams.get("repository_id"));
|
|
2064
|
-
if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
|
|
2065
|
-
throw new EgressPolicyDenied(
|
|
2066
|
-
"GitHub asset upload request is missing a valid repository_id."
|
|
2067
|
-
);
|
|
2068
|
-
}
|
|
2069
|
-
return repositoryId;
|
|
2070
|
-
}
|
|
2071
|
-
function githubRepositoryIdLeaseScope(repositoryId) {
|
|
2072
|
-
return `repository-id:${repositoryId}`;
|
|
2073
|
-
}
|
|
2074
|
-
function githubRepositoryIdFromLeaseScope(leaseScope) {
|
|
2075
|
-
const match = /^repository-id:(\d+)$/.exec(leaseScope ?? "");
|
|
2076
|
-
const repositoryId = Number(match?.[1]);
|
|
2077
|
-
if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
|
|
2078
|
-
throw new GitHubPluginSetupError(
|
|
2079
|
-
"GitHub asset upload grant is missing a repository id lease scope."
|
|
2080
|
-
);
|
|
2081
|
-
}
|
|
2082
|
-
return repositoryId;
|
|
2083
|
-
}
|
|
2084
2707
|
async function resolveUserAccount(tokens) {
|
|
2085
2708
|
const account = await githubRequest("https://api.github.com", "/user", {
|
|
2086
2709
|
token: tokens.accessToken
|
|
@@ -2250,7 +2873,7 @@ async function issueUserCredential(ctx, options) {
|
|
|
2250
2873
|
}
|
|
2251
2874
|
return credentialNeeded("Your GitHub authorization has expired.", scope);
|
|
2252
2875
|
}
|
|
2253
|
-
async function
|
|
2876
|
+
async function issueInstallationToken(options) {
|
|
2254
2877
|
const appId = requireEnv(options.appIdEnv);
|
|
2255
2878
|
const installationIdRaw = requireEnv(options.installationIdEnv);
|
|
2256
2879
|
const installationId = Number(installationIdRaw);
|
|
@@ -2258,8 +2881,10 @@ async function issueInstallationCredential(options) {
|
|
|
2258
2881
|
throw new GitHubPluginSetupError(`Invalid ${options.installationIdEnv}`);
|
|
2259
2882
|
}
|
|
2260
2883
|
const appJwt = createAppJwt(appId, options.privateKeyEnv);
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2884
|
+
const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
|
|
2885
|
+
const body = {
|
|
2886
|
+
...permissions ? { permissions } : {},
|
|
2887
|
+
..."repositories" in options ? { repositories: options.repositories } : {}
|
|
2263
2888
|
};
|
|
2264
2889
|
const accessTokenResponse = await githubRequest(
|
|
2265
2890
|
"https://api.github.com",
|
|
@@ -2271,14 +2896,16 @@ async function issueInstallationCredential(options) {
|
|
|
2271
2896
|
}
|
|
2272
2897
|
);
|
|
2273
2898
|
const parsedToken = parseInstallationTokenResponse(accessTokenResponse);
|
|
2274
|
-
|
|
2275
|
-
parsedToken.expiresAtMs,
|
|
2276
|
-
|
|
2277
|
-
|
|
2899
|
+
return {
|
|
2900
|
+
expiresAtMs: Math.min(parsedToken.expiresAtMs, Date.now() + MAX_LEASE_MS),
|
|
2901
|
+
token: parsedToken.token
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
async function issueInstallationCredential(options) {
|
|
2905
|
+
const token = await issueInstallationToken(options);
|
|
2278
2906
|
return createCredentialLease({
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
expiresAtMs
|
|
2907
|
+
token: token.token,
|
|
2908
|
+
expiresAtMs: token.expiresAtMs
|
|
2282
2909
|
});
|
|
2283
2910
|
}
|
|
2284
2911
|
function createPermissionCache() {
|
|
@@ -2595,13 +3222,7 @@ async function githubGrantForEgress(ctx) {
|
|
|
2595
3222
|
upstreamUrl
|
|
2596
3223
|
});
|
|
2597
3224
|
if (isGitHubAssetUploadRequest(method, upstreamUrl)) {
|
|
2598
|
-
|
|
2599
|
-
return grantForAccess(
|
|
2600
|
-
"write",
|
|
2601
|
-
"github.asset-upload",
|
|
2602
|
-
"installation-write",
|
|
2603
|
-
githubRepositoryIdLeaseScope(repositoryId)
|
|
2604
|
-
);
|
|
3225
|
+
return grantForAccess("write", "github.asset-upload", "user-write");
|
|
2605
3226
|
}
|
|
2606
3227
|
const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
|
|
2607
3228
|
if (smartHttpAccess) {
|
|
@@ -2725,7 +3346,34 @@ function githubPlugin(options = {}) {
|
|
|
2725
3346
|
return [
|
|
2726
3347
|
createGitHubWebhookRoute({
|
|
2727
3348
|
botEmail: () => readEnv(botEmailEnv),
|
|
3349
|
+
classifyPullRequestCommits: async ({
|
|
3350
|
+
number,
|
|
3351
|
+
repositoryFullName
|
|
3352
|
+
}) => {
|
|
3353
|
+
const [owner, name, ...extra] = repositoryFullName.split("/");
|
|
3354
|
+
if (!owner || !name || extra.length > 0) {
|
|
3355
|
+
throw new Error(
|
|
3356
|
+
"GitHub pull request commit classification received an invalid repository name"
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
const token = await issueInstallationToken({
|
|
3360
|
+
appIdEnv,
|
|
3361
|
+
privateKeyEnv,
|
|
3362
|
+
installationIdEnv,
|
|
3363
|
+
permissions: { pull_requests: "read" },
|
|
3364
|
+
repositories: [name]
|
|
3365
|
+
});
|
|
3366
|
+
return await classifyGitHubPullRequestCommitComposition({
|
|
3367
|
+
botEmail: requireEnv(botEmailEnv),
|
|
3368
|
+
loadPage: async (page, perPage) => await githubRequest(
|
|
3369
|
+
"https://api.github.com",
|
|
3370
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls/${number}/commits?per_page=${perPage}&page=${page}`,
|
|
3371
|
+
{ token: token.token }
|
|
3372
|
+
)
|
|
3373
|
+
});
|
|
3374
|
+
},
|
|
2728
3375
|
db: ctx.db,
|
|
3376
|
+
log: ctx.log,
|
|
2729
3377
|
resourceEvents: ctx.resourceEvents,
|
|
2730
3378
|
webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")
|
|
2731
3379
|
})
|
|
@@ -2808,18 +3456,6 @@ function githubPlugin(options = {}) {
|
|
|
2808
3456
|
});
|
|
2809
3457
|
}
|
|
2810
3458
|
if (ctx.grant.name === "installation-write") {
|
|
2811
|
-
if (ctx.grant.reason === "github.asset-upload") {
|
|
2812
|
-
const repositoryId = githubRepositoryIdFromLeaseScope(
|
|
2813
|
-
ctx.grant.leaseScope
|
|
2814
|
-
);
|
|
2815
|
-
return await issueInstallationCredential({
|
|
2816
|
-
appIdEnv,
|
|
2817
|
-
domains: GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS,
|
|
2818
|
-
privateKeyEnv,
|
|
2819
|
-
installationIdEnv,
|
|
2820
|
-
repositoryIds: [repositoryId]
|
|
2821
|
-
});
|
|
2822
|
-
}
|
|
2823
3459
|
const repository = githubRepositoryFromLeaseScope(
|
|
2824
3460
|
ctx.grant.leaseScope
|
|
2825
3461
|
);
|