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
Original file line number Diff line number Diff line change
Expand Up @@ -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, unknown>): string | null {
const pr = payload.pull_request as Record<string, unknown> | null | undefined;
if (!pr) return null;

const headBranch = str((pr.head as Record<string, unknown> | 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
// ──────────────────────────────────────────────
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ──────────────────────────────────────────────
Expand Down