feat(FN-4777): complete Step 3 — wire GitHub tracking dedup pre-check
Fusion-Task-Id: FN-4777 Fusion-Task-Lineage: ed00f639-c928-4c14-be1f-2b43db6022c7
This commit is contained in:
committed by
gsxdsm
parent
de5a5c91d4
commit
f60dde01fe
@@ -2858,6 +2858,9 @@ export interface ProjectSettings {
|
||||
/** Project default GitHub tracking repo in `owner/repo` format (FN-3868).
|
||||
* Falls back to global githubTrackingDefaultRepo when unset. */
|
||||
githubTrackingDefaultRepo?: string;
|
||||
/** When true, tracking issue creation searches open/closed repo issues for likely duplicates before opening a new issue.
|
||||
* Default: true (set false to opt out). */
|
||||
githubTrackingDedupEnabled?: boolean;
|
||||
/** GitHub auth strategy for issue-tracking API calls in this project (FN-3868).
|
||||
* Default: "gh-cli". */
|
||||
githubAuthMode?: GithubAuthMode;
|
||||
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
import type { CreatedIssue } from "./github.js";
|
||||
import { GitHubClient } from "./github.js";
|
||||
import { resolveGithubTrackingAuth } from "./github-auth.js";
|
||||
import {
|
||||
buildIssueSearchQueries,
|
||||
DEDUP_MATCH_THRESHOLD,
|
||||
extractFileScopePaths,
|
||||
extractSymptomKeywords,
|
||||
scoreCandidateIssue,
|
||||
} from "./github-tracking-dedup.js";
|
||||
|
||||
const TRACKING_ISSUE_TITLE_LIMIT = 240;
|
||||
const TRACKING_ISSUE_BODY_SUMMARY_LIMIT = 500;
|
||||
@@ -144,6 +151,7 @@ export type MaybeCreateTrackingIssueReason =
|
||||
| "issue_already_linked"
|
||||
| "no_repo_configured"
|
||||
| "no_title_available"
|
||||
| "existing_issue_found"
|
||||
| "github_error"
|
||||
| "auth_token_missing"
|
||||
| "auth_gh_not_installed"
|
||||
@@ -311,6 +319,72 @@ export async function maybeCreateTrackingIssue(
|
||||
const title = formatTrackingIssueTitle(latestTask);
|
||||
const body = formatTrackingIssueBody(latestTask);
|
||||
|
||||
if (deps.projectSettings.githubTrackingDedupEnabled !== false) {
|
||||
try {
|
||||
const paths = extractFileScopePaths(latestTask as Task & { prompt?: string });
|
||||
const keywords = extractSymptomKeywords(latestTask, { max: 6 });
|
||||
if (paths.length > 0 || keywords.length > 0) {
|
||||
const queries = buildIssueSearchQueries(paths, keywords);
|
||||
const byNumber = new Map<number, {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
html_url: string;
|
||||
state: "open" | "closed";
|
||||
updatedAt?: string;
|
||||
}>();
|
||||
|
||||
for (const query of queries) {
|
||||
const candidates = await githubClient.searchIssues(repo.owner, repo.repo, query, { state: "all", limit: 10 });
|
||||
for (const candidate of candidates) {
|
||||
if (!byNumber.has(candidate.number)) {
|
||||
byNumber.set(candidate.number, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const scored = [...byNumber.values()]
|
||||
.map((candidate) => ({ candidate, ...scoreCandidateIssue(candidate, paths, keywords) }))
|
||||
.filter((entry) => entry.score >= DEDUP_MATCH_THRESHOLD)
|
||||
.filter((entry) => entry.matchedPaths.length > 0 || entry.matchedKeywords.length >= 2)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const bestMatch = scored[0];
|
||||
if (bestMatch) {
|
||||
await deps.taskStore.linkGithubIssue(task.id, {
|
||||
owner: repo.owner,
|
||||
repo: repo.repo,
|
||||
number: bestMatch.candidate.number,
|
||||
url: bestMatch.candidate.html_url,
|
||||
createdAt: bestMatch.candidate.updatedAt ?? new Date().toISOString(),
|
||||
});
|
||||
|
||||
await deps.taskStore.recordActivity({
|
||||
type: "task:updated",
|
||||
taskId: task.id,
|
||||
taskTitle: latestTask.title,
|
||||
details: `Linked existing issue ${repo.owner}/${repo.repo}#${bestMatch.candidate.number} (dedup match; see docs/triage-duplicate-detection-postmortem.md)`,
|
||||
metadata: {
|
||||
type: "github-issue-dedup-matched",
|
||||
repo: `${repo.owner}/${repo.repo}`,
|
||||
number: bestMatch.candidate.number,
|
||||
htmlUrl: bestMatch.candidate.html_url,
|
||||
score: bestMatch.score,
|
||||
matchedPaths: bestMatch.matchedPaths,
|
||||
matchedKeywords: bestMatch.matchedKeywords,
|
||||
state: bestMatch.candidate.state,
|
||||
},
|
||||
});
|
||||
|
||||
return { created: false, reason: "existing_issue_found" };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
deps.logger?.warn?.(`[github-tracking] ${task.id}: duplicate-search failed; falling back to issue creation: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const issue = await githubClient.createIssue({ owner: repo.owner, repo: repo.repo, title, body });
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ export {
|
||||
export { createSkillsAdapter, getProjectSettingsPath, type SkillsAdapter, type DiscoveredSkill, type CatalogEntry, type CatalogFetchResult, type ToggleSkillResult, type UpstreamError, type UpstreamErrorCode, type SkillContent, type SkillFileEntry } from "./skills-adapter.js";
|
||||
export { GitHubClient, isPrMergeReady, type GitHubClientOptions, type PrMergeStatus, type PrCheckStatus, type ReviewDecision, type MergePrParams, type FindPrParams, type CreateIssueParams, type CreatedIssue } from "./github.js";
|
||||
export { maybeCreateTrackingIssue, type MaybeCreateTrackingIssueDeps } from "./github-tracking.js";
|
||||
export {
|
||||
buildIssueSearchQueries,
|
||||
DEDUP_MATCH_THRESHOLD,
|
||||
extractFileScopePaths,
|
||||
extractSymptomKeywords,
|
||||
scoreCandidateIssue,
|
||||
} from "./github-tracking-dedup.js";
|
||||
export { registerGithubTrackingHook } from "./github-tracking-hook.js";
|
||||
export {
|
||||
resolveGithubTrackingAuth,
|
||||
|
||||
Reference in New Issue
Block a user