@sentry/junior-github 0.184.0 → 0.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/SETUP.md +10 -10
- package/dist/{chunk-KXAUXJ3Q.js → chunk-QWTU33O7.js} +2 -15
- package/dist/credential-support.d.ts +1 -9
- package/dist/index.js +39 -1124
- package/dist/testing.js +1 -1
- package/dist/tools/footer.d.ts +3 -3
- package/dist/tools/resolve-pull-request-review-thread.d.ts +1 -1
- package/dist/tools/update-issue.d.ts +1 -1
- package/dist/tools/update-pull-request.d.ts +1 -1
- package/package.json +2 -2
- package/skills/github-code/SKILL.md +24 -70
- package/skills/github-code/SPEC.md +15 -19
- package/skills/github-code/references/api-surface.md +56 -86
- package/skills/github-code/references/troubleshooting-workarounds.md +30 -34
- package/skills/github-code/references/workflow.md +47 -0
- package/skills/github-issues/SKILL.md +2 -2
- package/skills/github-issues/references/api-surface.md +1 -1
- package/skills/github-issues/references/troubleshooting-workarounds.md +1 -1
- package/dist/outcomes/cost.d.ts +0 -56
- package/dist/outcomes/profile-report.d.ts +0 -11
- package/dist/outcomes/report.d.ts +0 -7
package/dist/index.js
CHANGED
|
@@ -26,9 +26,7 @@ import {
|
|
|
26
26
|
gitHubPullRequestSubscribable,
|
|
27
27
|
gitHubReleaseSourceSubscribable,
|
|
28
28
|
gitHubRepositorySubscribable,
|
|
29
|
-
githubRepositoryFromLeaseScope,
|
|
30
29
|
githubRepositoryFromUrl,
|
|
31
|
-
githubRepositoryLeaseScope,
|
|
32
30
|
githubRequest,
|
|
33
31
|
isRecord,
|
|
34
32
|
issueInstallationCredential,
|
|
@@ -44,7 +42,7 @@ import {
|
|
|
44
42
|
readGrantPermissions,
|
|
45
43
|
requireEnv,
|
|
46
44
|
resolveUserAccount
|
|
47
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-QWTU33O7.js";
|
|
48
46
|
|
|
49
47
|
// src/plugin.ts
|
|
50
48
|
import {
|
|
@@ -461,7 +459,7 @@ var createIssueInputSchema = Type.Object(
|
|
|
461
459
|
}),
|
|
462
460
|
body: Type.Optional(
|
|
463
461
|
Type.String({
|
|
464
|
-
description: "Issue body.
|
|
462
|
+
description: "Issue body. The runtime appends the conversation footer."
|
|
465
463
|
})
|
|
466
464
|
),
|
|
467
465
|
labels: Type.Optional(
|
|
@@ -475,7 +473,7 @@ var createIssueInputSchema = Type.Object(
|
|
|
475
473
|
var createIssueToolInputSchema = z2.object({
|
|
476
474
|
repo: z2.string().describe('Repository in "owner/name" format.'),
|
|
477
475
|
title: z2.string().describe("Issue title."),
|
|
478
|
-
body: z2.string().describe("Issue body.
|
|
476
|
+
body: z2.string().describe("Issue body. The runtime appends the conversation footer.").optional(),
|
|
479
477
|
labels: z2.array(z2.string()).describe("Labels to apply to the issue.").optional()
|
|
480
478
|
}).strict();
|
|
481
479
|
var createIssueStateSchema = Type.Union([
|
|
@@ -664,7 +662,7 @@ function createGitHubIssueTool(ctx) {
|
|
|
664
662
|
openWorldHint: true,
|
|
665
663
|
readOnlyHint: false
|
|
666
664
|
},
|
|
667
|
-
description: "Create a GitHub issue with a runtime-owned
|
|
665
|
+
description: "Create a GitHub issue with a runtime-owned conversation footer. Use this instead of shelling out to gh issue create when creating issues.",
|
|
668
666
|
inputSchema: createIssueToolInputSchema,
|
|
669
667
|
outputSchema: gitHubIssueOutputSchema,
|
|
670
668
|
async execute(input, options) {
|
|
@@ -725,7 +723,7 @@ function createGitHubIssueTool(ctx) {
|
|
|
725
723
|
);
|
|
726
724
|
} catch (error) {
|
|
727
725
|
throw new Error(
|
|
728
|
-
"GitHub issue was created, but
|
|
726
|
+
"GitHub issue was created, but the runtime could not persist the completed issue state.",
|
|
729
727
|
{ cause: error }
|
|
730
728
|
);
|
|
731
729
|
}
|
|
@@ -987,7 +985,7 @@ var createPullRequestInputSchema = Type2.Object(
|
|
|
987
985
|
}),
|
|
988
986
|
body: Type2.Optional(
|
|
989
987
|
Type2.String({
|
|
990
|
-
description: "Pull request body.
|
|
988
|
+
description: "Pull request body. The runtime appends the conversation footer."
|
|
991
989
|
})
|
|
992
990
|
),
|
|
993
991
|
draft: Type2.Optional(
|
|
@@ -1003,7 +1001,7 @@ var createPullRequestToolInputSchema = z4.object({
|
|
|
1003
1001
|
title: z4.string().describe("Pull request title."),
|
|
1004
1002
|
head: z4.string().describe("Head branch or owner:branch ref."),
|
|
1005
1003
|
base: z4.string().describe("Base branch."),
|
|
1006
|
-
body: z4.string().describe("Pull request body.
|
|
1004
|
+
body: z4.string().describe("Pull request body. The runtime appends the conversation footer.").optional(),
|
|
1007
1005
|
draft: z4.boolean().describe("Whether to open the pull request as a draft.").optional()
|
|
1008
1006
|
}).strict();
|
|
1009
1007
|
var createPullRequestStateSchema = Type2.Union([
|
|
@@ -1253,7 +1251,7 @@ function createGitHubPullRequestTool(ctx, subscriptionConfig) {
|
|
|
1253
1251
|
openWorldHint: true,
|
|
1254
1252
|
readOnlyHint: false
|
|
1255
1253
|
},
|
|
1256
|
-
description: "Create a GitHub pull request with a runtime-owned
|
|
1254
|
+
description: "Create a GitHub pull request with a runtime-owned conversation footer. Use this instead of shelling out to gh pr create when creating pull requests.",
|
|
1257
1255
|
inputSchema: createPullRequestToolInputSchema,
|
|
1258
1256
|
outputSchema: gitHubPullRequestOutputSchema,
|
|
1259
1257
|
async execute(input, options) {
|
|
@@ -1315,7 +1313,7 @@ function createGitHubPullRequestTool(ctx, subscriptionConfig) {
|
|
|
1315
1313
|
);
|
|
1316
1314
|
} catch (error) {
|
|
1317
1315
|
throw new Error(
|
|
1318
|
-
"GitHub pull request was created, but
|
|
1316
|
+
"GitHub pull request was created, but the runtime could not persist the completed pull request state.",
|
|
1319
1317
|
{ cause: error }
|
|
1320
1318
|
);
|
|
1321
1319
|
}
|
|
@@ -1719,7 +1717,7 @@ var inputSchema6 = z8.object({
|
|
|
1719
1717
|
number: z8.number().int().positive().describe("Issue number."),
|
|
1720
1718
|
title: z8.string().trim().min(1).optional().describe("Replacement issue title."),
|
|
1721
1719
|
body: z8.string().optional().describe(
|
|
1722
|
-
"Replacement issue body.
|
|
1720
|
+
"Replacement issue body. The runtime appends requester attribution and the conversation footer."
|
|
1723
1721
|
),
|
|
1724
1722
|
state: z8.enum(["open", "closed"]).optional().describe("Replacement issue state.")
|
|
1725
1723
|
}).strict().refine(
|
|
@@ -1857,7 +1855,7 @@ var inputSchema7 = z9.object({
|
|
|
1857
1855
|
number: z9.number().int().positive().describe("Pull request number."),
|
|
1858
1856
|
title: z9.string().trim().min(1).optional().describe("Replacement pull request title."),
|
|
1859
1857
|
body: z9.string().optional().describe(
|
|
1860
|
-
"Replacement pull request body.
|
|
1858
|
+
"Replacement pull request body. The runtime appends requester attribution and the conversation footer."
|
|
1861
1859
|
),
|
|
1862
1860
|
base: z9.string().trim().min(1).optional().describe("Replacement base branch."),
|
|
1863
1861
|
state: z9.enum(["open", "closed"]).optional().describe("Replacement pull request state.")
|
|
@@ -2024,7 +2022,7 @@ function botUserIdFromEmail(value) {
|
|
|
2024
2022
|
// src/tools/resolve-pull-request-review-thread.ts
|
|
2025
2023
|
var inputSchema8 = z10.object({
|
|
2026
2024
|
repo: z10.string().describe(
|
|
2027
|
-
'Repository in "owner/name" format. Required
|
|
2025
|
+
'Repository in "owner/name" format. Required because GraphQL has no repo path.'
|
|
2028
2026
|
),
|
|
2029
2027
|
threadId: z10.string().trim().min(1).describe(
|
|
2030
2028
|
"GitHub pull request review thread node ID (the same `threadId` / `id` variable used by `gh api graphql` resolveReviewThread)."
|
|
@@ -2068,7 +2066,7 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
|
|
|
2068
2066
|
openWorldHint: true,
|
|
2069
2067
|
readOnlyHint: false
|
|
2070
2068
|
},
|
|
2071
|
-
description: "Resolve a GitHub pull request review thread. Use this instead of shelling out to `gh api graphql` for resolveReviewThread (GraphQL-only; no REST or `gh pr` equivalent). Only works on pull requests
|
|
2069
|
+
description: "Resolve a GitHub pull request review thread. Use this instead of shelling out to `gh api graphql` for resolveReviewThread (GraphQL-only; no REST or `gh pr` equivalent). Only works on pull requests the bot authored.",
|
|
2072
2070
|
inputSchema: inputSchema8,
|
|
2073
2071
|
outputSchema: outputSchema8,
|
|
2074
2072
|
async execute(input) {
|
|
@@ -2136,7 +2134,7 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
|
|
|
2136
2134
|
const ownsPullRequest = pullRequest.repository.nameWithOwner.toLowerCase() === repo.ref.toLowerCase() && pullRequest.author?.databaseId === botUserId;
|
|
2137
2135
|
if (!ownsPullRequest) {
|
|
2138
2136
|
throw new PluginToolInputError11(
|
|
2139
|
-
"
|
|
2137
|
+
"This bot can only resolve review threads on pull requests it authored."
|
|
2140
2138
|
);
|
|
2141
2139
|
}
|
|
2142
2140
|
if (thread.isResolved) {
|
|
@@ -3076,1073 +3074,19 @@ function createGitHubWebhookRoute(args) {
|
|
|
3076
3074
|
};
|
|
3077
3075
|
}
|
|
3078
3076
|
|
|
3079
|
-
// src/outcomes/profile-report.ts
|
|
3080
|
-
import { sql as sql4 } from "drizzle-orm";
|
|
3081
|
-
import { z as z16 } from "zod";
|
|
3082
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3083
|
-
var WINDOWS = [7, 30, 90];
|
|
3084
|
-
var pullRequestStatsSchema = z16.object({
|
|
3085
|
-
closed: z16.number().int().nonnegative(),
|
|
3086
|
-
created: z16.number().int().nonnegative(),
|
|
3087
|
-
days: z16.number().int().positive(),
|
|
3088
|
-
merged: z16.number().int().nonnegative()
|
|
3089
|
-
}).strict().transform((row) => {
|
|
3090
|
-
const terminal = row.merged + row.closed;
|
|
3091
|
-
return {
|
|
3092
|
-
...row,
|
|
3093
|
-
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3094
|
-
};
|
|
3095
|
-
});
|
|
3096
|
-
var issueStatsSchema = z16.object({
|
|
3097
|
-
created: z16.number().int().nonnegative(),
|
|
3098
|
-
days: z16.number().int().positive()
|
|
3099
|
-
}).strict();
|
|
3100
|
-
var daySchema = z16.object({
|
|
3101
|
-
created: z16.number().int().nonnegative(),
|
|
3102
|
-
date: z16.string().date()
|
|
3103
|
-
}).strict();
|
|
3104
|
-
function queryRows(result) {
|
|
3105
|
-
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3106
|
-
throw new TypeError("GitHub profile report query did not return rows");
|
|
3107
|
-
}
|
|
3108
|
-
return result.rows;
|
|
3109
|
-
}
|
|
3110
|
-
function ownedByUserSql(conversationIds, userId) {
|
|
3111
|
-
return sql4`EXISTS (
|
|
3112
|
-
SELECT 1
|
|
3113
|
-
FROM unnest(${conversationIds}) AS linked(conversation_id)
|
|
3114
|
-
INNER JOIN junior_conversations AS conversations
|
|
3115
|
-
ON conversations.conversation_id = linked.conversation_id
|
|
3116
|
-
INNER JOIN junior_identities AS identities
|
|
3117
|
-
ON identities.id = conversations.actor_identity_id
|
|
3118
|
-
WHERE identities.user_id = ${userId}
|
|
3119
|
-
)`;
|
|
3120
|
-
}
|
|
3121
|
-
function formatPercent(value) {
|
|
3122
|
-
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
3123
|
-
}
|
|
3124
|
-
function startOfUtcDay(timestampMs) {
|
|
3125
|
-
const date = new Date(timestampMs);
|
|
3126
|
-
date.setUTCHours(0, 0, 0, 0);
|
|
3127
|
-
return date;
|
|
3128
|
-
}
|
|
3129
|
-
async function aggregatePullRequestWindows(args) {
|
|
3130
|
-
const starts = WINDOWS.map(
|
|
3131
|
-
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
3132
|
-
);
|
|
3133
|
-
const oldestStart = starts.at(-1)[1];
|
|
3134
|
-
const table = juniorGitHubPullRequests;
|
|
3135
|
-
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3136
|
-
const result = await args.db.execute(sql4`
|
|
3137
|
-
WITH windows(days, start_at) AS (
|
|
3138
|
-
VALUES
|
|
3139
|
-
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3140
|
-
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3141
|
-
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3142
|
-
), recent_pull_requests AS MATERIALIZED (
|
|
3143
|
-
SELECT
|
|
3144
|
-
${table.pullRequestId},
|
|
3145
|
-
${table.state},
|
|
3146
|
-
${table.openedAt},
|
|
3147
|
-
${table.mergedAt},
|
|
3148
|
-
${table.closedAt}
|
|
3149
|
-
FROM ${table}
|
|
3150
|
-
WHERE (${table.openedAt} >= ${oldestStart}
|
|
3151
|
-
OR ${table.mergedAt} >= ${oldestStart}
|
|
3152
|
-
OR ${table.closedAt} >= ${oldestStart})
|
|
3153
|
-
AND ${owned}
|
|
3154
|
-
)
|
|
3155
|
-
SELECT
|
|
3156
|
-
windows.days AS "days",
|
|
3157
|
-
count(recent_pull_requests.pull_request_id)
|
|
3158
|
-
FILTER (WHERE recent_pull_requests.opened_at >= windows.start_at)::integer
|
|
3159
|
-
AS "created",
|
|
3160
|
-
count(recent_pull_requests.pull_request_id)
|
|
3161
|
-
FILTER (
|
|
3162
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
3163
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
3164
|
-
)::integer AS "merged",
|
|
3165
|
-
count(recent_pull_requests.pull_request_id)
|
|
3166
|
-
FILTER (
|
|
3167
|
-
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
3168
|
-
AND recent_pull_requests.closed_at >= windows.start_at
|
|
3169
|
-
)::integer AS "closed"
|
|
3170
|
-
FROM windows
|
|
3171
|
-
LEFT JOIN recent_pull_requests ON true
|
|
3172
|
-
GROUP BY windows.days
|
|
3173
|
-
ORDER BY windows.days
|
|
3174
|
-
`);
|
|
3175
|
-
return z16.array(pullRequestStatsSchema).parse(queryRows(result));
|
|
3176
|
-
}
|
|
3177
|
-
async function aggregateIssueWindows(args) {
|
|
3178
|
-
const starts = WINDOWS.map(
|
|
3179
|
-
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
3180
|
-
);
|
|
3181
|
-
const oldestStart = starts.at(-1)[1];
|
|
3182
|
-
const table = juniorGitHubIssues;
|
|
3183
|
-
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3184
|
-
const result = await args.db.execute(sql4`
|
|
3185
|
-
WITH windows(days, start_at) AS (
|
|
3186
|
-
VALUES
|
|
3187
|
-
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3188
|
-
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3189
|
-
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3190
|
-
), recent_issues AS MATERIALIZED (
|
|
3191
|
-
SELECT
|
|
3192
|
-
${table.issueId},
|
|
3193
|
-
${table.openedAt}
|
|
3194
|
-
FROM ${table}
|
|
3195
|
-
WHERE ${table.openedAt} >= ${oldestStart}
|
|
3196
|
-
AND ${owned}
|
|
3197
|
-
)
|
|
3198
|
-
SELECT
|
|
3199
|
-
windows.days AS "days",
|
|
3200
|
-
count(recent_issues.issue_id)
|
|
3201
|
-
FILTER (WHERE recent_issues.opened_at >= windows.start_at)::integer
|
|
3202
|
-
AS "created"
|
|
3203
|
-
FROM windows
|
|
3204
|
-
LEFT JOIN recent_issues ON true
|
|
3205
|
-
GROUP BY windows.days
|
|
3206
|
-
ORDER BY windows.days
|
|
3207
|
-
`);
|
|
3208
|
-
return z16.array(issueStatsSchema).parse(queryRows(result));
|
|
3209
|
-
}
|
|
3210
|
-
async function aggregateOpenedDays(args) {
|
|
3211
|
-
const end = new Date(args.nowMs);
|
|
3212
|
-
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
|
|
3213
|
-
const table = args.table;
|
|
3214
|
-
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3215
|
-
const result = await args.db.execute(sql4`
|
|
3216
|
-
WITH days AS (
|
|
3217
|
-
SELECT generate_series(
|
|
3218
|
-
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
3219
|
-
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
3220
|
-
interval '1 day'
|
|
3221
|
-
) AS day
|
|
3222
|
-
), daily AS (
|
|
3223
|
-
SELECT
|
|
3224
|
-
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
3225
|
-
count(*)::integer AS created
|
|
3226
|
-
FROM ${table}
|
|
3227
|
-
WHERE ${table.openedAt} >= ${start}
|
|
3228
|
-
AND ${owned}
|
|
3229
|
-
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
3230
|
-
)
|
|
3231
|
-
SELECT
|
|
3232
|
-
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
3233
|
-
coalesce(daily.created, 0)::integer AS "created"
|
|
3234
|
-
FROM days
|
|
3235
|
-
LEFT JOIN daily ON daily.day = days.day
|
|
3236
|
-
ORDER BY days.day
|
|
3237
|
-
`);
|
|
3238
|
-
return z16.array(daySchema).parse(queryRows(result));
|
|
3239
|
-
}
|
|
3240
|
-
async function buildGitHubProfileReport(args) {
|
|
3241
|
-
const [windows, pullRequestDays, issueWindows, issueDays] = await Promise.all(
|
|
3242
|
-
[
|
|
3243
|
-
aggregatePullRequestWindows(args),
|
|
3244
|
-
aggregateOpenedDays({
|
|
3245
|
-
db: args.db,
|
|
3246
|
-
nowMs: args.nowMs,
|
|
3247
|
-
table: juniorGitHubPullRequests,
|
|
3248
|
-
userId: args.userId
|
|
3249
|
-
}),
|
|
3250
|
-
aggregateIssueWindows(args),
|
|
3251
|
-
aggregateOpenedDays({
|
|
3252
|
-
db: args.db,
|
|
3253
|
-
nowMs: args.nowMs,
|
|
3254
|
-
table: juniorGitHubIssues,
|
|
3255
|
-
userId: args.userId
|
|
3256
|
-
})
|
|
3257
|
-
]
|
|
3258
|
-
);
|
|
3259
|
-
const thirtyDays = windows.find((window) => window.days === 30);
|
|
3260
|
-
const issueThirtyDays = issueWindows.find((window) => window.days === 30);
|
|
3261
|
-
const hasActivity = windows.some((window) => window.created + window.merged + window.closed > 0) || issueWindows.some((window) => window.created > 0);
|
|
3262
|
-
if (!hasActivity) {
|
|
3263
|
-
return void 0;
|
|
3264
|
-
}
|
|
3265
|
-
return {
|
|
3266
|
-
generatedAt: new Date(args.nowMs).toISOString(),
|
|
3267
|
-
title: "GitHub",
|
|
3268
|
-
metrics: [
|
|
3269
|
-
{
|
|
3270
|
-
label: "PRs opened \xB7 30d",
|
|
3271
|
-
value: String(thirtyDays.created)
|
|
3272
|
-
},
|
|
3273
|
-
{
|
|
3274
|
-
label: "PRs merged \xB7 30d",
|
|
3275
|
-
value: String(thirtyDays.merged)
|
|
3276
|
-
},
|
|
3277
|
-
{
|
|
3278
|
-
label: "Issues opened \xB7 30d",
|
|
3279
|
-
value: String(issueThirtyDays.created)
|
|
3280
|
-
},
|
|
3281
|
-
{
|
|
3282
|
-
label: "PR merge rate \xB7 30d",
|
|
3283
|
-
value: formatPercent(thirtyDays.mergeRate)
|
|
3284
|
-
}
|
|
3285
|
-
],
|
|
3286
|
-
widgets: [
|
|
3287
|
-
{
|
|
3288
|
-
id: "pull-requests-created",
|
|
3289
|
-
type: "bar_chart",
|
|
3290
|
-
title: "Pull requests opened",
|
|
3291
|
-
description: "Junior-owned pull requests opened for this person per day",
|
|
3292
|
-
timeRangeDays: [...WINDOWS],
|
|
3293
|
-
series: [{ key: "created", label: "Opened" }],
|
|
3294
|
-
categories: pullRequestDays.map((stats) => ({
|
|
3295
|
-
id: stats.date,
|
|
3296
|
-
label: stats.date,
|
|
3297
|
-
values: { created: stats.created }
|
|
3298
|
-
}))
|
|
3299
|
-
},
|
|
3300
|
-
{
|
|
3301
|
-
id: "issues-created",
|
|
3302
|
-
type: "bar_chart",
|
|
3303
|
-
title: "Issues opened",
|
|
3304
|
-
description: "Junior-owned issues opened for this person per day",
|
|
3305
|
-
timeRangeDays: [...WINDOWS],
|
|
3306
|
-
series: [{ key: "created", label: "Opened" }],
|
|
3307
|
-
categories: issueDays.map((stats) => ({
|
|
3308
|
-
id: stats.date,
|
|
3309
|
-
label: stats.date,
|
|
3310
|
-
values: { created: stats.created }
|
|
3311
|
-
}))
|
|
3312
|
-
}
|
|
3313
|
-
]
|
|
3314
|
-
};
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
// src/outcomes/report.ts
|
|
3318
|
-
import { sql as sql6 } from "drizzle-orm";
|
|
3319
|
-
import { z as z18 } from "zod";
|
|
3320
|
-
|
|
3321
|
-
// src/outcomes/cost.ts
|
|
3322
|
-
import { sql as sql5 } from "drizzle-orm";
|
|
3323
|
-
import { z as z17 } from "zod";
|
|
3324
|
-
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
3325
|
-
var costWindowSchema = z17.object({
|
|
3326
|
-
days: z17.number().int().positive(),
|
|
3327
|
-
issueCostUsd: z17.number().nonnegative().nullable(),
|
|
3328
|
-
medianIssueCostUsd: z17.number().nonnegative().nullable(),
|
|
3329
|
-
medianPullRequestCostUsd: z17.number().nonnegative().nullable(),
|
|
3330
|
-
pullRequestCostUsd: z17.number().nonnegative().nullable()
|
|
3331
|
-
}).strict().transform((row) => ({
|
|
3332
|
-
days: row.days,
|
|
3333
|
-
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
3334
|
-
medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
|
|
3335
|
-
medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
|
|
3336
|
-
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
|
|
3337
|
-
}));
|
|
3338
|
-
var repositoryCostSchema = z17.object({
|
|
3339
|
-
issueCostUsd: z17.number().nonnegative().nullable(),
|
|
3340
|
-
medianIssueCostUsd: z17.number().nonnegative().nullable(),
|
|
3341
|
-
medianPullRequestCostUsd: z17.number().nonnegative().nullable(),
|
|
3342
|
-
pullRequestCostUsd: z17.number().nonnegative().nullable(),
|
|
3343
|
-
repository: z17.string().min(1)
|
|
3344
|
-
}).strict().transform((row) => ({
|
|
3345
|
-
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
3346
|
-
medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
|
|
3347
|
-
medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
|
|
3348
|
-
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
|
|
3349
|
-
repository: row.repository
|
|
3350
|
-
}));
|
|
3351
|
-
function queryRows2(result) {
|
|
3352
|
-
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3353
|
-
throw new TypeError("GitHub cost query did not return rows");
|
|
3354
|
-
}
|
|
3355
|
-
return result.rows;
|
|
3356
|
-
}
|
|
3357
|
-
async function hasConversationUsageTable(db) {
|
|
3358
|
-
const result = await db.execute(sql5`
|
|
3359
|
-
SELECT to_regclass('public.junior_conversations') IS NOT NULL AS "present"
|
|
3360
|
-
`);
|
|
3361
|
-
const row = queryRows2(result)[0];
|
|
3362
|
-
return typeof row === "object" && row !== null && "present" in row && row.present === true;
|
|
3363
|
-
}
|
|
3364
|
-
function emptyCostWindows(windows) {
|
|
3365
|
-
return windows.map((days) => ({
|
|
3366
|
-
days,
|
|
3367
|
-
issueCostUsd: 0,
|
|
3368
|
-
medianIssueCostUsd: void 0,
|
|
3369
|
-
medianPullRequestCostUsd: void 0,
|
|
3370
|
-
pullRequestCostUsd: 0
|
|
3371
|
-
}));
|
|
3372
|
-
}
|
|
3373
|
-
function conversationTreeCostExpr() {
|
|
3374
|
-
return sql5`
|
|
3375
|
-
coalesce((
|
|
3376
|
-
SELECT sum(
|
|
3377
|
-
CASE
|
|
3378
|
-
WHEN conversations.usage_json->'cost'->>'total' IS NOT NULL
|
|
3379
|
-
THEN (conversations.usage_json->'cost'->>'total')::double precision
|
|
3380
|
-
WHEN coalesce(
|
|
3381
|
-
conversations.usage_json->'cost'->>'input',
|
|
3382
|
-
conversations.usage_json->'cost'->>'output',
|
|
3383
|
-
conversations.usage_json->'cost'->>'cacheRead',
|
|
3384
|
-
conversations.usage_json->'cost'->>'cacheWrite'
|
|
3385
|
-
) IS NOT NULL
|
|
3386
|
-
THEN coalesce((conversations.usage_json->'cost'->>'input')::double precision, 0)
|
|
3387
|
-
+ coalesce((conversations.usage_json->'cost'->>'output')::double precision, 0)
|
|
3388
|
-
+ coalesce((conversations.usage_json->'cost'->>'cacheRead')::double precision, 0)
|
|
3389
|
-
+ coalesce((conversations.usage_json->'cost'->>'cacheWrite')::double precision, 0)
|
|
3390
|
-
ELSE 0
|
|
3391
|
-
END
|
|
3392
|
-
)
|
|
3393
|
-
FROM junior_conversations AS conversations
|
|
3394
|
-
WHERE conversations.root_conversation_id IN (
|
|
3395
|
-
SELECT coalesce(roots.root_conversation_id, roots.conversation_id)
|
|
3396
|
-
FROM junior_conversations AS roots
|
|
3397
|
-
WHERE roots.conversation_id = ANY (conversation_ids.ids)
|
|
3398
|
-
)
|
|
3399
|
-
), 0::double precision)
|
|
3400
|
-
`;
|
|
3401
|
-
}
|
|
3402
|
-
function pullRequestConversationIdsExpr() {
|
|
3403
|
-
const pullRequests = juniorGitHubPullRequests;
|
|
3404
|
-
const issues = juniorGitHubIssues;
|
|
3405
|
-
return sql5`
|
|
3406
|
-
ARRAY(
|
|
3407
|
-
SELECT DISTINCT unnest(
|
|
3408
|
-
${pullRequests.conversationIds}
|
|
3409
|
-
|| coalesce((
|
|
3410
|
-
SELECT array_agg(DISTINCT issue_conversation_id)
|
|
3411
|
-
FROM ${juniorGitHubPullRequestIssues} AS issue_links
|
|
3412
|
-
INNER JOIN ${issues} AS linked_issues
|
|
3413
|
-
ON lower(linked_issues.repository_full_name) =
|
|
3414
|
-
issue_links.issue_repository_full_name
|
|
3415
|
-
AND linked_issues.number = issue_links.issue_number
|
|
3416
|
-
CROSS JOIN LATERAL unnest(linked_issues.conversation_ids)
|
|
3417
|
-
AS issue_conversation_id
|
|
3418
|
-
WHERE issue_links.pull_request_id = ${pullRequests.pullRequestId}
|
|
3419
|
-
), ARRAY[]::text[])
|
|
3420
|
-
)
|
|
3421
|
-
)
|
|
3422
|
-
`;
|
|
3423
|
-
}
|
|
3424
|
-
function issueConversationIdsExpr() {
|
|
3425
|
-
const pullRequests = juniorGitHubPullRequests;
|
|
3426
|
-
const issues = juniorGitHubIssues;
|
|
3427
|
-
return sql5`
|
|
3428
|
-
ARRAY(
|
|
3429
|
-
SELECT DISTINCT unnest(
|
|
3430
|
-
${issues.conversationIds}
|
|
3431
|
-
|| coalesce((
|
|
3432
|
-
SELECT array_agg(DISTINCT pr_conversation_id)
|
|
3433
|
-
FROM ${juniorGitHubPullRequestIssues} AS issue_links
|
|
3434
|
-
INNER JOIN ${pullRequests} AS linked_prs
|
|
3435
|
-
ON linked_prs.pull_request_id = issue_links.pull_request_id
|
|
3436
|
-
CROSS JOIN LATERAL unnest(linked_prs.conversation_ids)
|
|
3437
|
-
AS pr_conversation_id
|
|
3438
|
-
WHERE issue_links.issue_repository_full_name =
|
|
3439
|
-
lower(${issues.repositoryFullName})
|
|
3440
|
-
AND issue_links.issue_number = ${issues.number}
|
|
3441
|
-
), ARRAY[]::text[])
|
|
3442
|
-
)
|
|
3443
|
-
)
|
|
3444
|
-
`;
|
|
3445
|
-
}
|
|
3446
|
-
async function aggregateGitHubCostWindows(args) {
|
|
3447
|
-
if (!await hasConversationUsageTable(args.db)) {
|
|
3448
|
-
return emptyCostWindows(args.windows);
|
|
3449
|
-
}
|
|
3450
|
-
const starts = args.windows.map(
|
|
3451
|
-
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
3452
|
-
);
|
|
3453
|
-
const oldestStart = starts.at(-1)[1];
|
|
3454
|
-
const pullRequests = juniorGitHubPullRequests;
|
|
3455
|
-
const issues = juniorGitHubIssues;
|
|
3456
|
-
const windowValues = sql5.join(
|
|
3457
|
-
starts.map(
|
|
3458
|
-
([days, start]) => sql5`(${days}::integer, ${start}::timestamptz)`
|
|
3459
|
-
),
|
|
3460
|
-
sql5`, `
|
|
3461
|
-
);
|
|
3462
|
-
const conversationTreeCost = conversationTreeCostExpr();
|
|
3463
|
-
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
3464
|
-
const issueConversationIds = issueConversationIdsExpr();
|
|
3465
|
-
const result = await args.db.execute(sql5`
|
|
3466
|
-
WITH windows(days, start_at) AS (
|
|
3467
|
-
VALUES ${windowValues}
|
|
3468
|
-
), pull_request_entities AS (
|
|
3469
|
-
SELECT
|
|
3470
|
-
${pullRequests.openedAt} AS opened_at,
|
|
3471
|
-
conversation_ids.ids AS ids,
|
|
3472
|
-
${conversationTreeCost} AS cost_usd
|
|
3473
|
-
FROM ${pullRequests}
|
|
3474
|
-
CROSS JOIN LATERAL (
|
|
3475
|
-
SELECT ${pullRequestConversationIds} AS ids
|
|
3476
|
-
) AS conversation_ids
|
|
3477
|
-
WHERE ${pullRequests.openedAt} >= ${oldestStart}
|
|
3478
|
-
), issue_entities AS (
|
|
3479
|
-
SELECT
|
|
3480
|
-
${issues.openedAt} AS opened_at,
|
|
3481
|
-
conversation_ids.ids AS ids,
|
|
3482
|
-
${conversationTreeCost} AS cost_usd
|
|
3483
|
-
FROM ${issues}
|
|
3484
|
-
CROSS JOIN LATERAL (
|
|
3485
|
-
SELECT ${issueConversationIds} AS ids
|
|
3486
|
-
) AS conversation_ids
|
|
3487
|
-
WHERE ${issues.openedAt} >= ${oldestStart}
|
|
3488
|
-
), pull_request_window AS (
|
|
3489
|
-
SELECT
|
|
3490
|
-
windows.days AS days,
|
|
3491
|
-
coalesce((
|
|
3492
|
-
SELECT ${conversationTreeCost}
|
|
3493
|
-
FROM (
|
|
3494
|
-
SELECT ARRAY(
|
|
3495
|
-
SELECT DISTINCT unnest(pull_request_entities.ids)
|
|
3496
|
-
FROM pull_request_entities
|
|
3497
|
-
WHERE pull_request_entities.opened_at >= windows.start_at
|
|
3498
|
-
) AS ids
|
|
3499
|
-
) AS conversation_ids
|
|
3500
|
-
), 0)::double precision AS pull_request_cost_usd,
|
|
3501
|
-
(
|
|
3502
|
-
percentile_cont(0.5) WITHIN GROUP (
|
|
3503
|
-
ORDER BY pull_request_entities.cost_usd
|
|
3504
|
-
) FILTER (
|
|
3505
|
-
WHERE pull_request_entities.opened_at >= windows.start_at
|
|
3506
|
-
AND pull_request_entities.cost_usd > 0
|
|
3507
|
-
)
|
|
3508
|
-
)::double precision AS median_pull_request_cost_usd
|
|
3509
|
-
FROM windows
|
|
3510
|
-
LEFT JOIN pull_request_entities ON true
|
|
3511
|
-
GROUP BY windows.days, windows.start_at
|
|
3512
|
-
), issue_window AS (
|
|
3513
|
-
SELECT
|
|
3514
|
-
windows.days AS days,
|
|
3515
|
-
coalesce((
|
|
3516
|
-
SELECT ${conversationTreeCost}
|
|
3517
|
-
FROM (
|
|
3518
|
-
SELECT ARRAY(
|
|
3519
|
-
SELECT DISTINCT unnest(issue_entities.ids)
|
|
3520
|
-
FROM issue_entities
|
|
3521
|
-
WHERE issue_entities.opened_at >= windows.start_at
|
|
3522
|
-
) AS ids
|
|
3523
|
-
) AS conversation_ids
|
|
3524
|
-
), 0)::double precision AS issue_cost_usd,
|
|
3525
|
-
(
|
|
3526
|
-
percentile_cont(0.5) WITHIN GROUP (
|
|
3527
|
-
ORDER BY issue_entities.cost_usd
|
|
3528
|
-
) FILTER (
|
|
3529
|
-
WHERE issue_entities.opened_at >= windows.start_at
|
|
3530
|
-
AND issue_entities.cost_usd > 0
|
|
3531
|
-
)
|
|
3532
|
-
)::double precision AS median_issue_cost_usd
|
|
3533
|
-
FROM windows
|
|
3534
|
-
LEFT JOIN issue_entities ON true
|
|
3535
|
-
GROUP BY windows.days, windows.start_at
|
|
3536
|
-
)
|
|
3537
|
-
SELECT
|
|
3538
|
-
pull_request_window.days AS "days",
|
|
3539
|
-
pull_request_window.pull_request_cost_usd AS "pullRequestCostUsd",
|
|
3540
|
-
pull_request_window.median_pull_request_cost_usd
|
|
3541
|
-
AS "medianPullRequestCostUsd",
|
|
3542
|
-
issue_window.issue_cost_usd AS "issueCostUsd",
|
|
3543
|
-
issue_window.median_issue_cost_usd AS "medianIssueCostUsd"
|
|
3544
|
-
FROM pull_request_window
|
|
3545
|
-
INNER JOIN issue_window ON issue_window.days = pull_request_window.days
|
|
3546
|
-
ORDER BY pull_request_window.days
|
|
3547
|
-
`);
|
|
3548
|
-
return z17.array(costWindowSchema).parse(queryRows2(result));
|
|
3549
|
-
}
|
|
3550
|
-
async function aggregateGitHubRepositoryCosts(args) {
|
|
3551
|
-
if (!await hasConversationUsageTable(args.db)) {
|
|
3552
|
-
return [];
|
|
3553
|
-
}
|
|
3554
|
-
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
3555
|
-
const pullRequests = juniorGitHubPullRequests;
|
|
3556
|
-
const issues = juniorGitHubIssues;
|
|
3557
|
-
const conversationTreeCost = conversationTreeCostExpr();
|
|
3558
|
-
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
3559
|
-
const issueConversationIds = issueConversationIdsExpr();
|
|
3560
|
-
const result = await args.db.execute(sql5`
|
|
3561
|
-
WITH pull_request_entities AS (
|
|
3562
|
-
SELECT
|
|
3563
|
-
${pullRequests.repositoryFullName} AS repository,
|
|
3564
|
-
conversation_ids.ids AS ids,
|
|
3565
|
-
${conversationTreeCost} AS cost_usd
|
|
3566
|
-
FROM ${pullRequests}
|
|
3567
|
-
CROSS JOIN LATERAL (
|
|
3568
|
-
SELECT ${pullRequestConversationIds} AS ids
|
|
3569
|
-
) AS conversation_ids
|
|
3570
|
-
WHERE ${pullRequests.openedAt} >= ${start}
|
|
3571
|
-
), issue_entities AS (
|
|
3572
|
-
SELECT
|
|
3573
|
-
${issues.repositoryFullName} AS repository,
|
|
3574
|
-
conversation_ids.ids AS ids,
|
|
3575
|
-
${conversationTreeCost} AS cost_usd
|
|
3576
|
-
FROM ${issues}
|
|
3577
|
-
CROSS JOIN LATERAL (
|
|
3578
|
-
SELECT ${issueConversationIds} AS ids
|
|
3579
|
-
) AS conversation_ids
|
|
3580
|
-
WHERE ${issues.openedAt} >= ${start}
|
|
3581
|
-
), repositories AS (
|
|
3582
|
-
SELECT repository FROM pull_request_entities
|
|
3583
|
-
UNION
|
|
3584
|
-
SELECT repository FROM issue_entities
|
|
3585
|
-
), pull_request_totals AS (
|
|
3586
|
-
SELECT
|
|
3587
|
-
repositories.repository AS repository,
|
|
3588
|
-
coalesce((
|
|
3589
|
-
SELECT ${conversationTreeCost}
|
|
3590
|
-
FROM (
|
|
3591
|
-
SELECT ARRAY(
|
|
3592
|
-
SELECT DISTINCT unnest(pull_request_entities.ids)
|
|
3593
|
-
FROM pull_request_entities
|
|
3594
|
-
WHERE pull_request_entities.repository = repositories.repository
|
|
3595
|
-
) AS ids
|
|
3596
|
-
) AS conversation_ids
|
|
3597
|
-
), 0)::double precision AS pull_request_cost_usd,
|
|
3598
|
-
(
|
|
3599
|
-
SELECT percentile_cont(0.5) WITHIN GROUP (
|
|
3600
|
-
ORDER BY pull_request_entities.cost_usd
|
|
3601
|
-
)
|
|
3602
|
-
FROM pull_request_entities
|
|
3603
|
-
WHERE pull_request_entities.repository = repositories.repository
|
|
3604
|
-
AND pull_request_entities.cost_usd > 0
|
|
3605
|
-
)::double precision AS median_pull_request_cost_usd
|
|
3606
|
-
FROM repositories
|
|
3607
|
-
), issue_totals AS (
|
|
3608
|
-
SELECT
|
|
3609
|
-
repositories.repository AS repository,
|
|
3610
|
-
coalesce((
|
|
3611
|
-
SELECT ${conversationTreeCost}
|
|
3612
|
-
FROM (
|
|
3613
|
-
SELECT ARRAY(
|
|
3614
|
-
SELECT DISTINCT unnest(issue_entities.ids)
|
|
3615
|
-
FROM issue_entities
|
|
3616
|
-
WHERE issue_entities.repository = repositories.repository
|
|
3617
|
-
) AS ids
|
|
3618
|
-
) AS conversation_ids
|
|
3619
|
-
), 0)::double precision AS issue_cost_usd,
|
|
3620
|
-
(
|
|
3621
|
-
SELECT percentile_cont(0.5) WITHIN GROUP (
|
|
3622
|
-
ORDER BY issue_entities.cost_usd
|
|
3623
|
-
)
|
|
3624
|
-
FROM issue_entities
|
|
3625
|
-
WHERE issue_entities.repository = repositories.repository
|
|
3626
|
-
AND issue_entities.cost_usd > 0
|
|
3627
|
-
)::double precision AS median_issue_cost_usd
|
|
3628
|
-
FROM repositories
|
|
3629
|
-
)
|
|
3630
|
-
SELECT
|
|
3631
|
-
repositories.repository AS "repository",
|
|
3632
|
-
coalesce(pull_request_totals.pull_request_cost_usd, 0)::double precision
|
|
3633
|
-
AS "pullRequestCostUsd",
|
|
3634
|
-
pull_request_totals.median_pull_request_cost_usd
|
|
3635
|
-
AS "medianPullRequestCostUsd",
|
|
3636
|
-
coalesce(issue_totals.issue_cost_usd, 0)::double precision
|
|
3637
|
-
AS "issueCostUsd",
|
|
3638
|
-
issue_totals.median_issue_cost_usd AS "medianIssueCostUsd"
|
|
3639
|
-
FROM repositories
|
|
3640
|
-
LEFT JOIN pull_request_totals
|
|
3641
|
-
ON pull_request_totals.repository = repositories.repository
|
|
3642
|
-
LEFT JOIN issue_totals
|
|
3643
|
-
ON issue_totals.repository = repositories.repository
|
|
3644
|
-
ORDER BY "repository" ASC
|
|
3645
|
-
`);
|
|
3646
|
-
return z17.array(repositoryCostSchema).parse(queryRows2(result));
|
|
3647
|
-
}
|
|
3648
|
-
function formatCostUsd(value) {
|
|
3649
|
-
if (value === void 0) return "\u2014";
|
|
3650
|
-
return new Intl.NumberFormat("en-US", {
|
|
3651
|
-
style: "currency",
|
|
3652
|
-
currency: "USD",
|
|
3653
|
-
minimumFractionDigits: 2,
|
|
3654
|
-
maximumFractionDigits: 2
|
|
3655
|
-
}).format(value);
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
|
-
// src/outcomes/report.ts
|
|
3659
|
-
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
3660
|
-
var WINDOWS2 = [7, 30, 90];
|
|
3661
|
-
var pullRequestStatsSchema2 = z18.object({
|
|
3662
|
-
closed: z18.number().int().nonnegative(),
|
|
3663
|
-
created: z18.number().int().nonnegative(),
|
|
3664
|
-
days: z18.number().int().positive(),
|
|
3665
|
-
medianMergeTimeMs: z18.number().nonnegative().nullable(),
|
|
3666
|
-
merged: z18.number().int().nonnegative()
|
|
3667
|
-
}).strict().transform((row) => {
|
|
3668
|
-
const terminal = row.merged + row.closed;
|
|
3669
|
-
return {
|
|
3670
|
-
...row,
|
|
3671
|
-
medianMergeTimeMs: row.medianMergeTimeMs ?? void 0,
|
|
3672
|
-
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3673
|
-
};
|
|
3674
|
-
});
|
|
3675
|
-
var pullRequestRepositoryStatsSchema = z18.object({
|
|
3676
|
-
closed: z18.number().int().nonnegative(),
|
|
3677
|
-
created: z18.number().int().nonnegative(),
|
|
3678
|
-
juniorOnly: z18.number().int().nonnegative(),
|
|
3679
|
-
merged: z18.number().int().nonnegative(),
|
|
3680
|
-
repository: z18.string().min(1)
|
|
3681
|
-
}).strict().transform((row) => {
|
|
3682
|
-
const terminal = row.merged + row.closed;
|
|
3683
|
-
return {
|
|
3684
|
-
...row,
|
|
3685
|
-
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3686
|
-
};
|
|
3687
|
-
});
|
|
3688
|
-
var issueStatsSchema2 = z18.object({
|
|
3689
|
-
closedCompleted: z18.number().int().nonnegative(),
|
|
3690
|
-
closedDuplicate: z18.number().int().nonnegative(),
|
|
3691
|
-
closedNotPlanned: z18.number().int().nonnegative(),
|
|
3692
|
-
closedUnknown: z18.number().int().nonnegative(),
|
|
3693
|
-
created: z18.number().int().nonnegative(),
|
|
3694
|
-
days: z18.number().int().positive(),
|
|
3695
|
-
medianCloseTimeMs: z18.number().nonnegative().nullable()
|
|
3696
|
-
}).strict().transform((row) => ({
|
|
3697
|
-
...row,
|
|
3698
|
-
medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
|
|
3699
|
-
}));
|
|
3700
|
-
var pullRequestDaySchema = z18.object({
|
|
3701
|
-
created: z18.number().int().nonnegative(),
|
|
3702
|
-
date: z18.string().date()
|
|
3703
|
-
}).strict();
|
|
3704
|
-
var issueDaySchema = z18.object({
|
|
3705
|
-
created: z18.number().int().nonnegative(),
|
|
3706
|
-
date: z18.string().date()
|
|
3707
|
-
}).strict();
|
|
3708
|
-
var issueRepositoryStatsSchema = z18.object({
|
|
3709
|
-
closedCompleted: z18.number().int().nonnegative(),
|
|
3710
|
-
closedDuplicate: z18.number().int().nonnegative(),
|
|
3711
|
-
closedNotPlanned: z18.number().int().nonnegative(),
|
|
3712
|
-
closedUnknown: z18.number().int().nonnegative(),
|
|
3713
|
-
created: z18.number().int().nonnegative(),
|
|
3714
|
-
repository: z18.string().min(1)
|
|
3715
|
-
}).strict();
|
|
3716
|
-
function queryRows3(result) {
|
|
3717
|
-
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3718
|
-
throw new TypeError("GitHub outcome query did not return rows");
|
|
3719
|
-
}
|
|
3720
|
-
return result.rows;
|
|
3721
|
-
}
|
|
3722
|
-
async function aggregatePullRequestWindows2(args) {
|
|
3723
|
-
const starts = WINDOWS2.map(
|
|
3724
|
-
(days) => [days, new Date(args.nowMs - days * DAY_MS3)]
|
|
3725
|
-
);
|
|
3726
|
-
const oldestStart = starts.at(-1)[1];
|
|
3727
|
-
const table = juniorGitHubPullRequests;
|
|
3728
|
-
const result = await args.db.execute(sql6`
|
|
3729
|
-
WITH windows(days, start_at) AS (
|
|
3730
|
-
VALUES
|
|
3731
|
-
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3732
|
-
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3733
|
-
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3734
|
-
), recent_pull_requests AS MATERIALIZED (
|
|
3735
|
-
SELECT
|
|
3736
|
-
${table.pullRequestId},
|
|
3737
|
-
${table.state},
|
|
3738
|
-
${table.openedAt},
|
|
3739
|
-
${table.mergedAt},
|
|
3740
|
-
${table.closedAt}
|
|
3741
|
-
FROM ${table}
|
|
3742
|
-
WHERE ${table.openedAt} >= ${oldestStart}
|
|
3743
|
-
OR ${table.mergedAt} >= ${oldestStart}
|
|
3744
|
-
OR ${table.closedAt} >= ${oldestStart}
|
|
3745
|
-
)
|
|
3746
|
-
SELECT
|
|
3747
|
-
windows.days AS "days",
|
|
3748
|
-
count(recent_pull_requests.pull_request_id)
|
|
3749
|
-
FILTER (WHERE recent_pull_requests.opened_at >= windows.start_at)::integer
|
|
3750
|
-
AS "created",
|
|
3751
|
-
count(recent_pull_requests.pull_request_id)
|
|
3752
|
-
FILTER (
|
|
3753
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
3754
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
3755
|
-
)::integer AS "merged",
|
|
3756
|
-
count(recent_pull_requests.pull_request_id)
|
|
3757
|
-
FILTER (
|
|
3758
|
-
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
3759
|
-
AND recent_pull_requests.closed_at >= windows.start_at
|
|
3760
|
-
)::integer AS "closed",
|
|
3761
|
-
(
|
|
3762
|
-
percentile_cont(0.5) WITHIN GROUP (
|
|
3763
|
-
ORDER BY extract(
|
|
3764
|
-
epoch FROM (
|
|
3765
|
-
recent_pull_requests.merged_at - recent_pull_requests.opened_at
|
|
3766
|
-
)
|
|
3767
|
-
) * 1000
|
|
3768
|
-
) FILTER (
|
|
3769
|
-
WHERE recent_pull_requests.state = 'merged'
|
|
3770
|
-
AND recent_pull_requests.merged_at >= windows.start_at
|
|
3771
|
-
)
|
|
3772
|
-
)::double precision AS "medianMergeTimeMs"
|
|
3773
|
-
FROM windows
|
|
3774
|
-
LEFT JOIN recent_pull_requests ON true
|
|
3775
|
-
GROUP BY windows.days
|
|
3776
|
-
ORDER BY windows.days
|
|
3777
|
-
`);
|
|
3778
|
-
return z18.array(pullRequestStatsSchema2).parse(queryRows3(result));
|
|
3779
|
-
}
|
|
3780
|
-
async function aggregatePullRequestDays(args) {
|
|
3781
|
-
const end = new Date(args.nowMs);
|
|
3782
|
-
const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
|
|
3783
|
-
const table = juniorGitHubPullRequests;
|
|
3784
|
-
const result = await args.db.execute(sql6`
|
|
3785
|
-
WITH days AS (
|
|
3786
|
-
SELECT generate_series(
|
|
3787
|
-
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
3788
|
-
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
3789
|
-
interval '1 day'
|
|
3790
|
-
) AS day
|
|
3791
|
-
), daily AS (
|
|
3792
|
-
SELECT
|
|
3793
|
-
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
3794
|
-
count(*)::integer AS created
|
|
3795
|
-
FROM ${table}
|
|
3796
|
-
WHERE ${table.openedAt} >= ${start}
|
|
3797
|
-
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
3798
|
-
)
|
|
3799
|
-
SELECT
|
|
3800
|
-
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
3801
|
-
coalesce(daily.created, 0)::integer AS "created"
|
|
3802
|
-
FROM days
|
|
3803
|
-
LEFT JOIN daily ON daily.day = days.day
|
|
3804
|
-
ORDER BY days.day
|
|
3805
|
-
`);
|
|
3806
|
-
return z18.array(pullRequestDaySchema).parse(queryRows3(result));
|
|
3807
|
-
}
|
|
3808
|
-
async function aggregatePullRequestRepositories(args) {
|
|
3809
|
-
const start = new Date(args.nowMs - 30 * DAY_MS3);
|
|
3810
|
-
const table = juniorGitHubPullRequests;
|
|
3811
|
-
const result = await args.db.execute(sql6`
|
|
3812
|
-
SELECT
|
|
3813
|
-
${table.repositoryFullName} AS "repository",
|
|
3814
|
-
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
3815
|
-
AS "created",
|
|
3816
|
-
count(*) FILTER (
|
|
3817
|
-
WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
|
|
3818
|
-
)::integer AS "merged",
|
|
3819
|
-
count(*) FILTER (
|
|
3820
|
-
WHERE ${table.state} = 'closed_unmerged'
|
|
3821
|
-
AND ${table.closedAt} >= ${start}
|
|
3822
|
-
)::integer AS "closed",
|
|
3823
|
-
count(*) FILTER (
|
|
3824
|
-
WHERE ${table.state} = 'merged'
|
|
3825
|
-
AND ${table.mergedAt} >= ${start}
|
|
3826
|
-
AND ${table.commitComposition} = 'junior_only'
|
|
3827
|
-
)::integer AS "juniorOnly"
|
|
3828
|
-
FROM ${table}
|
|
3829
|
-
WHERE ${table.openedAt} >= ${start}
|
|
3830
|
-
OR ${table.mergedAt} >= ${start}
|
|
3831
|
-
OR ${table.closedAt} >= ${start}
|
|
3832
|
-
GROUP BY ${table.repositoryFullName}
|
|
3833
|
-
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
3834
|
-
LIMIT 25
|
|
3835
|
-
`);
|
|
3836
|
-
return z18.array(pullRequestRepositoryStatsSchema).parse(queryRows3(result));
|
|
3837
|
-
}
|
|
3838
|
-
async function aggregateIssueWindows2(args) {
|
|
3839
|
-
const starts = WINDOWS2.map(
|
|
3840
|
-
(days) => [days, new Date(args.nowMs - days * DAY_MS3)]
|
|
3841
|
-
);
|
|
3842
|
-
const oldestStart = starts.at(-1)[1];
|
|
3843
|
-
const table = juniorGitHubIssues;
|
|
3844
|
-
const result = await args.db.execute(sql6`
|
|
3845
|
-
WITH windows(days, start_at) AS (
|
|
3846
|
-
VALUES
|
|
3847
|
-
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3848
|
-
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3849
|
-
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3850
|
-
), recent_issues AS MATERIALIZED (
|
|
3851
|
-
SELECT
|
|
3852
|
-
${table.issueId},
|
|
3853
|
-
${table.state},
|
|
3854
|
-
${table.stateReason},
|
|
3855
|
-
${table.openedAt},
|
|
3856
|
-
${table.closedAt}
|
|
3857
|
-
FROM ${table}
|
|
3858
|
-
WHERE ${table.openedAt} >= ${oldestStart}
|
|
3859
|
-
OR ${table.closedAt} >= ${oldestStart}
|
|
3860
|
-
)
|
|
3861
|
-
SELECT
|
|
3862
|
-
windows.days AS "days",
|
|
3863
|
-
count(recent_issues.issue_id)
|
|
3864
|
-
FILTER (WHERE recent_issues.opened_at >= windows.start_at)::integer
|
|
3865
|
-
AS "created",
|
|
3866
|
-
count(recent_issues.issue_id)
|
|
3867
|
-
FILTER (
|
|
3868
|
-
WHERE recent_issues.state = 'closed'
|
|
3869
|
-
AND recent_issues.state_reason = 'completed'
|
|
3870
|
-
AND recent_issues.closed_at >= windows.start_at
|
|
3871
|
-
)::integer AS "closedCompleted",
|
|
3872
|
-
count(recent_issues.issue_id)
|
|
3873
|
-
FILTER (
|
|
3874
|
-
WHERE recent_issues.state = 'closed'
|
|
3875
|
-
AND recent_issues.state_reason = 'duplicate'
|
|
3876
|
-
AND recent_issues.closed_at >= windows.start_at
|
|
3877
|
-
)::integer AS "closedDuplicate",
|
|
3878
|
-
count(recent_issues.issue_id)
|
|
3879
|
-
FILTER (
|
|
3880
|
-
WHERE recent_issues.state = 'closed'
|
|
3881
|
-
AND recent_issues.state_reason = 'not_planned'
|
|
3882
|
-
AND recent_issues.closed_at >= windows.start_at
|
|
3883
|
-
)::integer AS "closedNotPlanned",
|
|
3884
|
-
count(recent_issues.issue_id)
|
|
3885
|
-
FILTER (
|
|
3886
|
-
WHERE recent_issues.state = 'closed'
|
|
3887
|
-
AND recent_issues.state_reason IS NULL
|
|
3888
|
-
AND recent_issues.closed_at >= windows.start_at
|
|
3889
|
-
)::integer AS "closedUnknown",
|
|
3890
|
-
(
|
|
3891
|
-
percentile_cont(0.5) WITHIN GROUP (
|
|
3892
|
-
ORDER BY extract(
|
|
3893
|
-
epoch FROM (recent_issues.closed_at - recent_issues.opened_at)
|
|
3894
|
-
) * 1000
|
|
3895
|
-
) FILTER (
|
|
3896
|
-
WHERE recent_issues.state = 'closed'
|
|
3897
|
-
AND recent_issues.closed_at >= windows.start_at
|
|
3898
|
-
)
|
|
3899
|
-
)::double precision AS "medianCloseTimeMs"
|
|
3900
|
-
FROM windows
|
|
3901
|
-
LEFT JOIN recent_issues ON true
|
|
3902
|
-
GROUP BY windows.days
|
|
3903
|
-
ORDER BY windows.days
|
|
3904
|
-
`);
|
|
3905
|
-
return z18.array(issueStatsSchema2).parse(queryRows3(result));
|
|
3906
|
-
}
|
|
3907
|
-
async function aggregateIssueDays(args) {
|
|
3908
|
-
const end = new Date(args.nowMs);
|
|
3909
|
-
const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
|
|
3910
|
-
const table = juniorGitHubIssues;
|
|
3911
|
-
const result = await args.db.execute(sql6`
|
|
3912
|
-
WITH days AS (
|
|
3913
|
-
SELECT generate_series(
|
|
3914
|
-
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
3915
|
-
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
3916
|
-
interval '1 day'
|
|
3917
|
-
) AS day
|
|
3918
|
-
), daily AS (
|
|
3919
|
-
SELECT
|
|
3920
|
-
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
3921
|
-
count(*)::integer AS created
|
|
3922
|
-
FROM ${table}
|
|
3923
|
-
WHERE ${table.openedAt} >= ${start}
|
|
3924
|
-
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
3925
|
-
)
|
|
3926
|
-
SELECT
|
|
3927
|
-
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
3928
|
-
coalesce(daily.created, 0)::integer AS "created"
|
|
3929
|
-
FROM days
|
|
3930
|
-
LEFT JOIN daily ON daily.day = days.day
|
|
3931
|
-
ORDER BY days.day
|
|
3932
|
-
`);
|
|
3933
|
-
return z18.array(issueDaySchema).parse(queryRows3(result));
|
|
3934
|
-
}
|
|
3935
|
-
async function aggregateIssueRepositories(args) {
|
|
3936
|
-
const start = new Date(args.nowMs - 30 * DAY_MS3);
|
|
3937
|
-
const table = juniorGitHubIssues;
|
|
3938
|
-
const result = await args.db.execute(sql6`
|
|
3939
|
-
SELECT
|
|
3940
|
-
${table.repositoryFullName} AS "repository",
|
|
3941
|
-
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
3942
|
-
AS "created",
|
|
3943
|
-
count(*) FILTER (
|
|
3944
|
-
WHERE ${table.state} = 'closed'
|
|
3945
|
-
AND ${table.stateReason} = 'completed'
|
|
3946
|
-
AND ${table.closedAt} >= ${start}
|
|
3947
|
-
)::integer AS "closedCompleted",
|
|
3948
|
-
count(*) FILTER (
|
|
3949
|
-
WHERE ${table.state} = 'closed'
|
|
3950
|
-
AND ${table.stateReason} = 'duplicate'
|
|
3951
|
-
AND ${table.closedAt} >= ${start}
|
|
3952
|
-
)::integer AS "closedDuplicate",
|
|
3953
|
-
count(*) FILTER (
|
|
3954
|
-
WHERE ${table.state} = 'closed'
|
|
3955
|
-
AND ${table.stateReason} = 'not_planned'
|
|
3956
|
-
AND ${table.closedAt} >= ${start}
|
|
3957
|
-
)::integer AS "closedNotPlanned",
|
|
3958
|
-
count(*) FILTER (
|
|
3959
|
-
WHERE ${table.state} = 'closed'
|
|
3960
|
-
AND ${table.stateReason} IS NULL
|
|
3961
|
-
AND ${table.closedAt} >= ${start}
|
|
3962
|
-
)::integer AS "closedUnknown"
|
|
3963
|
-
FROM ${table}
|
|
3964
|
-
WHERE ${table.openedAt} >= ${start}
|
|
3965
|
-
OR ${table.closedAt} >= ${start}
|
|
3966
|
-
GROUP BY ${table.repositoryFullName}
|
|
3967
|
-
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
3968
|
-
LIMIT 25
|
|
3969
|
-
`);
|
|
3970
|
-
return z18.array(issueRepositoryStatsSchema).parse(queryRows3(result));
|
|
3971
|
-
}
|
|
3972
|
-
function formatPercent2(value) {
|
|
3973
|
-
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
3974
|
-
}
|
|
3975
|
-
function formatDuration(value) {
|
|
3976
|
-
if (value === void 0) return "\u2014";
|
|
3977
|
-
const hours = value / (60 * 60 * 1e3);
|
|
3978
|
-
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
|
3979
|
-
if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
|
|
3980
|
-
return `${Math.round(hours / 24 * 10) / 10}d`;
|
|
3981
|
-
}
|
|
3982
|
-
function startOfUtcDay2(timestampMs) {
|
|
3983
|
-
const date = new Date(timestampMs);
|
|
3984
|
-
date.setUTCHours(0, 0, 0, 0);
|
|
3985
|
-
return date;
|
|
3986
|
-
}
|
|
3987
|
-
async function buildGitHubOutcomeReport(args) {
|
|
3988
|
-
const [
|
|
3989
|
-
windows,
|
|
3990
|
-
pullRequestDays,
|
|
3991
|
-
repositories,
|
|
3992
|
-
issueWindows,
|
|
3993
|
-
issueDays,
|
|
3994
|
-
issueRepositories,
|
|
3995
|
-
costWindows,
|
|
3996
|
-
repositoryCosts
|
|
3997
|
-
] = await Promise.all([
|
|
3998
|
-
aggregatePullRequestWindows2(args),
|
|
3999
|
-
aggregatePullRequestDays(args),
|
|
4000
|
-
aggregatePullRequestRepositories(args),
|
|
4001
|
-
aggregateIssueWindows2(args),
|
|
4002
|
-
aggregateIssueDays(args),
|
|
4003
|
-
aggregateIssueRepositories(args),
|
|
4004
|
-
aggregateGitHubCostWindows({ ...args, windows: WINDOWS2 }),
|
|
4005
|
-
aggregateGitHubRepositoryCosts(args)
|
|
4006
|
-
]);
|
|
4007
|
-
const thirtyDays = windows.find((window) => window.days === 30);
|
|
4008
|
-
const issueThirtyDays = issueWindows.find((window) => window.days === 30);
|
|
4009
|
-
const costThirtyDays = costWindows.find((window) => window.days === 30);
|
|
4010
|
-
const repositoryCostByName = new Map(
|
|
4011
|
-
repositoryCosts.map((row) => [row.repository, row])
|
|
4012
|
-
);
|
|
4013
|
-
return {
|
|
4014
|
-
generatedAt: new Date(args.nowMs).toISOString(),
|
|
4015
|
-
title: "GitHub activity",
|
|
4016
|
-
metrics: [
|
|
4017
|
-
{
|
|
4018
|
-
label: "PR closure merge rate \xB7 30d",
|
|
4019
|
-
value: formatPercent2(thirtyDays.mergeRate)
|
|
4020
|
-
},
|
|
4021
|
-
{
|
|
4022
|
-
label: "Median PR merge time \xB7 merged in 30d",
|
|
4023
|
-
value: formatDuration(thirtyDays.medianMergeTimeMs)
|
|
4024
|
-
},
|
|
4025
|
-
{
|
|
4026
|
-
label: "Median issue close time \xB7 closed in 30d",
|
|
4027
|
-
value: formatDuration(issueThirtyDays.medianCloseTimeMs)
|
|
4028
|
-
},
|
|
4029
|
-
{
|
|
4030
|
-
label: "PR cost \xB7 opened in 30d",
|
|
4031
|
-
value: formatCostUsd(costThirtyDays.pullRequestCostUsd)
|
|
4032
|
-
},
|
|
4033
|
-
{
|
|
4034
|
-
label: "Median PR cost \xB7 opened in 30d",
|
|
4035
|
-
value: formatCostUsd(costThirtyDays.medianPullRequestCostUsd)
|
|
4036
|
-
},
|
|
4037
|
-
{
|
|
4038
|
-
label: "Issue cost \xB7 opened in 30d",
|
|
4039
|
-
value: formatCostUsd(costThirtyDays.issueCostUsd)
|
|
4040
|
-
},
|
|
4041
|
-
{
|
|
4042
|
-
label: "Median issue cost \xB7 opened in 30d",
|
|
4043
|
-
value: formatCostUsd(costThirtyDays.medianIssueCostUsd)
|
|
4044
|
-
}
|
|
4045
|
-
],
|
|
4046
|
-
widgets: [
|
|
4047
|
-
{
|
|
4048
|
-
id: "pull-requests-created",
|
|
4049
|
-
type: "bar_chart",
|
|
4050
|
-
title: "Pull requests created",
|
|
4051
|
-
description: "Junior-owned pull requests opened per day",
|
|
4052
|
-
timeRangeDays: [...WINDOWS2],
|
|
4053
|
-
series: [{ key: "created", label: "Created" }],
|
|
4054
|
-
categories: pullRequestDays.map((stats) => ({
|
|
4055
|
-
id: stats.date,
|
|
4056
|
-
label: stats.date,
|
|
4057
|
-
values: { created: stats.created }
|
|
4058
|
-
}))
|
|
4059
|
-
},
|
|
4060
|
-
{
|
|
4061
|
-
id: "issues-created",
|
|
4062
|
-
type: "bar_chart",
|
|
4063
|
-
title: "Issues created",
|
|
4064
|
-
description: "Junior-owned issues opened per day",
|
|
4065
|
-
timeRangeDays: [...WINDOWS2],
|
|
4066
|
-
series: [{ key: "created", label: "Created" }],
|
|
4067
|
-
categories: issueDays.map((stats) => ({
|
|
4068
|
-
id: stats.date,
|
|
4069
|
-
label: stats.date,
|
|
4070
|
-
values: { created: stats.created }
|
|
4071
|
-
}))
|
|
4072
|
-
}
|
|
4073
|
-
],
|
|
4074
|
-
recordSets: [
|
|
4075
|
-
{
|
|
4076
|
-
title: "Pull request repositories \xB7 30d",
|
|
4077
|
-
emptyText: "No Junior-owned pull request activity yet.",
|
|
4078
|
-
fields: [
|
|
4079
|
-
{ key: "repository", label: "Repository" },
|
|
4080
|
-
{ key: "created", label: "Created" },
|
|
4081
|
-
{ key: "juniorOnly", label: "Junior-only merges" },
|
|
4082
|
-
{ key: "merged", label: "Merged" },
|
|
4083
|
-
{ key: "closed", label: "Closed unmerged" },
|
|
4084
|
-
{ key: "mergeRate", label: "Closure merge rate" },
|
|
4085
|
-
{ key: "medianCost", label: "Median cost" }
|
|
4086
|
-
],
|
|
4087
|
-
records: repositories.map(({ repository, ...stats }) => ({
|
|
4088
|
-
id: repository,
|
|
4089
|
-
values: {
|
|
4090
|
-
repository,
|
|
4091
|
-
created: String(stats.created),
|
|
4092
|
-
merged: String(stats.merged),
|
|
4093
|
-
closed: String(stats.closed),
|
|
4094
|
-
juniorOnly: String(stats.juniorOnly),
|
|
4095
|
-
mergeRate: formatPercent2(stats.mergeRate),
|
|
4096
|
-
medianCost: formatCostUsd(
|
|
4097
|
-
repositoryCostByName.get(repository)?.medianPullRequestCostUsd
|
|
4098
|
-
)
|
|
4099
|
-
}
|
|
4100
|
-
}))
|
|
4101
|
-
},
|
|
4102
|
-
{
|
|
4103
|
-
title: "Issue repositories \xB7 30d",
|
|
4104
|
-
emptyText: "No Junior-owned issue activity yet.",
|
|
4105
|
-
fields: [
|
|
4106
|
-
{ key: "repository", label: "Repository" },
|
|
4107
|
-
{ key: "created", label: "Created" },
|
|
4108
|
-
{ key: "completed", label: "Completed" },
|
|
4109
|
-
{ key: "duplicate", label: "Duplicate" },
|
|
4110
|
-
{ key: "notPlanned", label: "Not planned" },
|
|
4111
|
-
{ key: "unknown", label: "Unknown reason" },
|
|
4112
|
-
{ key: "medianCost", label: "Median cost" }
|
|
4113
|
-
],
|
|
4114
|
-
records: issueRepositories.map(({ repository, ...stats }) => ({
|
|
4115
|
-
id: repository,
|
|
4116
|
-
values: {
|
|
4117
|
-
repository,
|
|
4118
|
-
created: String(stats.created),
|
|
4119
|
-
completed: String(stats.closedCompleted),
|
|
4120
|
-
duplicate: String(stats.closedDuplicate),
|
|
4121
|
-
notPlanned: String(stats.closedNotPlanned),
|
|
4122
|
-
unknown: String(stats.closedUnknown),
|
|
4123
|
-
medianCost: formatCostUsd(
|
|
4124
|
-
repositoryCostByName.get(repository)?.medianIssueCostUsd
|
|
4125
|
-
)
|
|
4126
|
-
}
|
|
4127
|
-
}))
|
|
4128
|
-
}
|
|
4129
|
-
]
|
|
4130
|
-
};
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
3077
|
// src/pull-request-outcomes/commit-composition.ts
|
|
4134
|
-
import { z as
|
|
4135
|
-
var canonicalCommitSchema =
|
|
4136
|
-
authorEmail:
|
|
4137
|
-
authorLogin:
|
|
3078
|
+
import { z as z16 } from "zod";
|
|
3079
|
+
var canonicalCommitSchema = z16.object({
|
|
3080
|
+
authorEmail: z16.string().nullable(),
|
|
3081
|
+
authorLogin: z16.string().nullable()
|
|
4138
3082
|
}).strict();
|
|
4139
|
-
var providerCommitSchema =
|
|
4140
|
-
author:
|
|
4141
|
-
commit:
|
|
4142
|
-
author:
|
|
3083
|
+
var providerCommitSchema = z16.object({
|
|
3084
|
+
author: z16.object({ login: z16.string() }).passthrough().nullable(),
|
|
3085
|
+
commit: z16.object({
|
|
3086
|
+
author: z16.object({ email: z16.string() }).passthrough().nullable()
|
|
4143
3087
|
}).passthrough()
|
|
4144
3088
|
}).passthrough();
|
|
4145
|
-
var commitPageSchema =
|
|
3089
|
+
var commitPageSchema = z16.array(providerCommitSchema).transform(
|
|
4146
3090
|
(commits) => commits.map(
|
|
4147
3091
|
(commit) => canonicalCommitSchema.parse({
|
|
4148
3092
|
authorEmail: commit.commit.author?.email ?? null,
|
|
@@ -5079,15 +4023,6 @@ function reviewThreadResolveRepository(operation, method, upstreamUrl, bodyText)
|
|
|
5079
4023
|
}
|
|
5080
4024
|
return repository;
|
|
5081
4025
|
}
|
|
5082
|
-
function repositoryLeaseScopeFromRef(repository) {
|
|
5083
|
-
const [owner, name] = repository.split("/");
|
|
5084
|
-
if (!owner || !name) {
|
|
5085
|
-
throw new EgressPolicyDenied2(
|
|
5086
|
-
"GitHub review thread resolution does not identify a target repository."
|
|
5087
|
-
);
|
|
5088
|
-
}
|
|
5089
|
-
return githubRepositoryLeaseScope({ owner, name });
|
|
5090
|
-
}
|
|
5091
4026
|
function isGitHubGraphqlMutation(method, upstreamUrl, bodyText, field) {
|
|
5092
4027
|
if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl)) return false;
|
|
5093
4028
|
const parsed = parseGitHubGraphqlRequest(bodyText);
|
|
@@ -5120,23 +4055,21 @@ function applyGitHubEgressPolicy(input) {
|
|
|
5120
4055
|
denialMessage: write.denialMessage
|
|
5121
4056
|
});
|
|
5122
4057
|
}
|
|
5123
|
-
function grantForAccess(access, reason, name
|
|
4058
|
+
function grantForAccess(access, reason, name) {
|
|
5124
4059
|
return {
|
|
5125
4060
|
name,
|
|
5126
4061
|
access,
|
|
5127
|
-
...leaseScope ? { leaseScope } : void 0,
|
|
5128
4062
|
reason,
|
|
5129
4063
|
...name === "user-write" ? { requirements: USER_WRITE_REQUIREMENTS } : void 0
|
|
5130
4064
|
};
|
|
5131
4065
|
}
|
|
5132
|
-
function
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
throw new EgressPolicyDenied2(
|
|
5136
|
-
"GitHub write request does not identify a target repository."
|
|
5137
|
-
);
|
|
4066
|
+
function requireRepositoryTarget(upstreamUrl) {
|
|
4067
|
+
if (githubRepositoryFromUrl(upstreamUrl)) {
|
|
4068
|
+
return;
|
|
5138
4069
|
}
|
|
5139
|
-
|
|
4070
|
+
throw new EgressPolicyDenied2(
|
|
4071
|
+
"GitHub write request does not identify a target repository."
|
|
4072
|
+
);
|
|
5140
4073
|
}
|
|
5141
4074
|
async function githubGrantForEgress(ctx) {
|
|
5142
4075
|
const method = ctx.request.method.toUpperCase();
|
|
@@ -5153,11 +4086,11 @@ async function githubGrantForEgress(ctx) {
|
|
|
5153
4086
|
const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
|
|
5154
4087
|
if (smartHttpAccess) {
|
|
5155
4088
|
if (smartHttpAccess === "write") {
|
|
4089
|
+
requireRepositoryTarget(upstreamUrl);
|
|
5156
4090
|
return grantForAccess(
|
|
5157
4091
|
"write",
|
|
5158
4092
|
"github.installation-write",
|
|
5159
|
-
"installation-write"
|
|
5160
|
-
repositoryLeaseScope(upstreamUrl)
|
|
4093
|
+
"installation-write"
|
|
5161
4094
|
);
|
|
5162
4095
|
}
|
|
5163
4096
|
return grantForAccess(
|
|
@@ -5172,11 +4105,13 @@ async function githubGrantForEgress(ctx) {
|
|
|
5172
4105
|
}
|
|
5173
4106
|
const writeGrantName = githubApiWriteGrantName(method, upstreamUrl);
|
|
5174
4107
|
if (writeGrantName) {
|
|
4108
|
+
if (writeGrantName === "installation-write") {
|
|
4109
|
+
requireRepositoryTarget(upstreamUrl);
|
|
4110
|
+
}
|
|
5175
4111
|
return grantForAccess(
|
|
5176
4112
|
"write",
|
|
5177
4113
|
writeGrantName === "user-write" ? "github.user-write" : "github.installation-write",
|
|
5178
|
-
writeGrantName
|
|
5179
|
-
repositoryLeaseScope(upstreamUrl)
|
|
4114
|
+
writeGrantName
|
|
5180
4115
|
);
|
|
5181
4116
|
}
|
|
5182
4117
|
const reviewThreadRepository = reviewThreadResolveRepository(
|
|
@@ -5189,8 +4124,7 @@ async function githubGrantForEgress(ctx) {
|
|
|
5189
4124
|
return grantForAccess(
|
|
5190
4125
|
"write",
|
|
5191
4126
|
"github.installation-write",
|
|
5192
|
-
"installation-write"
|
|
5193
|
-
repositoryLeaseScopeFromRef(reviewThreadRepository)
|
|
4127
|
+
"installation-write"
|
|
5194
4128
|
);
|
|
5195
4129
|
}
|
|
5196
4130
|
const graphqlAccess = githubGraphqlAccess(
|
|
@@ -5201,7 +4135,7 @@ async function githubGrantForEgress(ctx) {
|
|
|
5201
4135
|
if (graphqlAccess) {
|
|
5202
4136
|
if (graphqlAccess === "write") {
|
|
5203
4137
|
throw new EgressPolicyDenied2(
|
|
5204
|
-
"GitHub GraphQL mutations are not enabled for
|
|
4138
|
+
"GitHub GraphQL mutations are not enabled for runtime credentials."
|
|
5205
4139
|
);
|
|
5206
4140
|
}
|
|
5207
4141
|
return grantForAccess(
|
|
@@ -5396,19 +4330,6 @@ function githubPlugin(options = {}) {
|
|
|
5396
4330
|
})
|
|
5397
4331
|
];
|
|
5398
4332
|
},
|
|
5399
|
-
async operationalReport(ctx) {
|
|
5400
|
-
return await buildGitHubOutcomeReport({
|
|
5401
|
-
db: ctx.db,
|
|
5402
|
-
nowMs: ctx.nowMs
|
|
5403
|
-
});
|
|
5404
|
-
},
|
|
5405
|
-
async profileReport(ctx) {
|
|
5406
|
-
return await buildGitHubProfileReport({
|
|
5407
|
-
db: ctx.db,
|
|
5408
|
-
nowMs: ctx.nowMs,
|
|
5409
|
-
userId: ctx.subject.id
|
|
5410
|
-
});
|
|
5411
|
-
},
|
|
5412
4333
|
tools(ctx) {
|
|
5413
4334
|
return createGitHubTools(
|
|
5414
4335
|
ctx,
|
|
@@ -5482,16 +4403,10 @@ function githubPlugin(options = {}) {
|
|
|
5482
4403
|
});
|
|
5483
4404
|
}
|
|
5484
4405
|
if (ctx.grant.name === "installation-write") {
|
|
5485
|
-
const repository = githubRepositoryFromLeaseScope(
|
|
5486
|
-
ctx.grant.leaseScope
|
|
5487
|
-
);
|
|
5488
4406
|
return await issueInstallationCredential({
|
|
5489
4407
|
appIdEnv,
|
|
5490
4408
|
privateKeyEnv,
|
|
5491
|
-
installationIdEnv
|
|
5492
|
-
// This repository-only variant cannot downscope the installed
|
|
5493
|
-
// App envelope with an operation-specific permission body.
|
|
5494
|
-
repositories: [repository.name]
|
|
4409
|
+
installationIdEnv
|
|
5495
4410
|
});
|
|
5496
4411
|
}
|
|
5497
4412
|
if (USER_TOKEN_GRANTS.has(ctx.grant.name)) {
|