@sentry/junior-github 0.159.0 → 0.161.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 +2 -0
- package/dist/annotations.d.ts +3 -0
- package/dist/index.js +556 -112
- package/dist/issue-outcomes/store.d.ts +6 -1
- package/dist/outcomes/profile-report.d.ts +11 -0
- package/dist/reply-markdown.d.ts +2 -0
- package/package.json +2 -2
- package/skills/github-code/SKILL.md +0 -2
- package/skills/github-code/references/api-surface.md +1 -0
- package/skills/github-issues/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -29,3 +29,5 @@ The plugin owns its signed webhook route, deployment, pull request, and release
|
|
|
29
29
|
resource events, normalized pull request and issue outcome projections, and
|
|
30
30
|
dashboard operational report. Core only owns delivery from plugin-published
|
|
31
31
|
resource events into matching conversation subscriptions.
|
|
32
|
+
|
|
33
|
+
Person profiles call `hooks.profileReport` for Junior-owned GitHub work attributed through conversation actors.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ConversationAnnotation, ConversationSidebarAnnotation } from "@sentry/junior-plugin-api";
|
|
2
|
+
/** Select the one GitHub annotation summary shown in a conversation row. */
|
|
3
|
+
export declare function githubSidebarAnnotation(annotations: ConversationAnnotation[]): ConversationSidebarAnnotation | undefined;
|
package/dist/index.js
CHANGED
|
@@ -2695,19 +2695,30 @@ async function recordGitHubIssueOutcome(db, input) {
|
|
|
2695
2695
|
const outcome = githubIssueOutcomeInputSchema.parse(input);
|
|
2696
2696
|
const values = projectionValues(outcome);
|
|
2697
2697
|
if (!outcome.candidateOwned) {
|
|
2698
|
-
await db.update(juniorGitHubIssues).set(values).where(
|
|
2698
|
+
const updated = await db.update(juniorGitHubIssues).set(values).where(
|
|
2699
2699
|
and(
|
|
2700
2700
|
eq(juniorGitHubIssues.issueId, outcome.issueId),
|
|
2701
2701
|
lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
|
|
2702
2702
|
)
|
|
2703
|
-
)
|
|
2704
|
-
|
|
2703
|
+
).returning({
|
|
2704
|
+
conversationIds: juniorGitHubIssues.conversationIds
|
|
2705
|
+
});
|
|
2706
|
+
return {
|
|
2707
|
+
applied: updated.length > 0,
|
|
2708
|
+
conversationIds: updated[0]?.conversationIds ?? []
|
|
2709
|
+
};
|
|
2705
2710
|
}
|
|
2706
|
-
await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
|
|
2711
|
+
const inserted = await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
|
|
2707
2712
|
target: juniorGitHubIssues.issueId,
|
|
2708
2713
|
set: values,
|
|
2709
2714
|
where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
|
|
2715
|
+
}).returning({
|
|
2716
|
+
conversationIds: juniorGitHubIssues.conversationIds
|
|
2710
2717
|
});
|
|
2718
|
+
return {
|
|
2719
|
+
applied: inserted.length > 0,
|
|
2720
|
+
conversationIds: inserted[0]?.conversationIds ?? []
|
|
2721
|
+
};
|
|
2711
2722
|
}
|
|
2712
2723
|
async function recordGitHubIssueConversations(db, input) {
|
|
2713
2724
|
const association = githubIssueConversationsInputSchema.parse(input);
|
|
@@ -3342,7 +3353,23 @@ function createGitHubWebhookRoute(args) {
|
|
|
3342
3353
|
}
|
|
3343
3354
|
}
|
|
3344
3355
|
if (issueOutcome) {
|
|
3345
|
-
await recordGitHubIssueOutcome(
|
|
3356
|
+
const recordedOutcome = await recordGitHubIssueOutcome(
|
|
3357
|
+
args.db,
|
|
3358
|
+
issueOutcome
|
|
3359
|
+
);
|
|
3360
|
+
if (recordedOutcome.applied && issueOutcome.state === "closed") {
|
|
3361
|
+
await Promise.all(
|
|
3362
|
+
recordedOutcome.conversationIds.map(
|
|
3363
|
+
(conversationId) => args.annotations.forConversation(conversationId).upsert({
|
|
3364
|
+
kind: "resource_link",
|
|
3365
|
+
key: `${issueOutcome.repositoryFullName.toLowerCase()}#${issueOutcome.number}`,
|
|
3366
|
+
label: `${issueOutcome.repositoryFullName}#${issueOutcome.number}`,
|
|
3367
|
+
url: `https://github.com/${issueOutcome.repositoryFullName}/issues/${issueOutcome.number}`,
|
|
3368
|
+
status: "closed"
|
|
3369
|
+
})
|
|
3370
|
+
)
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3346
3373
|
}
|
|
3347
3374
|
const recordedIssueConversations = issueConversations ? await recordGitHubIssueConversations(args.db, issueConversations) : false;
|
|
3348
3375
|
const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
|
|
@@ -3371,20 +3398,258 @@ function createGitHubWebhookRoute(args) {
|
|
|
3371
3398
|
};
|
|
3372
3399
|
}
|
|
3373
3400
|
|
|
3374
|
-
// src/outcomes/report.ts
|
|
3375
|
-
import { sql as sql5 } from "drizzle-orm";
|
|
3376
|
-
import { z as z16 } from "zod";
|
|
3377
|
-
|
|
3378
|
-
// src/outcomes/cost.ts
|
|
3401
|
+
// src/outcomes/profile-report.ts
|
|
3379
3402
|
import { sql as sql4 } from "drizzle-orm";
|
|
3380
3403
|
import { z as z15 } from "zod";
|
|
3381
3404
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3382
|
-
var
|
|
3405
|
+
var WINDOWS = [7, 30, 90];
|
|
3406
|
+
var pullRequestStatsSchema = z15.object({
|
|
3407
|
+
closed: z15.number().int().nonnegative(),
|
|
3408
|
+
created: z15.number().int().nonnegative(),
|
|
3383
3409
|
days: z15.number().int().positive(),
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3410
|
+
merged: z15.number().int().nonnegative()
|
|
3411
|
+
}).strict().transform((row) => {
|
|
3412
|
+
const terminal = row.merged + row.closed;
|
|
3413
|
+
return {
|
|
3414
|
+
...row,
|
|
3415
|
+
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3416
|
+
};
|
|
3417
|
+
});
|
|
3418
|
+
var issueStatsSchema = z15.object({
|
|
3419
|
+
created: z15.number().int().nonnegative(),
|
|
3420
|
+
days: z15.number().int().positive()
|
|
3421
|
+
}).strict();
|
|
3422
|
+
var daySchema = z15.object({
|
|
3423
|
+
created: z15.number().int().nonnegative(),
|
|
3424
|
+
date: z15.string().date()
|
|
3425
|
+
}).strict();
|
|
3426
|
+
function queryRows(result) {
|
|
3427
|
+
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3428
|
+
throw new TypeError("GitHub profile report query did not return rows");
|
|
3429
|
+
}
|
|
3430
|
+
return result.rows;
|
|
3431
|
+
}
|
|
3432
|
+
function ownedByUserSql(conversationIds, userId) {
|
|
3433
|
+
return sql4`EXISTS (
|
|
3434
|
+
SELECT 1
|
|
3435
|
+
FROM unnest(${conversationIds}) AS linked(conversation_id)
|
|
3436
|
+
INNER JOIN junior_conversations AS conversations
|
|
3437
|
+
ON conversations.conversation_id = linked.conversation_id
|
|
3438
|
+
INNER JOIN junior_identities AS identities
|
|
3439
|
+
ON identities.id = conversations.actor_identity_id
|
|
3440
|
+
WHERE identities.user_id = ${userId}
|
|
3441
|
+
)`;
|
|
3442
|
+
}
|
|
3443
|
+
function formatPercent(value) {
|
|
3444
|
+
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
3445
|
+
}
|
|
3446
|
+
function startOfUtcDay(timestampMs) {
|
|
3447
|
+
const date = new Date(timestampMs);
|
|
3448
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
3449
|
+
return date;
|
|
3450
|
+
}
|
|
3451
|
+
async function aggregatePullRequestWindows(args) {
|
|
3452
|
+
const starts = WINDOWS.map(
|
|
3453
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
3454
|
+
);
|
|
3455
|
+
const oldestStart = starts.at(-1)[1];
|
|
3456
|
+
const table = juniorGitHubPullRequests;
|
|
3457
|
+
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3458
|
+
const result = await args.db.execute(sql4`
|
|
3459
|
+
WITH windows(days, start_at) AS (
|
|
3460
|
+
VALUES
|
|
3461
|
+
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3462
|
+
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3463
|
+
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3464
|
+
), recent_pull_requests AS MATERIALIZED (
|
|
3465
|
+
SELECT
|
|
3466
|
+
${table.pullRequestId},
|
|
3467
|
+
${table.state},
|
|
3468
|
+
${table.openedAt},
|
|
3469
|
+
${table.mergedAt},
|
|
3470
|
+
${table.closedAt}
|
|
3471
|
+
FROM ${table}
|
|
3472
|
+
WHERE (${table.openedAt} >= ${oldestStart}
|
|
3473
|
+
OR ${table.mergedAt} >= ${oldestStart}
|
|
3474
|
+
OR ${table.closedAt} >= ${oldestStart})
|
|
3475
|
+
AND ${owned}
|
|
3476
|
+
)
|
|
3477
|
+
SELECT
|
|
3478
|
+
windows.days AS "days",
|
|
3479
|
+
count(recent_pull_requests.pull_request_id)
|
|
3480
|
+
FILTER (WHERE recent_pull_requests.opened_at >= windows.start_at)::integer
|
|
3481
|
+
AS "created",
|
|
3482
|
+
count(recent_pull_requests.pull_request_id)
|
|
3483
|
+
FILTER (
|
|
3484
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
3485
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
3486
|
+
)::integer AS "merged",
|
|
3487
|
+
count(recent_pull_requests.pull_request_id)
|
|
3488
|
+
FILTER (
|
|
3489
|
+
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
3490
|
+
AND recent_pull_requests.closed_at >= windows.start_at
|
|
3491
|
+
)::integer AS "closed"
|
|
3492
|
+
FROM windows
|
|
3493
|
+
LEFT JOIN recent_pull_requests ON true
|
|
3494
|
+
GROUP BY windows.days
|
|
3495
|
+
ORDER BY windows.days
|
|
3496
|
+
`);
|
|
3497
|
+
return z15.array(pullRequestStatsSchema).parse(queryRows(result));
|
|
3498
|
+
}
|
|
3499
|
+
async function aggregateIssueWindows(args) {
|
|
3500
|
+
const starts = WINDOWS.map(
|
|
3501
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
3502
|
+
);
|
|
3503
|
+
const oldestStart = starts.at(-1)[1];
|
|
3504
|
+
const table = juniorGitHubIssues;
|
|
3505
|
+
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3506
|
+
const result = await args.db.execute(sql4`
|
|
3507
|
+
WITH windows(days, start_at) AS (
|
|
3508
|
+
VALUES
|
|
3509
|
+
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
3510
|
+
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
3511
|
+
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
3512
|
+
), recent_issues AS MATERIALIZED (
|
|
3513
|
+
SELECT
|
|
3514
|
+
${table.issueId},
|
|
3515
|
+
${table.openedAt}
|
|
3516
|
+
FROM ${table}
|
|
3517
|
+
WHERE ${table.openedAt} >= ${oldestStart}
|
|
3518
|
+
AND ${owned}
|
|
3519
|
+
)
|
|
3520
|
+
SELECT
|
|
3521
|
+
windows.days AS "days",
|
|
3522
|
+
count(recent_issues.issue_id)
|
|
3523
|
+
FILTER (WHERE recent_issues.opened_at >= windows.start_at)::integer
|
|
3524
|
+
AS "created"
|
|
3525
|
+
FROM windows
|
|
3526
|
+
LEFT JOIN recent_issues ON true
|
|
3527
|
+
GROUP BY windows.days
|
|
3528
|
+
ORDER BY windows.days
|
|
3529
|
+
`);
|
|
3530
|
+
return z15.array(issueStatsSchema).parse(queryRows(result));
|
|
3531
|
+
}
|
|
3532
|
+
async function aggregateOpenedDays(args) {
|
|
3533
|
+
const end = new Date(args.nowMs);
|
|
3534
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
|
|
3535
|
+
const table = args.table;
|
|
3536
|
+
const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
|
|
3537
|
+
const result = await args.db.execute(sql4`
|
|
3538
|
+
WITH days AS (
|
|
3539
|
+
SELECT generate_series(
|
|
3540
|
+
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
3541
|
+
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
3542
|
+
interval '1 day'
|
|
3543
|
+
) AS day
|
|
3544
|
+
), daily AS (
|
|
3545
|
+
SELECT
|
|
3546
|
+
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
3547
|
+
count(*)::integer AS created
|
|
3548
|
+
FROM ${table}
|
|
3549
|
+
WHERE ${table.openedAt} >= ${start}
|
|
3550
|
+
AND ${owned}
|
|
3551
|
+
GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
|
|
3552
|
+
)
|
|
3553
|
+
SELECT
|
|
3554
|
+
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
3555
|
+
coalesce(daily.created, 0)::integer AS "created"
|
|
3556
|
+
FROM days
|
|
3557
|
+
LEFT JOIN daily ON daily.day = days.day
|
|
3558
|
+
ORDER BY days.day
|
|
3559
|
+
`);
|
|
3560
|
+
return z15.array(daySchema).parse(queryRows(result));
|
|
3561
|
+
}
|
|
3562
|
+
async function buildGitHubProfileReport(args) {
|
|
3563
|
+
const [windows, pullRequestDays, issueWindows, issueDays] = await Promise.all(
|
|
3564
|
+
[
|
|
3565
|
+
aggregatePullRequestWindows(args),
|
|
3566
|
+
aggregateOpenedDays({
|
|
3567
|
+
db: args.db,
|
|
3568
|
+
nowMs: args.nowMs,
|
|
3569
|
+
table: juniorGitHubPullRequests,
|
|
3570
|
+
userId: args.userId
|
|
3571
|
+
}),
|
|
3572
|
+
aggregateIssueWindows(args),
|
|
3573
|
+
aggregateOpenedDays({
|
|
3574
|
+
db: args.db,
|
|
3575
|
+
nowMs: args.nowMs,
|
|
3576
|
+
table: juniorGitHubIssues,
|
|
3577
|
+
userId: args.userId
|
|
3578
|
+
})
|
|
3579
|
+
]
|
|
3580
|
+
);
|
|
3581
|
+
const thirtyDays = windows.find((window) => window.days === 30);
|
|
3582
|
+
const issueThirtyDays = issueWindows.find((window) => window.days === 30);
|
|
3583
|
+
const hasActivity = windows.some((window) => window.created + window.merged + window.closed > 0) || issueWindows.some((window) => window.created > 0);
|
|
3584
|
+
if (!hasActivity) {
|
|
3585
|
+
return void 0;
|
|
3586
|
+
}
|
|
3587
|
+
return {
|
|
3588
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
3589
|
+
title: "GitHub",
|
|
3590
|
+
metrics: [
|
|
3591
|
+
{
|
|
3592
|
+
label: "PRs opened \xB7 30d",
|
|
3593
|
+
value: String(thirtyDays.created)
|
|
3594
|
+
},
|
|
3595
|
+
{
|
|
3596
|
+
label: "PRs merged \xB7 30d",
|
|
3597
|
+
value: String(thirtyDays.merged)
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
label: "Issues opened \xB7 30d",
|
|
3601
|
+
value: String(issueThirtyDays.created)
|
|
3602
|
+
},
|
|
3603
|
+
{
|
|
3604
|
+
label: "PR merge rate \xB7 30d",
|
|
3605
|
+
value: formatPercent(thirtyDays.mergeRate)
|
|
3606
|
+
}
|
|
3607
|
+
],
|
|
3608
|
+
widgets: [
|
|
3609
|
+
{
|
|
3610
|
+
id: "pull-requests-created",
|
|
3611
|
+
type: "bar_chart",
|
|
3612
|
+
title: "Pull requests opened",
|
|
3613
|
+
description: "Junior-owned pull requests opened for this person per day",
|
|
3614
|
+
timeRangeDays: [...WINDOWS],
|
|
3615
|
+
series: [{ key: "created", label: "Opened" }],
|
|
3616
|
+
categories: pullRequestDays.map((stats) => ({
|
|
3617
|
+
id: stats.date,
|
|
3618
|
+
label: stats.date,
|
|
3619
|
+
values: { created: stats.created }
|
|
3620
|
+
}))
|
|
3621
|
+
},
|
|
3622
|
+
{
|
|
3623
|
+
id: "issues-created",
|
|
3624
|
+
type: "bar_chart",
|
|
3625
|
+
title: "Issues opened",
|
|
3626
|
+
description: "Junior-owned issues opened for this person per day",
|
|
3627
|
+
timeRangeDays: [...WINDOWS],
|
|
3628
|
+
series: [{ key: "created", label: "Opened" }],
|
|
3629
|
+
categories: issueDays.map((stats) => ({
|
|
3630
|
+
id: stats.date,
|
|
3631
|
+
label: stats.date,
|
|
3632
|
+
values: { created: stats.created }
|
|
3633
|
+
}))
|
|
3634
|
+
}
|
|
3635
|
+
]
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// src/outcomes/report.ts
|
|
3640
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
3641
|
+
import { z as z17 } from "zod";
|
|
3642
|
+
|
|
3643
|
+
// src/outcomes/cost.ts
|
|
3644
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
3645
|
+
import { z as z16 } from "zod";
|
|
3646
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
3647
|
+
var costWindowSchema = z16.object({
|
|
3648
|
+
days: z16.number().int().positive(),
|
|
3649
|
+
issueCostUsd: z16.number().nonnegative().nullable(),
|
|
3650
|
+
medianIssueCostUsd: z16.number().nonnegative().nullable(),
|
|
3651
|
+
medianPullRequestCostUsd: z16.number().nonnegative().nullable(),
|
|
3652
|
+
pullRequestCostUsd: z16.number().nonnegative().nullable()
|
|
3388
3653
|
}).strict().transform((row) => ({
|
|
3389
3654
|
days: row.days,
|
|
3390
3655
|
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
@@ -3392,12 +3657,12 @@ var costWindowSchema = z15.object({
|
|
|
3392
3657
|
medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
|
|
3393
3658
|
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
|
|
3394
3659
|
}));
|
|
3395
|
-
var repositoryCostSchema =
|
|
3396
|
-
issueCostUsd:
|
|
3397
|
-
medianIssueCostUsd:
|
|
3398
|
-
medianPullRequestCostUsd:
|
|
3399
|
-
pullRequestCostUsd:
|
|
3400
|
-
repository:
|
|
3660
|
+
var repositoryCostSchema = z16.object({
|
|
3661
|
+
issueCostUsd: z16.number().nonnegative().nullable(),
|
|
3662
|
+
medianIssueCostUsd: z16.number().nonnegative().nullable(),
|
|
3663
|
+
medianPullRequestCostUsd: z16.number().nonnegative().nullable(),
|
|
3664
|
+
pullRequestCostUsd: z16.number().nonnegative().nullable(),
|
|
3665
|
+
repository: z16.string().min(1)
|
|
3401
3666
|
}).strict().transform((row) => ({
|
|
3402
3667
|
issueCostUsd: row.issueCostUsd ?? void 0,
|
|
3403
3668
|
medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
|
|
@@ -3405,17 +3670,17 @@ var repositoryCostSchema = z15.object({
|
|
|
3405
3670
|
pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
|
|
3406
3671
|
repository: row.repository
|
|
3407
3672
|
}));
|
|
3408
|
-
function
|
|
3673
|
+
function queryRows2(result) {
|
|
3409
3674
|
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3410
3675
|
throw new TypeError("GitHub cost query did not return rows");
|
|
3411
3676
|
}
|
|
3412
3677
|
return result.rows;
|
|
3413
3678
|
}
|
|
3414
3679
|
async function hasConversationUsageTable(db) {
|
|
3415
|
-
const result = await db.execute(
|
|
3680
|
+
const result = await db.execute(sql5`
|
|
3416
3681
|
SELECT to_regclass('public.junior_conversations') IS NOT NULL AS "present"
|
|
3417
3682
|
`);
|
|
3418
|
-
const row =
|
|
3683
|
+
const row = queryRows2(result)[0];
|
|
3419
3684
|
return typeof row === "object" && row !== null && "present" in row && row.present === true;
|
|
3420
3685
|
}
|
|
3421
3686
|
function emptyCostWindows(windows) {
|
|
@@ -3428,7 +3693,7 @@ function emptyCostWindows(windows) {
|
|
|
3428
3693
|
}));
|
|
3429
3694
|
}
|
|
3430
3695
|
function conversationTreeCostExpr() {
|
|
3431
|
-
return
|
|
3696
|
+
return sql5`
|
|
3432
3697
|
coalesce((
|
|
3433
3698
|
SELECT sum(
|
|
3434
3699
|
CASE
|
|
@@ -3459,7 +3724,7 @@ function conversationTreeCostExpr() {
|
|
|
3459
3724
|
function pullRequestConversationIdsExpr() {
|
|
3460
3725
|
const pullRequests = juniorGitHubPullRequests;
|
|
3461
3726
|
const issues = juniorGitHubIssues;
|
|
3462
|
-
return
|
|
3727
|
+
return sql5`
|
|
3463
3728
|
ARRAY(
|
|
3464
3729
|
SELECT DISTINCT unnest(
|
|
3465
3730
|
${pullRequests.conversationIds}
|
|
@@ -3481,7 +3746,7 @@ function pullRequestConversationIdsExpr() {
|
|
|
3481
3746
|
function issueConversationIdsExpr() {
|
|
3482
3747
|
const pullRequests = juniorGitHubPullRequests;
|
|
3483
3748
|
const issues = juniorGitHubIssues;
|
|
3484
|
-
return
|
|
3749
|
+
return sql5`
|
|
3485
3750
|
ARRAY(
|
|
3486
3751
|
SELECT DISTINCT unnest(
|
|
3487
3752
|
${issues.conversationIds}
|
|
@@ -3505,21 +3770,21 @@ async function aggregateGitHubCostWindows(args) {
|
|
|
3505
3770
|
return emptyCostWindows(args.windows);
|
|
3506
3771
|
}
|
|
3507
3772
|
const starts = args.windows.map(
|
|
3508
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
3773
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS2)]
|
|
3509
3774
|
);
|
|
3510
3775
|
const oldestStart = starts.at(-1)[1];
|
|
3511
3776
|
const pullRequests = juniorGitHubPullRequests;
|
|
3512
3777
|
const issues = juniorGitHubIssues;
|
|
3513
|
-
const windowValues =
|
|
3778
|
+
const windowValues = sql5.join(
|
|
3514
3779
|
starts.map(
|
|
3515
|
-
([days, start]) =>
|
|
3780
|
+
([days, start]) => sql5`(${days}::integer, ${start}::timestamptz)`
|
|
3516
3781
|
),
|
|
3517
|
-
|
|
3782
|
+
sql5`, `
|
|
3518
3783
|
);
|
|
3519
3784
|
const conversationTreeCost = conversationTreeCostExpr();
|
|
3520
3785
|
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
3521
3786
|
const issueConversationIds = issueConversationIdsExpr();
|
|
3522
|
-
const result = await args.db.execute(
|
|
3787
|
+
const result = await args.db.execute(sql5`
|
|
3523
3788
|
WITH windows(days, start_at) AS (
|
|
3524
3789
|
VALUES ${windowValues}
|
|
3525
3790
|
), pull_request_entities AS (
|
|
@@ -3602,19 +3867,19 @@ async function aggregateGitHubCostWindows(args) {
|
|
|
3602
3867
|
INNER JOIN issue_window ON issue_window.days = pull_request_window.days
|
|
3603
3868
|
ORDER BY pull_request_window.days
|
|
3604
3869
|
`);
|
|
3605
|
-
return
|
|
3870
|
+
return z16.array(costWindowSchema).parse(queryRows2(result));
|
|
3606
3871
|
}
|
|
3607
3872
|
async function aggregateGitHubRepositoryCosts(args) {
|
|
3608
3873
|
if (!await hasConversationUsageTable(args.db)) {
|
|
3609
3874
|
return [];
|
|
3610
3875
|
}
|
|
3611
|
-
const start = new Date(args.nowMs - 30 *
|
|
3876
|
+
const start = new Date(args.nowMs - 30 * DAY_MS2);
|
|
3612
3877
|
const pullRequests = juniorGitHubPullRequests;
|
|
3613
3878
|
const issues = juniorGitHubIssues;
|
|
3614
3879
|
const conversationTreeCost = conversationTreeCostExpr();
|
|
3615
3880
|
const pullRequestConversationIds = pullRequestConversationIdsExpr();
|
|
3616
3881
|
const issueConversationIds = issueConversationIdsExpr();
|
|
3617
|
-
const result = await args.db.execute(
|
|
3882
|
+
const result = await args.db.execute(sql5`
|
|
3618
3883
|
WITH pull_request_entities AS (
|
|
3619
3884
|
SELECT
|
|
3620
3885
|
${pullRequests.repositoryFullName} AS repository,
|
|
@@ -3700,7 +3965,7 @@ async function aggregateGitHubRepositoryCosts(args) {
|
|
|
3700
3965
|
ON issue_totals.repository = repositories.repository
|
|
3701
3966
|
ORDER BY "repository" ASC
|
|
3702
3967
|
`);
|
|
3703
|
-
return
|
|
3968
|
+
return z16.array(repositoryCostSchema).parse(queryRows2(result));
|
|
3704
3969
|
}
|
|
3705
3970
|
function formatCostUsd(value) {
|
|
3706
3971
|
if (value === void 0) return "\u2014";
|
|
@@ -3713,14 +3978,14 @@ function formatCostUsd(value) {
|
|
|
3713
3978
|
}
|
|
3714
3979
|
|
|
3715
3980
|
// src/outcomes/report.ts
|
|
3716
|
-
var
|
|
3717
|
-
var
|
|
3718
|
-
var
|
|
3719
|
-
closed:
|
|
3720
|
-
created:
|
|
3721
|
-
days:
|
|
3722
|
-
medianMergeTimeMs:
|
|
3723
|
-
merged:
|
|
3981
|
+
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
3982
|
+
var WINDOWS2 = [7, 30, 90];
|
|
3983
|
+
var pullRequestStatsSchema2 = z17.object({
|
|
3984
|
+
closed: z17.number().int().nonnegative(),
|
|
3985
|
+
created: z17.number().int().nonnegative(),
|
|
3986
|
+
days: z17.number().int().positive(),
|
|
3987
|
+
medianMergeTimeMs: z17.number().nonnegative().nullable(),
|
|
3988
|
+
merged: z17.number().int().nonnegative()
|
|
3724
3989
|
}).strict().transform((row) => {
|
|
3725
3990
|
const terminal = row.merged + row.closed;
|
|
3726
3991
|
return {
|
|
@@ -3729,12 +3994,12 @@ var pullRequestStatsSchema = z16.object({
|
|
|
3729
3994
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3730
3995
|
};
|
|
3731
3996
|
});
|
|
3732
|
-
var pullRequestRepositoryStatsSchema =
|
|
3733
|
-
closed:
|
|
3734
|
-
created:
|
|
3735
|
-
juniorOnly:
|
|
3736
|
-
merged:
|
|
3737
|
-
repository:
|
|
3997
|
+
var pullRequestRepositoryStatsSchema = z17.object({
|
|
3998
|
+
closed: z17.number().int().nonnegative(),
|
|
3999
|
+
created: z17.number().int().nonnegative(),
|
|
4000
|
+
juniorOnly: z17.number().int().nonnegative(),
|
|
4001
|
+
merged: z17.number().int().nonnegative(),
|
|
4002
|
+
repository: z17.string().min(1)
|
|
3738
4003
|
}).strict().transform((row) => {
|
|
3739
4004
|
const terminal = row.merged + row.closed;
|
|
3740
4005
|
return {
|
|
@@ -3742,47 +4007,47 @@ var pullRequestRepositoryStatsSchema = z16.object({
|
|
|
3742
4007
|
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
3743
4008
|
};
|
|
3744
4009
|
});
|
|
3745
|
-
var
|
|
3746
|
-
closedCompleted:
|
|
3747
|
-
closedDuplicate:
|
|
3748
|
-
closedNotPlanned:
|
|
3749
|
-
closedUnknown:
|
|
3750
|
-
created:
|
|
3751
|
-
days:
|
|
3752
|
-
medianCloseTimeMs:
|
|
4010
|
+
var issueStatsSchema2 = z17.object({
|
|
4011
|
+
closedCompleted: z17.number().int().nonnegative(),
|
|
4012
|
+
closedDuplicate: z17.number().int().nonnegative(),
|
|
4013
|
+
closedNotPlanned: z17.number().int().nonnegative(),
|
|
4014
|
+
closedUnknown: z17.number().int().nonnegative(),
|
|
4015
|
+
created: z17.number().int().nonnegative(),
|
|
4016
|
+
days: z17.number().int().positive(),
|
|
4017
|
+
medianCloseTimeMs: z17.number().nonnegative().nullable()
|
|
3753
4018
|
}).strict().transform((row) => ({
|
|
3754
4019
|
...row,
|
|
3755
4020
|
medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
|
|
3756
4021
|
}));
|
|
3757
|
-
var pullRequestDaySchema =
|
|
3758
|
-
created:
|
|
3759
|
-
date:
|
|
4022
|
+
var pullRequestDaySchema = z17.object({
|
|
4023
|
+
created: z17.number().int().nonnegative(),
|
|
4024
|
+
date: z17.string().date()
|
|
3760
4025
|
}).strict();
|
|
3761
|
-
var issueDaySchema =
|
|
3762
|
-
created:
|
|
3763
|
-
date:
|
|
4026
|
+
var issueDaySchema = z17.object({
|
|
4027
|
+
created: z17.number().int().nonnegative(),
|
|
4028
|
+
date: z17.string().date()
|
|
3764
4029
|
}).strict();
|
|
3765
|
-
var issueRepositoryStatsSchema =
|
|
3766
|
-
closedCompleted:
|
|
3767
|
-
closedDuplicate:
|
|
3768
|
-
closedNotPlanned:
|
|
3769
|
-
closedUnknown:
|
|
3770
|
-
created:
|
|
3771
|
-
repository:
|
|
4030
|
+
var issueRepositoryStatsSchema = z17.object({
|
|
4031
|
+
closedCompleted: z17.number().int().nonnegative(),
|
|
4032
|
+
closedDuplicate: z17.number().int().nonnegative(),
|
|
4033
|
+
closedNotPlanned: z17.number().int().nonnegative(),
|
|
4034
|
+
closedUnknown: z17.number().int().nonnegative(),
|
|
4035
|
+
created: z17.number().int().nonnegative(),
|
|
4036
|
+
repository: z17.string().min(1)
|
|
3772
4037
|
}).strict();
|
|
3773
|
-
function
|
|
4038
|
+
function queryRows3(result) {
|
|
3774
4039
|
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
3775
4040
|
throw new TypeError("GitHub outcome query did not return rows");
|
|
3776
4041
|
}
|
|
3777
4042
|
return result.rows;
|
|
3778
4043
|
}
|
|
3779
|
-
async function
|
|
3780
|
-
const starts =
|
|
3781
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
4044
|
+
async function aggregatePullRequestWindows2(args) {
|
|
4045
|
+
const starts = WINDOWS2.map(
|
|
4046
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS3)]
|
|
3782
4047
|
);
|
|
3783
4048
|
const oldestStart = starts.at(-1)[1];
|
|
3784
4049
|
const table = juniorGitHubPullRequests;
|
|
3785
|
-
const result = await args.db.execute(
|
|
4050
|
+
const result = await args.db.execute(sql6`
|
|
3786
4051
|
WITH windows(days, start_at) AS (
|
|
3787
4052
|
VALUES
|
|
3788
4053
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -3832,13 +4097,13 @@ async function aggregatePullRequestWindows(args) {
|
|
|
3832
4097
|
GROUP BY windows.days
|
|
3833
4098
|
ORDER BY windows.days
|
|
3834
4099
|
`);
|
|
3835
|
-
return
|
|
4100
|
+
return z17.array(pullRequestStatsSchema2).parse(queryRows3(result));
|
|
3836
4101
|
}
|
|
3837
4102
|
async function aggregatePullRequestDays(args) {
|
|
3838
4103
|
const end = new Date(args.nowMs);
|
|
3839
|
-
const start =
|
|
4104
|
+
const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
|
|
3840
4105
|
const table = juniorGitHubPullRequests;
|
|
3841
|
-
const result = await args.db.execute(
|
|
4106
|
+
const result = await args.db.execute(sql6`
|
|
3842
4107
|
WITH days AS (
|
|
3843
4108
|
SELECT generate_series(
|
|
3844
4109
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
@@ -3860,12 +4125,12 @@ async function aggregatePullRequestDays(args) {
|
|
|
3860
4125
|
LEFT JOIN daily ON daily.day = days.day
|
|
3861
4126
|
ORDER BY days.day
|
|
3862
4127
|
`);
|
|
3863
|
-
return
|
|
4128
|
+
return z17.array(pullRequestDaySchema).parse(queryRows3(result));
|
|
3864
4129
|
}
|
|
3865
4130
|
async function aggregatePullRequestRepositories(args) {
|
|
3866
|
-
const start = new Date(args.nowMs - 30 *
|
|
4131
|
+
const start = new Date(args.nowMs - 30 * DAY_MS3);
|
|
3867
4132
|
const table = juniorGitHubPullRequests;
|
|
3868
|
-
const result = await args.db.execute(
|
|
4133
|
+
const result = await args.db.execute(sql6`
|
|
3869
4134
|
SELECT
|
|
3870
4135
|
${table.repositoryFullName} AS "repository",
|
|
3871
4136
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -3890,15 +4155,15 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
3890
4155
|
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
3891
4156
|
LIMIT 25
|
|
3892
4157
|
`);
|
|
3893
|
-
return
|
|
4158
|
+
return z17.array(pullRequestRepositoryStatsSchema).parse(queryRows3(result));
|
|
3894
4159
|
}
|
|
3895
|
-
async function
|
|
3896
|
-
const starts =
|
|
3897
|
-
(days) => [days, new Date(args.nowMs - days *
|
|
4160
|
+
async function aggregateIssueWindows2(args) {
|
|
4161
|
+
const starts = WINDOWS2.map(
|
|
4162
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS3)]
|
|
3898
4163
|
);
|
|
3899
4164
|
const oldestStart = starts.at(-1)[1];
|
|
3900
4165
|
const table = juniorGitHubIssues;
|
|
3901
|
-
const result = await args.db.execute(
|
|
4166
|
+
const result = await args.db.execute(sql6`
|
|
3902
4167
|
WITH windows(days, start_at) AS (
|
|
3903
4168
|
VALUES
|
|
3904
4169
|
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
@@ -3959,13 +4224,13 @@ async function aggregateIssueWindows(args) {
|
|
|
3959
4224
|
GROUP BY windows.days
|
|
3960
4225
|
ORDER BY windows.days
|
|
3961
4226
|
`);
|
|
3962
|
-
return
|
|
4227
|
+
return z17.array(issueStatsSchema2).parse(queryRows3(result));
|
|
3963
4228
|
}
|
|
3964
4229
|
async function aggregateIssueDays(args) {
|
|
3965
4230
|
const end = new Date(args.nowMs);
|
|
3966
|
-
const start =
|
|
4231
|
+
const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
|
|
3967
4232
|
const table = juniorGitHubIssues;
|
|
3968
|
-
const result = await args.db.execute(
|
|
4233
|
+
const result = await args.db.execute(sql6`
|
|
3969
4234
|
WITH days AS (
|
|
3970
4235
|
SELECT generate_series(
|
|
3971
4236
|
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
@@ -3987,12 +4252,12 @@ async function aggregateIssueDays(args) {
|
|
|
3987
4252
|
LEFT JOIN daily ON daily.day = days.day
|
|
3988
4253
|
ORDER BY days.day
|
|
3989
4254
|
`);
|
|
3990
|
-
return
|
|
4255
|
+
return z17.array(issueDaySchema).parse(queryRows3(result));
|
|
3991
4256
|
}
|
|
3992
4257
|
async function aggregateIssueRepositories(args) {
|
|
3993
|
-
const start = new Date(args.nowMs - 30 *
|
|
4258
|
+
const start = new Date(args.nowMs - 30 * DAY_MS3);
|
|
3994
4259
|
const table = juniorGitHubIssues;
|
|
3995
|
-
const result = await args.db.execute(
|
|
4260
|
+
const result = await args.db.execute(sql6`
|
|
3996
4261
|
SELECT
|
|
3997
4262
|
${table.repositoryFullName} AS "repository",
|
|
3998
4263
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
@@ -4024,9 +4289,9 @@ async function aggregateIssueRepositories(args) {
|
|
|
4024
4289
|
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
4025
4290
|
LIMIT 25
|
|
4026
4291
|
`);
|
|
4027
|
-
return
|
|
4292
|
+
return z17.array(issueRepositoryStatsSchema).parse(queryRows3(result));
|
|
4028
4293
|
}
|
|
4029
|
-
function
|
|
4294
|
+
function formatPercent2(value) {
|
|
4030
4295
|
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
4031
4296
|
}
|
|
4032
4297
|
function formatDuration(value) {
|
|
@@ -4036,7 +4301,7 @@ function formatDuration(value) {
|
|
|
4036
4301
|
if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
|
|
4037
4302
|
return `${Math.round(hours / 24 * 10) / 10}d`;
|
|
4038
4303
|
}
|
|
4039
|
-
function
|
|
4304
|
+
function startOfUtcDay2(timestampMs) {
|
|
4040
4305
|
const date = new Date(timestampMs);
|
|
4041
4306
|
date.setUTCHours(0, 0, 0, 0);
|
|
4042
4307
|
return date;
|
|
@@ -4052,13 +4317,13 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4052
4317
|
costWindows,
|
|
4053
4318
|
repositoryCosts
|
|
4054
4319
|
] = await Promise.all([
|
|
4055
|
-
|
|
4320
|
+
aggregatePullRequestWindows2(args),
|
|
4056
4321
|
aggregatePullRequestDays(args),
|
|
4057
4322
|
aggregatePullRequestRepositories(args),
|
|
4058
|
-
|
|
4323
|
+
aggregateIssueWindows2(args),
|
|
4059
4324
|
aggregateIssueDays(args),
|
|
4060
4325
|
aggregateIssueRepositories(args),
|
|
4061
|
-
aggregateGitHubCostWindows({ ...args, windows:
|
|
4326
|
+
aggregateGitHubCostWindows({ ...args, windows: WINDOWS2 }),
|
|
4062
4327
|
aggregateGitHubRepositoryCosts(args)
|
|
4063
4328
|
]);
|
|
4064
4329
|
const thirtyDays = windows.find((window) => window.days === 30);
|
|
@@ -4073,7 +4338,7 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4073
4338
|
metrics: [
|
|
4074
4339
|
{
|
|
4075
4340
|
label: "PR closure merge rate \xB7 30d",
|
|
4076
|
-
value:
|
|
4341
|
+
value: formatPercent2(thirtyDays.mergeRate)
|
|
4077
4342
|
},
|
|
4078
4343
|
{
|
|
4079
4344
|
label: "Median PR merge time \xB7 merged in 30d",
|
|
@@ -4106,7 +4371,7 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4106
4371
|
type: "bar_chart",
|
|
4107
4372
|
title: "Pull requests created",
|
|
4108
4373
|
description: "Junior-owned pull requests opened per day",
|
|
4109
|
-
timeRangeDays: [...
|
|
4374
|
+
timeRangeDays: [...WINDOWS2],
|
|
4110
4375
|
series: [{ key: "created", label: "Created" }],
|
|
4111
4376
|
categories: pullRequestDays.map((stats) => ({
|
|
4112
4377
|
id: stats.date,
|
|
@@ -4119,7 +4384,7 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4119
4384
|
type: "bar_chart",
|
|
4120
4385
|
title: "Issues created",
|
|
4121
4386
|
description: "Junior-owned issues opened per day",
|
|
4122
|
-
timeRangeDays: [...
|
|
4387
|
+
timeRangeDays: [...WINDOWS2],
|
|
4123
4388
|
series: [{ key: "created", label: "Created" }],
|
|
4124
4389
|
categories: issueDays.map((stats) => ({
|
|
4125
4390
|
id: stats.date,
|
|
@@ -4149,7 +4414,7 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4149
4414
|
merged: String(stats.merged),
|
|
4150
4415
|
closed: String(stats.closed),
|
|
4151
4416
|
juniorOnly: String(stats.juniorOnly),
|
|
4152
|
-
mergeRate:
|
|
4417
|
+
mergeRate: formatPercent2(stats.mergeRate),
|
|
4153
4418
|
medianCost: formatCostUsd(
|
|
4154
4419
|
repositoryCostByName.get(repository)?.medianPullRequestCostUsd
|
|
4155
4420
|
)
|
|
@@ -4188,18 +4453,18 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
4188
4453
|
}
|
|
4189
4454
|
|
|
4190
4455
|
// src/pull-request-outcomes/commit-composition.ts
|
|
4191
|
-
import { z as
|
|
4192
|
-
var canonicalCommitSchema =
|
|
4193
|
-
authorEmail:
|
|
4194
|
-
authorLogin:
|
|
4456
|
+
import { z as z18 } from "zod";
|
|
4457
|
+
var canonicalCommitSchema = z18.object({
|
|
4458
|
+
authorEmail: z18.string().nullable(),
|
|
4459
|
+
authorLogin: z18.string().nullable()
|
|
4195
4460
|
}).strict();
|
|
4196
|
-
var providerCommitSchema =
|
|
4197
|
-
author:
|
|
4198
|
-
commit:
|
|
4199
|
-
author:
|
|
4461
|
+
var providerCommitSchema = z18.object({
|
|
4462
|
+
author: z18.object({ login: z18.string() }).passthrough().nullable(),
|
|
4463
|
+
commit: z18.object({
|
|
4464
|
+
author: z18.object({ email: z18.string() }).passthrough().nullable()
|
|
4200
4465
|
}).passthrough()
|
|
4201
4466
|
}).passthrough();
|
|
4202
|
-
var commitPageSchema =
|
|
4467
|
+
var commitPageSchema = z18.array(providerCommitSchema).transform(
|
|
4203
4468
|
(commits) => commits.map(
|
|
4204
4469
|
(commit) => canonicalCommitSchema.parse({
|
|
4205
4470
|
authorEmail: commit.commit.author?.email ?? null,
|
|
@@ -4240,6 +4505,48 @@ async function classifyGitHubPullRequestCommitComposition(args) {
|
|
|
4240
4505
|
return foundCommit ? "junior_only" : void 0;
|
|
4241
4506
|
}
|
|
4242
4507
|
|
|
4508
|
+
// src/annotations.ts
|
|
4509
|
+
var STATUS_RANK = {
|
|
4510
|
+
warning: 5,
|
|
4511
|
+
open: 4,
|
|
4512
|
+
draft: 3,
|
|
4513
|
+
merged: 2,
|
|
4514
|
+
closed: 1
|
|
4515
|
+
};
|
|
4516
|
+
var STATUS_ICON = {
|
|
4517
|
+
warning: "triangle-alert",
|
|
4518
|
+
open: "circle-dot",
|
|
4519
|
+
draft: "circle-dashed",
|
|
4520
|
+
merged: "git-merge",
|
|
4521
|
+
closed: "circle-x"
|
|
4522
|
+
};
|
|
4523
|
+
function repositoryScope(annotation) {
|
|
4524
|
+
try {
|
|
4525
|
+
const [, owner, repo] = new URL(annotation.url).pathname.split("/");
|
|
4526
|
+
return owner && repo ? { key: `${owner}/${repo}`, label: repo } : void 0;
|
|
4527
|
+
} catch {
|
|
4528
|
+
return void 0;
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
function githubSidebarAnnotation(annotations) {
|
|
4532
|
+
const links = annotations.flatMap((annotation) => {
|
|
4533
|
+
const repo = repositoryScope(annotation);
|
|
4534
|
+
const status2 = annotation.status;
|
|
4535
|
+
return repo && status2 ? [{ repo, status: status2 }] : [];
|
|
4536
|
+
});
|
|
4537
|
+
if (links.length === 0) return void 0;
|
|
4538
|
+
const repos = new Map(links.map((link) => [link.repo.key, link.repo.label]));
|
|
4539
|
+
const status = links.reduce(
|
|
4540
|
+
(current, link) => STATUS_RANK[link.status] > STATUS_RANK[current] ? link.status : current,
|
|
4541
|
+
"closed"
|
|
4542
|
+
);
|
|
4543
|
+
return {
|
|
4544
|
+
icon: STATUS_ICON[status],
|
|
4545
|
+
key: "github",
|
|
4546
|
+
label: repos.size === 1 ? [...repos.values()][0] : `${repos.size} repos`
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4243
4550
|
// src/webhooks/check-suite-enrichment.ts
|
|
4244
4551
|
function checkRunsFromResponse(value) {
|
|
4245
4552
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4487,6 +4794,121 @@ async function configureGit(ctx, key, value) {
|
|
|
4487
4794
|
}
|
|
4488
4795
|
}
|
|
4489
4796
|
|
|
4797
|
+
// src/reply-markdown.ts
|
|
4798
|
+
var GITHUB_OWNER_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?";
|
|
4799
|
+
var GITHUB_REPOSITORY_PATTERN = "[A-Za-z0-9._-]+";
|
|
4800
|
+
var GITHUB_ISSUE_REFERENCE_PATTERN = new RegExp(
|
|
4801
|
+
`^(${GITHUB_OWNER_PATTERN})\\/(${GITHUB_REPOSITORY_PATTERN})#(\\d+)\\b`
|
|
4802
|
+
);
|
|
4803
|
+
function isReferenceBoundary(char) {
|
|
4804
|
+
return char === void 0 || !/[A-Za-z0-9._-]/.test(char);
|
|
4805
|
+
}
|
|
4806
|
+
function readInlineCode(text2, start) {
|
|
4807
|
+
if (text2[start] !== "`") {
|
|
4808
|
+
return void 0;
|
|
4809
|
+
}
|
|
4810
|
+
let markerLength = 1;
|
|
4811
|
+
while (text2[start + markerLength] === "`") {
|
|
4812
|
+
markerLength++;
|
|
4813
|
+
}
|
|
4814
|
+
const marker = "`".repeat(markerLength);
|
|
4815
|
+
const end = text2.indexOf(marker, start + markerLength);
|
|
4816
|
+
return end === -1 ? void 0 : end + markerLength;
|
|
4817
|
+
}
|
|
4818
|
+
function readFenceLength(line) {
|
|
4819
|
+
const trimmed = line.trimStart();
|
|
4820
|
+
if (!trimmed.startsWith("```")) {
|
|
4821
|
+
return void 0;
|
|
4822
|
+
}
|
|
4823
|
+
let length = 0;
|
|
4824
|
+
while (trimmed[length] === "`") {
|
|
4825
|
+
length++;
|
|
4826
|
+
}
|
|
4827
|
+
return length >= 3 ? length : void 0;
|
|
4828
|
+
}
|
|
4829
|
+
function readMarkdownLink(text2, start) {
|
|
4830
|
+
if (text2[start] !== "[") {
|
|
4831
|
+
return void 0;
|
|
4832
|
+
}
|
|
4833
|
+
const labelEnd = text2.indexOf("](", start + 1);
|
|
4834
|
+
if (labelEnd === -1) {
|
|
4835
|
+
return void 0;
|
|
4836
|
+
}
|
|
4837
|
+
const label = text2.slice(start + 1, labelEnd);
|
|
4838
|
+
if (label.includes("[") || label.includes("]") || label.includes("\n")) {
|
|
4839
|
+
return void 0;
|
|
4840
|
+
}
|
|
4841
|
+
const destStart = labelEnd + 2;
|
|
4842
|
+
const closeParens = text2.indexOf(")", destStart);
|
|
4843
|
+
return closeParens === -1 ? void 0 : closeParens + 1;
|
|
4844
|
+
}
|
|
4845
|
+
function readAngleToken(text2, start) {
|
|
4846
|
+
if (text2[start] !== "<") {
|
|
4847
|
+
return void 0;
|
|
4848
|
+
}
|
|
4849
|
+
const end = text2.indexOf(">", start + 1);
|
|
4850
|
+
return end === -1 ? void 0 : end + 1;
|
|
4851
|
+
}
|
|
4852
|
+
function linkifyLine(line) {
|
|
4853
|
+
let output = "";
|
|
4854
|
+
let index2 = 0;
|
|
4855
|
+
while (index2 < line.length) {
|
|
4856
|
+
const codeEnd = readInlineCode(line, index2);
|
|
4857
|
+
if (codeEnd !== void 0) {
|
|
4858
|
+
output += line.slice(index2, codeEnd);
|
|
4859
|
+
index2 = codeEnd;
|
|
4860
|
+
continue;
|
|
4861
|
+
}
|
|
4862
|
+
const markdownLinkEnd = readMarkdownLink(line, index2);
|
|
4863
|
+
if (markdownLinkEnd !== void 0) {
|
|
4864
|
+
output += line.slice(index2, markdownLinkEnd);
|
|
4865
|
+
index2 = markdownLinkEnd;
|
|
4866
|
+
continue;
|
|
4867
|
+
}
|
|
4868
|
+
const angleTokenEnd = readAngleToken(line, index2);
|
|
4869
|
+
if (angleTokenEnd !== void 0) {
|
|
4870
|
+
output += line.slice(index2, angleTokenEnd);
|
|
4871
|
+
index2 = angleTokenEnd;
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
if (line.startsWith("https://", index2) || line.startsWith("http://", index2)) {
|
|
4875
|
+
const match = /^https?:\/\/\S+/.exec(line.slice(index2));
|
|
4876
|
+
if (match) {
|
|
4877
|
+
output += match[0];
|
|
4878
|
+
index2 += match[0].length;
|
|
4879
|
+
continue;
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
if (isReferenceBoundary(index2 === 0 ? void 0 : line[index2 - 1])) {
|
|
4883
|
+
const match = GITHUB_ISSUE_REFERENCE_PATTERN.exec(line.slice(index2));
|
|
4884
|
+
if (match) {
|
|
4885
|
+
const [reference, owner, repository, number] = match;
|
|
4886
|
+
output += `[${reference}](https://github.com/${owner}/${repository}/issues/${number})`;
|
|
4887
|
+
index2 += reference.length;
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
output += line[index2];
|
|
4892
|
+
index2++;
|
|
4893
|
+
}
|
|
4894
|
+
return output;
|
|
4895
|
+
}
|
|
4896
|
+
function linkifyGitHubReferences(text2) {
|
|
4897
|
+
let openFenceLength = 0;
|
|
4898
|
+
return text2.split("\n").map((line) => {
|
|
4899
|
+
const fenceLength = readFenceLength(line);
|
|
4900
|
+
if (fenceLength !== void 0) {
|
|
4901
|
+
if (openFenceLength === 0) {
|
|
4902
|
+
openFenceLength = fenceLength;
|
|
4903
|
+
} else if (fenceLength >= openFenceLength) {
|
|
4904
|
+
openFenceLength = 0;
|
|
4905
|
+
}
|
|
4906
|
+
return line;
|
|
4907
|
+
}
|
|
4908
|
+
return openFenceLength > 0 ? line : linkifyLine(line);
|
|
4909
|
+
}).join("\n");
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4490
4912
|
// src/plugin.ts
|
|
4491
4913
|
function githubSmartHttpAccess(upstreamUrl) {
|
|
4492
4914
|
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
@@ -4935,6 +5357,21 @@ function githubPlugin(options = {}) {
|
|
|
4935
5357
|
]
|
|
4936
5358
|
},
|
|
4937
5359
|
hooks: {
|
|
5360
|
+
conversationSidebar(ctx) {
|
|
5361
|
+
return {
|
|
5362
|
+
annotationsByConversationId: Object.fromEntries(
|
|
5363
|
+
ctx.conversationIds.flatMap((conversationId) => {
|
|
5364
|
+
const annotation = githubSidebarAnnotation(
|
|
5365
|
+
ctx.annotationsByConversationId[conversationId] ?? []
|
|
5366
|
+
);
|
|
5367
|
+
return annotation ? [[conversationId, [annotation]]] : [];
|
|
5368
|
+
})
|
|
5369
|
+
)
|
|
5370
|
+
};
|
|
5371
|
+
},
|
|
5372
|
+
formatMarkdown({ text: text2 }) {
|
|
5373
|
+
return linkifyGitHubReferences(text2);
|
|
5374
|
+
},
|
|
4938
5375
|
async unfinishedWork(ctx) {
|
|
4939
5376
|
const db = ctx.db;
|
|
4940
5377
|
const [
|
|
@@ -5004,6 +5441,13 @@ function githubPlugin(options = {}) {
|
|
|
5004
5441
|
nowMs: ctx.nowMs
|
|
5005
5442
|
});
|
|
5006
5443
|
},
|
|
5444
|
+
async profileReport(ctx) {
|
|
5445
|
+
return await buildGitHubProfileReport({
|
|
5446
|
+
db: ctx.db,
|
|
5447
|
+
nowMs: ctx.nowMs,
|
|
5448
|
+
userId: ctx.subject.id
|
|
5449
|
+
});
|
|
5450
|
+
},
|
|
5007
5451
|
tools(ctx) {
|
|
5008
5452
|
return createGitHubTools(ctx);
|
|
5009
5453
|
},
|
|
@@ -30,8 +30,13 @@ export type GitHubIssueConversationsInput = z.output<typeof githubIssueConversat
|
|
|
30
30
|
* Record the newest lifecycle projection for one Junior-owned issue.
|
|
31
31
|
* Ownership-qualified opening or closing events insert; later events update
|
|
32
32
|
* existing rows, and older provider timestamps cannot regress them.
|
|
33
|
+
* Returns the written row state so follow-up annotation updates stay scoped to
|
|
34
|
+
* associated conversations.
|
|
33
35
|
*/
|
|
34
|
-
export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<
|
|
36
|
+
export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<{
|
|
37
|
+
applied: boolean;
|
|
38
|
+
conversationIds: string[];
|
|
39
|
+
}>;
|
|
35
40
|
/** Append native conversation ids to an existing Junior-owned issue projection. */
|
|
36
41
|
export declare function recordGitHubIssueConversations(db: GitHubDb, input: GitHubIssueConversationsInput): Promise<boolean>;
|
|
37
42
|
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
|
|
2
|
+
import type { GitHubDb } from "../db/database.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build one person-scoped GitHub report for Junior-owned work linked to the
|
|
5
|
+
* subject's conversations.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildGitHubProfileReport(args: {
|
|
8
|
+
db: GitHubDb;
|
|
9
|
+
nowMs: number;
|
|
10
|
+
userId: string;
|
|
11
|
+
}): Promise<PluginOperationalReportContent | undefined>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.161.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@sinclair/typebox": "^0.34.49",
|
|
32
32
|
"drizzle-orm": "^0.45.2",
|
|
33
33
|
"zod": "^4.4.3",
|
|
34
|
-
"@sentry/junior-plugin-api": "0.
|
|
34
|
+
"@sentry/junior-plugin-api": "0.161.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.9.1",
|
|
@@ -81,5 +81,3 @@ If PR creation or update is blocked, report the exact failed command/tool call a
|
|
|
81
81
|
When PR creation returns a subscribable resource hint, subscribe to suggested review/CI events. Report only actionable feedback addressed, build failures fixed, fully green/ready state, or merge.
|
|
82
82
|
|
|
83
83
|
Return: repo, branch, PR URL/number, checks and results, pre-existing failures, and anything not run with the reason.
|
|
84
|
-
|
|
85
|
-
When you mention a pull request in a user-facing reply, always include a direct link. Prefer the full PR URL or a Markdown link such as `[#123](https://github.com/owner/repo/pull/123)`. `owner/repo#number` is also fine. Do not leave bare `PR #123` text without a URL or repo.
|
|
@@ -80,4 +80,5 @@ jr-rpc config set github.repo owner/repo
|
|
|
80
80
|
- Pull request reviews and inline review comments use the same repository-scoped `installation-write` credential as other bot-owned PR writes, so they post as Junior even on headless turns. Merge remains denied.
|
|
81
81
|
- If the explicit `git push` fails with 401/403 or another access/permission error, verify the repo context and retry once. If it still fails, load troubleshooting guidance and report the exact command failure.
|
|
82
82
|
- PR comments, labels, and assignees use GitHub's issue endpoints; use the `github-issues` REST guidance for those operations. All allowlisted bot writes share the same repository-scoped `installation-write` credential.
|
|
83
|
+
- To embed a local image in a GitHub issue, pull request, review, or comment, call `publishImage` first. That tool returns a durable public URL. The published image is public to anyone on the internet who has the URL. Embed the URL with normal GitHub Markdown. Do not use private Slack file links or conversation attachment URLs.
|
|
83
84
|
- Return actionable errors for access, permission, not-found, and validation failures.
|
|
@@ -76,7 +76,7 @@ Follow [references/research-rules.md](references/research-rules.md) for cross-ty
|
|
|
76
76
|
|
|
77
77
|
- The runtime adds the verified `Requested by` block. Do not add or rewrite requester attribution in model-authored body text.
|
|
78
78
|
- If the person who originally reported or observed the problem differs from the issue creator, capture that with durable body text such as `Reported by Alice.` or `Raised by Alice during incident triage.`
|
|
79
|
-
- Attach screenshots from the thread
|
|
79
|
+
- Attach screenshots from the thread when present. For GitHub Markdown, first publish each local image with `publishImage` (the image becomes public to anyone with the URL), then embed the returned URL in the issue body or comment. Do not use private Slack file links or conversation attachment URLs.
|
|
80
80
|
- Include code snippets, related issues, and related PRs only when they materially improve the issue.
|
|
81
81
|
|
|
82
82
|
### 4. Verify draft
|