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).
|
/** Project default GitHub tracking repo in `owner/repo` format (FN-3868).
|
||||||
* Falls back to global githubTrackingDefaultRepo when unset. */
|
* Falls back to global githubTrackingDefaultRepo when unset. */
|
||||||
githubTrackingDefaultRepo?: string;
|
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).
|
/** GitHub auth strategy for issue-tracking API calls in this project (FN-3868).
|
||||||
* Default: "gh-cli". */
|
* Default: "gh-cli". */
|
||||||
githubAuthMode?: GithubAuthMode;
|
githubAuthMode?: GithubAuthMode;
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
import type { CreatedIssue } from "./github.js";
|
import type { CreatedIssue } from "./github.js";
|
||||||
import { GitHubClient } from "./github.js";
|
import { GitHubClient } from "./github.js";
|
||||||
import { resolveGithubTrackingAuth } from "./github-auth.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_TITLE_LIMIT = 240;
|
||||||
const TRACKING_ISSUE_BODY_SUMMARY_LIMIT = 500;
|
const TRACKING_ISSUE_BODY_SUMMARY_LIMIT = 500;
|
||||||
@@ -144,6 +151,7 @@ export type MaybeCreateTrackingIssueReason =
|
|||||||
| "issue_already_linked"
|
| "issue_already_linked"
|
||||||
| "no_repo_configured"
|
| "no_repo_configured"
|
||||||
| "no_title_available"
|
| "no_title_available"
|
||||||
|
| "existing_issue_found"
|
||||||
| "github_error"
|
| "github_error"
|
||||||
| "auth_token_missing"
|
| "auth_token_missing"
|
||||||
| "auth_gh_not_installed"
|
| "auth_gh_not_installed"
|
||||||
@@ -311,6 +319,72 @@ export async function maybeCreateTrackingIssue(
|
|||||||
const title = formatTrackingIssueTitle(latestTask);
|
const title = formatTrackingIssueTitle(latestTask);
|
||||||
const body = formatTrackingIssueBody(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 {
|
try {
|
||||||
const issue = await githubClient.createIssue({ owner: repo.owner, repo: repo.repo, title, body });
|
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 { 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 { 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 { 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 { registerGithubTrackingHook } from "./github-tracking-hook.js";
|
||||||
export {
|
export {
|
||||||
resolveGithubTrackingAuth,
|
resolveGithubTrackingAuth,
|
||||||
|
|||||||
Reference in New Issue
Block a user