@sentry/junior-github 0.146.0 → 0.147.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/SETUP.md +1 -0
- package/dist/{chunk-AI2A6UER.js → chunk-IG5VPAKL.js} +137 -17
- package/dist/index.js +262 -212
- package/dist/testing.js +1 -1
- package/dist/webhooks/check-suite-enrichment.d.ts +11 -0
- package/dist/webhooks/handler.d.ts +2 -0
- package/dist/webhooks/resource-events.d.ts +37 -0
- package/package.json +2 -2
package/SETUP.md
CHANGED
|
@@ -292,36 +292,138 @@ function deploymentSourceTargets(input) {
|
|
|
292
292
|
var checkSuiteWebhookSchema = z.object({
|
|
293
293
|
action: z.string(),
|
|
294
294
|
check_suite: z.object({
|
|
295
|
+
app: z.object({
|
|
296
|
+
name: z.string().optional().nullable(),
|
|
297
|
+
slug: z.string().optional().nullable()
|
|
298
|
+
}).optional().nullable(),
|
|
295
299
|
conclusion: z.string().optional().nullable(),
|
|
296
300
|
head_sha: z.string().optional(),
|
|
301
|
+
id: z.number().optional(),
|
|
302
|
+
latest_check_runs_count: z.number().optional().nullable(),
|
|
297
303
|
pull_requests: z.array(z.object({ number: z.number() }))
|
|
298
304
|
}),
|
|
299
305
|
repository: repositorySchema
|
|
300
306
|
});
|
|
301
|
-
|
|
307
|
+
var FAILING_CHECK_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
308
|
+
"failure",
|
|
309
|
+
"timed_out",
|
|
310
|
+
"cancelled",
|
|
311
|
+
"action_required",
|
|
312
|
+
"startup_failure"
|
|
313
|
+
]);
|
|
314
|
+
function buildCheckSuiteUrl(args) {
|
|
315
|
+
return `https://github.com/${args.repo}/commit/${args.headSha}/checks?check_suite_id=${args.checkSuiteId}`;
|
|
316
|
+
}
|
|
317
|
+
function oneLineLabel(value, maxLength = 80) {
|
|
318
|
+
return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
319
|
+
}
|
|
320
|
+
function buildCheckSuiteResourceEvent(args) {
|
|
321
|
+
const resource = gitHubPullRequestResource({
|
|
322
|
+
number: args.pullRequestNumber,
|
|
323
|
+
repo: args.repo
|
|
324
|
+
});
|
|
325
|
+
const shortSha = args.headSha?.slice(0, 12);
|
|
326
|
+
const failingChecks = (args.failingChecks ?? []).slice(0, 12);
|
|
327
|
+
const failingCount = failingChecks.length;
|
|
328
|
+
const trustedSummary = args.eventType === "pull_request.checks.failed" ? `${resource.label} checks failed${failingCount > 0 ? ` (${failingCount})` : ""}${shortSha ? ` for ${shortSha}` : ""}.` : `${resource.label} check suite recovered${shortSha ? ` for ${shortSha}` : ""}.`;
|
|
329
|
+
const data = {
|
|
330
|
+
repo: args.repo,
|
|
331
|
+
pullRequest: args.pullRequestNumber,
|
|
332
|
+
scope: "check_suite",
|
|
333
|
+
suiteConclusion: args.suiteConclusion
|
|
334
|
+
};
|
|
335
|
+
if (args.headSha) data.headSha = args.headSha;
|
|
336
|
+
if (args.checkSuiteId !== void 0) data.checkSuiteId = args.checkSuiteId;
|
|
337
|
+
if (args.checkSuiteId !== void 0 && args.headSha) {
|
|
338
|
+
data.checkSuiteUrl = buildCheckSuiteUrl({
|
|
339
|
+
checkSuiteId: args.checkSuiteId,
|
|
340
|
+
headSha: args.headSha,
|
|
341
|
+
repo: args.repo
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (args.appName) data.appName = oneLineLabel(args.appName, 120);
|
|
345
|
+
if (args.latestCheckRunsCount !== void 0) {
|
|
346
|
+
data.latestCheckRunsCount = args.latestCheckRunsCount;
|
|
347
|
+
}
|
|
348
|
+
if (args.eventType === "pull_request.checks.failed" && failingCount > 0) {
|
|
349
|
+
data.failingChecks = failingChecks.map((check) => ({
|
|
350
|
+
conclusion: check.conclusion,
|
|
351
|
+
...check.htmlUrl ? { htmlUrl: check.htmlUrl } : {},
|
|
352
|
+
checkRunId: check.checkRunId
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
const untrustedParts = args.eventType === "pull_request.checks.failed" ? failingChecks.map((check) => {
|
|
356
|
+
const name = oneLineLabel(check.name);
|
|
357
|
+
if (!name) return void 0;
|
|
358
|
+
return check.htmlUrl ? `${name}: ${check.htmlUrl}` : name;
|
|
359
|
+
}).filter((part) => part !== void 0) : [];
|
|
360
|
+
const untrustedText = untrustedParts.length > 0 ? [`Failed checks:`, ...untrustedParts.map((part) => `- ${part}`)].join(
|
|
361
|
+
"\n"
|
|
362
|
+
) : void 0;
|
|
363
|
+
return {
|
|
364
|
+
eventKey: gitHubEventKey(
|
|
365
|
+
args.deliveryId,
|
|
366
|
+
`${args.eventType}:${args.pullRequestNumber}`
|
|
367
|
+
),
|
|
368
|
+
eventType: args.eventType,
|
|
369
|
+
occurredAtMs: Date.now(),
|
|
370
|
+
identifier: resource.identifier,
|
|
371
|
+
trustedSummary,
|
|
372
|
+
data,
|
|
373
|
+
...untrustedText ? { untrustedText } : {}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function selectFailingChecks(checkRuns, options) {
|
|
377
|
+
if (!Array.isArray(checkRuns)) return [];
|
|
378
|
+
const failing = [];
|
|
379
|
+
for (const run of checkRuns) {
|
|
380
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) continue;
|
|
381
|
+
const record = run;
|
|
382
|
+
if (options?.checkSuiteId !== void 0) {
|
|
383
|
+
const suite = record.check_suite;
|
|
384
|
+
const suiteId = suite && typeof suite === "object" && !Array.isArray(suite) && typeof suite.id === "number" ? suite.id : void 0;
|
|
385
|
+
if (suiteId !== void 0 && suiteId !== options.checkSuiteId) continue;
|
|
386
|
+
}
|
|
387
|
+
const conclusion = typeof record.conclusion === "string" ? record.conclusion : void 0;
|
|
388
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
389
|
+
const checkRunId = typeof record.id === "number" && Number.isSafeInteger(record.id) ? record.id : void 0;
|
|
390
|
+
if (!conclusion || !FAILING_CHECK_CONCLUSIONS.has(conclusion)) continue;
|
|
391
|
+
if (!name || checkRunId === void 0) continue;
|
|
392
|
+
const htmlUrl = typeof record.html_url === "string" && record.html_url.length > 0 ? record.html_url : void 0;
|
|
393
|
+
failing.push({
|
|
394
|
+
checkRunId,
|
|
395
|
+
conclusion,
|
|
396
|
+
...htmlUrl ? { htmlUrl } : {},
|
|
397
|
+
name
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return failing.slice(0, 12);
|
|
401
|
+
}
|
|
402
|
+
function normalizeCheckSuiteEvents(deliveryId, body, options) {
|
|
302
403
|
const parsed = checkSuiteWebhookSchema.safeParse(body);
|
|
303
404
|
if (!parsed.success || parsed.data.action !== "completed") return [];
|
|
304
405
|
const conclusion = parsed.data.check_suite.conclusion;
|
|
406
|
+
if (!conclusion) return [];
|
|
305
407
|
const eventType = conclusion === "failure" || conclusion === "timed_out" ? "pull_request.checks.failed" : conclusion === "success" ? "pull_request.checks.recovered" : void 0;
|
|
306
408
|
if (!eventType) return [];
|
|
307
|
-
const
|
|
308
|
-
|
|
409
|
+
const suite = parsed.data.check_suite;
|
|
410
|
+
const appName = suite.app?.name?.trim() || suite.app?.slug?.trim() || void 0;
|
|
411
|
+
const headSha = typeof suite.head_sha === "string" && /^[0-9a-f]{7,40}$/i.test(suite.head_sha) ? suite.head_sha : void 0;
|
|
412
|
+
return suite.pull_requests.flatMap((pullRequest) => {
|
|
309
413
|
const repo = parsed.data.repository.full_name;
|
|
310
|
-
const resource = gitHubPullRequestResource({
|
|
311
|
-
number: pullRequest.number,
|
|
312
|
-
repo
|
|
313
|
-
});
|
|
314
414
|
return pullRequestTargets(
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
),
|
|
415
|
+
buildCheckSuiteResourceEvent({
|
|
416
|
+
appName,
|
|
417
|
+
checkSuiteId: suite.id,
|
|
418
|
+
deliveryId,
|
|
320
419
|
eventType,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
420
|
+
failingChecks: eventType === "pull_request.checks.failed" ? options?.failingChecks : void 0,
|
|
421
|
+
headSha,
|
|
422
|
+
latestCheckRunsCount: typeof suite.latest_check_runs_count === "number" ? suite.latest_check_runs_count : void 0,
|
|
423
|
+
pullRequestNumber: pullRequest.number,
|
|
424
|
+
repo,
|
|
425
|
+
suiteConclusion: conclusion
|
|
426
|
+
}),
|
|
325
427
|
repo
|
|
326
428
|
);
|
|
327
429
|
});
|
|
@@ -651,6 +753,20 @@ function normalizeReleaseEvent(deliveryId, body) {
|
|
|
651
753
|
})
|
|
652
754
|
);
|
|
653
755
|
}
|
|
756
|
+
function parseCheckSuiteEnrichmentTarget(body) {
|
|
757
|
+
const parsed = checkSuiteWebhookSchema.safeParse(body);
|
|
758
|
+
if (!parsed.success || parsed.data.action !== "completed") return void 0;
|
|
759
|
+
const conclusion = parsed.data.check_suite.conclusion;
|
|
760
|
+
if (conclusion !== "failure" && conclusion !== "timed_out") return void 0;
|
|
761
|
+
const headSha = parsed.data.check_suite.head_sha;
|
|
762
|
+
const checkSuiteId = parsed.data.check_suite.id;
|
|
763
|
+
if (typeof headSha !== "string" || !/^[0-9a-f]{7,40}$/i.test(headSha) || typeof checkSuiteId !== "number") {
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
const [owner, repoName, ...extra] = parsed.data.repository.full_name.split("/");
|
|
767
|
+
if (!owner || !repoName || extra.length > 0) return void 0;
|
|
768
|
+
return { checkSuiteId, headSha, owner, repoName };
|
|
769
|
+
}
|
|
654
770
|
function normalizeGitHubResourceEvents(args) {
|
|
655
771
|
switch (args.eventName) {
|
|
656
772
|
case "deployment": {
|
|
@@ -675,7 +791,9 @@ function normalizeGitHubResourceEvents(args) {
|
|
|
675
791
|
return normalizePullRequestReviewCommentEvent(args.deliveryId, args.body);
|
|
676
792
|
}
|
|
677
793
|
case "check_suite":
|
|
678
|
-
return normalizeCheckSuiteEvents(args.deliveryId, args.body
|
|
794
|
+
return normalizeCheckSuiteEvents(args.deliveryId, args.body, {
|
|
795
|
+
failingChecks: args.failingChecks
|
|
796
|
+
});
|
|
679
797
|
case "release":
|
|
680
798
|
return normalizeReleaseEvent(args.deliveryId, args.body);
|
|
681
799
|
default:
|
|
@@ -697,5 +815,7 @@ export {
|
|
|
697
815
|
GITHUB_RELEASE_SUGGESTED_EVENTS,
|
|
698
816
|
gitHubReleaseSourceSubscribable,
|
|
699
817
|
gitHubRepositorySubscribable,
|
|
818
|
+
selectFailingChecks,
|
|
819
|
+
parseCheckSuiteEnrichmentTarget,
|
|
700
820
|
normalizeGitHubResourceEvents
|
|
701
821
|
};
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
gitHubPullRequestSubscribable,
|
|
13
13
|
gitHubReleaseSourceSubscribable,
|
|
14
14
|
gitHubRepositorySubscribable,
|
|
15
|
-
normalizeGitHubResourceEvents
|
|
16
|
-
|
|
15
|
+
normalizeGitHubResourceEvents,
|
|
16
|
+
parseCheckSuiteEnrichmentTarget,
|
|
17
|
+
selectFailingChecks
|
|
18
|
+
} from "./chunk-IG5VPAKL.js";
|
|
17
19
|
|
|
18
20
|
// src/plugin.ts
|
|
19
21
|
import {
|
|
@@ -2445,10 +2447,12 @@ function createGitHubWebhookRoute(args) {
|
|
|
2445
2447
|
args.db,
|
|
2446
2448
|
pullRequestLinkedIssues
|
|
2447
2449
|
) : false;
|
|
2450
|
+
const failingChecks = eventName === "check_suite" && args.loadFailingChecks ? await args.loadFailingChecks(body) : void 0;
|
|
2448
2451
|
const resourceEvents = normalizeGitHubResourceEvents({
|
|
2449
2452
|
body,
|
|
2450
2453
|
deliveryId,
|
|
2451
|
-
eventName
|
|
2454
|
+
eventName,
|
|
2455
|
+
failingChecks
|
|
2452
2456
|
});
|
|
2453
2457
|
for (const event of resourceEvents) {
|
|
2454
2458
|
await args.resourceEvents.publish(event);
|
|
@@ -3330,215 +3334,6 @@ async function classifyGitHubPullRequestCommitComposition(args) {
|
|
|
3330
3334
|
return foundCommit ? "junior_only" : void 0;
|
|
3331
3335
|
}
|
|
3332
3336
|
|
|
3333
|
-
// src/git-config.ts
|
|
3334
|
-
function cleanIdentityPart(value) {
|
|
3335
|
-
return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
|
|
3336
|
-
}
|
|
3337
|
-
function isSlackUserId(value) {
|
|
3338
|
-
return /^[UW][A-Z0-9]{5,}$/.test(value);
|
|
3339
|
-
}
|
|
3340
|
-
function isUserActor(actor) {
|
|
3341
|
-
return Boolean(actor && "userId" in actor);
|
|
3342
|
-
}
|
|
3343
|
-
function actorDisplayName(value, actor) {
|
|
3344
|
-
const name = cleanIdentityPart(value);
|
|
3345
|
-
if (!name || name.toLowerCase() === "unknown" || name === cleanIdentityPart(isUserActor(actor) ? actor.userId : void 0)) {
|
|
3346
|
-
return void 0;
|
|
3347
|
-
}
|
|
3348
|
-
return isSlackUserId(name) ? void 0 : name;
|
|
3349
|
-
}
|
|
3350
|
-
function actorName(actor) {
|
|
3351
|
-
if (!isUserActor(actor)) {
|
|
3352
|
-
return void 0;
|
|
3353
|
-
}
|
|
3354
|
-
return actorDisplayName(actor?.fullName, actor) || actorDisplayName(actor?.userName, actor) || void 0;
|
|
3355
|
-
}
|
|
3356
|
-
function actorEmail(actor) {
|
|
3357
|
-
if (!isUserActor(actor)) {
|
|
3358
|
-
return void 0;
|
|
3359
|
-
}
|
|
3360
|
-
const email = cleanIdentityPart(actor?.email);
|
|
3361
|
-
return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email) ? email : void 0;
|
|
3362
|
-
}
|
|
3363
|
-
function actorIdentityKey(actor) {
|
|
3364
|
-
if (actor.platform === "system") {
|
|
3365
|
-
return `system ${actor.name}`;
|
|
3366
|
-
}
|
|
3367
|
-
return actor.platform === "slack" ? `slack ${actor.teamId} ${actor.userId}` : `${actor.platform} ${actor.userId}`;
|
|
3368
|
-
}
|
|
3369
|
-
function additionalActorCoauthorTrailers(args) {
|
|
3370
|
-
if (!args.actors || args.actors.length === 0) {
|
|
3371
|
-
return [];
|
|
3372
|
-
}
|
|
3373
|
-
const seenEmails = /* @__PURE__ */ new Set([args.botEmail.toLowerCase()]);
|
|
3374
|
-
const seenActors = /* @__PURE__ */ new Set();
|
|
3375
|
-
const trailers = [];
|
|
3376
|
-
for (const candidate of args.actors) {
|
|
3377
|
-
const actorKey = actorIdentityKey(candidate);
|
|
3378
|
-
if (seenActors.has(actorKey)) {
|
|
3379
|
-
continue;
|
|
3380
|
-
}
|
|
3381
|
-
const name = actorName(candidate);
|
|
3382
|
-
const email = actorEmail(candidate);
|
|
3383
|
-
if (!name || !email) {
|
|
3384
|
-
continue;
|
|
3385
|
-
}
|
|
3386
|
-
const emailKey = email.toLowerCase();
|
|
3387
|
-
if (seenEmails.has(emailKey)) {
|
|
3388
|
-
continue;
|
|
3389
|
-
}
|
|
3390
|
-
seenActors.add(actorKey);
|
|
3391
|
-
seenEmails.add(emailKey);
|
|
3392
|
-
trailers.push(`Co-Authored-By: ${name} <${email}>`);
|
|
3393
|
-
}
|
|
3394
|
-
return trailers;
|
|
3395
|
-
}
|
|
3396
|
-
function prepareCommitMsgHook() {
|
|
3397
|
-
return `#!/usr/bin/env bash
|
|
3398
|
-
set -eu
|
|
3399
|
-
|
|
3400
|
-
message_file="\${1:-}"
|
|
3401
|
-
if [ -z "$message_file" ]; then
|
|
3402
|
-
exit 1
|
|
3403
|
-
fi
|
|
3404
|
-
|
|
3405
|
-
if [ -z "\${JUNIOR_GIT_AUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_AUTHOR_EMAIL:-}" ]; then
|
|
3406
|
-
echo "Junior GitHub plugin internal error: Junior author identity was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
|
|
3407
|
-
exit 1
|
|
3408
|
-
fi
|
|
3409
|
-
|
|
3410
|
-
if [ "\${GIT_AUTHOR_NAME:-}" != "$JUNIOR_GIT_AUTHOR_NAME" ] || [ "\${GIT_AUTHOR_EMAIL:-}" != "$JUNIOR_GIT_AUTHOR_EMAIL" ]; then
|
|
3411
|
-
echo "Junior GitHub plugin internal error: Git author was not set to the Junior identity. Do not override Git author manually; report this configuration error." >&2
|
|
3412
|
-
exit 1
|
|
3413
|
-
fi
|
|
3414
|
-
|
|
3415
|
-
# Git and GitHub only interpret the final contiguous paragraph as the trailer
|
|
3416
|
-
# block, so all missing trailers are collected and appended as one block:
|
|
3417
|
-
# human actor trailers in run order.
|
|
3418
|
-
desired_trailers=""
|
|
3419
|
-
add_trailer() {
|
|
3420
|
-
desired_trailers="$desired_trailers$1"$'\\n'
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
|
-
if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
|
|
3424
|
-
while IFS= read -r actor_trailer; do
|
|
3425
|
-
if [ -n "$actor_trailer" ]; then
|
|
3426
|
-
add_trailer "$actor_trailer"
|
|
3427
|
-
fi
|
|
3428
|
-
done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
|
|
3429
|
-
fi
|
|
3430
|
-
|
|
3431
|
-
# The hook owns co-author attribution. Remove every model-supplied co-author
|
|
3432
|
-
# from the final Git trailer block before appending the host-derived actors.
|
|
3433
|
-
tmp_file=$(mktemp)
|
|
3434
|
-
trap 'rm -f "$tmp_file"' EXIT
|
|
3435
|
-
awk '
|
|
3436
|
-
{ lines[NR] = $0 }
|
|
3437
|
-
END {
|
|
3438
|
-
line = NR
|
|
3439
|
-
while (line > 0 && lines[line] == "") {
|
|
3440
|
-
line--
|
|
3441
|
-
}
|
|
3442
|
-
start = line
|
|
3443
|
-
while (start > 0 && lines[start] != "") {
|
|
3444
|
-
start--
|
|
3445
|
-
}
|
|
3446
|
-
valid = line > 0
|
|
3447
|
-
for (i = start + 1; valid && i <= line; i++) {
|
|
3448
|
-
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
3449
|
-
valid = 0
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
if (!valid) {
|
|
3453
|
-
for (i = 1; i <= NR; i++) {
|
|
3454
|
-
print lines[i]
|
|
3455
|
-
}
|
|
3456
|
-
exit 0
|
|
3457
|
-
}
|
|
3458
|
-
kept = 0
|
|
3459
|
-
for (i = start + 1; i <= line; i++) {
|
|
3460
|
-
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
3461
|
-
kept++
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
before = kept > 0 ? start : start - 1
|
|
3465
|
-
for (i = 1; i <= before; i++) {
|
|
3466
|
-
print lines[i]
|
|
3467
|
-
}
|
|
3468
|
-
for (i = start + 1; i <= line; i++) {
|
|
3469
|
-
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
3470
|
-
print lines[i]
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
' "$message_file" > "$tmp_file"
|
|
3475
|
-
cat "$tmp_file" > "$message_file"
|
|
3476
|
-
|
|
3477
|
-
final_trailer_block=$(awk '
|
|
3478
|
-
{ lines[NR] = $0 }
|
|
3479
|
-
END {
|
|
3480
|
-
line = NR
|
|
3481
|
-
while (line > 0 && lines[line] == "") {
|
|
3482
|
-
line--
|
|
3483
|
-
}
|
|
3484
|
-
if (line == 0) {
|
|
3485
|
-
exit 0
|
|
3486
|
-
}
|
|
3487
|
-
start = line
|
|
3488
|
-
while (start > 0 && lines[start] != "") {
|
|
3489
|
-
start--
|
|
3490
|
-
}
|
|
3491
|
-
for (i = start + 1; i <= line; i++) {
|
|
3492
|
-
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
3493
|
-
exit 0
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
for (i = start + 1; i <= line; i++) {
|
|
3497
|
-
print lines[i]
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
' "$message_file")
|
|
3501
|
-
|
|
3502
|
-
missing_trailers=""
|
|
3503
|
-
collect_missing_trailer() {
|
|
3504
|
-
if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
|
|
3505
|
-
missing_trailers="\${missing_trailers}\${1}"$'\\n'
|
|
3506
|
-
fi
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
while IFS= read -r desired_trailer; do
|
|
3510
|
-
if [ -n "$desired_trailer" ]; then
|
|
3511
|
-
collect_missing_trailer "$desired_trailer"
|
|
3512
|
-
fi
|
|
3513
|
-
done <<< "$desired_trailers"
|
|
3514
|
-
|
|
3515
|
-
if [ -z "$missing_trailers" ]; then
|
|
3516
|
-
exit 0
|
|
3517
|
-
fi
|
|
3518
|
-
|
|
3519
|
-
if [ -n "$(tail -c 1 "$message_file")" ]; then
|
|
3520
|
-
printf '\\n' >> "$message_file"
|
|
3521
|
-
fi
|
|
3522
|
-
|
|
3523
|
-
if [ -n "$final_trailer_block" ]; then
|
|
3524
|
-
printf '%s' "$missing_trailers" >> "$message_file"
|
|
3525
|
-
else
|
|
3526
|
-
printf '\\n%s' "$missing_trailers" >> "$message_file"
|
|
3527
|
-
fi
|
|
3528
|
-
`;
|
|
3529
|
-
}
|
|
3530
|
-
async function configureGit(ctx, key, value) {
|
|
3531
|
-
const result = await ctx.sandbox.run({
|
|
3532
|
-
cmd: "git",
|
|
3533
|
-
args: ["config", "--global", key, value]
|
|
3534
|
-
});
|
|
3535
|
-
if (result.exitCode !== 0) {
|
|
3536
|
-
throw new Error(
|
|
3537
|
-
`Failed to configure git ${key}: ${result.stderr || result.stdout}`
|
|
3538
|
-
);
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
3337
|
// src/credential-support.ts
|
|
3543
3338
|
import { createPrivateKey, createSign } from "crypto";
|
|
3544
3339
|
var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
|
|
@@ -3910,6 +3705,7 @@ async function resolveUserAccount(tokens) {
|
|
|
3910
3705
|
}
|
|
3911
3706
|
const url = typeof account.html_url === "string" ? account.html_url : void 0;
|
|
3912
3707
|
return {
|
|
3708
|
+
handle: login.trim(),
|
|
3913
3709
|
id: String(id),
|
|
3914
3710
|
label: login.trim(),
|
|
3915
3711
|
...url ? { url } : {}
|
|
@@ -4125,6 +3921,253 @@ function createPermissionCache() {
|
|
|
4125
3921
|
};
|
|
4126
3922
|
}
|
|
4127
3923
|
|
|
3924
|
+
// src/webhooks/check-suite-enrichment.ts
|
|
3925
|
+
function checkRunsFromResponse(value) {
|
|
3926
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3927
|
+
return [];
|
|
3928
|
+
}
|
|
3929
|
+
const checkRuns = value.check_runs;
|
|
3930
|
+
return Array.isArray(checkRuns) ? checkRuns : [];
|
|
3931
|
+
}
|
|
3932
|
+
async function loadFailingChecksForSuite(args) {
|
|
3933
|
+
const target = parseCheckSuiteEnrichmentTarget(args.body);
|
|
3934
|
+
if (!target) return void 0;
|
|
3935
|
+
try {
|
|
3936
|
+
const token = await issueInstallationToken({
|
|
3937
|
+
appIdEnv: args.appIdEnv,
|
|
3938
|
+
installationIdEnv: args.installationIdEnv,
|
|
3939
|
+
permissions: { checks: "read" },
|
|
3940
|
+
privateKeyEnv: args.privateKeyEnv,
|
|
3941
|
+
repositories: [target.repoName]
|
|
3942
|
+
});
|
|
3943
|
+
const response = await githubRequest(
|
|
3944
|
+
"https://api.github.com",
|
|
3945
|
+
`/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repoName)}/check-suites/${target.checkSuiteId}/check-runs?filter=latest&per_page=100`,
|
|
3946
|
+
{ token: token.token }
|
|
3947
|
+
);
|
|
3948
|
+
const failing = selectFailingChecks(checkRunsFromResponse(response), {
|
|
3949
|
+
checkSuiteId: target.checkSuiteId
|
|
3950
|
+
});
|
|
3951
|
+
return failing.length > 0 ? failing : void 0;
|
|
3952
|
+
} catch (error) {
|
|
3953
|
+
args.log?.error("GitHub check suite enrichment failed", {
|
|
3954
|
+
checkSuiteId: target.checkSuiteId,
|
|
3955
|
+
errorType: error instanceof Error ? error.name : "UnknownError",
|
|
3956
|
+
repository: `${target.owner}/${target.repoName}`
|
|
3957
|
+
});
|
|
3958
|
+
return void 0;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
// src/git-config.ts
|
|
3963
|
+
function cleanIdentityPart(value) {
|
|
3964
|
+
return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
|
|
3965
|
+
}
|
|
3966
|
+
function isSlackUserId(value) {
|
|
3967
|
+
return /^[UW][A-Z0-9]{5,}$/.test(value);
|
|
3968
|
+
}
|
|
3969
|
+
function isUserActor(actor) {
|
|
3970
|
+
return Boolean(actor && "userId" in actor);
|
|
3971
|
+
}
|
|
3972
|
+
function actorDisplayName(value, actor) {
|
|
3973
|
+
const name = cleanIdentityPart(value);
|
|
3974
|
+
if (!name || name.toLowerCase() === "unknown" || name === cleanIdentityPart(isUserActor(actor) ? actor.userId : void 0)) {
|
|
3975
|
+
return void 0;
|
|
3976
|
+
}
|
|
3977
|
+
return isSlackUserId(name) ? void 0 : name;
|
|
3978
|
+
}
|
|
3979
|
+
function actorName(actor) {
|
|
3980
|
+
if (!isUserActor(actor)) {
|
|
3981
|
+
return void 0;
|
|
3982
|
+
}
|
|
3983
|
+
return actorDisplayName(actor?.fullName, actor) || actorDisplayName(actor?.userName, actor) || void 0;
|
|
3984
|
+
}
|
|
3985
|
+
function actorEmail(actor) {
|
|
3986
|
+
if (!isUserActor(actor)) {
|
|
3987
|
+
return void 0;
|
|
3988
|
+
}
|
|
3989
|
+
const email = cleanIdentityPart(actor?.email);
|
|
3990
|
+
return /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(email) ? email : void 0;
|
|
3991
|
+
}
|
|
3992
|
+
function actorIdentityKey(actor) {
|
|
3993
|
+
if (actor.platform === "system") {
|
|
3994
|
+
return `system ${actor.name}`;
|
|
3995
|
+
}
|
|
3996
|
+
return actor.platform === "slack" ? `slack ${actor.teamId} ${actor.userId}` : `${actor.platform} ${actor.userId}`;
|
|
3997
|
+
}
|
|
3998
|
+
function additionalActorCoauthorTrailers(args) {
|
|
3999
|
+
if (!args.actors || args.actors.length === 0) {
|
|
4000
|
+
return [];
|
|
4001
|
+
}
|
|
4002
|
+
const seenEmails = /* @__PURE__ */ new Set([args.botEmail.toLowerCase()]);
|
|
4003
|
+
const seenActors = /* @__PURE__ */ new Set();
|
|
4004
|
+
const trailers = [];
|
|
4005
|
+
for (const candidate of args.actors) {
|
|
4006
|
+
const actorKey = actorIdentityKey(candidate);
|
|
4007
|
+
if (seenActors.has(actorKey)) {
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
const name = actorName(candidate);
|
|
4011
|
+
const email = actorEmail(candidate);
|
|
4012
|
+
if (!name || !email) {
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
const emailKey = email.toLowerCase();
|
|
4016
|
+
if (seenEmails.has(emailKey)) {
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
seenActors.add(actorKey);
|
|
4020
|
+
seenEmails.add(emailKey);
|
|
4021
|
+
trailers.push(`Co-Authored-By: ${name} <${email}>`);
|
|
4022
|
+
}
|
|
4023
|
+
return trailers;
|
|
4024
|
+
}
|
|
4025
|
+
function prepareCommitMsgHook() {
|
|
4026
|
+
return `#!/usr/bin/env bash
|
|
4027
|
+
set -eu
|
|
4028
|
+
|
|
4029
|
+
message_file="\${1:-}"
|
|
4030
|
+
if [ -z "$message_file" ]; then
|
|
4031
|
+
exit 1
|
|
4032
|
+
fi
|
|
4033
|
+
|
|
4034
|
+
if [ -z "\${JUNIOR_GIT_AUTHOR_NAME:-}" ] || [ -z "\${JUNIOR_GIT_AUTHOR_EMAIL:-}" ]; then
|
|
4035
|
+
echo "Junior GitHub plugin internal error: Junior author identity was not injected by the host runtime. Do not set Git author env vars manually; report this configuration error." >&2
|
|
4036
|
+
exit 1
|
|
4037
|
+
fi
|
|
4038
|
+
|
|
4039
|
+
if [ "\${GIT_AUTHOR_NAME:-}" != "$JUNIOR_GIT_AUTHOR_NAME" ] || [ "\${GIT_AUTHOR_EMAIL:-}" != "$JUNIOR_GIT_AUTHOR_EMAIL" ]; then
|
|
4040
|
+
echo "Junior GitHub plugin internal error: Git author was not set to the Junior identity. Do not override Git author manually; report this configuration error." >&2
|
|
4041
|
+
exit 1
|
|
4042
|
+
fi
|
|
4043
|
+
|
|
4044
|
+
# Git and GitHub only interpret the final contiguous paragraph as the trailer
|
|
4045
|
+
# block, so all missing trailers are collected and appended as one block:
|
|
4046
|
+
# human actor trailers in run order.
|
|
4047
|
+
desired_trailers=""
|
|
4048
|
+
add_trailer() {
|
|
4049
|
+
desired_trailers="$desired_trailers$1"$'\\n'
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
|
|
4053
|
+
while IFS= read -r actor_trailer; do
|
|
4054
|
+
if [ -n "$actor_trailer" ]; then
|
|
4055
|
+
add_trailer "$actor_trailer"
|
|
4056
|
+
fi
|
|
4057
|
+
done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
|
|
4058
|
+
fi
|
|
4059
|
+
|
|
4060
|
+
# The hook owns co-author attribution. Remove every model-supplied co-author
|
|
4061
|
+
# from the final Git trailer block before appending the host-derived actors.
|
|
4062
|
+
tmp_file=$(mktemp)
|
|
4063
|
+
trap 'rm -f "$tmp_file"' EXIT
|
|
4064
|
+
awk '
|
|
4065
|
+
{ lines[NR] = $0 }
|
|
4066
|
+
END {
|
|
4067
|
+
line = NR
|
|
4068
|
+
while (line > 0 && lines[line] == "") {
|
|
4069
|
+
line--
|
|
4070
|
+
}
|
|
4071
|
+
start = line
|
|
4072
|
+
while (start > 0 && lines[start] != "") {
|
|
4073
|
+
start--
|
|
4074
|
+
}
|
|
4075
|
+
valid = line > 0
|
|
4076
|
+
for (i = start + 1; valid && i <= line; i++) {
|
|
4077
|
+
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
4078
|
+
valid = 0
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
if (!valid) {
|
|
4082
|
+
for (i = 1; i <= NR; i++) {
|
|
4083
|
+
print lines[i]
|
|
4084
|
+
}
|
|
4085
|
+
exit 0
|
|
4086
|
+
}
|
|
4087
|
+
kept = 0
|
|
4088
|
+
for (i = start + 1; i <= line; i++) {
|
|
4089
|
+
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
4090
|
+
kept++
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
before = kept > 0 ? start : start - 1
|
|
4094
|
+
for (i = 1; i <= before; i++) {
|
|
4095
|
+
print lines[i]
|
|
4096
|
+
}
|
|
4097
|
+
for (i = start + 1; i <= line; i++) {
|
|
4098
|
+
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
4099
|
+
print lines[i]
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
' "$message_file" > "$tmp_file"
|
|
4104
|
+
cat "$tmp_file" > "$message_file"
|
|
4105
|
+
|
|
4106
|
+
final_trailer_block=$(awk '
|
|
4107
|
+
{ lines[NR] = $0 }
|
|
4108
|
+
END {
|
|
4109
|
+
line = NR
|
|
4110
|
+
while (line > 0 && lines[line] == "") {
|
|
4111
|
+
line--
|
|
4112
|
+
}
|
|
4113
|
+
if (line == 0) {
|
|
4114
|
+
exit 0
|
|
4115
|
+
}
|
|
4116
|
+
start = line
|
|
4117
|
+
while (start > 0 && lines[start] != "") {
|
|
4118
|
+
start--
|
|
4119
|
+
}
|
|
4120
|
+
for (i = start + 1; i <= line; i++) {
|
|
4121
|
+
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
4122
|
+
exit 0
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
for (i = start + 1; i <= line; i++) {
|
|
4126
|
+
print lines[i]
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
' "$message_file")
|
|
4130
|
+
|
|
4131
|
+
missing_trailers=""
|
|
4132
|
+
collect_missing_trailer() {
|
|
4133
|
+
if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
|
|
4134
|
+
missing_trailers="\${missing_trailers}\${1}"$'\\n'
|
|
4135
|
+
fi
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
while IFS= read -r desired_trailer; do
|
|
4139
|
+
if [ -n "$desired_trailer" ]; then
|
|
4140
|
+
collect_missing_trailer "$desired_trailer"
|
|
4141
|
+
fi
|
|
4142
|
+
done <<< "$desired_trailers"
|
|
4143
|
+
|
|
4144
|
+
if [ -z "$missing_trailers" ]; then
|
|
4145
|
+
exit 0
|
|
4146
|
+
fi
|
|
4147
|
+
|
|
4148
|
+
if [ -n "$(tail -c 1 "$message_file")" ]; then
|
|
4149
|
+
printf '\\n' >> "$message_file"
|
|
4150
|
+
fi
|
|
4151
|
+
|
|
4152
|
+
if [ -n "$final_trailer_block" ]; then
|
|
4153
|
+
printf '%s' "$missing_trailers" >> "$message_file"
|
|
4154
|
+
else
|
|
4155
|
+
printf '\\n%s' "$missing_trailers" >> "$message_file"
|
|
4156
|
+
fi
|
|
4157
|
+
`;
|
|
4158
|
+
}
|
|
4159
|
+
async function configureGit(ctx, key, value) {
|
|
4160
|
+
const result = await ctx.sandbox.run({
|
|
4161
|
+
cmd: "git",
|
|
4162
|
+
args: ["config", "--global", key, value]
|
|
4163
|
+
});
|
|
4164
|
+
if (result.exitCode !== 0) {
|
|
4165
|
+
throw new Error(
|
|
4166
|
+
`Failed to configure git ${key}: ${result.stderr || result.stdout}`
|
|
4167
|
+
);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4128
4171
|
// src/plugin.ts
|
|
4129
4172
|
function githubSmartHttpAccess(upstreamUrl) {
|
|
4130
4173
|
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
@@ -4618,6 +4661,13 @@ function githubPlugin(options = {}) {
|
|
|
4618
4661
|
},
|
|
4619
4662
|
db: ctx.db,
|
|
4620
4663
|
installationId: () => readEnv(installationIdEnv),
|
|
4664
|
+
loadFailingChecks: async (body) => await loadFailingChecksForSuite({
|
|
4665
|
+
appIdEnv,
|
|
4666
|
+
body,
|
|
4667
|
+
installationIdEnv,
|
|
4668
|
+
log: ctx.log,
|
|
4669
|
+
privateKeyEnv
|
|
4670
|
+
}),
|
|
4621
4671
|
log: ctx.log,
|
|
4622
4672
|
resourceEvents: ctx.resourceEvents,
|
|
4623
4673
|
webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")
|
package/dist/testing.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type GitHubFailingCheck } from "./resource-events.js";
|
|
2
|
+
/** Load failed check-run names and urls for one failed check suite. */
|
|
3
|
+
export declare function loadFailingChecksForSuite(args: {
|
|
4
|
+
appIdEnv: string;
|
|
5
|
+
body: unknown;
|
|
6
|
+
installationIdEnv: string;
|
|
7
|
+
log?: {
|
|
8
|
+
error(message: string, metadata?: Record<string, unknown>): void;
|
|
9
|
+
};
|
|
10
|
+
privateKeyEnv: string;
|
|
11
|
+
}): Promise<GitHubFailingCheck[] | undefined>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PluginLogger, PluginRoute, ResourceEventPublisher } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { GitHubDb } from "../db/database.js";
|
|
3
3
|
import type { GitHubPullRequestCommitComposition } from "../db/schema.js";
|
|
4
|
+
import { type GitHubFailingCheck } from "./resource-events.js";
|
|
4
5
|
/** Create the public, signed GitHub webhook route owned by the plugin. */
|
|
5
6
|
export declare function createGitHubWebhookRoute(args: {
|
|
6
7
|
botEmail(): string | undefined;
|
|
@@ -10,6 +11,7 @@ export declare function createGitHubWebhookRoute(args: {
|
|
|
10
11
|
}): Promise<GitHubPullRequestCommitComposition | undefined>;
|
|
11
12
|
db: GitHubDb;
|
|
12
13
|
installationId(): string | undefined;
|
|
14
|
+
loadFailingChecks?(body: unknown): Promise<GitHubFailingCheck[] | undefined>;
|
|
13
15
|
log?: Pick<PluginLogger, "error">;
|
|
14
16
|
resourceEvents: ResourceEventPublisher;
|
|
15
17
|
webhookSecret(): string | undefined;
|
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import type { ResourceEventInput } from "@sentry/junior-plugin-api";
|
|
2
|
+
export type GitHubFailingCheck = {
|
|
3
|
+
checkRunId: number;
|
|
4
|
+
conclusion: string;
|
|
5
|
+
htmlUrl?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
/** Build a browser URL for one check suite. GitHub does not send html_url. */
|
|
9
|
+
export declare function buildCheckSuiteUrl(args: {
|
|
10
|
+
checkSuiteId: number;
|
|
11
|
+
headSha: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/** Build the trusted data and summary for one check-suite PR event. */
|
|
15
|
+
export declare function buildCheckSuiteResourceEvent(args: {
|
|
16
|
+
appName?: string;
|
|
17
|
+
checkSuiteId?: number;
|
|
18
|
+
deliveryId: string;
|
|
19
|
+
eventType: "pull_request.checks.failed" | "pull_request.checks.recovered";
|
|
20
|
+
failingChecks?: GitHubFailingCheck[];
|
|
21
|
+
headSha?: string;
|
|
22
|
+
latestCheckRunsCount?: number;
|
|
23
|
+
pullRequestNumber: number;
|
|
24
|
+
repo: string;
|
|
25
|
+
suiteConclusion: string;
|
|
26
|
+
}): ResourceEventInput;
|
|
27
|
+
/** Keep only failed check runs from one suite. */
|
|
28
|
+
export declare function selectFailingChecks(checkRuns: unknown, options?: {
|
|
29
|
+
checkSuiteId?: number;
|
|
30
|
+
}): GitHubFailingCheck[];
|
|
31
|
+
/** Read the check suite target used to load failed check runs. */
|
|
32
|
+
export declare function parseCheckSuiteEnrichmentTarget(body: unknown): {
|
|
33
|
+
checkSuiteId: number;
|
|
34
|
+
headSha: string;
|
|
35
|
+
owner: string;
|
|
36
|
+
repoName: string;
|
|
37
|
+
} | undefined;
|
|
2
38
|
/** Normalize one verified GitHub delivery into conversation resource events. */
|
|
3
39
|
export declare function normalizeGitHubResourceEvents(args: {
|
|
4
40
|
body: unknown;
|
|
5
41
|
deliveryId: string;
|
|
6
42
|
eventName: string;
|
|
43
|
+
failingChecks?: GitHubFailingCheck[];
|
|
7
44
|
}): ResourceEventInput[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.147.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.147.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.9.1",
|