@mastra/github-signals 0.1.0 → 0.1.1
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/CHANGELOG.md +31 -0
- package/dist/index.cjs +247 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +247 -44
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @mastra/github-signals
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Gate GitHub signal notifications behind author permission checks to guard against prompt injection from random commenters. Only comments from users with write access (admin, maintain, write) trigger notifications. Bot comments are opt-in via an allowlist that defaults to CodeRabbit and Devin, with `ignoredBots` still available as an explicit blocklist. Unauthorized latest comments are excluded before notification classification so noisy bot edits do not render in notification metadata or mask the latest authorized comment. Scheduled polls now include comments and detect latest-comment timestamp changes so comment notifications are not lost behind stale or unchanged thread hashes. Comment activity notifications render the latest authorized comment author and excerpt as high-priority GitHub signal updates. ([#17590](https://github.com/mastra-ai/mastra/pull/17590))
|
|
8
|
+
|
|
9
|
+
New options on `GithubSignalsOptions`:
|
|
10
|
+
- `authorizedPermissions` — permission levels that authorize human commenters (default: `['admin', 'maintain', 'write']`)
|
|
11
|
+
- `authorizedBots` — bot logins authorized to trigger notifications (default: `['coderabbitai[bot]', 'devin-ai-integration[bot]']`)
|
|
12
|
+
- `ignoredBots` — bot logins whose comments should NOT trigger notifications, even if authorized
|
|
13
|
+
- `permissionResolver` — injectable resolver for looking up collaborator permissions (default: `gh api`)
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`d468acb`](https://github.com/mastra-ai/mastra/commit/d468acb07aec1bb19a2cb0ada8042b05b46746b2), [`575f815`](https://github.com/mastra-ai/mastra/commit/575f815c5c3567b71c0b83cbb7fa98c8253a9d9c), [`34839c1`](https://github.com/mastra-ai/mastra/commit/34839c1910b6964bf59ed0cee58844efebbb684e), [`053735a`](https://github.com/mastra-ai/mastra/commit/053735a75c2c18e23ce34d9468007efa4a45f4c4), [`306909a`](https://github.com/mastra-ai/mastra/commit/306909a693de77d709b38706e2673c9547d24a28), [`5191af8`](https://github.com/mastra-ai/mastra/commit/5191af80c799eea25357c545fc05d91b3883531d), [`43bd3d4`](https://github.com/mastra-ai/mastra/commit/43bd3d421987463fdf35386a45199c49499ed069), [`e6fa79e`](https://github.com/mastra-ai/mastra/commit/e6fa79ec72a2ddffdd25e85270398951e9d552a4), [`904bcdf`](https://github.com/mastra-ai/mastra/commit/904bcdf7b8004aa7be823f9f70ca63580e47e470), [`7f5ee1d`](https://github.com/mastra-ai/mastra/commit/7f5ee1dca46daee8d2817f2ebe49e6335da81956), [`1e9aab5`](https://github.com/mastra-ai/mastra/commit/1e9aab50ff11e6e88fde4d7cbf512c44a9fe8d61), [`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`bf8eb6d`](https://github.com/mastra-ai/mastra/commit/bf8eb6d0ec213a403eb9265a594ad283c44ab3dc), [`e9be4e7`](https://github.com/mastra-ai/mastra/commit/e9be4e747ec3d8b65548bff92f9377db06105376), [`493a328`](https://github.com/mastra-ai/mastra/commit/493a328f4346a1deeb9f1e2e44c8f2a3a4d7591b), [`d53cfc2`](https://github.com/mastra-ai/mastra/commit/d53cfc2c7f8d78343a4aa84ec4e129ba25f3325e), [`65799d4`](https://github.com/mastra-ai/mastra/commit/65799d4d549e5ebb9c848fbe3f51ac090f64becf), [`c268c89`](https://github.com/mastra-ai/mastra/commit/c268c89f4c63a93ee474d3cffdf3ea60bf00d4f2), [`34839c1`](https://github.com/mastra-ai/mastra/commit/34839c1910b6964bf59ed0cee58844efebbb684e), [`014e00f`](https://github.com/mastra-ai/mastra/commit/014e00f2b3a597a016b72f9901c6ab27d491f822), [`029a414`](https://github.com/mastra-ai/mastra/commit/029a4141719793bd3e898a39eb5a0466a55f5f3a), [`d468acb`](https://github.com/mastra-ai/mastra/commit/d468acb07aec1bb19a2cb0ada8042b05b46746b2), [`b147b29`](https://github.com/mastra-ai/mastra/commit/b147b2907f0cd1aa812efe6d6e3f58d22e66fc88), [`d371ac1`](https://github.com/mastra-ai/mastra/commit/d371ac1d9820afaaf7cfdbc380a475946a994d8f), [`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`0c72f03`](https://github.com/mastra-ai/mastra/commit/0c72f032abb13254df5a7856d64be2f207b8006d), [`cf182b7`](https://github.com/mastra-ai/mastra/commit/cf182b7fb495767946d9840ef29f19cfa906f31f), [`3b45ea9`](https://github.com/mastra-ai/mastra/commit/3b45ea95015557a6cb9d70dc5252af54ab1b78ac), [`a049c2a`](https://github.com/mastra-ai/mastra/commit/a049c2a9dfb41d0ee2e7a28874a88cd64fd5669f), [`f084be1`](https://github.com/mastra-ai/mastra/commit/f084be1fcbe33ad7480913e44d6130c421c0976f), [`b147b29`](https://github.com/mastra-ai/mastra/commit/b147b2907f0cd1aa812efe6d6e3f58d22e66fc88), [`2a96528`](https://github.com/mastra-ai/mastra/commit/2a9652848dfa3c5a2426f952e9d93554c26fd90f), [`f2ab060`](https://github.com/mastra-ai/mastra/commit/f2ab060162bea81505fda553e2cee29c1979fd04), [`5d302c8`](https://github.com/mastra-ai/mastra/commit/5d302c8eda1a6ac74eab5e442c4f64db6cc97a06), [`34839c1`](https://github.com/mastra-ai/mastra/commit/34839c1910b6964bf59ed0cee58844efebbb684e), [`a952852`](https://github.com/mastra-ai/mastra/commit/a952852c971a21fb646cd907c75fcf4443cdc963), [`2656d9c`](https://github.com/mastra-ai/mastra/commit/2656d9c2976d4f3354253bfbbbf9b88a1b2bbf34), [`63e3fe1`](https://github.com/mastra-ai/mastra/commit/63e3fe13cc1ea96f91d7c68aea92f400faf9e4da), [`1d4ce8d`](https://github.com/mastra-ai/mastra/commit/1d4ce8daaa54511f325c1b609d31b8e54009d677), [`8c68372`](https://github.com/mastra-ai/mastra/commit/8c68372e85fe0b066ec12c58bd29ffb93e54c552)]:
|
|
16
|
+
- @mastra/core@1.42.0
|
|
17
|
+
|
|
18
|
+
## 0.1.1-alpha.0
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Gate GitHub signal notifications behind author permission checks to guard against prompt injection from random commenters. Only comments from users with write access (admin, maintain, write) trigger notifications. Bot comments are opt-in via an allowlist that defaults to CodeRabbit and Devin, with `ignoredBots` still available as an explicit blocklist. Unauthorized latest comments are excluded before notification classification so noisy bot edits do not render in notification metadata or mask the latest authorized comment. Scheduled polls now include comments and detect latest-comment timestamp changes so comment notifications are not lost behind stale or unchanged thread hashes. Comment activity notifications render the latest authorized comment author and excerpt as high-priority GitHub signal updates. ([#17590](https://github.com/mastra-ai/mastra/pull/17590))
|
|
23
|
+
|
|
24
|
+
New options on `GithubSignalsOptions`:
|
|
25
|
+
- `authorizedPermissions` — permission levels that authorize human commenters (default: `['admin', 'maintain', 'write']`)
|
|
26
|
+
- `authorizedBots` — bot logins authorized to trigger notifications (default: `['coderabbitai[bot]', 'devin-ai-integration[bot]']`)
|
|
27
|
+
- `ignoredBots` — bot logins whose comments should NOT trigger notifications, even if authorized
|
|
28
|
+
- `permissionResolver` — injectable resolver for looking up collaborator permissions (default: `gh api`)
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [[`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`f2ab060`](https://github.com/mastra-ai/mastra/commit/f2ab060162bea81505fda553e2cee29c1979fd04), [`5d302c8`](https://github.com/mastra-ai/mastra/commit/5d302c8eda1a6ac74eab5e442c4f64db6cc97a06)]:
|
|
31
|
+
- @mastra/core@1.42.0-alpha.1
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,10 @@ var GITHUB_SUBSCRIBE_PR_TAG = "github-subscribe-pr";
|
|
|
26
26
|
var GITHUB_UNSUBSCRIBE_PR_TAG = "github-unsubscribe-pr";
|
|
27
27
|
var GITHUB_SYNC_STATUS_TAG = "github-sync-status";
|
|
28
28
|
var GITHUB_SIGNALS_METADATA_KEY = "githubSignals";
|
|
29
|
+
var DEFAULT_AUTHORIZED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
30
|
+
var DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
|
|
31
|
+
var PERMISSION_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
32
|
+
var AUTHOR_GATED_NOTIFICATION_KINDS = /* @__PURE__ */ new Set(["pull-request-activity", "pull-request-review-activity"]);
|
|
29
33
|
var createGithubTool = tools.createTool;
|
|
30
34
|
function isPlainObject(value) {
|
|
31
35
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -146,6 +150,36 @@ function getPrLabel(subscription, snapshot) {
|
|
|
146
150
|
function getMergedNotificationSummary(label) {
|
|
147
151
|
return `${label} was merged. This thread has been automatically unsubscribed from this PR. Resubscribe if you still need updates.`;
|
|
148
152
|
}
|
|
153
|
+
function getCommentExcerpt(body) {
|
|
154
|
+
const excerpt = body.replace(/\s+/g, " ").trim();
|
|
155
|
+
return excerpt.length > 240 ? `${excerpt.slice(0, 237)}...` : excerpt;
|
|
156
|
+
}
|
|
157
|
+
function getCommentNotificationSummary(pr, snapshot) {
|
|
158
|
+
if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
|
|
159
|
+
return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
|
|
160
|
+
}
|
|
161
|
+
var githubActivityNotificationPriority = {
|
|
162
|
+
high: 0,
|
|
163
|
+
medium: 1
|
|
164
|
+
};
|
|
165
|
+
function getGithubActivityNotificationRank(notification) {
|
|
166
|
+
return notification.kind === "pull-request-activity" ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
function compareGithubActivityNotifications(a, b) {
|
|
169
|
+
if (!a && !b) return 0;
|
|
170
|
+
if (!a) return 1;
|
|
171
|
+
if (!b) return -1;
|
|
172
|
+
const priorityComparison = githubActivityNotificationPriority[a.priority] - githubActivityNotificationPriority[b.priority];
|
|
173
|
+
if (priorityComparison !== 0) return priorityComparison;
|
|
174
|
+
return getGithubActivityNotificationRank(a) - getGithubActivityNotificationRank(b);
|
|
175
|
+
}
|
|
176
|
+
function classifyGithubCommentActivityNotification(input) {
|
|
177
|
+
if (isBotOnlyActivity(input.snapshot)) return void 0;
|
|
178
|
+
const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
|
|
179
|
+
const summary = getCommentNotificationSummary(pr, input.snapshot);
|
|
180
|
+
if (!summary) return void 0;
|
|
181
|
+
return { kind: "pull-request-activity", priority: "high", summary };
|
|
182
|
+
}
|
|
149
183
|
function getCheckUpdatedTime(check) {
|
|
150
184
|
const value = check.updatedAt ? Date.parse(check.updatedAt) : Number.NaN;
|
|
151
185
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -269,10 +303,11 @@ function classifyGithubActivityNotification(input) {
|
|
|
269
303
|
}
|
|
270
304
|
if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState === "pending") return void 0;
|
|
271
305
|
if (isBotOnlyActivity(input.snapshot)) return void 0;
|
|
306
|
+
const commentSummary = getCommentNotificationSummary(pr, input.snapshot);
|
|
272
307
|
return {
|
|
273
308
|
kind: "pull-request-activity",
|
|
274
|
-
priority: "medium",
|
|
275
|
-
summary: `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
|
|
309
|
+
priority: commentSummary ? "high" : "medium",
|
|
310
|
+
summary: commentSummary ?? `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
|
|
276
311
|
};
|
|
277
312
|
}
|
|
278
313
|
function classifyGithubBaselineNotification(input) {
|
|
@@ -397,13 +432,15 @@ var GitcrawlSyncClient = class {
|
|
|
397
432
|
join threads t on t.id=rt.thread_id
|
|
398
433
|
join repositories r on r.id=t.repo_id
|
|
399
434
|
where r.owner=${owner} and r.name=${repo} and t.number=${number} and rt.is_resolved=0`);
|
|
400
|
-
const
|
|
435
|
+
const latestComments = await queryGitcrawlDb(`select c.author_login, c.author_type, c.is_bot, c.body, json_extract(c.raw_json, '$.html_url') as html_url,
|
|
436
|
+
coalesce(c.updated_at_gh, c.created_at_gh) as updated_at
|
|
401
437
|
from comments c
|
|
402
438
|
join threads t on t.id=c.thread_id
|
|
403
439
|
join repositories r on r.id=t.repo_id
|
|
404
440
|
where r.owner=${owner} and r.name=${repo} and t.number=${number}
|
|
405
441
|
order by coalesce(c.updated_at_gh, c.created_at_gh) desc
|
|
406
|
-
limit
|
|
442
|
+
limit 20`);
|
|
443
|
+
const latestComment = latestComments[0];
|
|
407
444
|
const checks = normalizeGithubChecksForSnapshot({
|
|
408
445
|
checkRows: checkRows.map((row) => ({
|
|
409
446
|
source: "check",
|
|
@@ -465,7 +502,18 @@ var GitcrawlSyncClient = class {
|
|
|
465
502
|
latestReviewThreadAt: readString(reviewState?.latest_review_thread_at),
|
|
466
503
|
latestCommentAuthor: readString(latestComment?.author_login),
|
|
467
504
|
latestCommentAuthorType: readString(latestComment?.author_type),
|
|
468
|
-
latestCommentIsBot: latestComment?.is_bot === 1
|
|
505
|
+
latestCommentIsBot: latestComment?.is_bot === 1,
|
|
506
|
+
latestCommentBody: readString(latestComment?.body),
|
|
507
|
+
latestCommentUrl: readString(latestComment?.html_url),
|
|
508
|
+
latestCommentUpdatedAt: readString(latestComment?.updated_at),
|
|
509
|
+
latestComments: latestComments.map((comment) => ({
|
|
510
|
+
author: readString(comment.author_login),
|
|
511
|
+
authorType: readString(comment.author_type),
|
|
512
|
+
isBot: comment.is_bot === 1,
|
|
513
|
+
body: readString(comment.body),
|
|
514
|
+
url: readString(comment.html_url),
|
|
515
|
+
updatedAt: readString(comment.updated_at)
|
|
516
|
+
}))
|
|
469
517
|
};
|
|
470
518
|
} catch {
|
|
471
519
|
return void 0;
|
|
@@ -520,9 +568,11 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
520
568
|
#syncClient;
|
|
521
569
|
#repositoryResolver;
|
|
522
570
|
#polling = /* @__PURE__ */ new Map();
|
|
571
|
+
#permissionCache = /* @__PURE__ */ new Map();
|
|
523
572
|
#agent;
|
|
524
573
|
#agentOptions = {};
|
|
525
574
|
#subscriptionsChangedHandler;
|
|
575
|
+
#pollingChangedHandler;
|
|
526
576
|
constructor(options = {}) {
|
|
527
577
|
super();
|
|
528
578
|
this.#options = options;
|
|
@@ -557,6 +607,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
557
607
|
onSubscriptionsChanged(handler) {
|
|
558
608
|
this.#subscriptionsChangedHandler = handler;
|
|
559
609
|
}
|
|
610
|
+
onPollingChanged(handler) {
|
|
611
|
+
this.#pollingChangedHandler = handler;
|
|
612
|
+
}
|
|
560
613
|
__registerMastra(mastra) {
|
|
561
614
|
super.__registerMastra(mastra);
|
|
562
615
|
this.#ghMastra = mastra;
|
|
@@ -595,15 +648,13 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
595
648
|
this.#polling.delete(pollingKey);
|
|
596
649
|
}
|
|
597
650
|
if (this.#polling.has(key)) return true;
|
|
598
|
-
let scheduledPollCount = options.pollImmediately ? 1 : 0;
|
|
599
651
|
const runPoll = (pollOptions = {}) => {
|
|
600
652
|
void this.#pollThread(input, pollOptions).catch((error) => {
|
|
601
653
|
console.warn("GitHub PR polling failed:", error);
|
|
602
654
|
});
|
|
603
655
|
};
|
|
604
656
|
const timer = setInterval(() => {
|
|
605
|
-
|
|
606
|
-
runPoll({ includeComments: scheduledPollCount % 2 === 1 });
|
|
657
|
+
runPoll({ includeComments: true });
|
|
607
658
|
}, this.#options.pollIntervalMs ?? 3e5);
|
|
608
659
|
if (options.pollImmediately) runPoll({ includeComments: true });
|
|
609
660
|
timer.unref?.();
|
|
@@ -620,6 +671,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
620
671
|
isPollingThread(input) {
|
|
621
672
|
return this.#polling.has(this.#pollingKey(input));
|
|
622
673
|
}
|
|
674
|
+
isPollingThreadRunning(input) {
|
|
675
|
+
return this.#polling.get(this.#pollingKey(input))?.running ?? false;
|
|
676
|
+
}
|
|
623
677
|
getPollIntervalMs() {
|
|
624
678
|
return this.#options.pollIntervalMs ?? 3e5;
|
|
625
679
|
}
|
|
@@ -711,6 +765,10 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
711
765
|
}
|
|
712
766
|
#createTools(args) {
|
|
713
767
|
const threadContext = this.#getThreadContext(args);
|
|
768
|
+
const getExecutionThreadContext = (context) => ({
|
|
769
|
+
threadId: context?.agent?.threadId ?? threadContext.threadId,
|
|
770
|
+
resourceId: context?.agent?.resourceId ?? threadContext.resourceId
|
|
771
|
+
});
|
|
714
772
|
return {
|
|
715
773
|
...args.tools,
|
|
716
774
|
github_subscribe_pr: createGithubTool({
|
|
@@ -721,14 +779,15 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
721
779
|
owner: z__default.default.string().optional(),
|
|
722
780
|
repo: z__default.default.string().optional()
|
|
723
781
|
}),
|
|
724
|
-
execute: async (input) => {
|
|
782
|
+
execute: async (input, context) => {
|
|
783
|
+
const executionThreadContext = getExecutionThreadContext(context);
|
|
725
784
|
const result = await this.#subscribe({
|
|
726
785
|
id: `github-tool-subscribe-${crypto.randomUUID()}`,
|
|
727
786
|
owner: input.owner,
|
|
728
787
|
repo: input.repo,
|
|
729
788
|
number: input.number,
|
|
730
|
-
threadId:
|
|
731
|
-
resourceId:
|
|
789
|
+
threadId: executionThreadContext.threadId,
|
|
790
|
+
resourceId: executionThreadContext.resourceId
|
|
732
791
|
});
|
|
733
792
|
return {
|
|
734
793
|
subscribed: true,
|
|
@@ -748,14 +807,15 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
748
807
|
owner: z__default.default.string().optional(),
|
|
749
808
|
repo: z__default.default.string().optional()
|
|
750
809
|
}),
|
|
751
|
-
execute: async (input) => {
|
|
810
|
+
execute: async (input, context) => {
|
|
811
|
+
const executionThreadContext = getExecutionThreadContext(context);
|
|
752
812
|
const result = await this.#unsubscribe({
|
|
753
813
|
id: `github-tool-unsubscribe-${crypto.randomUUID()}`,
|
|
754
814
|
owner: input.owner,
|
|
755
815
|
repo: input.repo,
|
|
756
816
|
number: input.number,
|
|
757
|
-
threadId:
|
|
758
|
-
resourceId:
|
|
817
|
+
threadId: executionThreadContext.threadId,
|
|
818
|
+
resourceId: executionThreadContext.resourceId
|
|
759
819
|
});
|
|
760
820
|
return {
|
|
761
821
|
unsubscribed: result.removed ?? false,
|
|
@@ -805,6 +865,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
805
865
|
#notifySubscriptionsChanged(input) {
|
|
806
866
|
this.#subscriptionsChangedHandler?.(input);
|
|
807
867
|
}
|
|
868
|
+
#notifyPollingChanged(input) {
|
|
869
|
+
this.#pollingChangedHandler?.(input);
|
|
870
|
+
}
|
|
808
871
|
async #pollThread(input, options = {}) {
|
|
809
872
|
const key = this.#pollingKey(input);
|
|
810
873
|
const state = this.#polling.get(key);
|
|
@@ -812,6 +875,7 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
812
875
|
return 0;
|
|
813
876
|
}
|
|
814
877
|
if (state) state.running = true;
|
|
878
|
+
this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: true });
|
|
815
879
|
try {
|
|
816
880
|
const { threadStore, loadedThread } = await this.#loadThread(input);
|
|
817
881
|
const githubMetadata = getGithubMetadata(loadedThread.metadata);
|
|
@@ -830,7 +894,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
830
894
|
includeComments: options.includeComments
|
|
831
895
|
};
|
|
832
896
|
const syncResult = await this.#syncClient.syncPullRequest(syncInput);
|
|
833
|
-
|
|
897
|
+
let snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
|
|
898
|
+
if (snapshot)
|
|
899
|
+
snapshot = await this.#filterUnauthorizedLatestComment(subscription.owner, subscription.repo, snapshot);
|
|
834
900
|
const nextSubscription = {
|
|
835
901
|
...subscription,
|
|
836
902
|
updatedAt: now,
|
|
@@ -843,25 +909,28 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
843
909
|
const previousContentHash = subscription.lastObservedContentHash;
|
|
844
910
|
const previousThreadContentHash = subscription.lastObservedThreadContentHash;
|
|
845
911
|
const previousHeadSha = subscription.lastObservedHeadSha;
|
|
912
|
+
const latestCommentChanged = !!previousGithubUpdatedAt && !!snapshot?.latestCommentUpdatedAt && Date.parse(snapshot.latestCommentUpdatedAt) > Date.parse(previousGithubUpdatedAt);
|
|
846
913
|
if (snapshot) applySnapshotCursor(nextSubscription, snapshot);
|
|
847
914
|
const isFirstObservation = syncResult.ok && snapshot && !previousGithubUpdatedAt && !previousContentHash;
|
|
848
915
|
const legacyAggregateChanged = previousContentHash && snapshot?.contentHash && previousContentHash !== snapshot.contentHash && !previousThreadContentHash && !previousHeadSha;
|
|
849
|
-
const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
|
|
916
|
+
const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || latestCommentChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
|
|
850
917
|
let shouldKeepSubscription = true;
|
|
851
|
-
if (changed) {
|
|
852
|
-
const
|
|
918
|
+
if (changed && snapshot) {
|
|
919
|
+
const notifications = await this.#sendActivityNotifications({
|
|
853
920
|
polling: input,
|
|
854
921
|
subscription,
|
|
855
922
|
snapshot,
|
|
856
923
|
previousGithubUpdatedAt,
|
|
857
|
-
previousContentHash
|
|
924
|
+
previousContentHash,
|
|
925
|
+
latestCommentChanged
|
|
858
926
|
});
|
|
859
|
-
|
|
927
|
+
const primaryNotification = notifications[0];
|
|
928
|
+
if (primaryNotification) {
|
|
860
929
|
nextSubscription.lastNotificationAt = now;
|
|
861
|
-
nextSubscription.lastNotificationKind =
|
|
862
|
-
nextSubscription.lastNotificationPriority =
|
|
863
|
-
nextSubscription.lastNotificationSummary =
|
|
864
|
-
shouldKeepSubscription = notification.kind !== "pull-request-merged";
|
|
930
|
+
nextSubscription.lastNotificationKind = primaryNotification.kind;
|
|
931
|
+
nextSubscription.lastNotificationPriority = primaryNotification.priority;
|
|
932
|
+
nextSubscription.lastNotificationSummary = primaryNotification.summary;
|
|
933
|
+
shouldKeepSubscription = notifications.every((notification) => notification.kind !== "pull-request-merged");
|
|
865
934
|
}
|
|
866
935
|
}
|
|
867
936
|
if (shouldKeepSubscription) subscriptions.push(nextSubscription);
|
|
@@ -884,17 +953,20 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
884
953
|
} finally {
|
|
885
954
|
const latestState = this.#polling.get(key);
|
|
886
955
|
if (latestState) latestState.running = false;
|
|
956
|
+
this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: false });
|
|
887
957
|
}
|
|
888
958
|
}
|
|
889
|
-
|
|
959
|
+
#createGithubNotificationInput(input) {
|
|
890
960
|
const failingChecks = getFailingChecks(input.snapshot);
|
|
891
961
|
const pendingChecks = getPendingChecks(input.snapshot);
|
|
962
|
+
const latestCommentExcerpt = input.snapshot.latestCommentBody ? getCommentExcerpt(input.snapshot.latestCommentBody) : void 0;
|
|
963
|
+
const latestCommentDedupeSuffix = input.notification.kind === "pull-request-activity" && input.snapshot.latestCommentUrl ? `comment:${input.snapshot.latestCommentUrl}:${input.snapshot.latestCommentUpdatedAt ?? ""}` : input.dedupeSuffix;
|
|
892
964
|
const notificationInput = {
|
|
893
965
|
source: "github",
|
|
894
966
|
kind: input.notification.kind,
|
|
895
967
|
priority: input.notification.priority,
|
|
896
968
|
summary: input.notification.summary,
|
|
897
|
-
dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${
|
|
969
|
+
dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${latestCommentDedupeSuffix}`,
|
|
898
970
|
coalesceKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.notification.kind}`,
|
|
899
971
|
attributes: {
|
|
900
972
|
owner: input.subscription.owner,
|
|
@@ -908,6 +980,10 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
908
980
|
...input.snapshot.mergeableState ? { mergeableState: input.snapshot.mergeableState } : {},
|
|
909
981
|
...input.snapshot.ciState ? { ciState: input.snapshot.ciState } : {},
|
|
910
982
|
...input.snapshot.unresolvedReviewThreads !== void 0 ? { unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads } : {},
|
|
983
|
+
...input.snapshot.latestCommentAuthor ? { latestCommentAuthor: input.snapshot.latestCommentAuthor } : {},
|
|
984
|
+
...latestCommentExcerpt ? { latestCommentExcerpt } : {},
|
|
985
|
+
...input.snapshot.latestCommentUrl ? { latestCommentUrl: input.snapshot.latestCommentUrl } : {},
|
|
986
|
+
...input.snapshot.latestCommentUpdatedAt ? { latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt } : {},
|
|
911
987
|
...failingChecks.length > 0 ? { failingChecks: failingChecks.map((check) => check.name).join(", ") } : {},
|
|
912
988
|
...pendingChecks.length > 0 ? { pendingChecks: pendingChecks.map((check) => check.name).join(", ") } : {}
|
|
913
989
|
},
|
|
@@ -936,11 +1012,19 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
936
1012
|
latestCommentAuthor: input.snapshot.latestCommentAuthor,
|
|
937
1013
|
latestCommentAuthorType: input.snapshot.latestCommentAuthorType,
|
|
938
1014
|
latestCommentIsBot: input.snapshot.latestCommentIsBot,
|
|
1015
|
+
latestCommentBody: input.snapshot.latestCommentBody,
|
|
1016
|
+
latestCommentExcerpt,
|
|
1017
|
+
latestCommentUrl: input.snapshot.latestCommentUrl,
|
|
1018
|
+
latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt,
|
|
939
1019
|
failingChecks,
|
|
940
1020
|
pendingChecks
|
|
941
1021
|
}
|
|
942
1022
|
}
|
|
943
1023
|
};
|
|
1024
|
+
return notificationInput;
|
|
1025
|
+
}
|
|
1026
|
+
async #sendGithubNotification(input) {
|
|
1027
|
+
const notificationInput = this.#createGithubNotificationInput(input);
|
|
944
1028
|
const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(input.target);
|
|
945
1029
|
await input.agent?.sendNotificationSignal?.(
|
|
946
1030
|
notificationInput,
|
|
@@ -959,25 +1043,144 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
959
1043
|
dedupeSuffix: `baseline:${input.subscription.lastSubscribeSignalId}`
|
|
960
1044
|
});
|
|
961
1045
|
}
|
|
962
|
-
async #
|
|
1046
|
+
async #isAuthorizedAuthor(owner, repo, user, metadata = {}) {
|
|
1047
|
+
if (!user) return false;
|
|
1048
|
+
const normalizedUser = user.toLowerCase();
|
|
1049
|
+
const isBot = metadata.isBot === true || metadata.authorType?.toLowerCase() === "bot" || normalizedUser.endsWith("[bot]");
|
|
1050
|
+
if (isBot) {
|
|
1051
|
+
const ignoredBots = this.#options.ignoredBots ?? [];
|
|
1052
|
+
if (ignoredBots.some((bot) => bot.toLowerCase() === normalizedUser)) return false;
|
|
1053
|
+
const authorizedBots = this.#options.authorizedBots ?? DEFAULT_AUTHORIZED_BOTS;
|
|
1054
|
+
return authorizedBots.some((bot) => bot.toLowerCase() === normalizedUser);
|
|
1055
|
+
}
|
|
1056
|
+
const permission = await this.#loadAuthorPermission(owner, repo, user);
|
|
1057
|
+
const authorizedPermissions = this.#options.authorizedPermissions ?? DEFAULT_AUTHORIZED_PERMISSIONS;
|
|
1058
|
+
return !!permission && authorizedPermissions.includes(permission);
|
|
1059
|
+
}
|
|
1060
|
+
async #filterUnauthorizedLatestComment(owner, repo, snapshot) {
|
|
1061
|
+
const comments = snapshot.latestComments?.length ? snapshot.latestComments : [
|
|
1062
|
+
{
|
|
1063
|
+
author: snapshot.latestCommentAuthor,
|
|
1064
|
+
authorType: snapshot.latestCommentAuthorType,
|
|
1065
|
+
isBot: snapshot.latestCommentIsBot,
|
|
1066
|
+
body: snapshot.latestCommentBody,
|
|
1067
|
+
url: snapshot.latestCommentUrl,
|
|
1068
|
+
updatedAt: snapshot.latestCommentUpdatedAt
|
|
1069
|
+
}
|
|
1070
|
+
];
|
|
1071
|
+
if (!comments.some((comment) => comment.author)) return snapshot;
|
|
1072
|
+
if (!comments.some((comment) => comment.body || comment.url || comment.updatedAt)) return snapshot;
|
|
1073
|
+
for (const comment of comments) {
|
|
1074
|
+
if (!await this.#isAuthorizedAuthor(owner, repo, comment.author, {
|
|
1075
|
+
authorType: comment.authorType,
|
|
1076
|
+
isBot: comment.isBot
|
|
1077
|
+
})) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
...snapshot,
|
|
1082
|
+
latestCommentAuthor: comment.author,
|
|
1083
|
+
latestCommentAuthorType: comment.authorType,
|
|
1084
|
+
latestCommentIsBot: comment.isBot,
|
|
1085
|
+
latestCommentBody: comment.body,
|
|
1086
|
+
latestCommentUrl: comment.url,
|
|
1087
|
+
latestCommentUpdatedAt: comment.updatedAt
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
...snapshot,
|
|
1092
|
+
latestCommentAuthor: void 0,
|
|
1093
|
+
latestCommentAuthorType: void 0,
|
|
1094
|
+
latestCommentIsBot: void 0,
|
|
1095
|
+
latestCommentBody: void 0,
|
|
1096
|
+
latestCommentUrl: void 0,
|
|
1097
|
+
latestCommentUpdatedAt: void 0
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
async #loadAuthorPermission(owner, repo, user) {
|
|
1101
|
+
const cacheKey = `${owner}/${repo}:${user.toLowerCase()}`;
|
|
1102
|
+
const cached = this.#permissionCache.get(cacheKey);
|
|
1103
|
+
if (cached && cached.expiresAt > Date.now()) return cached.permission;
|
|
1104
|
+
if (cached) this.#permissionCache.delete(cacheKey);
|
|
1105
|
+
try {
|
|
1106
|
+
let permission;
|
|
1107
|
+
if (this.#options.permissionResolver) {
|
|
1108
|
+
permission = await this.#options.permissionResolver.getPermission(owner, repo, user);
|
|
1109
|
+
} else {
|
|
1110
|
+
const { stdout } = await execFileAsync("gh", [
|
|
1111
|
+
"api",
|
|
1112
|
+
`repos/${owner}/${repo}/collaborators/${user}/permission`,
|
|
1113
|
+
"--jq",
|
|
1114
|
+
".permission"
|
|
1115
|
+
]);
|
|
1116
|
+
const raw = stdout.trim();
|
|
1117
|
+
permission = ["admin", "maintain", "write", "triage", "read", "none"].includes(
|
|
1118
|
+
raw
|
|
1119
|
+
) ? raw : void 0;
|
|
1120
|
+
}
|
|
1121
|
+
if (permission) {
|
|
1122
|
+
this.#permissionCache.set(cacheKey, { permission, expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS });
|
|
1123
|
+
}
|
|
1124
|
+
return permission;
|
|
1125
|
+
} catch {
|
|
1126
|
+
this.#permissionCache.delete(cacheKey);
|
|
1127
|
+
return void 0;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
async #sendActivityNotifications(input) {
|
|
963
1131
|
const agent = this.#getNotificationAgent(input.polling);
|
|
964
|
-
if (!agent?.sendNotificationSignal) return
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1132
|
+
if (!agent?.sendNotificationSignal) return [];
|
|
1133
|
+
const notifications = [
|
|
1134
|
+
classifyGithubActivityNotification({
|
|
1135
|
+
subscription: input.subscription,
|
|
1136
|
+
snapshot: input.snapshot
|
|
1137
|
+
})
|
|
1138
|
+
];
|
|
1139
|
+
if (input.latestCommentChanged && notifications[0]?.kind !== "pull-request-activity") {
|
|
1140
|
+
notifications.push(
|
|
1141
|
+
classifyGithubCommentActivityNotification({
|
|
1142
|
+
subscription: input.subscription,
|
|
1143
|
+
snapshot: input.snapshot
|
|
1144
|
+
})
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const sent = [];
|
|
1148
|
+
const notificationInputs = [];
|
|
1149
|
+
for (const notification of notifications.sort(compareGithubActivityNotifications)) {
|
|
1150
|
+
if (!notification) continue;
|
|
1151
|
+
if (AUTHOR_GATED_NOTIFICATION_KINDS.has(notification.kind)) {
|
|
1152
|
+
const authorized = await this.#isAuthorizedAuthor(
|
|
1153
|
+
input.subscription.owner,
|
|
1154
|
+
input.subscription.repo,
|
|
1155
|
+
input.snapshot.latestCommentAuthor,
|
|
1156
|
+
{
|
|
1157
|
+
authorType: input.snapshot.latestCommentAuthorType,
|
|
1158
|
+
isBot: input.snapshot.latestCommentIsBot
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
if (!authorized) continue;
|
|
1162
|
+
}
|
|
1163
|
+
notificationInputs.push(
|
|
1164
|
+
this.#createGithubNotificationInput({
|
|
1165
|
+
subscription: input.subscription,
|
|
1166
|
+
snapshot: input.snapshot,
|
|
1167
|
+
notification,
|
|
1168
|
+
dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
|
|
1169
|
+
previousGithubUpdatedAt: input.previousGithubUpdatedAt,
|
|
1170
|
+
previousContentHash: input.previousContentHash
|
|
1171
|
+
})
|
|
1172
|
+
);
|
|
1173
|
+
sent.push(notification);
|
|
1174
|
+
}
|
|
1175
|
+
if (notificationInputs.length > 0) {
|
|
1176
|
+
const target = { resourceId: input.polling.resourceId, threadId: input.polling.threadId };
|
|
1177
|
+
const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(target);
|
|
1178
|
+
await agent.sendNotificationSignal(
|
|
1179
|
+
notificationInputs,
|
|
1180
|
+
streamOptions ? { ...target, ifIdle: { streamOptions } } : target
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return sent;
|
|
981
1184
|
}
|
|
982
1185
|
async #subscribe(input) {
|
|
983
1186
|
const { owner, repo } = await this.#resolveRepository(input);
|