@sentry/junior-github 0.105.0 → 0.107.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 +4 -0
- package/SETUP.md +32 -0
- package/dist/chunk-JDNXIBCQ.js +239 -0
- package/dist/db/schema.d.ts +334 -0
- package/dist/index.js +728 -58
- package/dist/pull-request-outcomes/report.d.ts +7 -0
- package/dist/pull-request-outcomes/store.d.ts +29 -0
- package/dist/resource-events/pull-request.d.ts +11 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +6 -0
- package/dist/tools/create-pull-request.d.ts +4 -4
- package/dist/tools/get-pull-request.d.ts +55 -0
- package/dist/tools/update-pull-request.d.ts +57 -0
- package/dist/webhooks/handler.d.ts +9 -0
- package/dist/webhooks/pull-request-outcome.d.ts +6 -0
- package/dist/webhooks/resource-events.d.ts +7 -0
- package/migrations/0000_pull_request_outcomes.sql +16 -0
- package/migrations/meta/0000_snapshot.json +148 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +12 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
gitHubPullRequestSubscribable,
|
|
3
|
+
normalizeGitHubResourceEvents
|
|
4
|
+
} from "./chunk-JDNXIBCQ.js";
|
|
5
|
+
|
|
1
6
|
// src/index.ts
|
|
2
7
|
import { createPrivateKey, createSign } from "crypto";
|
|
3
8
|
import {
|
|
@@ -293,14 +298,14 @@ function parseRepo(value) {
|
|
|
293
298
|
};
|
|
294
299
|
}
|
|
295
300
|
async function readJsonResponse(response) {
|
|
296
|
-
const
|
|
297
|
-
if (!
|
|
301
|
+
const text2 = await response.text();
|
|
302
|
+
if (!text2) {
|
|
298
303
|
return void 0;
|
|
299
304
|
}
|
|
300
305
|
try {
|
|
301
|
-
return JSON.parse(
|
|
306
|
+
return JSON.parse(text2);
|
|
302
307
|
} catch {
|
|
303
|
-
return
|
|
308
|
+
return text2;
|
|
304
309
|
}
|
|
305
310
|
}
|
|
306
311
|
function githubApiErrorMessage(payload) {
|
|
@@ -472,6 +477,7 @@ import {
|
|
|
472
477
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
473
478
|
import { Value as Value2 } from "@sinclair/typebox/value";
|
|
474
479
|
import { z as z2 } from "zod";
|
|
480
|
+
import { subscribableResourceSchema } from "@sentry/junior-plugin-api";
|
|
475
481
|
var GITHUB_PULL_REQUEST_CREATE_IDEMPOTENCY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
476
482
|
var GITHUB_PULL_REQUEST_CREATE_LOCK_TTL_MS = 6e4;
|
|
477
483
|
var GitHubPullRequestCreateRejectedError = class extends Error {
|
|
@@ -537,14 +543,6 @@ var createPullRequestStateSchema = Type2.Union([
|
|
|
537
543
|
{ additionalProperties: false }
|
|
538
544
|
)
|
|
539
545
|
]);
|
|
540
|
-
var subscribableResourceSchema = z2.object({
|
|
541
|
-
provider: z2.string(),
|
|
542
|
-
type: z2.string(),
|
|
543
|
-
resourceRef: z2.string(),
|
|
544
|
-
label: z2.string(),
|
|
545
|
-
supportedEvents: z2.array(z2.string()),
|
|
546
|
-
suggestedEvents: z2.array(z2.string()).optional()
|
|
547
|
-
}).strict();
|
|
548
546
|
var gitHubPullRequestDataSchema = z2.object({
|
|
549
547
|
number: z2.number(),
|
|
550
548
|
url: z2.string(),
|
|
@@ -586,14 +584,14 @@ function parseRepo2(value) {
|
|
|
586
584
|
};
|
|
587
585
|
}
|
|
588
586
|
async function readJsonResponse2(response) {
|
|
589
|
-
const
|
|
590
|
-
if (!
|
|
587
|
+
const text2 = await response.text();
|
|
588
|
+
if (!text2) {
|
|
591
589
|
return void 0;
|
|
592
590
|
}
|
|
593
591
|
try {
|
|
594
|
-
return JSON.parse(
|
|
592
|
+
return JSON.parse(text2);
|
|
595
593
|
} catch {
|
|
596
|
-
return
|
|
594
|
+
return text2;
|
|
597
595
|
}
|
|
598
596
|
}
|
|
599
597
|
function githubApiErrorMessage2(payload) {
|
|
@@ -706,45 +704,13 @@ async function createGitHubPullRequest(ctx, request) {
|
|
|
706
704
|
url: pullRequest.html_url
|
|
707
705
|
};
|
|
708
706
|
}
|
|
709
|
-
function gitHubPullRequestSubscribable(input, result) {
|
|
710
|
-
const repo = parseRepo2(input.repo);
|
|
711
|
-
const repoRef = `${repo.owner}/${repo.name}`;
|
|
712
|
-
const supportedEvents = [
|
|
713
|
-
"checks.failed",
|
|
714
|
-
"checks.recovered",
|
|
715
|
-
"comment.created",
|
|
716
|
-
"review.approved",
|
|
717
|
-
"review.changes_requested",
|
|
718
|
-
"review.commented",
|
|
719
|
-
"review_comment.created",
|
|
720
|
-
"state.merged",
|
|
721
|
-
"state.closed_unmerged"
|
|
722
|
-
];
|
|
723
|
-
return {
|
|
724
|
-
label: `GitHub PR ${repoRef}#${result.number}`,
|
|
725
|
-
provider: "github",
|
|
726
|
-
resourceRef: `github:pull_request:${repoRef}#${result.number}`,
|
|
727
|
-
suggestedEvents: [
|
|
728
|
-
"checks.failed",
|
|
729
|
-
"comment.created",
|
|
730
|
-
"review.changes_requested",
|
|
731
|
-
"review.commented",
|
|
732
|
-
"review_comment.created",
|
|
733
|
-
"state.merged",
|
|
734
|
-
"state.closed_unmerged"
|
|
735
|
-
],
|
|
736
|
-
supportedEvents,
|
|
737
|
-
type: "pull_request"
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
707
|
function gitHubPullRequestToolResult(input, result) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
};
|
|
708
|
+
const repo = parseRepo2(input.repo);
|
|
709
|
+
const subscribable = gitHubPullRequestSubscribable({
|
|
710
|
+
number: result.number,
|
|
711
|
+
repo: `${repo.owner}/${repo.name}`
|
|
712
|
+
});
|
|
713
|
+
return { ...result, ...subscribable ? { subscribable } : {} };
|
|
748
714
|
}
|
|
749
715
|
function gitHubPullRequestStructuredResult(input, result) {
|
|
750
716
|
const data = gitHubPullRequestToolResult(input, result);
|
|
@@ -832,11 +798,699 @@ function createGitHubPullRequestTool(ctx) {
|
|
|
832
798
|
});
|
|
833
799
|
}
|
|
834
800
|
|
|
801
|
+
// src/tools/get-pull-request.ts
|
|
802
|
+
import {
|
|
803
|
+
definePluginTool as definePluginTool3,
|
|
804
|
+
PluginToolInputError as PluginToolInputError4,
|
|
805
|
+
pluginToolResultSchema as pluginToolResultSchema3
|
|
806
|
+
} from "@sentry/junior-plugin-api";
|
|
807
|
+
import { z as z3 } from "zod";
|
|
808
|
+
import { subscribableResourceSchema as subscribableResourceSchema2 } from "@sentry/junior-plugin-api";
|
|
809
|
+
var inputSchema = z3.object({
|
|
810
|
+
repo: z3.string().describe('Repository in "owner/name" format.'),
|
|
811
|
+
number: z3.number().int().positive().describe("Pull request number.")
|
|
812
|
+
}).strict();
|
|
813
|
+
var pullRequestSchema = z3.object({
|
|
814
|
+
base: z3.string(),
|
|
815
|
+
draft: z3.boolean(),
|
|
816
|
+
head: z3.string(),
|
|
817
|
+
merged: z3.boolean(),
|
|
818
|
+
number: z3.number(),
|
|
819
|
+
state: z3.string(),
|
|
820
|
+
subscribable: subscribableResourceSchema2.optional(),
|
|
821
|
+
title: z3.string(),
|
|
822
|
+
url: z3.string()
|
|
823
|
+
});
|
|
824
|
+
var outputSchema = pluginToolResultSchema3.extend({
|
|
825
|
+
ok: z3.literal(true),
|
|
826
|
+
status: z3.literal("success"),
|
|
827
|
+
target: z3.literal("getPullRequest"),
|
|
828
|
+
data: pullRequestSchema,
|
|
829
|
+
...pullRequestSchema.shape
|
|
830
|
+
});
|
|
831
|
+
function parseRepo3(value) {
|
|
832
|
+
const parts = value.split("/").map((part) => part.trim());
|
|
833
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
834
|
+
throw new PluginToolInputError4('repo must use "owner/name" format');
|
|
835
|
+
}
|
|
836
|
+
return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
|
|
837
|
+
}
|
|
838
|
+
async function readJson(response) {
|
|
839
|
+
const text2 = await response.text();
|
|
840
|
+
if (!text2) return void 0;
|
|
841
|
+
try {
|
|
842
|
+
return JSON.parse(text2);
|
|
843
|
+
} catch {
|
|
844
|
+
return text2;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function createGitHubGetPullRequestTool(ctx) {
|
|
848
|
+
return definePluginTool3({
|
|
849
|
+
description: "Get a GitHub pull request. Use this when an existing PR may need resource-event monitoring; the result includes a subscribable hint when GitHub webhooks are configured.",
|
|
850
|
+
inputSchema,
|
|
851
|
+
outputSchema,
|
|
852
|
+
async execute(input) {
|
|
853
|
+
const repo = parseRepo3(input.repo);
|
|
854
|
+
const response = await ctx.egress.fetch({
|
|
855
|
+
provider: "github",
|
|
856
|
+
operation: "github.pull.get",
|
|
857
|
+
request: new Request(
|
|
858
|
+
`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}/pulls/${input.number}`,
|
|
859
|
+
{
|
|
860
|
+
headers: {
|
|
861
|
+
Accept: "application/vnd.github+json",
|
|
862
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
});
|
|
867
|
+
const parsed = await readJson(response);
|
|
868
|
+
if (!response.ok)
|
|
869
|
+
throw new Error(
|
|
870
|
+
`GitHub pull request lookup failed with HTTP ${response.status}`
|
|
871
|
+
);
|
|
872
|
+
const providerResult = z3.object({
|
|
873
|
+
base: z3.object({ ref: z3.string() }),
|
|
874
|
+
draft: z3.boolean(),
|
|
875
|
+
head: z3.object({ ref: z3.string() }),
|
|
876
|
+
html_url: z3.string(),
|
|
877
|
+
merged: z3.boolean().optional().default(false),
|
|
878
|
+
number: z3.number(),
|
|
879
|
+
state: z3.string(),
|
|
880
|
+
title: z3.string()
|
|
881
|
+
}).parse(parsed);
|
|
882
|
+
const subscribable = gitHubPullRequestSubscribable({
|
|
883
|
+
number: providerResult.number,
|
|
884
|
+
repo: repo.ref
|
|
885
|
+
});
|
|
886
|
+
const data = {
|
|
887
|
+
base: providerResult.base.ref,
|
|
888
|
+
draft: providerResult.draft,
|
|
889
|
+
head: providerResult.head.ref,
|
|
890
|
+
merged: providerResult.merged,
|
|
891
|
+
number: providerResult.number,
|
|
892
|
+
state: providerResult.state,
|
|
893
|
+
...subscribable ? { subscribable } : {},
|
|
894
|
+
title: providerResult.title,
|
|
895
|
+
url: providerResult.html_url
|
|
896
|
+
};
|
|
897
|
+
return {
|
|
898
|
+
ok: true,
|
|
899
|
+
status: "success",
|
|
900
|
+
target: "getPullRequest",
|
|
901
|
+
data,
|
|
902
|
+
...data
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/tools/update-pull-request.ts
|
|
909
|
+
import {
|
|
910
|
+
definePluginTool as definePluginTool4,
|
|
911
|
+
PluginToolInputError as PluginToolInputError5,
|
|
912
|
+
pluginToolResultSchema as pluginToolResultSchema4
|
|
913
|
+
} from "@sentry/junior-plugin-api";
|
|
914
|
+
import { z as z4 } from "zod";
|
|
915
|
+
import { subscribableResourceSchema as subscribableResourceSchema3 } from "@sentry/junior-plugin-api";
|
|
916
|
+
var inputSchema2 = z4.object({
|
|
917
|
+
repo: z4.string().describe('Repository in "owner/name" format.'),
|
|
918
|
+
number: z4.number().int().positive().describe("Pull request number."),
|
|
919
|
+
title: z4.string().trim().min(1).optional().describe("Replacement pull request title."),
|
|
920
|
+
body: z4.string().optional().describe(
|
|
921
|
+
"Replacement pull request body. Junior appends requester attribution and the conversation footer."
|
|
922
|
+
),
|
|
923
|
+
base: z4.string().trim().min(1).optional().describe("Replacement base branch."),
|
|
924
|
+
state: z4.enum(["open", "closed"]).optional().describe("Replacement pull request state.")
|
|
925
|
+
}).strict().refine(
|
|
926
|
+
({ title, body, base, state }) => title !== void 0 || body !== void 0 || base !== void 0 || state !== void 0,
|
|
927
|
+
{ message: "At least one pull request field must be provided." }
|
|
928
|
+
);
|
|
929
|
+
var pullRequestSchema2 = z4.object({
|
|
930
|
+
base: z4.string(),
|
|
931
|
+
body: z4.string().nullable(),
|
|
932
|
+
draft: z4.boolean(),
|
|
933
|
+
number: z4.number(),
|
|
934
|
+
state: z4.string(),
|
|
935
|
+
subscribable: subscribableResourceSchema3.optional(),
|
|
936
|
+
title: z4.string(),
|
|
937
|
+
url: z4.string()
|
|
938
|
+
});
|
|
939
|
+
var outputSchema2 = pluginToolResultSchema4.extend({
|
|
940
|
+
ok: z4.literal(true),
|
|
941
|
+
status: z4.literal("success"),
|
|
942
|
+
target: z4.literal("updatePullRequest"),
|
|
943
|
+
data: pullRequestSchema2,
|
|
944
|
+
...pullRequestSchema2.shape
|
|
945
|
+
});
|
|
946
|
+
function nonEmptyString4(value, name) {
|
|
947
|
+
if (!value?.trim()) {
|
|
948
|
+
throw new PluginToolInputError5(`${name} is required`);
|
|
949
|
+
}
|
|
950
|
+
return value.trim();
|
|
951
|
+
}
|
|
952
|
+
function parseRepo4(value) {
|
|
953
|
+
const parts = value.split("/").map((part) => part.trim());
|
|
954
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
955
|
+
throw new PluginToolInputError5('repo must use "owner/name" format');
|
|
956
|
+
}
|
|
957
|
+
return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
|
|
958
|
+
}
|
|
959
|
+
async function readJson2(response) {
|
|
960
|
+
const text2 = await response.text();
|
|
961
|
+
if (!text2) return void 0;
|
|
962
|
+
try {
|
|
963
|
+
return JSON.parse(text2);
|
|
964
|
+
} catch {
|
|
965
|
+
return text2;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function githubApiErrorMessage3(payload) {
|
|
969
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
970
|
+
const message = payload.message;
|
|
971
|
+
if (typeof message === "string" && message.trim()) return message.trim();
|
|
972
|
+
}
|
|
973
|
+
if (typeof payload === "string" && payload.trim()) return payload.trim();
|
|
974
|
+
return "GitHub request failed";
|
|
975
|
+
}
|
|
976
|
+
function createGitHubUpdatePullRequestTool(ctx) {
|
|
977
|
+
return definePluginTool4({
|
|
978
|
+
description: "Update an existing GitHub pull request's title, body, base branch, or open/closed state. Use this instead of raw GitHub API calls when changing PR metadata.",
|
|
979
|
+
inputSchema: inputSchema2,
|
|
980
|
+
outputSchema: outputSchema2,
|
|
981
|
+
async execute(input) {
|
|
982
|
+
const parsedInput = inputSchema2.safeParse(input);
|
|
983
|
+
if (!parsedInput.success) {
|
|
984
|
+
throw new PluginToolInputError5(
|
|
985
|
+
"Invalid GitHub updatePullRequest input.",
|
|
986
|
+
{ cause: parsedInput.error }
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const update = parsedInput.data;
|
|
990
|
+
const repo = parseRepo4(update.repo);
|
|
991
|
+
const payload = {
|
|
992
|
+
...update.title !== void 0 ? { title: update.title } : {},
|
|
993
|
+
...update.body !== void 0 ? {
|
|
994
|
+
body: appendGitHubFooter(
|
|
995
|
+
appendGitHubRequesterAttribution(update.body, ctx.actor),
|
|
996
|
+
nonEmptyString4(ctx.conversationId, "conversationId"),
|
|
997
|
+
ctx.slack?.conversationLink?.url
|
|
998
|
+
)
|
|
999
|
+
} : {},
|
|
1000
|
+
...update.base !== void 0 ? { base: update.base } : {},
|
|
1001
|
+
...update.state !== void 0 ? { state: update.state } : {}
|
|
1002
|
+
};
|
|
1003
|
+
const response = await ctx.egress.fetch({
|
|
1004
|
+
provider: "github",
|
|
1005
|
+
operation: "github.pull.update",
|
|
1006
|
+
request: new Request(
|
|
1007
|
+
`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}/pulls/${update.number}`,
|
|
1008
|
+
{
|
|
1009
|
+
method: "PATCH",
|
|
1010
|
+
headers: {
|
|
1011
|
+
Accept: "application/vnd.github+json",
|
|
1012
|
+
"Content-Type": "application/json",
|
|
1013
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1014
|
+
},
|
|
1015
|
+
body: JSON.stringify(payload)
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
});
|
|
1019
|
+
const parsed = await readJson2(response);
|
|
1020
|
+
if (!response.ok) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`GitHub pull request update failed with HTTP ${response.status}: ${githubApiErrorMessage3(parsed)}`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const providerResult = z4.object({
|
|
1026
|
+
base: z4.object({ ref: z4.string() }),
|
|
1027
|
+
body: z4.string().nullable().optional().default(null),
|
|
1028
|
+
draft: z4.boolean(),
|
|
1029
|
+
html_url: z4.string(),
|
|
1030
|
+
number: z4.number(),
|
|
1031
|
+
state: z4.string(),
|
|
1032
|
+
title: z4.string()
|
|
1033
|
+
}).parse(parsed);
|
|
1034
|
+
const subscribable = gitHubPullRequestSubscribable({
|
|
1035
|
+
number: providerResult.number,
|
|
1036
|
+
repo: repo.ref
|
|
1037
|
+
});
|
|
1038
|
+
const data = {
|
|
1039
|
+
base: providerResult.base.ref,
|
|
1040
|
+
body: providerResult.body,
|
|
1041
|
+
draft: providerResult.draft,
|
|
1042
|
+
number: providerResult.number,
|
|
1043
|
+
state: providerResult.state,
|
|
1044
|
+
...subscribable ? { subscribable } : {},
|
|
1045
|
+
title: providerResult.title,
|
|
1046
|
+
url: providerResult.html_url
|
|
1047
|
+
};
|
|
1048
|
+
return {
|
|
1049
|
+
ok: true,
|
|
1050
|
+
status: "success",
|
|
1051
|
+
target: "updatePullRequest",
|
|
1052
|
+
data,
|
|
1053
|
+
...data
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
835
1059
|
// src/tools.ts
|
|
836
1060
|
function createGitHubTools(ctx) {
|
|
837
1061
|
return {
|
|
838
1062
|
createIssue: createGitHubIssueTool(ctx),
|
|
839
|
-
createPullRequest: createGitHubPullRequestTool(ctx)
|
|
1063
|
+
createPullRequest: createGitHubPullRequestTool(ctx),
|
|
1064
|
+
getPullRequest: createGitHubGetPullRequestTool(ctx),
|
|
1065
|
+
updatePullRequest: createGitHubUpdatePullRequestTool(ctx)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/webhooks/handler.ts
|
|
1070
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
1071
|
+
|
|
1072
|
+
// src/pull-request-outcomes/store.ts
|
|
1073
|
+
import { and, eq, lte } from "drizzle-orm";
|
|
1074
|
+
import { z as z6 } from "zod";
|
|
1075
|
+
|
|
1076
|
+
// src/db/schema.ts
|
|
1077
|
+
import { sql } from "drizzle-orm";
|
|
1078
|
+
import { index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
1079
|
+
import { z as z5 } from "zod";
|
|
1080
|
+
var githubPullRequestStateSchema = z5.enum([
|
|
1081
|
+
"closed_unmerged",
|
|
1082
|
+
"merged",
|
|
1083
|
+
"open"
|
|
1084
|
+
]);
|
|
1085
|
+
var juniorGitHubPullRequests = pgTable(
|
|
1086
|
+
"junior_github_pull_requests",
|
|
1087
|
+
{
|
|
1088
|
+
pullRequestId: text("pull_request_id").primaryKey(),
|
|
1089
|
+
repositoryId: text("repository_id").notNull(),
|
|
1090
|
+
repositoryFullName: text("repository_full_name").notNull(),
|
|
1091
|
+
number: integer("number").notNull(),
|
|
1092
|
+
state: text("state").$type().notNull(),
|
|
1093
|
+
openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
|
|
1094
|
+
mergedAt: timestamp("merged_at", { withTimezone: true }),
|
|
1095
|
+
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
1096
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
|
|
1097
|
+
},
|
|
1098
|
+
(table) => [
|
|
1099
|
+
index("junior_github_pull_requests_opened_at_idx").on(table.openedAt),
|
|
1100
|
+
index("junior_github_pull_requests_merged_at_idx").on(table.mergedAt),
|
|
1101
|
+
index("junior_github_pull_requests_closed_at_idx").on(table.closedAt),
|
|
1102
|
+
index("junior_github_pull_requests_open_idx").on(table.pullRequestId).where(sql`${table.state} = 'open'`)
|
|
1103
|
+
]
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
// src/pull-request-outcomes/store.ts
|
|
1107
|
+
var githubPullRequestOutcomeInputSchema = z6.object({
|
|
1108
|
+
candidateOwned: z6.boolean(),
|
|
1109
|
+
closedAt: z6.date().optional(),
|
|
1110
|
+
mergedAt: z6.date().optional(),
|
|
1111
|
+
number: z6.number().int().positive(),
|
|
1112
|
+
openedAt: z6.date(),
|
|
1113
|
+
pullRequestId: z6.string().min(1),
|
|
1114
|
+
repositoryFullName: z6.string().min(1),
|
|
1115
|
+
repositoryId: z6.string().min(1),
|
|
1116
|
+
state: githubPullRequestStateSchema,
|
|
1117
|
+
updatedAt: z6.date()
|
|
1118
|
+
}).strict();
|
|
1119
|
+
function projectionValues(input) {
|
|
1120
|
+
return {
|
|
1121
|
+
closedAt: input.closedAt ?? null,
|
|
1122
|
+
mergedAt: input.mergedAt ?? null,
|
|
1123
|
+
number: input.number,
|
|
1124
|
+
openedAt: input.openedAt,
|
|
1125
|
+
repositoryFullName: input.repositoryFullName,
|
|
1126
|
+
repositoryId: input.repositoryId,
|
|
1127
|
+
state: input.state,
|
|
1128
|
+
updatedAt: input.updatedAt
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
async function recordGitHubPullRequestOutcome(db, input) {
|
|
1132
|
+
const outcome = githubPullRequestOutcomeInputSchema.parse(input);
|
|
1133
|
+
const values = projectionValues(outcome);
|
|
1134
|
+
if (!outcome.candidateOwned) {
|
|
1135
|
+
await db.update(juniorGitHubPullRequests).set(values).where(
|
|
1136
|
+
and(
|
|
1137
|
+
eq(juniorGitHubPullRequests.pullRequestId, outcome.pullRequestId),
|
|
1138
|
+
lte(juniorGitHubPullRequests.updatedAt, outcome.updatedAt)
|
|
1139
|
+
)
|
|
1140
|
+
);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
await db.insert(juniorGitHubPullRequests).values({ pullRequestId: outcome.pullRequestId, ...values }).onConflictDoUpdate({
|
|
1144
|
+
target: juniorGitHubPullRequests.pullRequestId,
|
|
1145
|
+
set: values,
|
|
1146
|
+
where: lte(juniorGitHubPullRequests.updatedAt, outcome.updatedAt)
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/webhooks/pull-request-outcome.ts
|
|
1151
|
+
import { z as z7 } from "zod";
|
|
1152
|
+
var pullRequestOutcomeSchema = z7.object({
|
|
1153
|
+
action: z7.enum(["opened", "closed", "reopened"]),
|
|
1154
|
+
pull_request: z7.object({
|
|
1155
|
+
body: z7.string().nullable().optional(),
|
|
1156
|
+
closed_at: z7.string().nullable().optional(),
|
|
1157
|
+
created_at: z7.string(),
|
|
1158
|
+
id: z7.number().int().positive(),
|
|
1159
|
+
merged: z7.boolean(),
|
|
1160
|
+
merged_at: z7.string().nullable().optional(),
|
|
1161
|
+
number: z7.number().int().positive(),
|
|
1162
|
+
updated_at: z7.string(),
|
|
1163
|
+
user: z7.object({ login: z7.string().min(1) })
|
|
1164
|
+
}),
|
|
1165
|
+
repository: z7.object({
|
|
1166
|
+
full_name: z7.string().min(1),
|
|
1167
|
+
id: z7.number().int().positive()
|
|
1168
|
+
})
|
|
1169
|
+
});
|
|
1170
|
+
var pullRequestLifecycleActionSchema = z7.object({ action: z7.string() });
|
|
1171
|
+
var GITHUB_NOREPLY_DOMAIN = "users.noreply.github.com";
|
|
1172
|
+
function botLoginFromEmail(value) {
|
|
1173
|
+
const email = value?.trim();
|
|
1174
|
+
if (!email) return void 0;
|
|
1175
|
+
const separator = email.lastIndexOf("@");
|
|
1176
|
+
if (separator <= 0) return void 0;
|
|
1177
|
+
const domain = email.slice(separator + 1).toLowerCase();
|
|
1178
|
+
if (domain !== GITHUB_NOREPLY_DOMAIN) return void 0;
|
|
1179
|
+
const localPart = email.slice(0, separator);
|
|
1180
|
+
const login = localPart.slice(localPart.indexOf("+") + 1).trim();
|
|
1181
|
+
return login.toLowerCase().endsWith("[bot]") ? login : void 0;
|
|
1182
|
+
}
|
|
1183
|
+
function timestamp2(value) {
|
|
1184
|
+
if (!value) return void 0;
|
|
1185
|
+
const parsed = new Date(value);
|
|
1186
|
+
return Number.isFinite(parsed.getTime()) ? parsed : void 0;
|
|
1187
|
+
}
|
|
1188
|
+
function normalizeGitHubPullRequestOutcome(args) {
|
|
1189
|
+
const lifecycle = pullRequestLifecycleActionSchema.safeParse(args.body);
|
|
1190
|
+
if (!lifecycle.success || !["opened", "closed", "reopened"].includes(lifecycle.data.action)) {
|
|
1191
|
+
return void 0;
|
|
1192
|
+
}
|
|
1193
|
+
const parsed = pullRequestOutcomeSchema.parse(args.body);
|
|
1194
|
+
const pullRequest = parsed.pull_request;
|
|
1195
|
+
const openedAt = timestamp2(pullRequest.created_at);
|
|
1196
|
+
const updatedAt = timestamp2(pullRequest.updated_at);
|
|
1197
|
+
if (!openedAt || !updatedAt) {
|
|
1198
|
+
throw new Error("GitHub pull request lifecycle timestamps are invalid");
|
|
1199
|
+
}
|
|
1200
|
+
const authorLogin = pullRequest.user.login.trim().toLowerCase();
|
|
1201
|
+
const botLogin = botLoginFromEmail(args.botEmail)?.toLowerCase();
|
|
1202
|
+
if (parsed.action === "opened" && !botLogin) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
"The configured GitHub App bot email must encode a [bot] login in GitHub's noreply format to classify pull request ownership"
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
const hasOwnershipMarker = Boolean(
|
|
1208
|
+
botLogin && authorLogin === botLogin && pullRequest.body?.includes(GITHUB_SESSION_FOOTER_START)
|
|
1209
|
+
);
|
|
1210
|
+
const candidateOwned = hasOwnershipMarker && (parsed.action === "opened" || parsed.action === "closed");
|
|
1211
|
+
const state = parsed.action !== "closed" ? "open" : pullRequest.merged ? "merged" : "closed_unmerged";
|
|
1212
|
+
const closedAt = timestamp2(pullRequest.closed_at);
|
|
1213
|
+
const mergedAt = timestamp2(pullRequest.merged_at);
|
|
1214
|
+
if (parsed.action === "closed" && (pullRequest.merged ? !mergedAt : !closedAt)) {
|
|
1215
|
+
throw new Error("GitHub pull request terminal timestamp is invalid");
|
|
1216
|
+
}
|
|
1217
|
+
return {
|
|
1218
|
+
candidateOwned,
|
|
1219
|
+
closedAt,
|
|
1220
|
+
mergedAt,
|
|
1221
|
+
number: pullRequest.number,
|
|
1222
|
+
openedAt,
|
|
1223
|
+
pullRequestId: String(pullRequest.id),
|
|
1224
|
+
repositoryFullName: parsed.repository.full_name,
|
|
1225
|
+
repositoryId: String(parsed.repository.id),
|
|
1226
|
+
state,
|
|
1227
|
+
updatedAt
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/webhooks/handler.ts
|
|
1232
|
+
function verifyGitHubSignature(body, signature, secret) {
|
|
1233
|
+
if (!secret || !signature.startsWith("sha256=")) return false;
|
|
1234
|
+
const actual = Buffer.from(signature);
|
|
1235
|
+
const expected = Buffer.from(
|
|
1236
|
+
`sha256=${createHmac("sha256", secret).update(body).digest("hex")}`
|
|
1237
|
+
);
|
|
1238
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
1239
|
+
}
|
|
1240
|
+
function parseJson(body) {
|
|
1241
|
+
try {
|
|
1242
|
+
return JSON.parse(body);
|
|
1243
|
+
} catch {
|
|
1244
|
+
return void 0;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function createGitHubWebhookRoute(args) {
|
|
1248
|
+
return {
|
|
1249
|
+
method: "POST",
|
|
1250
|
+
path: "/api/webhooks/github",
|
|
1251
|
+
async handler(request) {
|
|
1252
|
+
const rawBody = await request.text();
|
|
1253
|
+
const signature = request.headers.get("x-hub-signature-256") ?? "";
|
|
1254
|
+
if (!verifyGitHubSignature(rawBody, signature, args.webhookSecret())) {
|
|
1255
|
+
return new Response("Unauthorized", { status: 401 });
|
|
1256
|
+
}
|
|
1257
|
+
const deliveryId = request.headers.get("x-github-delivery");
|
|
1258
|
+
const eventName = request.headers.get("x-github-event");
|
|
1259
|
+
if (!deliveryId || !eventName) {
|
|
1260
|
+
return new Response("Malformed GitHub webhook", { status: 400 });
|
|
1261
|
+
}
|
|
1262
|
+
const body = parseJson(rawBody);
|
|
1263
|
+
const outcome = eventName === "pull_request" ? normalizeGitHubPullRequestOutcome({
|
|
1264
|
+
body,
|
|
1265
|
+
botEmail: args.botEmail()
|
|
1266
|
+
}) : void 0;
|
|
1267
|
+
if (outcome) {
|
|
1268
|
+
await recordGitHubPullRequestOutcome(args.db, outcome);
|
|
1269
|
+
}
|
|
1270
|
+
const resourceEvents = normalizeGitHubResourceEvents({
|
|
1271
|
+
body,
|
|
1272
|
+
deliveryId,
|
|
1273
|
+
eventName
|
|
1274
|
+
});
|
|
1275
|
+
for (const event of resourceEvents) {
|
|
1276
|
+
await args.resourceEvents.publish(event);
|
|
1277
|
+
}
|
|
1278
|
+
if (!outcome && resourceEvents.length === 0) {
|
|
1279
|
+
return new Response("Ignored", { status: 202 });
|
|
1280
|
+
}
|
|
1281
|
+
return new Response("Accepted", { status: 202 });
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/pull-request-outcomes/report.ts
|
|
1287
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
1288
|
+
import { z as z8 } from "zod";
|
|
1289
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1290
|
+
var WINDOWS = [7, 30, 90];
|
|
1291
|
+
var outcomeStatsSchema = z8.object({
|
|
1292
|
+
closed: z8.number().int().nonnegative(),
|
|
1293
|
+
created: z8.number().int().nonnegative(),
|
|
1294
|
+
days: z8.number().int().positive(),
|
|
1295
|
+
medianMergeTimeMs: z8.number().nonnegative().nullable(),
|
|
1296
|
+
merged: z8.number().int().nonnegative(),
|
|
1297
|
+
open: z8.number().int().nonnegative()
|
|
1298
|
+
}).strict().transform((row) => {
|
|
1299
|
+
const terminal = row.merged + row.closed;
|
|
1300
|
+
return {
|
|
1301
|
+
...row,
|
|
1302
|
+
medianMergeTimeMs: row.medianMergeTimeMs ?? void 0,
|
|
1303
|
+
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
var repositoryStatsSchema = z8.object({
|
|
1307
|
+
closed: z8.number().int().nonnegative(),
|
|
1308
|
+
created: z8.number().int().nonnegative(),
|
|
1309
|
+
merged: z8.number().int().nonnegative(),
|
|
1310
|
+
open: z8.number().int().nonnegative(),
|
|
1311
|
+
repository: z8.string().min(1)
|
|
1312
|
+
}).strict().transform((row) => {
|
|
1313
|
+
const terminal = row.merged + row.closed;
|
|
1314
|
+
return {
|
|
1315
|
+
...row,
|
|
1316
|
+
mergeRate: terminal > 0 ? row.merged / terminal : void 0
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
function queryRows(result) {
|
|
1320
|
+
if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
|
|
1321
|
+
throw new TypeError("GitHub outcome query did not return rows");
|
|
1322
|
+
}
|
|
1323
|
+
return result.rows;
|
|
1324
|
+
}
|
|
1325
|
+
async function aggregateOutcomeWindows(args) {
|
|
1326
|
+
const starts = WINDOWS.map(
|
|
1327
|
+
(days) => [days, new Date(args.nowMs - days * DAY_MS)]
|
|
1328
|
+
);
|
|
1329
|
+
const oldestStart = starts.at(-1)[1];
|
|
1330
|
+
const table = juniorGitHubPullRequests;
|
|
1331
|
+
const result = await args.db.execute(sql2`
|
|
1332
|
+
WITH windows(days, start_at) AS (
|
|
1333
|
+
VALUES
|
|
1334
|
+
(${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
|
|
1335
|
+
(${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
|
|
1336
|
+
(${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
|
|
1337
|
+
), recent_pull_requests AS MATERIALIZED (
|
|
1338
|
+
SELECT
|
|
1339
|
+
${table.pullRequestId},
|
|
1340
|
+
${table.state},
|
|
1341
|
+
${table.openedAt},
|
|
1342
|
+
${table.mergedAt},
|
|
1343
|
+
${table.closedAt}
|
|
1344
|
+
FROM ${table}
|
|
1345
|
+
WHERE ${table.state} = 'open'
|
|
1346
|
+
OR ${table.openedAt} >= ${oldestStart}
|
|
1347
|
+
OR ${table.mergedAt} >= ${oldestStart}
|
|
1348
|
+
OR ${table.closedAt} >= ${oldestStart}
|
|
1349
|
+
)
|
|
1350
|
+
SELECT
|
|
1351
|
+
windows.days AS "days",
|
|
1352
|
+
count(recent_pull_requests.pull_request_id)
|
|
1353
|
+
FILTER (WHERE recent_pull_requests.opened_at >= windows.start_at)::integer
|
|
1354
|
+
AS "created",
|
|
1355
|
+
count(recent_pull_requests.pull_request_id)
|
|
1356
|
+
FILTER (
|
|
1357
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
1358
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1359
|
+
)::integer AS "merged",
|
|
1360
|
+
count(recent_pull_requests.pull_request_id)
|
|
1361
|
+
FILTER (
|
|
1362
|
+
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
1363
|
+
AND recent_pull_requests.closed_at >= windows.start_at
|
|
1364
|
+
)::integer AS "closed",
|
|
1365
|
+
count(recent_pull_requests.pull_request_id)
|
|
1366
|
+
FILTER (WHERE recent_pull_requests.state = 'open')::integer AS "open",
|
|
1367
|
+
(
|
|
1368
|
+
percentile_cont(0.5) WITHIN GROUP (
|
|
1369
|
+
ORDER BY extract(
|
|
1370
|
+
epoch FROM (
|
|
1371
|
+
recent_pull_requests.merged_at - recent_pull_requests.opened_at
|
|
1372
|
+
)
|
|
1373
|
+
) * 1000
|
|
1374
|
+
) FILTER (
|
|
1375
|
+
WHERE recent_pull_requests.state = 'merged'
|
|
1376
|
+
AND recent_pull_requests.merged_at >= windows.start_at
|
|
1377
|
+
)
|
|
1378
|
+
)::double precision AS "medianMergeTimeMs"
|
|
1379
|
+
FROM windows
|
|
1380
|
+
LEFT JOIN recent_pull_requests ON true
|
|
1381
|
+
GROUP BY windows.days
|
|
1382
|
+
ORDER BY windows.days
|
|
1383
|
+
`);
|
|
1384
|
+
return z8.array(outcomeStatsSchema).parse(queryRows(result));
|
|
1385
|
+
}
|
|
1386
|
+
async function aggregateRepositories(args) {
|
|
1387
|
+
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
1388
|
+
const table = juniorGitHubPullRequests;
|
|
1389
|
+
const result = await args.db.execute(sql2`
|
|
1390
|
+
SELECT
|
|
1391
|
+
${table.repositoryFullName} AS "repository",
|
|
1392
|
+
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
1393
|
+
AS "created",
|
|
1394
|
+
count(*) FILTER (
|
|
1395
|
+
WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
|
|
1396
|
+
)::integer AS "merged",
|
|
1397
|
+
count(*) FILTER (
|
|
1398
|
+
WHERE ${table.state} = 'closed_unmerged'
|
|
1399
|
+
AND ${table.closedAt} >= ${start}
|
|
1400
|
+
)::integer AS "closed",
|
|
1401
|
+
count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open"
|
|
1402
|
+
FROM ${table}
|
|
1403
|
+
WHERE ${table.state} = 'open'
|
|
1404
|
+
OR ${table.openedAt} >= ${start}
|
|
1405
|
+
OR ${table.mergedAt} >= ${start}
|
|
1406
|
+
OR ${table.closedAt} >= ${start}
|
|
1407
|
+
GROUP BY ${table.repositoryFullName}
|
|
1408
|
+
ORDER BY "merged" DESC, "created" DESC, "repository" ASC
|
|
1409
|
+
LIMIT 25
|
|
1410
|
+
`);
|
|
1411
|
+
return z8.array(repositoryStatsSchema).parse(queryRows(result));
|
|
1412
|
+
}
|
|
1413
|
+
function formatPercent(value) {
|
|
1414
|
+
return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
|
|
1415
|
+
}
|
|
1416
|
+
function formatDuration(value) {
|
|
1417
|
+
if (value === void 0) return "\u2014";
|
|
1418
|
+
const hours = value / (60 * 60 * 1e3);
|
|
1419
|
+
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
|
1420
|
+
if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
|
|
1421
|
+
return `${Math.round(hours / 24 * 10) / 10}d`;
|
|
1422
|
+
}
|
|
1423
|
+
async function buildGitHubOutcomeReport(args) {
|
|
1424
|
+
const [windows, repositories] = await Promise.all([
|
|
1425
|
+
aggregateOutcomeWindows(args),
|
|
1426
|
+
aggregateRepositories(args)
|
|
1427
|
+
]);
|
|
1428
|
+
const thirtyDays = windows.find((window) => window.days === 30);
|
|
1429
|
+
return {
|
|
1430
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1431
|
+
title: "GitHub work delivered",
|
|
1432
|
+
metrics: [
|
|
1433
|
+
{ label: "created \xB7 30d", value: String(thirtyDays.created) },
|
|
1434
|
+
{
|
|
1435
|
+
label: "merged \xB7 30d",
|
|
1436
|
+
tone: thirtyDays.merged > 0 ? "good" : "neutral",
|
|
1437
|
+
value: String(thirtyDays.merged)
|
|
1438
|
+
},
|
|
1439
|
+
{ label: "closed unmerged \xB7 30d", value: String(thirtyDays.closed) },
|
|
1440
|
+
{ label: "open now", value: String(thirtyDays.open) },
|
|
1441
|
+
{ label: "merge rate \xB7 30d", value: formatPercent(thirtyDays.mergeRate) },
|
|
1442
|
+
{
|
|
1443
|
+
label: "median merge time \xB7 30d",
|
|
1444
|
+
value: formatDuration(thirtyDays.medianMergeTimeMs)
|
|
1445
|
+
}
|
|
1446
|
+
],
|
|
1447
|
+
recordSets: [
|
|
1448
|
+
{
|
|
1449
|
+
title: "Outcome windows",
|
|
1450
|
+
fields: [
|
|
1451
|
+
{ key: "window", label: "Window" },
|
|
1452
|
+
{ key: "created", label: "Created" },
|
|
1453
|
+
{ key: "merged", label: "Merged" },
|
|
1454
|
+
{ key: "closed", label: "Closed unmerged" },
|
|
1455
|
+
{ key: "mergeRate", label: "Merge rate" },
|
|
1456
|
+
{ key: "mergeTime", label: "Median merge time" }
|
|
1457
|
+
],
|
|
1458
|
+
records: windows.map((stats) => ({
|
|
1459
|
+
id: `${stats.days}d`,
|
|
1460
|
+
values: {
|
|
1461
|
+
window: `${stats.days} days`,
|
|
1462
|
+
created: String(stats.created),
|
|
1463
|
+
merged: String(stats.merged),
|
|
1464
|
+
closed: String(stats.closed),
|
|
1465
|
+
mergeRate: formatPercent(stats.mergeRate),
|
|
1466
|
+
mergeTime: formatDuration(stats.medianMergeTimeMs)
|
|
1467
|
+
}
|
|
1468
|
+
}))
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
title: "Repositories \xB7 30d",
|
|
1472
|
+
emptyText: "No Junior-owned pull request activity yet.",
|
|
1473
|
+
fields: [
|
|
1474
|
+
{ key: "repository", label: "Repository" },
|
|
1475
|
+
{ key: "created", label: "Created" },
|
|
1476
|
+
{ key: "merged", label: "Merged" },
|
|
1477
|
+
{ key: "closed", label: "Closed unmerged" },
|
|
1478
|
+
{ key: "open", label: "Open now" },
|
|
1479
|
+
{ key: "mergeRate", label: "Merge rate" }
|
|
1480
|
+
],
|
|
1481
|
+
records: repositories.map(({ repository, ...stats }) => ({
|
|
1482
|
+
id: repository,
|
|
1483
|
+
values: {
|
|
1484
|
+
repository,
|
|
1485
|
+
created: String(stats.created),
|
|
1486
|
+
merged: String(stats.merged),
|
|
1487
|
+
closed: String(stats.closed),
|
|
1488
|
+
open: String(stats.open),
|
|
1489
|
+
mergeRate: formatPercent(stats.mergeRate)
|
|
1490
|
+
}
|
|
1491
|
+
}))
|
|
1492
|
+
}
|
|
1493
|
+
]
|
|
840
1494
|
};
|
|
841
1495
|
}
|
|
842
1496
|
|
|
@@ -1177,11 +1831,11 @@ async function githubRequest(apiBase, path, params) {
|
|
|
1177
1831
|
},
|
|
1178
1832
|
...params.body ? { body: JSON.stringify(params.body) } : {}
|
|
1179
1833
|
});
|
|
1180
|
-
const
|
|
1834
|
+
const text2 = await response.text();
|
|
1181
1835
|
let parsed;
|
|
1182
|
-
if (
|
|
1836
|
+
if (text2) {
|
|
1183
1837
|
try {
|
|
1184
|
-
parsed = JSON.parse(
|
|
1838
|
+
parsed = JSON.parse(text2);
|
|
1185
1839
|
} catch {
|
|
1186
1840
|
parsed = void 0;
|
|
1187
1841
|
}
|
|
@@ -2067,6 +2721,22 @@ function githubPlugin(options = {}) {
|
|
|
2067
2721
|
]
|
|
2068
2722
|
},
|
|
2069
2723
|
hooks: {
|
|
2724
|
+
routes(ctx) {
|
|
2725
|
+
return [
|
|
2726
|
+
createGitHubWebhookRoute({
|
|
2727
|
+
botEmail: () => readEnv(botEmailEnv),
|
|
2728
|
+
db: ctx.db,
|
|
2729
|
+
resourceEvents: ctx.resourceEvents,
|
|
2730
|
+
webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")
|
|
2731
|
+
})
|
|
2732
|
+
];
|
|
2733
|
+
},
|
|
2734
|
+
async operationalReport(ctx) {
|
|
2735
|
+
return await buildGitHubOutcomeReport({
|
|
2736
|
+
db: ctx.db,
|
|
2737
|
+
nowMs: ctx.nowMs
|
|
2738
|
+
});
|
|
2739
|
+
},
|
|
2070
2740
|
tools(ctx) {
|
|
2071
2741
|
return createGitHubTools(ctx);
|
|
2072
2742
|
},
|