mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 21:59:38 +01:00
Handle GitHub mentions in descriptions and edited comments
This commit is contained in:
1 parent
1d9152ace4
commit
f2d89304fc
8 files changed
+388
-17
No files matched your search
@@ -14,7 +14,7 @@ import { buildChatCommunicationGuidance } from "./chat-communication-guidance.js
|
||||
function githubPolicyRecord(value: unknown): Record<string, unknown> { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
|
||||
import { githubChatManagementService } from "./chat-github-management.js";
|
||||
import { githubReviewCheckService } from "./chat-github-checks.js";
|
||||
import { githubAutomaticReviewEvent, githubAutomaticIssueEvent, githubAutomaticAdmission, githubPreviousAssessment } from "./chat-github-events.js";
|
||||
import { githubAutomaticReviewEvent, githubAutomaticIssueEvent, githubAutomaticAdmission, githubPreviousAssessment, githubBodyMentionsBot, githubExplicitMentionEvent } from "./chat-github-events.js";
|
||||
import { githubReviewPrompt } from "./chat-github-review-policy.js";
|
||||
import { chatGitHubConfigurations, chatGitHubRegistrations, chatGitHubReviews } from "@paperclipai/db";
|
||||
import type { GitHubReviewEventContext, GitHubIssueEventContext, GitHubAutomaticEventContext, GitHubReviewPolicy } from "@paperclipai/shared";
|
||||
@@ -812,6 +812,7 @@ const UNAVOIDABLE_GITHUB_EVENTS = [
|
||||
const SUPPORTED_GITHUB_WEBHOOK_EVENTS = new Set<string>([
|
||||
"issues",
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"ping",
|
||||
...REQUIRED_GITHUB_EVENTS,
|
||||
...UNAVOIDABLE_GITHUB_EVENTS,
|
||||
@@ -1098,6 +1099,7 @@ type LiveInboundMessage = {
|
||||
endpoint: EndpointRow;
|
||||
message: Message;
|
||||
receiptReactionSupported: boolean;
|
||||
providerUrl: string | null;
|
||||
runtimeContext?: InboundRuntimeContext;
|
||||
thread: Thread;
|
||||
trigger: ChatSdkMessageCallbackEvent["trigger"];
|
||||
@@ -6315,6 +6317,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
...REQUIRED_GITHUB_EVENTS,
|
||||
...UNAVOIDABLE_GITHUB_EVENTS,
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"issues",
|
||||
]);
|
||||
const excessiveEvents = [...configuredEvents].filter(
|
||||
@@ -14537,7 +14540,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (endpoint.provider === "github" && !githubAutomatic && !githubIssue && !githubManual) {
|
||||
const [config] = await db.select().from(chatGitHubConfigurations).where(eq(chatGitHubConfigurations.endpointId, endpoint.id));
|
||||
if (config) {
|
||||
const repository = thread.channelId.replace(/^github:/, "");
|
||||
const repository = thread.channelId.replace(/^github:/, "").toLowerCase();
|
||||
const [repo] = await db.select().from(chatEndpointResources).where(and(eq(chatEndpointResources.endpointId, endpoint.id), eq(chatEndpointResources.providerResourceId, repository)));
|
||||
const repositoryId = String(repo?.metadata?.providerRepositoryId ?? "");
|
||||
githubManual = { policy: { ...config.configuration.defaults, ...config.configuration.repositories[repositoryId] }, revision: config.revision, event: trigger === "mention" || message.isMention ? "mention" : "comment" };
|
||||
@@ -14632,6 +14635,10 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
(githubAutomatic
|
||||
? `https://github.com/${githubAutomatic.context.repository}/pull/${githubAutomatic.context.pullNumber}`
|
||||
: githubIssue ? `https://github.com/${githubIssue.context.repository}/issues/${githubIssue.context.issueNumber}` : null) ??
|
||||
// Description/review requests and edits have synthetic ledger IDs, not
|
||||
// native comment IDs. Their verified source URL must not acquire a fake
|
||||
// issuecomment anchor from that ledger identity.
|
||||
(endpoint.provider === "github" && githubManual && !receiptReactionSupported ? recoveredProviderUrl : null) ??
|
||||
chatProviderConversationUrl({
|
||||
provider: endpoint.provider,
|
||||
providerAccountId: endpoint.providerAccountId,
|
||||
@@ -15277,6 +15284,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
message,
|
||||
trigger,
|
||||
receiptReactionSupported,
|
||||
providerUrl,
|
||||
runtimeContext,
|
||||
});
|
||||
if (!deferDrainUntilFollowup) {
|
||||
@@ -16853,8 +16861,12 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
)
|
||||
return null;
|
||||
const normalized = delivery.normalizedEvent as {
|
||||
message?: { targetProviderEventId?: unknown };
|
||||
message?: { targetProviderEventId?: unknown; admissionDependencyEventId?: unknown };
|
||||
};
|
||||
// An edit can also introduce a new addressed request. Process that durable
|
||||
// admission before waiting for the old, potentially unadmitted source.
|
||||
if (typeof normalized.message?.admissionDependencyEventId === "string")
|
||||
return normalized.message.admissionDependencyEventId;
|
||||
return typeof normalized.message?.targetProviderEventId === "string"
|
||||
? normalized.message.targetProviderEventId
|
||||
: null;
|
||||
@@ -17458,7 +17470,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
live.message,
|
||||
live.trigger,
|
||||
false,
|
||||
null,
|
||||
live.providerUrl,
|
||||
live.runtimeContext,
|
||||
undefined,
|
||||
delivery.id,
|
||||
@@ -17688,6 +17700,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
providerUpdateId?: number | null;
|
||||
raw?: unknown;
|
||||
revision?: string | null;
|
||||
admissionDependencyEventId?: string;
|
||||
},
|
||||
runtimeContext?: RuntimeContext,
|
||||
) {
|
||||
@@ -17751,6 +17764,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
message: {
|
||||
providerMessageId: input.messageId,
|
||||
targetProviderEventId,
|
||||
...(input.admissionDependencyEventId ? { admissionDependencyEventId: input.admissionDependencyEventId } : {}),
|
||||
...(endpoint.provider === "github" && input.isBotMessage === true
|
||||
? { isBotMessage: true }
|
||||
: {}),
|
||||
@@ -24715,6 +24729,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
eventType === "issue_comment" ||
|
||||
eventType === "issues" ||
|
||||
eventType === "pull_request" ||
|
||||
eventType === "pull_request_review" ||
|
||||
eventType === "pull_request_review_comment"
|
||||
);
|
||||
}
|
||||
@@ -24728,15 +24743,7 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (!comment || typeof comment !== "object") return false;
|
||||
const body = (comment as { body?: unknown }).body;
|
||||
if (typeof body !== "string") return false;
|
||||
const identities = new Set([
|
||||
botUsername,
|
||||
botUsername.replace(/\[bot\]$/i, ""),
|
||||
]);
|
||||
return [...identities].some((identity) => {
|
||||
if (!identity) return false;
|
||||
const escaped = identity.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`(?<!\\w)@${escaped}(?![\\w-])`, "i").test(body);
|
||||
});
|
||||
return githubBodyMentionsBot(body, botUsername);
|
||||
}
|
||||
|
||||
function githubIngressAllowedInEndpointState(
|
||||
@@ -27239,11 +27246,44 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
// their normal transactional fences if a pause wins after this check.
|
||||
if (!matchesGitHubIngressFence(runtimeContext))
|
||||
return ignoreSupersededIngress();
|
||||
if (provider === "github") {
|
||||
const mention = githubExplicitMentionEvent(
|
||||
request.headers.get("x-github-event") ?? "",
|
||||
await request.clone().json(),
|
||||
request.headers.get("x-github-delivery") ?? "",
|
||||
endpoint.botUsername,
|
||||
);
|
||||
if (mention) {
|
||||
const thread = endpointRuntime.thread(mention.threadId);
|
||||
const message = {
|
||||
id: mention.messageId, threadId: thread.id, text: mention.body,
|
||||
formatted: { type: "root", children: [] }, raw: mention.raw,
|
||||
author: { userId: mention.sender.id, userName: mention.sender.login, fullName: mention.sender.login, isBot: false, isMe: false, isSystem: false },
|
||||
metadata: { dateSent: new Date(), edited: mention.edited }, attachments: [], links: [], isMention: true,
|
||||
} as unknown as Message;
|
||||
await processMessage(endpoint, thread, message, "mention", true, mention.url, { ...runtimeContext, endpointRuntime }, undefined, null, mention.receiptReactionSupported);
|
||||
if (mention.edited) {
|
||||
// Admit the new explicit request before its supplemental edit
|
||||
// record: an orphan lifecycle receipt must not wait for the old
|
||||
// unmentioned comment and block this newly addressed root.
|
||||
const lifecycle = await githubLifecycleEventFromRequest(request.clone());
|
||||
if (lifecycle) await recordLifecycleDelivery({
|
||||
endpointId: endpoint.id, ...lifecycle,
|
||||
admissionDependencyEventId: `${durableExternalThreadIdentity(thread.id)}:${message.id}`,
|
||||
}, runtimeContext);
|
||||
}
|
||||
return new Response("accepted", { status: 202 });
|
||||
}
|
||||
if (request.headers.get("x-github-event") === "pull_request_review") return new Response("ignored", { status: 200 });
|
||||
}
|
||||
if (provider === "github" && request.headers.get("x-github-event") === "issues") {
|
||||
const event = githubAutomaticIssueEvent(await request.clone().json(), request.headers.get("x-github-delivery") ?? "");
|
||||
if (!event) return new Response("ignored", { status: 200 });
|
||||
const admission = await githubAutomaticAdmission(db, endpoint, event);
|
||||
if (!admission?.allowed) return new Response("ignored", { status: 200 });
|
||||
if (!admission?.allowed) {
|
||||
await recordGitHubAutomaticSkip(endpoint, event.deliveryId, "issue opened", admission?.reason, runtimeContext);
|
||||
return new Response("ignored", { status: 200 });
|
||||
}
|
||||
const thread = endpointRuntime.thread(`github:${event.repository}:issue:${event.issueNumber}`);
|
||||
const message = {
|
||||
id: `issue-event:${event.deliveryId}`, threadId: thread.id,
|
||||
@@ -27267,7 +27307,10 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (!event) return new Response("ignored", { status: 200 });
|
||||
const admission = await githubAutomaticAdmission(db, endpoint, event);
|
||||
if (admission) await githubReviewCheckService(db, fetchImpl).enqueue(endpoint, event, admission.allowed, admission.reason);
|
||||
if (!admission?.allowed) return new Response("ignored", { status: 200 });
|
||||
if (!admission?.allowed) {
|
||||
await recordGitHubAutomaticSkip(endpoint, event.deliveryId, "pull request event", admission?.reason, runtimeContext);
|
||||
return new Response("ignored", { status: 200 });
|
||||
}
|
||||
const previousAssessment = await githubPreviousAssessment(db, endpoint, event.repositoryId, event.pullNumber);
|
||||
if (previousAssessment) event.priorReviewedHeadSha = previousAssessment.headSha;
|
||||
const thread = endpointRuntime.thread(`github:${event.repository}:${event.pullNumber}`);
|
||||
@@ -28706,6 +28749,33 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
async function recordGitHubAutomaticSkip(
|
||||
endpoint: EndpointRow, deliveryId: string, event: string, reason: string | undefined, runtimeContext: LifecycleRuntimeFence,
|
||||
) {
|
||||
const details: Record<string, string> = {
|
||||
mentions_only: "This bot runs only when mentioned. No new mention was found in this event.",
|
||||
event_disabled: "Automatic runs are off for this GitHub event.",
|
||||
person_not_authorized: "The GitHub author is not authorized to start automatic work.",
|
||||
identity_revoked: "The GitHub author's identity link was revoked.",
|
||||
automatic_reviews_disabled_for_person: "Automatic runs are off for this GitHub author.",
|
||||
guest_automatic_reviews_disabled: "Automatic runs are off for external contributors.",
|
||||
responsible_user_unavailable: "The member responsible for automatic tasks is unavailable.",
|
||||
repository_disabled: "Destination is not enabled in Paperclip",
|
||||
bot_tools_disabled: "This bot's GitHub tools are disabled.",
|
||||
};
|
||||
await db.transaction(async (tx) => {
|
||||
const current = await runtimeCallbackEndpoint(tx, endpoint.id, runtimeContext, ["verifying", "active"]);
|
||||
if (!current) return;
|
||||
const providerEventId = `github:policy_ingress:${createHash("sha256").update(deliveryId).digest("hex")}`;
|
||||
await tx.insert(chatDeliveries).values({
|
||||
companyId: current.companyId, endpointId: current.id,
|
||||
providerEventId, deduplicationKey: providerEventId, eventKind: "message", state: "filtered", processedAt: new Date(),
|
||||
redactedError: `GitHub ${event} ignored. ${details[reason ?? ""] ?? "Automatic runs are restricted by this bot's review policy."}`,
|
||||
normalizedEvent: { providerEventId, kind: "message", filtering: { contentRetained: false, reason: reason ?? "automatic_policy_unavailable" } },
|
||||
}).onConflictDoNothing();
|
||||
});
|
||||
}
|
||||
|
||||
async function listActivity(endpointId: string, page?: { limit: number; before?: { createdAt: string; id: string } }) {
|
||||
const limit = page ? page.limit + 1 : 100;
|
||||
// Match the millisecond precision of the public timestamp and use a UUID
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { githubAutomaticIssueEvent } from "./chat-github-events.js";
|
||||
import { githubAutomaticIssueEvent, githubBodyMentionsBot, githubExplicitMentionEvent } from "./chat-github-events.js";
|
||||
|
||||
function payload() {
|
||||
return {
|
||||
@@ -26,3 +26,52 @@ describe("GitHub issue event parsing", () => {
|
||||
expect(githubAutomaticIssueEvent({ ...input, issue: { ...input.issue, number: 0 } }, "delivery")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("explicit GitHub mentions", () => {
|
||||
it.each(["@maya help", "@MAYA[bot] help", "Hi, @maya."])("recognizes the bot's exact bare and bot identity: %s", (body) => {
|
||||
expect(githubBodyMentionsBot(body, "maya[bot]")).toBe(true);
|
||||
});
|
||||
it.each(["@maya-other", "@mayabot", "x@maya", "@someone"])("does not match a different identity: %s", (body) => {
|
||||
expect(githubBodyMentionsBot(body, "maya[bot]")).toBe(false);
|
||||
});
|
||||
it.each(["issues", "pull_request", "issue_comment", "pull_request_review_comment", "pull_request_review"])("normalizes a %s request into the matching issue or PR thread", (event) => {
|
||||
const issue = { ...payload().issue, body: "@maya help" };
|
||||
const comment = { id: 123, body: "@maya[bot] help", in_reply_to_id: 99 };
|
||||
const input = {
|
||||
...payload(), action: event === "pull_request_review" ? "submitted" : event.includes("comment") ? "created" : "opened",
|
||||
issue, pull_request: issue, comment, review: comment,
|
||||
};
|
||||
const mention = githubExplicitMentionEvent(event, input, "delivery", "maya[bot]");
|
||||
expect(mention).toMatchObject({ sender: { id: "77", login: "sender" }, edited: false });
|
||||
expect(mention!.threadId).toBe(`github:Test/Repo:${event === "issues" || event === "issue_comment" ? "issue:" : ""}2${event === "pull_request_review_comment" ? ":rc:99" : ""}`);
|
||||
});
|
||||
it("puts PR conversation comments in the PR thread, and preserves the SDK comment identity", () => {
|
||||
const input = { ...payload(), action: "created", issue: { ...payload().issue, pull_request: {} }, comment: { id: 123, body: "@maya help" } };
|
||||
expect(githubExplicitMentionEvent("issue_comment", input, "delivery", "maya")).toMatchObject({ threadId: "github:Test/Repo:2", messageId: "123", url: "https://github.com/test/repo/pull/2#issuecomment-123" });
|
||||
});
|
||||
it.each(["issues", "pull_request", "issue_comment", "pull_request_review_comment", "pull_request_review"])("recognizes a newly added mention in an edited %s body", (event) => {
|
||||
const target = { ...payload().issue, body: "@maya help" };
|
||||
const content = { id: 123, body: "@maya help" };
|
||||
const input = { ...payload(), action: "edited", issue: target, pull_request: target, comment: content, review: content, changes: { body: { from: null } } };
|
||||
expect(githubExplicitMentionEvent(event, input, "edit", "maya")).toMatchObject({ edited: true, messageId: "mention-event:edit", receiptReactionSupported: false });
|
||||
});
|
||||
it("admits an edit adding a mention under the editor's identity, but not a persistent mention", () => {
|
||||
const input = { ...payload(), action: "edited", issue: { ...payload().issue, body: "@maya help" }, changes: { body: { from: "Old description" } } };
|
||||
expect(githubExplicitMentionEvent("issues", input, "edit-delivery", "maya")).toMatchObject({ edited: true, messageId: "mention-event:edit-delivery", sender: { id: "77" } });
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, changes: { body: { from: "@maya[bot] old request" } } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, changes: { title: { from: "Old title" } } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, changes: undefined }, "delivery", "maya")).toBeNull();
|
||||
});
|
||||
it.each(["synchronize", "reopened", "ready_for_review", "closed", "deleted"])("does not replay a description mention on %s", (action) => {
|
||||
const input = { ...payload(), action, pull_request: { ...payload().issue, body: "@maya help" } };
|
||||
expect(githubExplicitMentionEvent("pull_request", input, "delivery", "maya")).toBeNull();
|
||||
});
|
||||
it("ignores bot senders, title-only mentions, malformed payloads and unsafe IDs", () => {
|
||||
const input = { ...payload(), issue: { ...payload().issue, body: "@maya help" } };
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, sender: { ...input.sender, type: "Bot" } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, issue: { ...input.issue, body: null, title: "@maya" } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, repository: { id: 1, full_name: "wrong/repo/extra" } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", { ...input, sender: { id: Number.MAX_SAFE_INTEGER + 1, login: "sender" } }, "delivery", "maya")).toBeNull();
|
||||
expect(githubExplicitMentionEvent("issues", input, "", "maya")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,87 @@ const person = z.object({
|
||||
login: z.string().min(1).max(100),
|
||||
type: z.string().optional(),
|
||||
});
|
||||
|
||||
export function githubBodyMentionsBot(body: string, username: string | null): boolean {
|
||||
if (!username) return false;
|
||||
const name = username.replace(/\[bot\]$/i, "");
|
||||
if (!name) return false;
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`(?<![\\w-])@${escaped}(?:\\[bot\\])?(?![\\w-])`, "i").test(body);
|
||||
}
|
||||
|
||||
const mentionPayloadSchema = z.object({
|
||||
action: z.string(),
|
||||
repository: z.object({ id, full_name: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/) }),
|
||||
sender: person,
|
||||
changes: z.object({ body: z.object({ from: z.string().nullable() }).optional() }).optional(),
|
||||
});
|
||||
const descriptionSchema = z.object({
|
||||
number: z.number().int().positive().safe(),
|
||||
body: z.string().nullable(),
|
||||
pull_request: z.unknown().optional(),
|
||||
});
|
||||
const commentSchema = z.object({
|
||||
id,
|
||||
body: z.string().nullable(),
|
||||
in_reply_to_id: id.optional(),
|
||||
});
|
||||
|
||||
/** Explicit requests use the ordinary message admission path, including the
|
||||
* actor who made an edit, rather than the automatic event's original author. */
|
||||
export function githubExplicitMentionEvent(
|
||||
eventType: string,
|
||||
payload: unknown,
|
||||
deliveryId: string,
|
||||
botUsername: string | null,
|
||||
) {
|
||||
const parsed = mentionPayloadSchema.safeParse(payload);
|
||||
if (!parsed.success || !deliveryId) return null;
|
||||
const envelope = parsed.data;
|
||||
const source = payload as Record<string, unknown>;
|
||||
const isIssue = eventType === "issues" || eventType === "issue_comment";
|
||||
const target = descriptionSchema.safeParse(source[isIssue ? "issue" : "pull_request"]);
|
||||
if (!target.success) return null;
|
||||
const description = eventType === "issues" || eventType === "pull_request";
|
||||
const review = eventType === "pull_request_review";
|
||||
if (!description && !review && !["issue_comment", "pull_request_review_comment"].includes(eventType)) return null;
|
||||
if (eventType === "issues" && target.data.pull_request !== undefined) return null;
|
||||
const content = description ? target : commentSchema.safeParse(source[review ? "review" : "comment"]);
|
||||
if (!content.success) return null;
|
||||
const body = content.data.body ?? "";
|
||||
if (!githubBodyMentionsBot(body, botUsername)) return null;
|
||||
const edited = envelope.action === "edited";
|
||||
if (edited) {
|
||||
// A title/label edit, push, or unrelated edit retaining an old mention
|
||||
// must not rerun the request. Missing previous content is not evidence
|
||||
// that the editor added a mention.
|
||||
if (!envelope.changes?.body || githubBodyMentionsBot(envelope.changes.body.from ?? "", botUsername)) return null;
|
||||
} else if (envelope.action !== (description ? "opened" : review ? "submitted" : "created")) return null;
|
||||
if (envelope.sender.type === "Bot") return null;
|
||||
const repository = envelope.repository.full_name.toLowerCase();
|
||||
const number = target.data.number;
|
||||
const issueThread = isIssue && target.data.pull_request === undefined;
|
||||
const comment = !description && !review ? commentSchema.parse(source.comment) : null;
|
||||
const reviewId = review ? commentSchema.parse(source.review).id : null;
|
||||
// Match the native adapter's repository casing so addressed descriptions
|
||||
// and ordinary follow-up comments share the same existing conversation.
|
||||
const threadId = `github:${envelope.repository.full_name}:${issueThread ? "issue:" : ""}${number}${eventType === "pull_request_review_comment" ? `:rc:${comment!.in_reply_to_id ?? comment!.id}` : ""}`;
|
||||
return {
|
||||
threadId,
|
||||
// Created comments retain the SDK's identity so alternate delivery paths
|
||||
// cannot duplicate a turn. Edits are separate, delivery-deduplicated asks.
|
||||
messageId: edited || description ? `mention-event:${deliveryId}` : review ? `review:${reviewId}` : comment!.id,
|
||||
body: body.slice(0, 65536),
|
||||
sender: envelope.sender,
|
||||
edited,
|
||||
receiptReactionSupported: !description && !review && !edited,
|
||||
url: `https://github.com/${repository}/${issueThread ? "issues" : "pull"}/${number}${description ? "" : review ? `#pullrequestreview-${reviewId}` : eventType === "pull_request_review_comment" ? `#discussion_r${comment!.id}` : `#issuecomment-${comment!.id}`}`,
|
||||
raw: comment ? {
|
||||
type: eventType === "pull_request_review_comment" ? "review_comment" : "issue_comment",
|
||||
comment: source.comment, repository: source.repository, prNumber: number, threadType: issueThread ? "issue" : "pr",
|
||||
} : {},
|
||||
};
|
||||
}
|
||||
const payloadSchema = z.object({
|
||||
action: z.enum(["opened", "synchronize", "reopened", "ready_for_review"]),
|
||||
repository: z.object({
|
||||
|
||||
@@ -228,6 +228,7 @@ export function githubChatRegistrationService(
|
||||
"issues",
|
||||
"issue_comment",
|
||||
"pull_request_review_comment",
|
||||
"pull_request_review",
|
||||
"pull_request",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -393,6 +393,7 @@ export function githubChatWizardService(
|
||||
"issues",
|
||||
"issue_comment",
|
||||
"pull_request_review_comment",
|
||||
"pull_request_review",
|
||||
"pull_request",
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user