@sentry/junior-github 0.184.0 → 0.186.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/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-KXAUXJ3Q.js";
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. Junior appends the conversation footer."
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. Junior appends the conversation footer.").optional(),
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 Junior conversation footer. Use this instead of shelling out to gh issue create when creating issues.",
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 Junior could not persist the completed issue state.",
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. Junior appends the conversation footer."
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. Junior appends the conversation footer.").optional(),
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 Junior conversation footer. Use this instead of shelling out to gh pr create when creating pull requests.",
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 Junior could not persist the completed pull request state.",
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. Junior appends requester attribution and the conversation footer."
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. Junior appends requester attribution and the conversation footer."
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 for repository-scoped credentials (GraphQL has no repo path).'
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 Junior authored.",
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
- "Junior can only resolve review threads on pull requests it authored."
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 z19 } from "zod";
4135
- var canonicalCommitSchema = z19.object({
4136
- authorEmail: z19.string().nullable(),
4137
- authorLogin: z19.string().nullable()
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 = z19.object({
4140
- author: z19.object({ login: z19.string() }).passthrough().nullable(),
4141
- commit: z19.object({
4142
- author: z19.object({ email: z19.string() }).passthrough().nullable()
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 = z19.array(providerCommitSchema).transform(
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, leaseScope) {
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 repositoryLeaseScope(upstreamUrl) {
5133
- const repository = githubRepositoryFromUrl(upstreamUrl);
5134
- if (!repository) {
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
- return githubRepositoryLeaseScope(repository);
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 Junior credentials."
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)) {