@sentry/junior-github 0.145.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 CHANGED
@@ -14,6 +14,7 @@ In GitHub:
14
14
  - Contents: Read and write
15
15
  - Pull requests: Read and write
16
16
  - Actions: Read and write
17
+ - Checks: Read
17
18
  - Deployments: Read
18
19
  - Workflows: Write
19
20
  - Metadata: Read
@@ -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
- function normalizeCheckSuiteEvents(deliveryId, body) {
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 sha = parsed.data.check_suite.head_sha?.slice(0, 12);
308
- return parsed.data.check_suite.pull_requests.flatMap((pullRequest) => {
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
- eventKey: gitHubEventKey(
317
- deliveryId,
318
- `${eventType}:${pullRequest.number}`
319
- ),
415
+ buildCheckSuiteResourceEvent({
416
+ appName,
417
+ checkSuiteId: suite.id,
418
+ deliveryId,
320
419
  eventType,
321
- occurredAtMs: Date.now(),
322
- identifier: resource.identifier,
323
- trustedSummary: eventType === "pull_request.checks.failed" ? `${resource.label} checks failed${sha ? ` for ${sha}` : ""}.` : `${resource.label} checks recovered${sha ? ` for ${sha}` : ""}.`
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
- } from "./chunk-AI2A6UER.js";
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normalizeGitHubResourceEvents
3
- } from "./chunk-AI2A6UER.js";
3
+ } from "./chunk-IG5VPAKL.js";
4
4
  export {
5
5
  normalizeGitHubResourceEvents
6
6
  };
@@ -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.145.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.145.0"
34
+ "@sentry/junior-plugin-api": "0.147.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",