@sentry/junior-github 0.111.0 → 0.113.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 +617 -215
- 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,37 +1806,344 @@ 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
|
-
compositionUnknown: z10.number().int().nonnegative(),
|
|
1647
|
-
created: z10.number().int().nonnegative(),
|
|
1816
|
+
var costWindowSchema = z10.object({
|
|
1648
1817
|
days: z10.number().int().positive(),
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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()
|
|
1653
2133
|
}).strict().transform((row) => {
|
|
1654
2134
|
const terminal = row.merged + row.closed;
|
|
1655
|
-
const classified = row.juniorOnly + row.mixed;
|
|
1656
2135
|
return {
|
|
1657
2136
|
...row,
|
|
1658
2137
|
medianMergeTimeMs: row.medianMergeTimeMs ?? void 0,
|
|
1659
|
-
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1660
|
-
juniorOnlyRate: classified > 0 ? row.juniorOnly / classified : void 0
|
|
2138
|
+
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1661
2139
|
};
|
|
1662
2140
|
});
|
|
1663
|
-
var pullRequestRepositoryStatsSchema =
|
|
1664
|
-
closed:
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
mixed: z10.number().int().nonnegative(),
|
|
1670
|
-
repository: z10.string().min(1)
|
|
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)
|
|
1671
2147
|
}).strict().transform((row) => {
|
|
1672
2148
|
const terminal = row.merged + row.closed;
|
|
1673
2149
|
return {
|
|
@@ -1675,41 +2151,35 @@ var pullRequestRepositoryStatsSchema = z10.object({
|
|
|
1675
2151
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1676
2152
|
};
|
|
1677
2153
|
});
|
|
1678
|
-
var issueStatsSchema =
|
|
1679
|
-
closedCompleted:
|
|
1680
|
-
closedDuplicate:
|
|
1681
|
-
closedNotPlanned:
|
|
1682
|
-
closedUnknown:
|
|
1683
|
-
created:
|
|
1684
|
-
days:
|
|
1685
|
-
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()
|
|
1686
2162
|
}).strict().transform((row) => ({
|
|
1687
2163
|
...row,
|
|
1688
2164
|
medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
|
|
1689
2165
|
}));
|
|
1690
|
-
var pullRequestDaySchema =
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
date: z10.string().date(),
|
|
1694
|
-
merged: z10.number().int().nonnegative()
|
|
2166
|
+
var pullRequestDaySchema = z11.object({
|
|
2167
|
+
created: z11.number().int().nonnegative(),
|
|
2168
|
+
date: z11.string().date()
|
|
1695
2169
|
}).strict();
|
|
1696
|
-
var issueDaySchema =
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
closedNotPlanned: z10.number().int().nonnegative(),
|
|
1700
|
-
closedUnknown: z10.number().int().nonnegative(),
|
|
1701
|
-
created: z10.number().int().nonnegative(),
|
|
1702
|
-
date: z10.string().date()
|
|
2170
|
+
var issueDaySchema = z11.object({
|
|
2171
|
+
created: z11.number().int().nonnegative(),
|
|
2172
|
+
date: z11.string().date()
|
|
1703
2173
|
}).strict();
|
|
1704
|
-
var issueRepositoryStatsSchema =
|
|
1705
|
-
closedCompleted:
|
|
1706
|
-
closedDuplicate:
|
|
1707
|
-
closedNotPlanned:
|
|
1708
|
-
closedUnknown:
|
|
1709
|
-
created:
|
|
1710
|
-
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)
|
|
1711
2181
|
}).strict();
|
|
1712
|
-
function
|
|
2182
|
+
function queryRows2(result) {
|
|
1713
2183
|
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
1714
2184
|
throw new TypeError("GitHub outcome query did not return rows");
|
|
1715
2185
|
}
|
|
@@ -1717,11 +2187,11 @@ function queryRows(result) {
|
|
|
1717
2187
|
}
|
|
1718
2188
|
async function aggregatePullRequestWindows(args) {
|
|
1719
2189
|
const starts = WINDOWS.map(
|
|
1720
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
2190
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
1721
2191
|
);
|
|
1722
2192
|
const oldestStart = starts.at(-1)[1];
|
|
1723
2193
|
const table = juniorGitHubPullRequests;
|
|
1724
|
-
const result = await args.db.execute(
|
|
2194
|
+
const result = await args.db.execute(sql5`
|
|
1725
2195
|
WITH windows(days, start_at) AS (
|
|
1726
2196
|
VALUES
|
|
1727
2197
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -1731,7 +2201,6 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1731
2201
|
SELECT
|
|
1732
2202
|
${table.pullRequestId},
|
|
1733
2203
|
${table.state},
|
|
1734
|
-
${table.commitComposition},
|
|
1735
2204
|
${table.openedAt},
|
|
1736
2205
|
${table.mergedAt},
|
|
1737
2206
|
${table.closedAt}
|
|
@@ -1755,24 +2224,6 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1755
2224
|
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
1756
2225
|
AND recent_pull_requests.closed_at >= windows.start_at
|
|
1757
2226
|
)::integer AS "closed",
|
|
1758
|
-
count(recent_pull_requests.pull_request_id)
|
|
1759
|
-
FILTER (
|
|
1760
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
1761
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1762
|
-
AND recent_pull_requests.commit_composition = 'junior_only'
|
|
1763
|
-
)::integer AS "juniorOnly",
|
|
1764
|
-
count(recent_pull_requests.pull_request_id)
|
|
1765
|
-
FILTER (
|
|
1766
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
1767
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1768
|
-
AND recent_pull_requests.commit_composition = 'mixed'
|
|
1769
|
-
)::integer AS "mixed",
|
|
1770
|
-
count(recent_pull_requests.pull_request_id)
|
|
1771
|
-
FILTER (
|
|
1772
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
1773
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1774
|
-
AND recent_pull_requests.commit_composition IS NULL
|
|
1775
|
-
)::integer AS "compositionUnknown",
|
|
1776
2227
|
(
|
|
1777
2228
|
percentile_cont(0.5) WITHIN GROUP (
|
|
1778
2229
|
ORDER BY extract(
|
|
@@ -1790,61 +2241,40 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1790
2241
|
GROUP BY windows.days
|
|
1791
2242
|
ORDER BY windows.days
|
|
1792
2243
|
`);
|
|
1793
|
-
return
|
|
2244
|
+
return z11.array(pullRequestStatsSchema).parse(queryRows2(result));
|
|
1794
2245
|
}
|
|
1795
2246
|
async function aggregatePullRequestDays(args) {
|
|
1796
2247
|
const end = new Date(args.nowMs);
|
|
1797
|
-
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) *
|
|
2248
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
|
|
1798
2249
|
const table = juniorGitHubPullRequests;
|
|
1799
|
-
const result = await args.db.execute(
|
|
2250
|
+
const result = await args.db.execute(sql5`
|
|
1800
2251
|
WITH days AS (
|
|
1801
2252
|
SELECT generate_series(
|
|
1802
2253
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
1803
2254
|
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
1804
2255
|
interval '1 day'
|
|
1805
2256
|
) AS day
|
|
1806
|
-
),
|
|
2257
|
+
), daily AS (
|
|
1807
2258
|
SELECT
|
|
1808
2259
|
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
1809
|
-
|
|
2260
|
+
count(*)::integer AS created
|
|
1810
2261
|
FROM ${table}
|
|
1811
2262
|
WHERE ${table.openedAt} >= ${start}
|
|
1812
|
-
|
|
1813
|
-
SELECT
|
|
1814
|
-
date_trunc('day', ${table.mergedAt} AT TIME ZONE 'UTC') AS day,
|
|
1815
|
-
'merged' AS kind
|
|
1816
|
-
FROM ${table}
|
|
1817
|
-
WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
|
|
1818
|
-
UNION ALL
|
|
1819
|
-
SELECT
|
|
1820
|
-
date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
|
|
1821
|
-
'closed' AS kind
|
|
1822
|
-
FROM ${table}
|
|
1823
|
-
WHERE ${table.state} = 'closed_unmerged' AND ${table.closedAt} >= ${start}
|
|
1824
|
-
), daily AS (
|
|
1825
|
-
SELECT
|
|
1826
|
-
events.day,
|
|
1827
|
-
count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
|
|
1828
|
-
count(*) FILTER (WHERE events.kind = 'merged')::integer AS merged,
|
|
1829
|
-
count(*) FILTER (WHERE events.kind = 'closed')::integer AS closed
|
|
1830
|
-
FROM events
|
|
1831
|
-
GROUP BY events.day
|
|
2263
|
+
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
1832
2264
|
)
|
|
1833
2265
|
SELECT
|
|
1834
2266
|
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
1835
|
-
coalesce(daily.created, 0)::integer AS "created"
|
|
1836
|
-
coalesce(daily.merged, 0)::integer AS "merged",
|
|
1837
|
-
coalesce(daily.closed, 0)::integer AS "closed"
|
|
2267
|
+
coalesce(daily.created, 0)::integer AS "created"
|
|
1838
2268
|
FROM days
|
|
1839
2269
|
LEFT JOIN daily ON daily.day = days.day
|
|
1840
2270
|
ORDER BY days.day
|
|
1841
2271
|
`);
|
|
1842
|
-
return
|
|
2272
|
+
return z11.array(pullRequestDaySchema).parse(queryRows2(result));
|
|
1843
2273
|
}
|
|
1844
2274
|
async function aggregatePullRequestRepositories(args) {
|
|
1845
|
-
const start = new Date(args.nowMs - 30 *
|
|
2275
|
+
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
1846
2276
|
const table = juniorGitHubPullRequests;
|
|
1847
|
-
const result = await args.db.execute(
|
|
2277
|
+
const result = await args.db.execute(sql5`
|
|
1848
2278
|
SELECT
|
|
1849
2279
|
${table.repositoryFullName} AS "repository",
|
|
1850
2280
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -1860,17 +2290,7 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
1860
2290
|
WHERE ${table.state} = 'merged'
|
|
1861
2291
|
AND ${table.mergedAt} >= ${start}
|
|
1862
2292
|
AND ${table.commitComposition} = 'junior_only'
|
|
1863
|
-
)::integer AS "juniorOnly"
|
|
1864
|
-
count(*) FILTER (
|
|
1865
|
-
WHERE ${table.state} = 'merged'
|
|
1866
|
-
AND ${table.mergedAt} >= ${start}
|
|
1867
|
-
AND ${table.commitComposition} = 'mixed'
|
|
1868
|
-
)::integer AS "mixed",
|
|
1869
|
-
count(*) FILTER (
|
|
1870
|
-
WHERE ${table.state} = 'merged'
|
|
1871
|
-
AND ${table.mergedAt} >= ${start}
|
|
1872
|
-
AND ${table.commitComposition} IS NULL
|
|
1873
|
-
)::integer AS "compositionUnknown"
|
|
2293
|
+
)::integer AS "juniorOnly"
|
|
1874
2294
|
FROM ${table}
|
|
1875
2295
|
WHERE ${table.openedAt} >= ${start}
|
|
1876
2296
|
OR ${table.mergedAt} >= ${start}
|
|
@@ -1879,15 +2299,15 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
1879
2299
|
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
1880
2300
|
LIMIT 25
|
|
1881
2301
|
`);
|
|
1882
|
-
return
|
|
2302
|
+
return z11.array(pullRequestRepositoryStatsSchema).parse(queryRows2(result));
|
|
1883
2303
|
}
|
|
1884
2304
|
async function aggregateIssueWindows(args) {
|
|
1885
2305
|
const starts = WINDOWS.map(
|
|
1886
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
2306
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
1887
2307
|
);
|
|
1888
2308
|
const oldestStart = starts.at(-1)[1];
|
|
1889
2309
|
const table = juniorGitHubIssues;
|
|
1890
|
-
const result = await args.db.execute(
|
|
2310
|
+
const result = await args.db.execute(sql5`
|
|
1891
2311
|
WITH windows(days, start_at) AS (
|
|
1892
2312
|
VALUES
|
|
1893
2313
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -1948,63 +2368,40 @@ async function aggregateIssueWindows(args) {
|
|
|
1948
2368
|
GROUP BY windows.days
|
|
1949
2369
|
ORDER BY windows.days
|
|
1950
2370
|
`);
|
|
1951
|
-
return
|
|
2371
|
+
return z11.array(issueStatsSchema).parse(queryRows2(result));
|
|
1952
2372
|
}
|
|
1953
2373
|
async function aggregateIssueDays(args) {
|
|
1954
2374
|
const end = new Date(args.nowMs);
|
|
1955
|
-
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) *
|
|
2375
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
|
|
1956
2376
|
const table = juniorGitHubIssues;
|
|
1957
|
-
const result = await args.db.execute(
|
|
2377
|
+
const result = await args.db.execute(sql5`
|
|
1958
2378
|
WITH days AS (
|
|
1959
2379
|
SELECT generate_series(
|
|
1960
2380
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
1961
2381
|
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
1962
2382
|
interval '1 day'
|
|
1963
2383
|
) AS day
|
|
1964
|
-
),
|
|
2384
|
+
), daily AS (
|
|
1965
2385
|
SELECT
|
|
1966
2386
|
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
1967
|
-
|
|
2387
|
+
count(*)::integer AS created
|
|
1968
2388
|
FROM ${table}
|
|
1969
2389
|
WHERE ${table.openedAt} >= ${start}
|
|
1970
|
-
|
|
1971
|
-
SELECT
|
|
1972
|
-
date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
|
|
1973
|
-
coalesce(${table.stateReason}, 'unknown') AS kind
|
|
1974
|
-
FROM ${table}
|
|
1975
|
-
WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
|
|
1976
|
-
), daily AS (
|
|
1977
|
-
SELECT
|
|
1978
|
-
events.day,
|
|
1979
|
-
count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
|
|
1980
|
-
count(*) FILTER (WHERE events.kind = 'completed')::integer
|
|
1981
|
-
AS closed_completed,
|
|
1982
|
-
count(*) FILTER (WHERE events.kind = 'duplicate')::integer
|
|
1983
|
-
AS closed_duplicate,
|
|
1984
|
-
count(*) FILTER (WHERE events.kind = 'not_planned')::integer
|
|
1985
|
-
AS closed_not_planned,
|
|
1986
|
-
count(*) FILTER (WHERE events.kind = 'unknown')::integer
|
|
1987
|
-
AS closed_unknown
|
|
1988
|
-
FROM events
|
|
1989
|
-
GROUP BY events.day
|
|
2390
|
+
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
1990
2391
|
)
|
|
1991
2392
|
SELECT
|
|
1992
2393
|
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
1993
|
-
coalesce(daily.created, 0)::integer AS "created"
|
|
1994
|
-
coalesce(daily.closed_completed, 0)::integer AS "closedCompleted",
|
|
1995
|
-
coalesce(daily.closed_duplicate, 0)::integer AS "closedDuplicate",
|
|
1996
|
-
coalesce(daily.closed_not_planned, 0)::integer AS "closedNotPlanned",
|
|
1997
|
-
coalesce(daily.closed_unknown, 0)::integer AS "closedUnknown"
|
|
2394
|
+
coalesce(daily.created, 0)::integer AS "created"
|
|
1998
2395
|
FROM days
|
|
1999
2396
|
LEFT JOIN daily ON daily.day = days.day
|
|
2000
2397
|
ORDER BY days.day
|
|
2001
2398
|
`);
|
|
2002
|
-
return
|
|
2399
|
+
return z11.array(issueDaySchema).parse(queryRows2(result));
|
|
2003
2400
|
}
|
|
2004
2401
|
async function aggregateIssueRepositories(args) {
|
|
2005
|
-
const start = new Date(args.nowMs - 30 *
|
|
2402
|
+
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
2006
2403
|
const table = juniorGitHubIssues;
|
|
2007
|
-
const result = await args.db.execute(
|
|
2404
|
+
const result = await args.db.execute(sql5`
|
|
2008
2405
|
SELECT
|
|
2009
2406
|
${table.repositoryFullName} AS "repository",
|
|
2010
2407
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -2036,7 +2433,7 @@ async function aggregateIssueRepositories(args) {
|
|
|
2036
2433
|
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
2037
2434
|
LIMIT 25
|
|
2038
2435
|
`);
|
|
2039
|
-
return
|
|
2436
|
+
return z11.array(issueRepositoryStatsSchema).parse(queryRows2(result));
|
|
2040
2437
|
}
|
|
2041
2438
|
function formatPercent(value) {
|
|
2042
2439
|
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
@@ -2048,9 +2445,6 @@ function formatDuration(value) {
|
|
|
2048
2445
|
if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
|
|
2049
2446
|
return `${Math.round(hours / 24 * 10) / 10}d`;
|
|
2050
2447
|
}
|
|
2051
|
-
function formatCommitComposition(stats) {
|
|
2052
|
-
return `${stats.juniorOnly} Junior-only \xB7 ${stats.mixed} mixed \xB7 ${stats.compositionUnknown} unknown`;
|
|
2053
|
-
}
|
|
2054
2448
|
function startOfUtcDay(timestampMs) {
|
|
2055
2449
|
const date = new Date(timestampMs);
|
|
2056
2450
|
date.setUTCHours(0, 0, 0, 0);
|
|
@@ -2063,83 +2457,83 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2063
2457
|
repositories,
|
|
2064
2458
|
issueWindows,
|
|
2065
2459
|
issueDays,
|
|
2066
|
-
issueRepositories
|
|
2460
|
+
issueRepositories,
|
|
2461
|
+
costWindows,
|
|
2462
|
+
repositoryCosts
|
|
2067
2463
|
] = await Promise.all([
|
|
2068
2464
|
aggregatePullRequestWindows(args),
|
|
2069
2465
|
aggregatePullRequestDays(args),
|
|
2070
2466
|
aggregatePullRequestRepositories(args),
|
|
2071
2467
|
aggregateIssueWindows(args),
|
|
2072
2468
|
aggregateIssueDays(args),
|
|
2073
|
-
aggregateIssueRepositories(args)
|
|
2469
|
+
aggregateIssueRepositories(args),
|
|
2470
|
+
aggregateGitHubCostWindows({ ...args, windows: WINDOWS }),
|
|
2471
|
+
aggregateGitHubRepositoryCosts(args)
|
|
2074
2472
|
]);
|
|
2075
2473
|
const thirtyDays = windows.find((window) => window.days === 30);
|
|
2076
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
|
+
);
|
|
2077
2479
|
return {
|
|
2078
2480
|
generatedAt: new Date(args.nowMs).toISOString(),
|
|
2079
|
-
title: "GitHub
|
|
2481
|
+
title: "GitHub activity",
|
|
2080
2482
|
metrics: [
|
|
2081
2483
|
{
|
|
2082
|
-
label: "
|
|
2083
|
-
value: formatPercent(thirtyDays.juniorOnlyRate)
|
|
2084
|
-
},
|
|
2085
|
-
{
|
|
2086
|
-
label: "PR merge rate \xB7 30d",
|
|
2484
|
+
label: "PR closure merge rate \xB7 30d",
|
|
2087
2485
|
value: formatPercent(thirtyDays.mergeRate)
|
|
2088
2486
|
},
|
|
2089
2487
|
{
|
|
2090
|
-
label: "
|
|
2488
|
+
label: "Median PR merge time \xB7 merged in 30d",
|
|
2091
2489
|
value: formatDuration(thirtyDays.medianMergeTimeMs)
|
|
2092
2490
|
},
|
|
2093
2491
|
{
|
|
2094
|
-
label: "
|
|
2492
|
+
label: "Median issue close time \xB7 closed in 30d",
|
|
2095
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)
|
|
2096
2510
|
}
|
|
2097
2511
|
],
|
|
2098
2512
|
widgets: [
|
|
2099
2513
|
{
|
|
2100
|
-
id: "pull-
|
|
2514
|
+
id: "pull-requests-created",
|
|
2101
2515
|
type: "bar_chart",
|
|
2102
|
-
title: "Pull
|
|
2103
|
-
description: "
|
|
2516
|
+
title: "Pull requests created",
|
|
2517
|
+
description: "Junior-owned pull requests opened per day",
|
|
2104
2518
|
timeRangeDays: [...WINDOWS],
|
|
2105
|
-
series: [
|
|
2106
|
-
{ key: "created", label: "Created" },
|
|
2107
|
-
{ key: "merged", label: "Merged", tone: "good" },
|
|
2108
|
-
{ key: "closed", label: "Closed unmerged", tone: "danger" }
|
|
2109
|
-
],
|
|
2519
|
+
series: [{ key: "created", label: "Created" }],
|
|
2110
2520
|
categories: pullRequestDays.map((stats) => ({
|
|
2111
2521
|
id: stats.date,
|
|
2112
2522
|
label: stats.date,
|
|
2113
|
-
values: {
|
|
2114
|
-
created: stats.created,
|
|
2115
|
-
merged: stats.merged,
|
|
2116
|
-
closed: stats.closed
|
|
2117
|
-
}
|
|
2523
|
+
values: { created: stats.created }
|
|
2118
2524
|
}))
|
|
2119
2525
|
},
|
|
2120
2526
|
{
|
|
2121
|
-
id: "
|
|
2527
|
+
id: "issues-created",
|
|
2122
2528
|
type: "bar_chart",
|
|
2123
|
-
title: "
|
|
2124
|
-
description: "
|
|
2529
|
+
title: "Issues created",
|
|
2530
|
+
description: "Junior-owned issues opened per day",
|
|
2125
2531
|
timeRangeDays: [...WINDOWS],
|
|
2126
|
-
series: [
|
|
2127
|
-
{ key: "created", label: "Created" },
|
|
2128
|
-
{ key: "completed", label: "Completed", tone: "good" },
|
|
2129
|
-
{ key: "duplicate", label: "Duplicate", tone: "warning" },
|
|
2130
|
-
{ key: "notPlanned", label: "Not planned", tone: "danger" },
|
|
2131
|
-
{ key: "unknown", label: "Unknown" }
|
|
2132
|
-
],
|
|
2532
|
+
series: [{ key: "created", label: "Created" }],
|
|
2133
2533
|
categories: issueDays.map((stats) => ({
|
|
2134
2534
|
id: stats.date,
|
|
2135
2535
|
label: stats.date,
|
|
2136
|
-
values: {
|
|
2137
|
-
created: stats.created,
|
|
2138
|
-
completed: stats.closedCompleted,
|
|
2139
|
-
duplicate: stats.closedDuplicate,
|
|
2140
|
-
notPlanned: stats.closedNotPlanned,
|
|
2141
|
-
unknown: stats.closedUnknown
|
|
2142
|
-
}
|
|
2536
|
+
values: { created: stats.created }
|
|
2143
2537
|
}))
|
|
2144
2538
|
}
|
|
2145
2539
|
],
|
|
@@ -2150,10 +2544,11 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2150
2544
|
fields: [
|
|
2151
2545
|
{ key: "repository", label: "Repository" },
|
|
2152
2546
|
{ key: "created", label: "Created" },
|
|
2547
|
+
{ key: "juniorOnly", label: "Junior-only merges" },
|
|
2153
2548
|
{ key: "merged", label: "Merged" },
|
|
2154
2549
|
{ key: "closed", label: "Closed unmerged" },
|
|
2155
|
-
{ key: "
|
|
2156
|
-
{ key: "
|
|
2550
|
+
{ key: "mergeRate", label: "Closure merge rate" },
|
|
2551
|
+
{ key: "cost", label: "Cost" }
|
|
2157
2552
|
],
|
|
2158
2553
|
records: repositories.map(({ repository, ...stats }) => ({
|
|
2159
2554
|
id: repository,
|
|
@@ -2162,8 +2557,11 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2162
2557
|
created: String(stats.created),
|
|
2163
2558
|
merged: String(stats.merged),
|
|
2164
2559
|
closed: String(stats.closed),
|
|
2165
|
-
|
|
2166
|
-
mergeRate: formatPercent(stats.mergeRate)
|
|
2560
|
+
juniorOnly: String(stats.juniorOnly),
|
|
2561
|
+
mergeRate: formatPercent(stats.mergeRate),
|
|
2562
|
+
cost: formatCostUsd(
|
|
2563
|
+
repositoryCostByName.get(repository)?.pullRequestCostUsd ?? 0
|
|
2564
|
+
)
|
|
2167
2565
|
}
|
|
2168
2566
|
}))
|
|
2169
2567
|
},
|
|
@@ -2176,7 +2574,8 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2176
2574
|
{ key: "completed", label: "Completed" },
|
|
2177
2575
|
{ key: "duplicate", label: "Duplicate" },
|
|
2178
2576
|
{ key: "notPlanned", label: "Not planned" },
|
|
2179
|
-
{ key: "unknown", label: "Unknown reason" }
|
|
2577
|
+
{ key: "unknown", label: "Unknown reason" },
|
|
2578
|
+
{ key: "cost", label: "Cost" }
|
|
2180
2579
|
],
|
|
2181
2580
|
records: issueRepositories.map(({ repository, ...stats }) => ({
|
|
2182
2581
|
id: repository,
|
|
@@ -2186,7 +2585,10 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2186
2585
|
completed: String(stats.closedCompleted),
|
|
2187
2586
|
duplicate: String(stats.closedDuplicate),
|
|
2188
2587
|
notPlanned: String(stats.closedNotPlanned),
|
|
2189
|
-
unknown: String(stats.closedUnknown)
|
|
2588
|
+
unknown: String(stats.closedUnknown),
|
|
2589
|
+
cost: formatCostUsd(
|
|
2590
|
+
repositoryCostByName.get(repository)?.issueCostUsd ?? 0
|
|
2591
|
+
)
|
|
2190
2592
|
}
|
|
2191
2593
|
}))
|
|
2192
2594
|
}
|
|
@@ -2195,18 +2597,18 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
2195
2597
|
}
|
|
2196
2598
|
|
|
2197
2599
|
// src/pull-request-outcomes/commit-composition.ts
|
|
2198
|
-
import { z as
|
|
2199
|
-
var canonicalCommitSchema =
|
|
2200
|
-
authorEmail:
|
|
2201
|
-
authorLogin:
|
|
2600
|
+
import { z as z12 } from "zod";
|
|
2601
|
+
var canonicalCommitSchema = z12.object({
|
|
2602
|
+
authorEmail: z12.string().nullable(),
|
|
2603
|
+
authorLogin: z12.string().nullable()
|
|
2202
2604
|
}).strict();
|
|
2203
|
-
var providerCommitSchema =
|
|
2204
|
-
author:
|
|
2205
|
-
commit:
|
|
2206
|
-
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()
|
|
2207
2609
|
}).passthrough()
|
|
2208
2610
|
}).passthrough();
|
|
2209
|
-
var commitPageSchema =
|
|
2611
|
+
var commitPageSchema = z12.array(providerCommitSchema).transform(
|
|
2210
2612
|
(commits) => commits.map(
|
|
2211
2613
|
(commit) => canonicalCommitSchema.parse({
|
|
2212
2614
|
authorEmail: commit.commit.author?.email ?? null,
|