@sentry/junior-github 0.112.0 → 0.114.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/dist/db/schema.d.ts +203 -0
- package/dist/index.js +591 -76
- package/dist/issue-outcomes/store.d.ts +7 -0
- package/dist/outcomes/cost.d.ts +50 -0
- package/dist/pull-request-outcomes/store.d.ts +10 -0
- package/dist/tools/footer.d.ts +5 -0
- package/dist/webhooks/issue-outcome.d.ts +6 -1
- package/dist/webhooks/pull-request-outcome.d.ts +6 -1
- package/migrations/0005_github_cost_associations.sql +2 -0
- package/migrations/0006_fat_korvac.sql +9 -0
- package/migrations/meta/0005_snapshot.json +289 -0
- package/migrations/meta/0006_snapshot.json +336 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -166,6 +166,28 @@ function githubConversationIds(body) {
|
|
|
166
166
|
}
|
|
167
167
|
return [...ids];
|
|
168
168
|
}
|
|
169
|
+
function githubLinkedIssues(body, repositoryFullName) {
|
|
170
|
+
if (!body) return [];
|
|
171
|
+
const references = /* @__PURE__ */ new Map();
|
|
172
|
+
const add = (linkedRepository, rawNumber) => {
|
|
173
|
+
const number = Number.parseInt(rawNumber, 10);
|
|
174
|
+
if (!Number.isInteger(number) || number <= 0) return;
|
|
175
|
+
const normalizedRepository = linkedRepository.toLowerCase();
|
|
176
|
+
references.set(`${normalizedRepository}#${number}`, {
|
|
177
|
+
number,
|
|
178
|
+
repositoryFullName: linkedRepository
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
for (const match of body.matchAll(/\b([\w.-]+\/[\w.-]+)#(\d+)\b/g)) {
|
|
182
|
+
add(match[1] ?? "", match[2] ?? "");
|
|
183
|
+
}
|
|
184
|
+
for (const match of body.matchAll(/(?:^|[^\w/])#(\d+)\b/g)) {
|
|
185
|
+
add(repositoryFullName, match[1] ?? "");
|
|
186
|
+
}
|
|
187
|
+
return [...references.values()].sort(
|
|
188
|
+
(left, right) => left.repositoryFullName.localeCompare(right.repositoryFullName) || left.number - right.number
|
|
189
|
+
);
|
|
190
|
+
}
|
|
169
191
|
function appendGitHubFooter(body, conversationId, dashboardUrl) {
|
|
170
192
|
const footer = githubConversationFooter(conversationId, dashboardUrl);
|
|
171
193
|
const normalizedBody = body.trimEnd();
|
|
@@ -1096,12 +1118,19 @@ function createGitHubTools(ctx) {
|
|
|
1096
1118
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
1097
1119
|
|
|
1098
1120
|
// src/issue-outcomes/store.ts
|
|
1099
|
-
import { and, eq, lte } from "drizzle-orm";
|
|
1121
|
+
import { and, eq, lte, sql as sql2 } from "drizzle-orm";
|
|
1100
1122
|
import { z as z6 } from "zod";
|
|
1101
1123
|
|
|
1102
1124
|
// src/db/schema.ts
|
|
1103
1125
|
import { sql } from "drizzle-orm";
|
|
1104
|
-
import {
|
|
1126
|
+
import {
|
|
1127
|
+
index,
|
|
1128
|
+
integer,
|
|
1129
|
+
pgTable,
|
|
1130
|
+
primaryKey,
|
|
1131
|
+
text,
|
|
1132
|
+
timestamp
|
|
1133
|
+
} from "drizzle-orm/pg-core";
|
|
1105
1134
|
import { z as z5 } from "zod";
|
|
1106
1135
|
var githubPullRequestStateSchema = z5.enum([
|
|
1107
1136
|
"closed_unmerged",
|
|
@@ -1128,6 +1157,7 @@ var juniorGitHubIssues = pgTable(
|
|
|
1128
1157
|
number: integer("number").notNull(),
|
|
1129
1158
|
state: text("state").$type().notNull(),
|
|
1130
1159
|
stateReason: text("state_reason").$type(),
|
|
1160
|
+
conversationIds: text("conversation_ids").array().notNull().default(sql`ARRAY[]::text[]`),
|
|
1131
1161
|
openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
|
|
1132
1162
|
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
1133
1163
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
@@ -1160,6 +1190,25 @@ var juniorGitHubPullRequests = pgTable(
|
|
|
1160
1190
|
index("junior_github_pull_requests_open_idx").on(table.pullRequestId).where(sql`${table.state} = 'open'`)
|
|
1161
1191
|
]
|
|
1162
1192
|
);
|
|
1193
|
+
var juniorGitHubPullRequestIssues = pgTable(
|
|
1194
|
+
"junior_github_pull_request_issues",
|
|
1195
|
+
{
|
|
1196
|
+
pullRequestId: text("pull_request_id").notNull().references(() => juniorGitHubPullRequests.pullRequestId, {
|
|
1197
|
+
onDelete: "cascade"
|
|
1198
|
+
}),
|
|
1199
|
+
issueRepositoryFullName: text("issue_repository_full_name").notNull(),
|
|
1200
|
+
issueNumber: integer("issue_number").notNull()
|
|
1201
|
+
},
|
|
1202
|
+
(table) => [
|
|
1203
|
+
primaryKey({
|
|
1204
|
+
columns: [
|
|
1205
|
+
table.pullRequestId,
|
|
1206
|
+
table.issueRepositoryFullName,
|
|
1207
|
+
table.issueNumber
|
|
1208
|
+
]
|
|
1209
|
+
})
|
|
1210
|
+
]
|
|
1211
|
+
);
|
|
1163
1212
|
|
|
1164
1213
|
// src/issue-outcomes/store.ts
|
|
1165
1214
|
var githubIssueOutcomeInputSchema = z6.object({
|
|
@@ -1174,6 +1223,10 @@ var githubIssueOutcomeInputSchema = z6.object({
|
|
|
1174
1223
|
stateReason: githubIssueStateReasonSchema.optional(),
|
|
1175
1224
|
updatedAt: z6.date()
|
|
1176
1225
|
}).strict();
|
|
1226
|
+
var githubIssueConversationsInputSchema = z6.object({
|
|
1227
|
+
conversationIds: z6.array(z6.string().min(1)).min(1),
|
|
1228
|
+
issueId: z6.string().min(1)
|
|
1229
|
+
}).strict();
|
|
1177
1230
|
function projectionValues(input) {
|
|
1178
1231
|
return {
|
|
1179
1232
|
closedAt: input.closedAt ?? null,
|
|
@@ -1204,9 +1257,27 @@ async function recordGitHubIssueOutcome(db, input) {
|
|
|
1204
1257
|
where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
|
|
1205
1258
|
});
|
|
1206
1259
|
}
|
|
1260
|
+
async function recordGitHubIssueConversations(db, input) {
|
|
1261
|
+
const association = githubIssueConversationsInputSchema.parse(input);
|
|
1262
|
+
const conversationIds = sql2.join(
|
|
1263
|
+
association.conversationIds.map((id) => sql2`${id}`),
|
|
1264
|
+
sql2`, `
|
|
1265
|
+
);
|
|
1266
|
+
const updated = await db.update(juniorGitHubIssues).set({
|
|
1267
|
+
conversationIds: sql2`ARRAY(
|
|
1268
|
+
SELECT DISTINCT value
|
|
1269
|
+
FROM unnest(
|
|
1270
|
+
${juniorGitHubIssues.conversationIds}
|
|
1271
|
+
|| ARRAY[${conversationIds}]::text[]
|
|
1272
|
+
) AS value
|
|
1273
|
+
ORDER BY value
|
|
1274
|
+
)`
|
|
1275
|
+
}).where(eq(juniorGitHubIssues.issueId, association.issueId)).returning({ issueId: juniorGitHubIssues.issueId });
|
|
1276
|
+
return updated.length > 0;
|
|
1277
|
+
}
|
|
1207
1278
|
|
|
1208
1279
|
// src/pull-request-outcomes/store.ts
|
|
1209
|
-
import { and as and2, eq as eq2, lte as lte2, sql as
|
|
1280
|
+
import { and as and2, eq as eq2, lte as lte2, sql as sql3 } from "drizzle-orm";
|
|
1210
1281
|
import { z as z7 } from "zod";
|
|
1211
1282
|
var githubPullRequestOutcomeInputSchema = z7.object({
|
|
1212
1283
|
candidateOwned: z7.boolean(),
|
|
@@ -1225,6 +1296,15 @@ var githubPullRequestConversationsInputSchema = z7.object({
|
|
|
1225
1296
|
conversationIds: z7.array(z7.string().min(1)).min(1),
|
|
1226
1297
|
pullRequestId: z7.string().min(1)
|
|
1227
1298
|
}).strict();
|
|
1299
|
+
var githubPullRequestLinkedIssuesInputSchema = z7.object({
|
|
1300
|
+
linkedIssues: z7.array(
|
|
1301
|
+
z7.object({
|
|
1302
|
+
number: z7.number().int().positive(),
|
|
1303
|
+
repositoryFullName: z7.string().min(1)
|
|
1304
|
+
}).strict()
|
|
1305
|
+
).min(1),
|
|
1306
|
+
pullRequestId: z7.string().min(1)
|
|
1307
|
+
}).strict();
|
|
1228
1308
|
function projectionValues2(input) {
|
|
1229
1309
|
return {
|
|
1230
1310
|
closedAt: input.closedAt ?? null,
|
|
@@ -1269,12 +1349,12 @@ async function recordGitHubPullRequestOutcome(db, input) {
|
|
|
1269
1349
|
}
|
|
1270
1350
|
async function recordGitHubPullRequestConversations(db, input) {
|
|
1271
1351
|
const association = githubPullRequestConversationsInputSchema.parse(input);
|
|
1272
|
-
const conversationIds =
|
|
1273
|
-
association.conversationIds.map((id) =>
|
|
1274
|
-
|
|
1352
|
+
const conversationIds = sql3.join(
|
|
1353
|
+
association.conversationIds.map((id) => sql3`${id}`),
|
|
1354
|
+
sql3`, `
|
|
1275
1355
|
);
|
|
1276
1356
|
const updated = await db.update(juniorGitHubPullRequests).set({
|
|
1277
|
-
conversationIds:
|
|
1357
|
+
conversationIds: sql3`ARRAY(
|
|
1278
1358
|
SELECT DISTINCT value
|
|
1279
1359
|
FROM unnest(
|
|
1280
1360
|
${juniorGitHubPullRequests.conversationIds}
|
|
@@ -1287,6 +1367,30 @@ async function recordGitHubPullRequestConversations(db, input) {
|
|
|
1287
1367
|
).returning({ pullRequestId: juniorGitHubPullRequests.pullRequestId });
|
|
1288
1368
|
return updated.length > 0;
|
|
1289
1369
|
}
|
|
1370
|
+
async function recordGitHubPullRequestLinkedIssues(db, input) {
|
|
1371
|
+
const association = githubPullRequestLinkedIssuesInputSchema.parse(input);
|
|
1372
|
+
const inserted = await Promise.all(
|
|
1373
|
+
association.linkedIssues.map(
|
|
1374
|
+
(issue) => db.insert(juniorGitHubPullRequestIssues).select(
|
|
1375
|
+
db.select({
|
|
1376
|
+
pullRequestId: juniorGitHubPullRequests.pullRequestId,
|
|
1377
|
+
issueRepositoryFullName: sql3`lower(${issue.repositoryFullName})`.as(
|
|
1378
|
+
"issue_repository_full_name"
|
|
1379
|
+
),
|
|
1380
|
+
issueNumber: sql3`${issue.number}`.as("issue_number")
|
|
1381
|
+
}).from(juniorGitHubPullRequests).where(
|
|
1382
|
+
eq2(
|
|
1383
|
+
juniorGitHubPullRequests.pullRequestId,
|
|
1384
|
+
association.pullRequestId
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
).onConflictDoNothing().returning({
|
|
1388
|
+
issueNumber: juniorGitHubPullRequestIssues.issueNumber
|
|
1389
|
+
})
|
|
1390
|
+
)
|
|
1391
|
+
);
|
|
1392
|
+
return inserted.some((rows) => rows.length > 0);
|
|
1393
|
+
}
|
|
1290
1394
|
|
|
1291
1395
|
// src/webhooks/issue-outcome.ts
|
|
1292
1396
|
import { z as z8 } from "zod";
|
|
@@ -1404,6 +1508,45 @@ function normalizeGitHubIssueOutcome(args) {
|
|
|
1404
1508
|
updatedAt
|
|
1405
1509
|
};
|
|
1406
1510
|
}
|
|
1511
|
+
var canonicalIssueConversationSchema = z8.object({
|
|
1512
|
+
issue: z8.object({
|
|
1513
|
+
body: z8.string().nullable().optional(),
|
|
1514
|
+
id: z8.number().int().positive(),
|
|
1515
|
+
user: z8.object({ login: z8.string().min(1) }).strict()
|
|
1516
|
+
}).strict(),
|
|
1517
|
+
sender: z8.object({ login: z8.string().min(1) }).strict().optional()
|
|
1518
|
+
}).strict();
|
|
1519
|
+
var issueConversationSchema = z8.object({
|
|
1520
|
+
issue: z8.object({
|
|
1521
|
+
body: z8.string().nullable().optional(),
|
|
1522
|
+
id: z8.number().int().positive(),
|
|
1523
|
+
user: z8.object({ login: z8.string().min(1) }).passthrough()
|
|
1524
|
+
}).passthrough(),
|
|
1525
|
+
sender: z8.object({ login: z8.string().min(1) }).passthrough().optional()
|
|
1526
|
+
}).passthrough().transform(
|
|
1527
|
+
(provider) => canonicalIssueConversationSchema.parse({
|
|
1528
|
+
issue: {
|
|
1529
|
+
body: provider.issue.body,
|
|
1530
|
+
id: provider.issue.id,
|
|
1531
|
+
user: { login: provider.issue.user.login }
|
|
1532
|
+
},
|
|
1533
|
+
...provider.sender ? { sender: { login: provider.sender.login } } : {}
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
function normalizeGitHubIssueConversations(args) {
|
|
1537
|
+
const parsed = issueConversationSchema.safeParse(args.body);
|
|
1538
|
+
const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
|
|
1539
|
+
if (!parsed.success || !botLogin) return void 0;
|
|
1540
|
+
const authorLogin = parsed.data.issue.user.login.trim().toLowerCase();
|
|
1541
|
+
if (authorLogin !== botLogin) return void 0;
|
|
1542
|
+
const senderLogin = parsed.data.sender?.login.trim().toLowerCase();
|
|
1543
|
+
if (senderLogin && senderLogin !== botLogin) return void 0;
|
|
1544
|
+
const conversationIds = githubConversationIds(parsed.data.issue.body);
|
|
1545
|
+
return conversationIds.length > 0 ? {
|
|
1546
|
+
conversationIds,
|
|
1547
|
+
issueId: String(parsed.data.issue.id)
|
|
1548
|
+
} : void 0;
|
|
1549
|
+
}
|
|
1407
1550
|
|
|
1408
1551
|
// src/webhooks/pull-request-outcome.ts
|
|
1409
1552
|
import { z as z9 } from "zod";
|
|
@@ -1469,6 +1612,7 @@ var canonicalPullRequestConversationSchema = z9.object({
|
|
|
1469
1612
|
id: z9.number().int().positive(),
|
|
1470
1613
|
user: z9.object({ login: z9.string().min(1) }).strict()
|
|
1471
1614
|
}).strict(),
|
|
1615
|
+
repository: z9.object({ full_name: z9.string().min(1) }).strict(),
|
|
1472
1616
|
sender: z9.object({ login: z9.string().min(1) }).strict()
|
|
1473
1617
|
}).strict();
|
|
1474
1618
|
var pullRequestConversationSchema = z9.object({
|
|
@@ -1477,6 +1621,7 @@ var pullRequestConversationSchema = z9.object({
|
|
|
1477
1621
|
id: z9.number().int().positive(),
|
|
1478
1622
|
user: z9.object({ login: z9.string().min(1) }).passthrough()
|
|
1479
1623
|
}).passthrough(),
|
|
1624
|
+
repository: z9.object({ full_name: z9.string().min(1) }).passthrough(),
|
|
1480
1625
|
sender: z9.object({ login: z9.string().min(1) }).passthrough()
|
|
1481
1626
|
}).passthrough().transform(
|
|
1482
1627
|
(provider) => canonicalPullRequestConversationSchema.parse({
|
|
@@ -1485,6 +1630,7 @@ var pullRequestConversationSchema = z9.object({
|
|
|
1485
1630
|
id: provider.pull_request.id,
|
|
1486
1631
|
user: { login: provider.pull_request.user.login }
|
|
1487
1632
|
},
|
|
1633
|
+
repository: { full_name: provider.repository.full_name },
|
|
1488
1634
|
sender: { login: provider.sender.login }
|
|
1489
1635
|
})
|
|
1490
1636
|
);
|
|
@@ -1548,6 +1694,22 @@ function normalizeGitHubPullRequestConversations(args) {
|
|
|
1548
1694
|
pullRequestId: String(parsed.data.pull_request.id)
|
|
1549
1695
|
} : void 0;
|
|
1550
1696
|
}
|
|
1697
|
+
function normalizeGitHubPullRequestLinkedIssues(args) {
|
|
1698
|
+
const parsed = pullRequestConversationSchema.safeParse(args.body);
|
|
1699
|
+
const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
|
|
1700
|
+
if (!parsed.success || !botLogin) return void 0;
|
|
1701
|
+
if (parsed.data.pull_request.user.login.trim().toLowerCase() !== botLogin || parsed.data.sender.login.trim().toLowerCase() !== botLogin) {
|
|
1702
|
+
return void 0;
|
|
1703
|
+
}
|
|
1704
|
+
const linkedIssues = githubLinkedIssues(
|
|
1705
|
+
parsed.data.pull_request.body,
|
|
1706
|
+
parsed.data.repository.full_name
|
|
1707
|
+
);
|
|
1708
|
+
return linkedIssues.length > 0 ? {
|
|
1709
|
+
linkedIssues,
|
|
1710
|
+
pullRequestId: String(parsed.data.pull_request.id)
|
|
1711
|
+
} : void 0;
|
|
1712
|
+
}
|
|
1551
1713
|
|
|
1552
1714
|
// src/webhooks/handler.ts
|
|
1553
1715
|
function verifyGitHubSignature(body, signature, secret) {
|
|
@@ -1584,7 +1746,9 @@ function createGitHubWebhookRoute(args) {
|
|
|
1584
1746
|
const botEmail = args.botEmail();
|
|
1585
1747
|
const pullRequestOutcome = eventName === "pull_request" ? normalizeGitHubPullRequestOutcome({ body, botEmail }) : void 0;
|
|
1586
1748
|
const issueOutcome = eventName === "issues" ? normalizeGitHubIssueOutcome({ body, botEmail }) : void 0;
|
|
1749
|
+
const issueConversations = eventName === "issues" ? normalizeGitHubIssueConversations({ body, botEmail }) : void 0;
|
|
1587
1750
|
const pullRequestConversations = eventName === "pull_request" ? normalizeGitHubPullRequestConversations({ body, botEmail }) : void 0;
|
|
1751
|
+
const pullRequestLinkedIssues = eventName === "pull_request" ? normalizeGitHubPullRequestLinkedIssues({ body, botEmail }) : void 0;
|
|
1588
1752
|
if (pullRequestOutcome) {
|
|
1589
1753
|
const recordedOutcome = await recordGitHubPullRequestOutcome(
|
|
1590
1754
|
args.db,
|
|
@@ -1616,10 +1780,15 @@ function createGitHubWebhookRoute(args) {
|
|
|
1616
1780
|
if (issueOutcome) {
|
|
1617
1781
|
await recordGitHubIssueOutcome(args.db, issueOutcome);
|
|
1618
1782
|
}
|
|
1783
|
+
const recordedIssueConversations = issueConversations ? await recordGitHubIssueConversations(args.db, issueConversations) : false;
|
|
1619
1784
|
const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
|
|
1620
1785
|
args.db,
|
|
1621
1786
|
pullRequestConversations
|
|
1622
1787
|
) : false;
|
|
1788
|
+
const recordedPullRequestLinkedIssues = pullRequestLinkedIssues ? await recordGitHubPullRequestLinkedIssues(
|
|
1789
|
+
args.db,
|
|
1790
|
+
pullRequestLinkedIssues
|
|
1791
|
+
) : false;
|
|
1623
1792
|
const resourceEvents = normalizeGitHubResourceEvents({
|
|
1624
1793
|
body,
|
|
1625
1794
|
deliveryId,
|
|
@@ -1628,7 +1797,7 @@ function createGitHubWebhookRoute(args) {
|
|
|
1628
1797
|
for (const event of resourceEvents) {
|
|
1629
1798
|
await args.resourceEvents.publish(event);
|
|
1630
1799
|
}
|
|
1631
|
-
if (!pullRequestOutcome && !issueOutcome && !recordedPullRequestConversations && resourceEvents.length === 0) {
|
|
1800
|
+
if (!pullRequestOutcome && !issueOutcome && !recordedIssueConversations && !recordedPullRequestConversations && !recordedPullRequestLinkedIssues && resourceEvents.length === 0) {
|
|
1632
1801
|
return new Response("Ignored", { status: 202 });
|
|
1633
1802
|
}
|
|
1634
1803
|
return new Response("Accepted", { status: 202 });
|
|
@@ -1637,16 +1806,330 @@ function createGitHubWebhookRoute(args) {
|
|
|
1637
1806
|
}
|
|
1638
1807
|
|
|
1639
1808
|
// src/outcomes/report.ts
|
|
1640
|
-
import { sql as
|
|
1809
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
1810
|
+
import { z as z11 } from "zod";
|
|
1811
|
+
|
|
1812
|
+
// src/outcomes/cost.ts
|
|
1813
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
1641
1814
|
import { z as z10 } from "zod";
|
|
1642
1815
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1643
|
-
var
|
|
1644
|
-
var pullRequestStatsSchema = z10.object({
|
|
1645
|
-
closed: z10.number().int().nonnegative(),
|
|
1646
|
-
created: z10.number().int().nonnegative(),
|
|
1816
|
+
var costWindowSchema = z10.object({
|
|
1647
1817
|
days: z10.number().int().positive(),
|
|
1648
|
-
|
|
1649
|
-
|
|
1818
|
+
issueCostUsd: z10.number().nonnegative().nullable(),
|
|
1819
|
+
medianIssueCostUsd: z10.number().nonnegative().nullable(),
|
|
1820
|
+
medianPullRequestCostUsd: z10.number().nonnegative().nullable(),
|
|
1821
|
+
pullRequestCostUsd: z10.number().nonnegative().nullable()
|
|
1822
|
+
}).strict().transform((row) => ({
|
|
1823
|
+
days: row.days,
|
|
1824
|
+
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
1825
|
+
medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
|
|
1826
|
+
medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
|
|
1827
|
+
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
|
|
1828
|
+
}));
|
|
1829
|
+
var repositoryCostSchema = z10.object({
|
|
1830
|
+
issueCostUsd: z10.number().nonnegative().nullable(),
|
|
1831
|
+
pullRequestCostUsd: z10.number().nonnegative().nullable(),
|
|
1832
|
+
repository: z10.string().min(1)
|
|
1833
|
+
}).strict().transform((row) => ({
|
|
1834
|
+
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
1835
|
+
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
|
|
1836
|
+
repository: row.repository
|
|
1837
|
+
}));
|
|
1838
|
+
function queryRows(result) {
|
|
1839
|
+
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
1840
|
+
throw new TypeError("GitHub cost query did not return rows");
|
|
1841
|
+
}
|
|
1842
|
+
return result.rows;
|
|
1843
|
+
}
|
|
1844
|
+
async function hasConversationUsageTable(db) {
|
|
1845
|
+
const result = await db.execute(sql4`
|
|
1846
|
+
SELECT to_regclass('public.junior_conversations') IS NOT NULL AS "present"
|
|
1847
|
+
`);
|
|
1848
|
+
const row = queryRows(result)[0];
|
|
1849
|
+
return typeof row === "object" && row !== null && "present" in row && row.present === true;
|
|
1850
|
+
}
|
|
1851
|
+
function emptyCostWindows(windows) {
|
|
1852
|
+
return windows.map((days) => ({
|
|
1853
|
+
days,
|
|
1854
|
+
issueCostUsd: 0,
|
|
1855
|
+
medianIssueCostUsd: void 0,
|
|
1856
|
+
medianPullRequestCostUsd: void 0,
|
|
1857
|
+
pullRequestCostUsd: 0
|
|
1858
|
+
}));
|
|
1859
|
+
}
|
|
1860
|
+
function conversationTreeCostExpr() {
|
|
1861
|
+
return sql4`
|
|
1862
|
+
coalesce((
|
|
1863
|
+
SELECT sum(
|
|
1864
|
+
CASE
|
|
1865
|
+
WHEN conversations.usage_json->'cost'->>'total' IS NOT NULL
|
|
1866
|
+
THEN (conversations.usage_json->'cost'->>'total')::double precision
|
|
1867
|
+
WHEN coalesce(
|
|
1868
|
+
conversations.usage_json->'cost'->>'input',
|
|
1869
|
+
conversations.usage_json->'cost'->>'output',
|
|
1870
|
+
conversations.usage_json->'cost'->>'cacheRead',
|
|
1871
|
+
conversations.usage_json->'cost'->>'cacheWrite'
|
|
1872
|
+
) IS NOT NULL
|
|
1873
|
+
THEN coalesce((conversations.usage_json->'cost'->>'input')::double precision, 0)
|
|
1874
|
+
+ coalesce((conversations.usage_json->'cost'->>'output')::double precision, 0)
|
|
1875
|
+
+ coalesce((conversations.usage_json->'cost'->>'cacheRead')::double precision, 0)
|
|
1876
|
+
+ coalesce((conversations.usage_json->'cost'->>'cacheWrite')::double precision, 0)
|
|
1877
|
+
ELSE 0
|
|
1878
|
+
END
|
|
1879
|
+
)
|
|
1880
|
+
FROM junior_conversations AS conversations
|
|
1881
|
+
WHERE conversations.root_conversation_id IN (
|
|
1882
|
+
SELECT coalesce(roots.root_conversation_id, roots.conversation_id)
|
|
1883
|
+
FROM junior_conversations AS roots
|
|
1884
|
+
WHERE roots.conversation_id = ANY (conversation_ids.ids)
|
|
1885
|
+
)
|
|
1886
|
+
), 0::double precision)
|
|
1887
|
+
`;
|
|
1888
|
+
}
|
|
1889
|
+
function pullRequestConversationIdsExpr() {
|
|
1890
|
+
const pullRequests = juniorGitHubPullRequests;
|
|
1891
|
+
const issues = juniorGitHubIssues;
|
|
1892
|
+
return sql4`
|
|
1893
|
+
ARRAY(
|
|
1894
|
+
SELECT DISTINCT unnest(
|
|
1895
|
+
${pullRequests.conversationIds}
|
|
1896
|
+
|| coalesce((
|
|
1897
|
+
SELECT array_agg(DISTINCT issue_conversation_id)
|
|
1898
|
+
FROM ${juniorGitHubPullRequestIssues} AS issue_links
|
|
1899
|
+
INNER JOIN ${issues} AS linked_issues
|
|
1900
|
+
ON lower(linked_issues.repository_full_name) =
|
|
1901
|
+
issue_links.issue_repository_full_name
|
|
1902
|
+
AND linked_issues.number = issue_links.issue_number
|
|
1903
|
+
CROSS JOIN LATERAL unnest(linked_issues.conversation_ids)
|
|
1904
|
+
AS issue_conversation_id
|
|
1905
|
+
WHERE issue_links.pull_request_id = ${pullRequests.pullRequestId}
|
|
1906
|
+
), ARRAY[]::text[])
|
|
1907
|
+
)
|
|
1908
|
+
)
|
|
1909
|
+
`;
|
|
1910
|
+
}
|
|
1911
|
+
function issueConversationIdsExpr() {
|
|
1912
|
+
const pullRequests = juniorGitHubPullRequests;
|
|
1913
|
+
const issues = juniorGitHubIssues;
|
|
1914
|
+
return sql4`
|
|
1915
|
+
ARRAY(
|
|
1916
|
+
SELECT DISTINCT unnest(
|
|
1917
|
+
${issues.conversationIds}
|
|
1918
|
+
|| coalesce((
|
|
1919
|
+
SELECT array_agg(DISTINCT pr_conversation_id)
|
|
1920
|
+
FROM ${juniorGitHubPullRequestIssues} AS issue_links
|
|
1921
|
+
INNER JOIN ${pullRequests} AS linked_prs
|
|
1922
|
+
ON linked_prs.pull_request_id = issue_links.pull_request_id
|
|
1923
|
+
CROSS JOIN LATERAL unnest(linked_prs.conversation_ids)
|
|
1924
|
+
AS pr_conversation_id
|
|
1925
|
+
WHERE issue_links.issue_repository_full_name =
|
|
1926
|
+
lower(${issues.repositoryFullName})
|
|
1927
|
+
AND issue_links.issue_number = ${issues.number}
|
|
1928
|
+
), ARRAY[]::text[])
|
|
1929
|
+
)
|
|
1930
|
+
)
|
|
1931
|
+
`;
|
|
1932
|
+
}
|
|
1933
|
+
async function aggregateGitHubCostWindows(args) {
|
|
1934
|
+
if (!await hasConversationUsageTable(args.db)) {
|
|
1935
|
+
return emptyCostWindows(args.windows);
|
|
1936
|
+
}
|
|
1937
|
+
const starts = args.windows.map(
|
|
1938
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
1939
|
+
);
|
|
1940
|
+
const oldestStart = starts.at(-1)[1];
|
|
1941
|
+
const pullRequests = juniorGitHubPullRequests;
|
|
1942
|
+
const issues = juniorGitHubIssues;
|
|
1943
|
+
const windowValues = sql4.join(
|
|
1944
|
+
starts.map(
|
|
1945
|
+
([days, start]) => sql4`(${days}::integer, ${start}::timestamptz)`
|
|
1946
|
+
),
|
|
1947
|
+
sql4`, `
|
|
1948
|
+
);
|
|
1949
|
+
const conversationTreeCost = conversationTreeCostExpr();
|
|
1950
|
+
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
1951
|
+
const issueConversationIds = issueConversationIdsExpr();
|
|
1952
|
+
const result = await args.db.execute(sql4`
|
|
1953
|
+
WITH windows(days, start_at) AS (
|
|
1954
|
+
VALUES ${windowValues}
|
|
1955
|
+
), pull_request_entities AS (
|
|
1956
|
+
SELECT
|
|
1957
|
+
${pullRequests.openedAt} AS opened_at,
|
|
1958
|
+
conversation_ids.ids AS ids,
|
|
1959
|
+
${conversationTreeCost} AS cost_usd
|
|
1960
|
+
FROM ${pullRequests}
|
|
1961
|
+
CROSS JOIN LATERAL (
|
|
1962
|
+
SELECT ${pullRequestConversationIds} AS ids
|
|
1963
|
+
) AS conversation_ids
|
|
1964
|
+
WHERE ${pullRequests.openedAt} >= ${oldestStart}
|
|
1965
|
+
), issue_entities AS (
|
|
1966
|
+
SELECT
|
|
1967
|
+
${issues.openedAt} AS opened_at,
|
|
1968
|
+
conversation_ids.ids AS ids,
|
|
1969
|
+
${conversationTreeCost} AS cost_usd
|
|
1970
|
+
FROM ${issues}
|
|
1971
|
+
CROSS JOIN LATERAL (
|
|
1972
|
+
SELECT ${issueConversationIds} AS ids
|
|
1973
|
+
) AS conversation_ids
|
|
1974
|
+
WHERE ${issues.openedAt} >= ${oldestStart}
|
|
1975
|
+
), pull_request_window AS (
|
|
1976
|
+
SELECT
|
|
1977
|
+
windows.days AS days,
|
|
1978
|
+
coalesce((
|
|
1979
|
+
SELECT ${conversationTreeCost}
|
|
1980
|
+
FROM (
|
|
1981
|
+
SELECT ARRAY(
|
|
1982
|
+
SELECT DISTINCT unnest(pull_request_entities.ids)
|
|
1983
|
+
FROM pull_request_entities
|
|
1984
|
+
WHERE pull_request_entities.opened_at >= windows.start_at
|
|
1985
|
+
) AS ids
|
|
1986
|
+
) AS conversation_ids
|
|
1987
|
+
), 0)::double precision AS pull_request_cost_usd,
|
|
1988
|
+
(
|
|
1989
|
+
percentile_cont(0.5) WITHIN GROUP (
|
|
1990
|
+
ORDER BY pull_request_entities.cost_usd
|
|
1991
|
+
) FILTER (
|
|
1992
|
+
WHERE pull_request_entities.opened_at >= windows.start_at
|
|
1993
|
+
AND pull_request_entities.cost_usd > 0
|
|
1994
|
+
)
|
|
1995
|
+
)::double precision AS median_pull_request_cost_usd
|
|
1996
|
+
FROM windows
|
|
1997
|
+
LEFT JOIN pull_request_entities ON true
|
|
1998
|
+
GROUP BY windows.days, windows.start_at
|
|
1999
|
+
), issue_window AS (
|
|
2000
|
+
SELECT
|
|
2001
|
+
windows.days AS days,
|
|
2002
|
+
coalesce((
|
|
2003
|
+
SELECT ${conversationTreeCost}
|
|
2004
|
+
FROM (
|
|
2005
|
+
SELECT ARRAY(
|
|
2006
|
+
SELECT DISTINCT unnest(issue_entities.ids)
|
|
2007
|
+
FROM issue_entities
|
|
2008
|
+
WHERE issue_entities.opened_at >= windows.start_at
|
|
2009
|
+
) AS ids
|
|
2010
|
+
) AS conversation_ids
|
|
2011
|
+
), 0)::double precision AS issue_cost_usd,
|
|
2012
|
+
(
|
|
2013
|
+
percentile_cont(0.5) WITHIN GROUP (
|
|
2014
|
+
ORDER BY issue_entities.cost_usd
|
|
2015
|
+
) FILTER (
|
|
2016
|
+
WHERE issue_entities.opened_at >= windows.start_at
|
|
2017
|
+
AND issue_entities.cost_usd > 0
|
|
2018
|
+
)
|
|
2019
|
+
)::double precision AS median_issue_cost_usd
|
|
2020
|
+
FROM windows
|
|
2021
|
+
LEFT JOIN issue_entities ON true
|
|
2022
|
+
GROUP BY windows.days, windows.start_at
|
|
2023
|
+
)
|
|
2024
|
+
SELECT
|
|
2025
|
+
pull_request_window.days AS "days",
|
|
2026
|
+
pull_request_window.pull_request_cost_usd AS "pullRequestCostUsd",
|
|
2027
|
+
pull_request_window.median_pull_request_cost_usd
|
|
2028
|
+
AS "medianPullRequestCostUsd",
|
|
2029
|
+
issue_window.issue_cost_usd AS "issueCostUsd",
|
|
2030
|
+
issue_window.median_issue_cost_usd AS "medianIssueCostUsd"
|
|
2031
|
+
FROM pull_request_window
|
|
2032
|
+
INNER JOIN issue_window ON issue_window.days = pull_request_window.days
|
|
2033
|
+
ORDER BY pull_request_window.days
|
|
2034
|
+
`);
|
|
2035
|
+
return z10.array(costWindowSchema).parse(queryRows(result));
|
|
2036
|
+
}
|
|
2037
|
+
async function aggregateGitHubRepositoryCosts(args) {
|
|
2038
|
+
if (!await hasConversationUsageTable(args.db)) {
|
|
2039
|
+
return [];
|
|
2040
|
+
}
|
|
2041
|
+
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
2042
|
+
const pullRequests = juniorGitHubPullRequests;
|
|
2043
|
+
const issues = juniorGitHubIssues;
|
|
2044
|
+
const conversationTreeCost = conversationTreeCostExpr();
|
|
2045
|
+
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
2046
|
+
const issueConversationIds = issueConversationIdsExpr();
|
|
2047
|
+
const result = await args.db.execute(sql4`
|
|
2048
|
+
WITH pull_request_entities AS (
|
|
2049
|
+
SELECT
|
|
2050
|
+
${pullRequests.repositoryFullName} AS repository,
|
|
2051
|
+
conversation_ids.ids AS ids
|
|
2052
|
+
FROM ${pullRequests}
|
|
2053
|
+
CROSS JOIN LATERAL (
|
|
2054
|
+
SELECT ${pullRequestConversationIds} AS ids
|
|
2055
|
+
) AS conversation_ids
|
|
2056
|
+
WHERE ${pullRequests.openedAt} >= ${start}
|
|
2057
|
+
), issue_entities AS (
|
|
2058
|
+
SELECT
|
|
2059
|
+
${issues.repositoryFullName} AS repository,
|
|
2060
|
+
conversation_ids.ids AS ids
|
|
2061
|
+
FROM ${issues}
|
|
2062
|
+
CROSS JOIN LATERAL (
|
|
2063
|
+
SELECT ${issueConversationIds} AS ids
|
|
2064
|
+
) AS conversation_ids
|
|
2065
|
+
WHERE ${issues.openedAt} >= ${start}
|
|
2066
|
+
), repositories AS (
|
|
2067
|
+
SELECT repository FROM pull_request_entities
|
|
2068
|
+
UNION
|
|
2069
|
+
SELECT repository FROM issue_entities
|
|
2070
|
+
), pull_request_totals AS (
|
|
2071
|
+
SELECT
|
|
2072
|
+
repositories.repository AS repository,
|
|
2073
|
+
coalesce((
|
|
2074
|
+
SELECT ${conversationTreeCost}
|
|
2075
|
+
FROM (
|
|
2076
|
+
SELECT ARRAY(
|
|
2077
|
+
SELECT DISTINCT unnest(pull_request_entities.ids)
|
|
2078
|
+
FROM pull_request_entities
|
|
2079
|
+
WHERE pull_request_entities.repository = repositories.repository
|
|
2080
|
+
) AS ids
|
|
2081
|
+
) AS conversation_ids
|
|
2082
|
+
), 0)::double precision AS pull_request_cost_usd
|
|
2083
|
+
FROM repositories
|
|
2084
|
+
), issue_totals AS (
|
|
2085
|
+
SELECT
|
|
2086
|
+
repositories.repository AS repository,
|
|
2087
|
+
coalesce((
|
|
2088
|
+
SELECT ${conversationTreeCost}
|
|
2089
|
+
FROM (
|
|
2090
|
+
SELECT ARRAY(
|
|
2091
|
+
SELECT DISTINCT unnest(issue_entities.ids)
|
|
2092
|
+
FROM issue_entities
|
|
2093
|
+
WHERE issue_entities.repository = repositories.repository
|
|
2094
|
+
) AS ids
|
|
2095
|
+
) AS conversation_ids
|
|
2096
|
+
), 0)::double precision AS issue_cost_usd
|
|
2097
|
+
FROM repositories
|
|
2098
|
+
)
|
|
2099
|
+
SELECT
|
|
2100
|
+
repositories.repository AS "repository",
|
|
2101
|
+
coalesce(pull_request_totals.pull_request_cost_usd, 0)::double precision
|
|
2102
|
+
AS "pullRequestCostUsd",
|
|
2103
|
+
coalesce(issue_totals.issue_cost_usd, 0)::double precision
|
|
2104
|
+
AS "issueCostUsd"
|
|
2105
|
+
FROM repositories
|
|
2106
|
+
LEFT JOIN pull_request_totals
|
|
2107
|
+
ON pull_request_totals.repository = repositories.repository
|
|
2108
|
+
LEFT JOIN issue_totals
|
|
2109
|
+
ON issue_totals.repository = repositories.repository
|
|
2110
|
+
ORDER BY "pullRequestCostUsd" DESC, "issueCostUsd" DESC, "repository" ASC
|
|
2111
|
+
`);
|
|
2112
|
+
return z10.array(repositoryCostSchema).parse(queryRows(result));
|
|
2113
|
+
}
|
|
2114
|
+
function formatCostUsd(value) {
|
|
2115
|
+
if (value === void 0) return "\u2014";
|
|
2116
|
+
return new Intl.NumberFormat("en-US", {
|
|
2117
|
+
style: "currency",
|
|
2118
|
+
currency: "USD",
|
|
2119
|
+
minimumFractionDigits: 2,
|
|
2120
|
+
maximumFractionDigits: 2
|
|
2121
|
+
}).format(value);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/outcomes/report.ts
|
|
2125
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
2126
|
+
var WINDOWS = [7, 30, 90];
|
|
2127
|
+
var pullRequestStatsSchema = z11.object({
|
|
2128
|
+
closed: z11.number().int().nonnegative(),
|
|
2129
|
+
created: z11.number().int().nonnegative(),
|
|
2130
|
+
days: z11.number().int().positive(),
|
|
2131
|
+
medianMergeTimeMs: z11.number().nonnegative().nullable(),
|
|
2132
|
+
merged: z11.number().int().nonnegative()
|
|
1650
2133
|
}).strict().transform((row) => {
|
|
1651
2134
|
const terminal = row.merged + row.closed;
|
|
1652
2135
|
return {
|
|
@@ -1655,12 +2138,12 @@ var pullRequestStatsSchema = z10.object({
|
|
|
1655
2138
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1656
2139
|
};
|
|
1657
2140
|
});
|
|
1658
|
-
var pullRequestRepositoryStatsSchema =
|
|
1659
|
-
closed:
|
|
1660
|
-
created:
|
|
1661
|
-
juniorOnly:
|
|
1662
|
-
merged:
|
|
1663
|
-
repository:
|
|
2141
|
+
var pullRequestRepositoryStatsSchema = z11.object({
|
|
2142
|
+
closed: z11.number().int().nonnegative(),
|
|
2143
|
+
created: z11.number().int().nonnegative(),
|
|
2144
|
+
juniorOnly: z11.number().int().nonnegative(),
|
|
2145
|
+
merged: z11.number().int().nonnegative(),
|
|
2146
|
+
repository: z11.string().min(1)
|
|
1664
2147
|
}).strict().transform((row) => {
|
|
1665
2148
|
const terminal = row.merged + row.closed;
|
|
1666
2149
|
return {
|
|
@@ -1668,35 +2151,35 @@ var pullRequestRepositoryStatsSchema = z10.object({
|
|
|
1668
2151
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1669
2152
|
};
|
|
1670
2153
|
});
|
|
1671
|
-
var issueStatsSchema =
|
|
1672
|
-
closedCompleted:
|
|
1673
|
-
closedDuplicate:
|
|
1674
|
-
closedNotPlanned:
|
|
1675
|
-
closedUnknown:
|
|
1676
|
-
created:
|
|
1677
|
-
days:
|
|
1678
|
-
medianCloseTimeMs:
|
|
2154
|
+
var issueStatsSchema = z11.object({
|
|
2155
|
+
closedCompleted: z11.number().int().nonnegative(),
|
|
2156
|
+
closedDuplicate: z11.number().int().nonnegative(),
|
|
2157
|
+
closedNotPlanned: z11.number().int().nonnegative(),
|
|
2158
|
+
closedUnknown: z11.number().int().nonnegative(),
|
|
2159
|
+
created: z11.number().int().nonnegative(),
|
|
2160
|
+
days: z11.number().int().positive(),
|
|
2161
|
+
medianCloseTimeMs: z11.number().nonnegative().nullable()
|
|
1679
2162
|
}).strict().transform((row) => ({
|
|
1680
2163
|
...row,
|
|
1681
2164
|
medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
|
|
1682
2165
|
}));
|
|
1683
|
-
var pullRequestDaySchema =
|
|
1684
|
-
created:
|
|
1685
|
-
date:
|
|
2166
|
+
var pullRequestDaySchema = z11.object({
|
|
2167
|
+
created: z11.number().int().nonnegative(),
|
|
2168
|
+
date: z11.string().date()
|
|
1686
2169
|
}).strict();
|
|
1687
|
-
var issueDaySchema =
|
|
1688
|
-
created:
|
|
1689
|
-
date:
|
|
2170
|
+
var issueDaySchema = z11.object({
|
|
2171
|
+
created: z11.number().int().nonnegative(),
|
|
2172
|
+
date: z11.string().date()
|
|
1690
2173
|
}).strict();
|
|
1691
|
-
var issueRepositoryStatsSchema =
|
|
1692
|
-
closedCompleted:
|
|
1693
|
-
closedDuplicate:
|
|
1694
|
-
closedNotPlanned:
|
|
1695
|
-
closedUnknown:
|
|
1696
|
-
created:
|
|
1697
|
-
repository:
|
|
2174
|
+
var issueRepositoryStatsSchema = z11.object({
|
|
2175
|
+
closedCompleted: z11.number().int().nonnegative(),
|
|
2176
|
+
closedDuplicate: z11.number().int().nonnegative(),
|
|
2177
|
+
closedNotPlanned: z11.number().int().nonnegative(),
|
|
2178
|
+
closedUnknown: z11.number().int().nonnegative(),
|
|
2179
|
+
created: z11.number().int().nonnegative(),
|
|
2180
|
+
repository: z11.string().min(1)
|
|
1698
2181
|
}).strict();
|
|
1699
|
-
function
|
|
2182
|
+
function queryRows2(result) {
|
|
1700
2183
|
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
1701
2184
|
throw new TypeError("GitHub outcome query did not return rows");
|
|
1702
2185
|
}
|
|
@@ -1704,11 +2187,11 @@ function queryRows(result) {
|
|
|
1704
2187
|
}
|
|
1705
2188
|
async function aggregatePullRequestWindows(args) {
|
|
1706
2189
|
const starts = WINDOWS.map(
|
|
1707
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
2190
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
1708
2191
|
);
|
|
1709
2192
|
const oldestStart = starts.at(-1)[1];
|
|
1710
2193
|
const table = juniorGitHubPullRequests;
|
|
1711
|
-
const result = await args.db.execute(
|
|
2194
|
+
const result = await args.db.execute(sql5`
|
|
1712
2195
|
WITH windows(days, start_at) AS (
|
|
1713
2196
|
VALUES
|
|
1714
2197
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -1758,13 +2241,13 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1758
2241
|
GROUP BY windows.days
|
|
1759
2242
|
ORDER BY windows.days
|
|
1760
2243
|
`);
|
|
1761
|
-
return
|
|
2244
|
+
return z11.array(pullRequestStatsSchema).parse(queryRows2(result));
|
|
1762
2245
|
}
|
|
1763
2246
|
async function aggregatePullRequestDays(args) {
|
|
1764
2247
|
const end = new Date(args.nowMs);
|
|
1765
|
-
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) *
|
|
2248
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
|
|
1766
2249
|
const table = juniorGitHubPullRequests;
|
|
1767
|
-
const result = await args.db.execute(
|
|
2250
|
+
const result = await args.db.execute(sql5`
|
|
1768
2251
|
WITH days AS (
|
|
1769
2252
|
SELECT generate_series(
|
|
1770
2253
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
@@ -1786,12 +2269,12 @@ async function aggregatePullRequestDays(args) {
|
|
|
1786
2269
|
LEFT JOIN daily ON daily.day = days.day
|
|
1787
2270
|
ORDER BY days.day
|
|
1788
2271
|
`);
|
|
1789
|
-
return
|
|
2272
|
+
return z11.array(pullRequestDaySchema).parse(queryRows2(result));
|
|
1790
2273
|
}
|
|
1791
2274
|
async function aggregatePullRequestRepositories(args) {
|
|
1792
|
-
const start = new Date(args.nowMs - 30 *
|
|
2275
|
+
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
1793
2276
|
const table = juniorGitHubPullRequests;
|
|
1794
|
-
const result = await args.db.execute(
|
|
2277
|
+
const result = await args.db.execute(sql5`
|
|
1795
2278
|
SELECT
|
|
1796
2279
|
${table.repositoryFullName} AS "repository",
|
|
1797
2280
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -1816,15 +2299,15 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
1816
2299
|
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
1817
2300
|
LIMIT 25
|
|
1818
2301
|
`);
|
|
1819
|
-
return
|
|
2302
|
+
return z11.array(pullRequestRepositoryStatsSchema).parse(queryRows2(result));
|
|
1820
2303
|
}
|
|
1821
2304
|
async function aggregateIssueWindows(args) {
|
|
1822
2305
|
const starts = WINDOWS.map(
|
|
1823
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
2306
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
1824
2307
|
);
|
|
1825
2308
|
const oldestStart = starts.at(-1)[1];
|
|
1826
2309
|
const table = juniorGitHubIssues;
|
|
1827
|
-
const result = await args.db.execute(
|
|
2310
|
+
const result = await args.db.execute(sql5`
|
|
1828
2311
|
WITH windows(days, start_at) AS (
|
|
1829
2312
|
VALUES
|
|
1830
2313
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -1885,13 +2368,13 @@ async function aggregateIssueWindows(args) {
|
|
|
1885
2368
|
GROUP BY windows.days
|
|
1886
2369
|
ORDER BY windows.days
|
|
1887
2370
|
`);
|
|
1888
|
-
return
|
|
2371
|
+
return z11.array(issueStatsSchema).parse(queryRows2(result));
|
|
1889
2372
|
}
|
|
1890
2373
|
async function aggregateIssueDays(args) {
|
|
1891
2374
|
const end = new Date(args.nowMs);
|
|
1892
|
-
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) *
|
|
2375
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
|
|
1893
2376
|
const table = juniorGitHubIssues;
|
|
1894
|
-
const result = await args.db.execute(
|
|
2377
|
+
const result = await args.db.execute(sql5`
|
|
1895
2378
|
WITH days AS (
|
|
1896
2379
|
SELECT generate_series(
|
|
1897
2380
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
@@ -1913,12 +2396,12 @@ async function aggregateIssueDays(args) {
|
|
|
1913
2396
|
LEFT JOIN daily ON daily.day = days.day
|
|
1914
2397
|
ORDER BY days.day
|
|
1915
2398
|
`);
|
|
1916
|
-
return
|
|
2399
|
+
return z11.array(issueDaySchema).parse(queryRows2(result));
|
|
1917
2400
|
}
|
|
1918
2401
|
async function aggregateIssueRepositories(args) {
|
|
1919
|
-
const start = new Date(args.nowMs - 30 *
|
|
2402
|
+
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
1920
2403
|
const table = juniorGitHubIssues;
|
|
1921
|
-
const result = await args.db.execute(
|
|
2404
|
+
const result = await args.db.execute(sql5`
|
|
1922
2405
|
SELECT
|
|
1923
2406
|
${table.repositoryFullName} AS "repository",
|
|
1924
2407
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -1950,7 +2433,7 @@ async function aggregateIssueRepositories(args) {
|
|
|
1950
2433
|
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
1951
2434
|
LIMIT 25
|
|
1952
2435
|
`);
|
|
1953
|
-
return
|
|
2436
|
+
return z11.array(issueRepositoryStatsSchema).parse(queryRows2(result));
|
|
1954
2437
|
}
|
|
1955
2438
|
function formatPercent(value) {
|
|
1956
2439
|
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
@@ -1974,17 +2457,25 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1974
2457
|
repositories,
|
|
1975
2458
|
issueWindows,
|
|
1976
2459
|
issueDays,
|
|
1977
|
-
issueRepositories
|
|
2460
|
+
issueRepositories,
|
|
2461
|
+
costWindows,
|
|
2462
|
+
repositoryCosts
|
|
1978
2463
|
] = await Promise.all([
|
|
1979
2464
|
aggregatePullRequestWindows(args),
|
|
1980
2465
|
aggregatePullRequestDays(args),
|
|
1981
2466
|
aggregatePullRequestRepositories(args),
|
|
1982
2467
|
aggregateIssueWindows(args),
|
|
1983
2468
|
aggregateIssueDays(args),
|
|
1984
|
-
aggregateIssueRepositories(args)
|
|
2469
|
+
aggregateIssueRepositories(args),
|
|
2470
|
+
aggregateGitHubCostWindows({ ...args, windows: WINDOWS }),
|
|
2471
|
+
aggregateGitHubRepositoryCosts(args)
|
|
1985
2472
|
]);
|
|
1986
2473
|
const thirtyDays = windows.find((window) => window.days === 30);
|
|
1987
2474
|
const issueThirtyDays = issueWindows.find((window) => window.days === 30);
|
|
2475
|
+
const costThirtyDays = costWindows.find((window) => window.days === 30);
|
|
2476
|
+
const repositoryCostByName = new Map(
|
|
2477
|
+
repositoryCosts.map((row) => [row.repository, row])
|
|
2478
|
+
);
|
|
1988
2479
|
return {
|
|
1989
2480
|
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1990
2481
|
title: "GitHub activity",
|
|
@@ -2000,6 +2491,22 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2000
2491
|
{
|
|
2001
2492
|
label: "Median issue close time \xB7 closed in 30d",
|
|
2002
2493
|
value: formatDuration(issueThirtyDays.medianCloseTimeMs)
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
label: "PR cost \xB7 opened in 30d",
|
|
2497
|
+
value: formatCostUsd(costThirtyDays.pullRequestCostUsd)
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
label: "Median PR cost \xB7 opened in 30d",
|
|
2501
|
+
value: formatCostUsd(costThirtyDays.medianPullRequestCostUsd)
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
label: "Issue cost \xB7 opened in 30d",
|
|
2505
|
+
value: formatCostUsd(costThirtyDays.issueCostUsd)
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
label: "Median issue cost \xB7 opened in 30d",
|
|
2509
|
+
value: formatCostUsd(costThirtyDays.medianIssueCostUsd)
|
|
2003
2510
|
}
|
|
2004
2511
|
],
|
|
2005
2512
|
widgets: [
|
|
@@ -2040,7 +2547,8 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2040
2547
|
{ key: "juniorOnly", label: "Junior-only merges" },
|
|
2041
2548
|
{ key: "merged", label: "Merged" },
|
|
2042
2549
|
{ key: "closed", label: "Closed unmerged" },
|
|
2043
|
-
{ key: "mergeRate", label: "Closure merge rate" }
|
|
2550
|
+
{ key: "mergeRate", label: "Closure merge rate" },
|
|
2551
|
+
{ key: "cost", label: "Cost" }
|
|
2044
2552
|
],
|
|
2045
2553
|
records: repositories.map(({ repository, ...stats }) => ({
|
|
2046
2554
|
id: repository,
|
|
@@ -2050,7 +2558,10 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2050
2558
|
merged: String(stats.merged),
|
|
2051
2559
|
closed: String(stats.closed),
|
|
2052
2560
|
juniorOnly: String(stats.juniorOnly),
|
|
2053
|
-
mergeRate: formatPercent(stats.mergeRate)
|
|
2561
|
+
mergeRate: formatPercent(stats.mergeRate),
|
|
2562
|
+
cost: formatCostUsd(
|
|
2563
|
+
repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
|
|
2564
|
+
)
|
|
2054
2565
|
}
|
|
2055
2566
|
}))
|
|
2056
2567
|
},
|
|
@@ -2063,7 +2574,8 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2063
2574
|
{ key: "completed", label: "Completed" },
|
|
2064
2575
|
{ key: "duplicate", label: "Duplicate" },
|
|
2065
2576
|
{ key: "notPlanned", label: "Not planned" },
|
|
2066
|
-
{ key: "unknown", label: "Unknown reason" }
|
|
2577
|
+
{ key: "unknown", label: "Unknown reason" },
|
|
2578
|
+
{ key: "cost", label: "Cost" }
|
|
2067
2579
|
],
|
|
2068
2580
|
records: issueRepositories.map(({ repository, ...stats }) => ({
|
|
2069
2581
|
id: repository,
|
|
@@ -2073,7 +2585,10 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2073
2585
|
completed: String(stats.closedCompleted),
|
|
2074
2586
|
duplicate: String(stats.closedDuplicate),
|
|
2075
2587
|
notPlanned: String(stats.closedNotPlanned),
|
|
2076
|
-
unknown: String(stats.closedUnknown)
|
|
2588
|
+
unknown: String(stats.closedUnknown),
|
|
2589
|
+
cost: formatCostUsd(
|
|
2590
|
+
repositoryCostByName.get(repository)?.issueCostUsd ?? 0
|
|
2591
|
+
)
|
|
2077
2592
|
}
|
|
2078
2593
|
}))
|
|
2079
2594
|
}
|
|
@@ -2082,18 +2597,18 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2082
2597
|
}
|
|
2083
2598
|
|
|
2084
2599
|
// src/pull-request-outcomes/commit-composition.ts
|
|
2085
|
-
import { z as
|
|
2086
|
-
var canonicalCommitSchema =
|
|
2087
|
-
authorEmail:
|
|
2088
|
-
authorLogin:
|
|
2600
|
+
import { z as z12 } from "zod";
|
|
2601
|
+
var canonicalCommitSchema = z12.object({
|
|
2602
|
+
authorEmail: z12.string().nullable(),
|
|
2603
|
+
authorLogin: z12.string().nullable()
|
|
2089
2604
|
}).strict();
|
|
2090
|
-
var providerCommitSchema =
|
|
2091
|
-
author:
|
|
2092
|
-
commit:
|
|
2093
|
-
author:
|
|
2605
|
+
var providerCommitSchema = z12.object({
|
|
2606
|
+
author: z12.object({ login: z12.string() }).passthrough().nullable(),
|
|
2607
|
+
commit: z12.object({
|
|
2608
|
+
author: z12.object({ email: z12.string() }).passthrough().nullable()
|
|
2094
2609
|
}).passthrough()
|
|
2095
2610
|
}).passthrough();
|
|
2096
|
-
var commitPageSchema =
|
|
2611
|
+
var commitPageSchema = z12.array(providerCommitSchema).transform(
|
|
2097
2612
|
(commits) => commits.map(
|
|
2098
2613
|
(commit) => canonicalCommitSchema.parse({
|
|
2099
2614
|
authorEmail: commit.commit.author?.email ?? null,
|