Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13349,6 +13349,12 @@ async function maybeThrottleReviewNagPing(
if (isAutoCloseExempt(commenter, settings.autoCloseExemptLogins)) return false;

const targetKey = `${repoFullName}#${issue.number}`;
// #8681: webhook-redelivery guard, backported from maybeThrottleLoopOverCommand/maybeThrottleIntentRouting.
// GitHub can redeliver the same issue_comment event; without this, a redelivery re-counts this ping toward
// the threshold (and can re-post the cooldown/close). The original delivery already recorded its ping under
// this deliveryId, so short-circuit the replay entirely.
const redeliverySinceIso = new Date(Date.now() - COMMAND_RATE_LIMIT_REDELIVERY_WINDOW_MS).toISOString();
if (await hasAuditEventForDelivery(env, commenter, REVIEW_NAG_PING_EVENT_TYPE, targetKey, deliveryId, redeliverySinceIso)) return true;
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer (NOT NULL DEFAULT 3); the undefined side is defensive against the field's optional TS type. */
const maxPings = settings.reviewNagMaxPings ?? 3;
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer (NOT NULL DEFAULT 5); the undefined side is defensive against the field's optional TS type. */
Expand Down Expand Up @@ -13538,6 +13544,11 @@ async function maybeThrottleMonitoredMentions(
// the repo-wide count's suffix filter, so a naming drift between the two can never silently under/over-count.
const mentionTargetSuffix = `mention:${mentionedLogin.toLowerCase()}`;
const targetKey = `${repoFullName}#${issue.number}#${mentionTargetSuffix}`;
// #8681: webhook-redelivery guard, backported from maybeThrottleLoopOverCommand/maybeThrottleIntentRouting.
// A redelivered issue_comment event must not re-count this mention toward the threshold; the original
// delivery already recorded it under this deliveryId + per-login targetKey, so short-circuit the replay.
const redeliverySinceIso = new Date(Date.now() - COMMAND_RATE_LIMIT_REDELIVERY_WINDOW_MS).toISOString();
if (await hasAuditEventForDelivery(env, commenter, MONITORED_MENTION_PING_EVENT_TYPE, targetKey, deliveryId, redeliverySinceIso)) return true;
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer (NOT NULL DEFAULT 3); the undefined side is defensive against the field's optional TS type. */
const maxPings = settings.reviewNagMaxPings ?? 3;
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer (NOT NULL DEFAULT 5); the undefined side is defensive against the field's optional TS type. */
Expand Down
28 changes: 24 additions & 4 deletions test/unit/queue-5.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,26 @@ describe("queue processors", () => {
expect(closeAudit?.n).toBeGreaterThanOrEqual(1);
});

it("REGRESSION (#8681): a redelivered issue_comment does NOT double-count a review-nag ping", async () => {
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 5 } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 231, title: "Redelivery", state: "open", user: { login: "chatty" }, author_association: "NONE", labels: [], body: "" });
const seen = { comments: [] as string[], labels: [] as string[], closed: false };
stubReviewNagFetch(231, seen);
const payload = {
action: "created" as const,
installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" as const } },
repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } },
issue: { number: 231, title: "Redelivery", state: "open", pull_request: {}, user: { login: "chatty" }, author_association: "NONE" },
comment: { id: 1, body: "@loopover help", user: { login: "chatty", type: "User" as const }, author_association: "NONE" },
};
await processJob(env, { type: "github-webhook", deliveryId: "nag-redelivery-same", eventName: "issue_comment", payload });
await processJob(env, { type: "github-webhook", deliveryId: "nag-redelivery-same", eventName: "issue_comment", payload });
// #8681: the webhook-redelivery guard suppresses the replay, so two deliveries of one real ping record once.
const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.review_nag_ping'").first<{ n: number }>();
expect(pings?.n).toBe(1);
});

it("close policy degrades to hold on an ISSUE thread (no closeIssue primitive yet)", async () => {
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { reviewNagPolicy: "close", reviewNagMaxPings: 3 } });
Expand Down Expand Up @@ -1110,10 +1130,10 @@ describe("queue processors", () => {
await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload });
await processJob(env, { type: "github-webhook", deliveryId: "mention-redelivery-same", eventName: "issue_comment", payload });
const pings = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'github_app.monitored_mention_ping'").first<{ n: number }>();
// NOTE: unlike #2560's per-command limiter, review-nag/monitored-mention ping recording does not itself
// dedup by deliveryId -- it always records. This assertion documents CURRENT behavior (2 pings from 2
// deliveries) rather than asserting an idempotency guarantee this handler does not provide.
expect(pings?.n).toBe(2);
// #8681: monitored-mention recording now carries the same webhook-redelivery guard as #2560's per-command
// limiter -- a redelivered issue_comment (same deliveryId) is suppressed rather than counted a second
// time, so two deliveries of one real mention record exactly ONE ping.
expect(pings?.n).toBe(1);
});
});

Expand Down