From 30c0414ac5ac730c319376858223d0d0f25c08e5 Mon Sep 17 00:00:00 2001 From: ConciergeLead Date: Wed, 6 May 2026 11:26:34 +0000 Subject: [PATCH] fix(ten-427): fall back to PR branch/title for TEN issue matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an agent opens a PR without backfilling the GitHub ref into the issue description, `findTenIssue`'s text search misses the parent issue and creates a spurious triage issue instead of waking the assignee. Add `extractTenIdentifierFromPR` to parse the PR's head branch name and title for a TEN-NNN identifier (e.g. `ten-334-exact-match` or `fix(ten-334): …`), and `findTenIssueByIdentifier` to look it up by exact identifier match. The fallback is tried only when the primary full-text search returns no result. Three new tests cover: branch-based match for pull_request_review, title-based match for pull_request.opened, and triage-issue creation when neither branch nor title contains a TEN identifier. Co-Authored-By: Paperclip --- .../src/worker.ts | 57 +++++++++++- .../tests/plugin.spec.ts | 92 +++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/packages/plugins/examples/plugin-github-webhook-dispatcher/src/worker.ts b/packages/plugins/examples/plugin-github-webhook-dispatcher/src/worker.ts index 70e49d447b7..1192a11f9d7 100644 --- a/packages/plugins/examples/plugin-github-webhook-dispatcher/src/worker.ts +++ b/packages/plugins/examples/plugin-github-webhook-dispatcher/src/worker.ts @@ -233,6 +233,40 @@ async function findTenIssue( return { id: exact.id, identifier: exact.identifier ?? exact.id }; } +// Extracts a TEN issue identifier (e.g. "TEN-334") from a PR's head branch name or title. +// Returns the canonical uppercase form, or null if no match found. +function extractTenIdentifierFromPR(payload: Record): string | null { + const pr = payload.pull_request as Record | null | undefined; + if (!pr) return null; + + const headBranch = str((pr.head as Record | null | undefined)?.ref); + const title = str(pr.title as unknown); + + const pattern = /\bten[_-](\d+)\b/i; + + // Check branch name first (more structured), then title + for (const text of [headBranch, title]) { + if (!text) continue; + const match = text.match(pattern); + if (match) return `TEN-${match[1]}`; + } + + return null; +} + +async function findTenIssueByIdentifier( + ctx: PluginContext, + companyId: string, + identifier: string, +): Promise<{ id: string; identifier: string } | null> { + const results = await ctx.issues.list({ companyId, q: identifier, limit: 10 }); + if (!results || results.length === 0) return null; + const normalizedId = identifier.toUpperCase(); + const exact = results.find((issue) => issue.identifier?.toUpperCase() === normalizedId); + if (!exact) return null; + return { id: exact.id, identifier: exact.identifier ?? exact.id }; +} + // ────────────────────────────────────────────── // Plugin definition // ────────────────────────────────────────────── @@ -337,8 +371,27 @@ const plugin = definePlugin({ const summary = buildSummary(eventType, action, payload); const commentBody = buildWakeComment(eventType, action, githubRef, summary, input.rawBody); - // Search for matching TEN issue - const matchedIssue = await findTenIssue(currentContext, config.companyId, githubRef); + // Search for matching TEN issue — primary strategy: full-text search for the GitHub ref + let matchedIssue = await findTenIssue(currentContext, config.companyId, githubRef); + + // Fallback: if no match, parse TEN identifier from PR branch/title and look up directly + if (!matchedIssue) { + const tenIdentifier = extractTenIdentifierFromPR(payload); + if (tenIdentifier) { + matchedIssue = await findTenIssueByIdentifier(currentContext, config.companyId, tenIdentifier); + if (matchedIssue) { + currentContext.logger.info("Matched issue via PR branch/title fallback", { + githubRef, + tenIdentifier, + issueId: matchedIssue.id, + identifier: matchedIssue.identifier, + eventType, + action, + deliveryId, + }); + } + } + } let matchedIssueId: string | null = null; let reason: string; diff --git a/packages/plugins/examples/plugin-github-webhook-dispatcher/tests/plugin.spec.ts b/packages/plugins/examples/plugin-github-webhook-dispatcher/tests/plugin.spec.ts index 2907f8d6482..73e378066e0 100644 --- a/packages/plugins/examples/plugin-github-webhook-dispatcher/tests/plugin.spec.ts +++ b/packages/plugins/examples/plugin-github-webhook-dispatcher/tests/plugin.spec.ts @@ -454,6 +454,98 @@ describe("check_suite events", () => { }); }); +// ────────────────────────────────────────────── +// PR branch/title fallback — TEN-427 +// ────────────────────────────────────────────── + +describe("PR branch/title fallback", () => { + it("dispatches pull_request_review.submitted to TEN-334 when branch contains ten-334", async () => { + const companyId = randomUUID(); + const secretRef = randomUUID(); + // Issue has no githubRef in description — primary text-search will miss it + const issue = makeIssue({ companyId, identifier: "TEN-334" }); + const harness = buildHarness(companyId, secretRef); + harness.seed({ issues: [issue] }); + await plugin.definition.setup(harness.ctx); + + const payload = { + action: "submitted", + review: { + state: "approved", + html_url: "https://github.com/tensorleap/paperclip/pull/15#pullrequestreview-1", + user: { login: "assaf" }, + }, + pull_request: { + number: 15, + title: "fix(ten-334): require exact-match when finding TEN issue by GitHub ref", + head: { ref: "ten-334-exact-match-issue-finder" }, + }, + repository: { full_name: "tensorleap/paperclip" }, + sender: { login: "assaf" }, + }; + const input = webhookInput({ secretRef, payload, eventType: "pull_request_review" }); + await plugin.definition.onWebhook?.(input); + + const comments = await harness.ctx.issues.listComments(issue.id, companyId); + expect(comments).toHaveLength(1); + expect(comments[0]?.body).toContain("## GitHub Event: pull_request_review.submitted"); + expect(comments[0]?.body).toContain("`tensorleap/paperclip#15`"); + }); + + it("dispatches pull_request.opened to TEN-334 when title contains fix(ten-334)", async () => { + const companyId = randomUUID(); + const secretRef = randomUUID(); + const issue = makeIssue({ companyId, identifier: "TEN-334" }); + const harness = buildHarness(companyId, secretRef); + harness.seed({ issues: [issue] }); + await plugin.definition.setup(harness.ctx); + + const payload = { + action: "opened", + pull_request: { + number: 99, + title: "fix(ten-334): require exact-match when finding TEN issue by GitHub ref", + head: { ref: "fix/no-ten-id-here" }, + html_url: "https://github.com/tensorleap/paperclip/pull/99", + merged: false, + user: { login: "cto-agent" }, + }, + repository: { full_name: "tensorleap/paperclip" }, + sender: { login: "cto-agent" }, + }; + const input = webhookInput({ secretRef, payload, eventType: "pull_request" }); + await plugin.definition.onWebhook?.(input); + + const comments = await harness.ctx.issues.listComments(issue.id, companyId); + expect(comments).toHaveLength(1); + expect(comments[0]?.body).toContain("## GitHub Event: pull_request.opened"); + }); + + it("still creates triage issue when neither branch nor title contains a TEN identifier", async () => { + const companyId = randomUUID(); + const secretRef = randomUUID(); + const harness = buildHarness(companyId, secretRef); + await plugin.definition.setup(harness.ctx); + + const payload = { + action: "opened", + pull_request: { + number: 999, + title: "chore: update deps", + head: { ref: "chore/update-deps" }, + }, + repository: { full_name: "tensorleap/fsd" }, + sender: { login: "devuser" }, + }; + const input = webhookInput({ secretRef, payload, eventType: "pull_request" }); + await plugin.definition.onWebhook?.(input); + + const allIssues = await harness.ctx.issues.list({ companyId }); + const triageIssue = allIssues.find((i) => i.title.includes("triage: unmapped tensorleap/fsd#999")); + expect(triageIssue).toBeDefined(); + }); +}); + // ────────────────────────────────────────────── // Unmapped refs — triage issue creation // ──────────────────────────────────────────────