FN-5835: reuse imported GitHub source issue tracking

Reuse the existing imported source issue record when tracking GitHub work instead of creating duplicate entries.

- update GitHub tracking logic to detect and reuse previously imported source issues
- add/expand dashboard tracking tests to cover duplicate-import and reuse behavior
- add a changeset documenting the FN-5835 tracking fix for @runfusion/fusion

Files changed:
 .changeset/fn-5835-source-issue-tracking.md        |   5 ++
 .../src/__tests__/github-tracking.test.ts          | 100 ++++++++++++++++++---
 packages/dashboard/src/github-tracking.ts          |  32 +++++++
 3 files changed, 126 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-5835

Fusion-Task-Lineage: d0827711-8bfb-471c-b2c4-69324da33373
This commit is contained in:
gsxdsm
2026-06-01 08:13:36 -07:00
parent e16893a3c7
commit b4230c06b1
3 changed files with 126 additions and 11 deletions

View File

@@ -375,7 +375,7 @@ describe("maybeCreateTrackingIssue", () => {
expect(createIssueMock).toHaveBeenCalledTimes(1);
});
it("creates issue for github_import tasks when tracking is explicitly enabled", async () => {
it("links GitHub sourceIssue instead of creating a duplicate", async () => {
const linkGithubIssue = vi.fn();
const recordActivity = vi.fn();
@@ -383,6 +383,13 @@ describe("maybeCreateTrackingIssue", () => {
sourceType: "github_import",
title: "Imported issue follow-up",
description: "Short body",
sourceIssue: {
provider: "github",
repository: "upstream/repo",
externalIssueId: "123",
issueNumber: 123,
url: "https://github.com/upstream/repo/issues/123",
},
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue, recordActivity } as any,
@@ -392,18 +399,89 @@ describe("maybeCreateTrackingIssue", () => {
logger: console,
});
expect(createIssueMock).toHaveBeenCalledWith(expect.objectContaining({
owner: "o",
repo: "r",
title: "[FN-1] Imported issue follow-up",
body: "Fusion task: FN-1\n\nShort body",
expect(result).toEqual({ created: false, reason: "source_issue_linked" });
expect(createIssueMock).not.toHaveBeenCalled();
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({
owner: "upstream",
repo: "repo",
number: 123,
url: "https://github.com/upstream/repo/issues/123",
}));
expect(result).toMatchObject({
created: true,
issue: { owner: "o", repo: "r", number: 12, htmlUrl: "https://github.com/o/r/issues/12" },
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
metadata: expect.objectContaining({ type: "github-issue-source-linked", repo: "upstream/repo", number: 123 }),
}));
});
it("constructs sourceIssue URL when sourceIssue.url is missing", async () => {
const linkGithubIssue = vi.fn();
const result = await maybeCreateTrackingIssue(buildTask({
sourceIssue: {
provider: "github",
repository: "upstream/repo",
externalIssueId: "987",
issueNumber: 987,
},
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue, recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({ owner: "o", repo: "r", number: 12 }));
expect(recordActivity).toHaveBeenCalled();
expect(result).toEqual({ created: false, reason: "source_issue_linked" });
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({
url: "https://github.com/upstream/repo/issues/987",
}));
expect(createIssueMock).not.toHaveBeenCalled();
});
it("falls through to normal create path when sourceIssue provider is non-github", async () => {
const result = await maybeCreateTrackingIssue(buildTask({
title: "Imported issue follow-up",
description: "Short body",
sourceIssue: {
provider: "gitlab",
repository: "group/project",
externalIssueId: "321",
issueNumber: 321,
},
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(result).toMatchObject({ created: true });
expect(createIssueMock).toHaveBeenCalledTimes(1);
});
it("falls through to normal create path when sourceIssue repository slug is invalid", async () => {
const result = await maybeCreateTrackingIssue(buildTask({
title: "Imported issue follow-up",
description: "Short body",
sourceIssue: {
provider: "github",
repository: "invalidslug",
externalIssueId: "222",
issueNumber: 222,
},
githubTracking: { enabled: true },
}), {
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
projectSettings: {},
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
rootDir,
logger: { warn: vi.fn(), info: vi.fn() },
});
expect(result).toMatchObject({ created: true });
expect(createIssueMock).toHaveBeenCalledTimes(1);
});
it("uses the AI summarizer when the title is missing and a summarizer model is configured", async () => {

View File

@@ -1,6 +1,7 @@
import {
AiServiceError,
MIN_DESCRIPTION_LENGTH,
parseRepoSlug,
resolveTaskGithubTracking,
summarizeTitle,
type GlobalSettings,
@@ -152,6 +153,7 @@ export type MaybeCreateTrackingIssueReason =
| "no_repo_configured"
| "no_title_available"
| "existing_issue_found"
| "source_issue_linked"
| "github_error"
| "auth_token_missing"
| "auth_gh_not_installed"
@@ -227,6 +229,36 @@ export async function maybeCreateTrackingIssue(
});
}
const sourceIssue = latestTask.sourceIssue;
if (sourceIssue?.provider === "github") {
const sourceRepo = parseRepoSlug(sourceIssue.repository);
if (sourceRepo && Number.isFinite(sourceIssue.issueNumber)) {
const url = sourceIssue.url
?? `https://github.com/${sourceRepo.owner}/${sourceRepo.repo}/issues/${sourceIssue.issueNumber}`;
const createdAt = new Date().toISOString();
await deps.taskStore.linkGithubIssue(task.id, {
owner: sourceRepo.owner,
repo: sourceRepo.repo,
number: sourceIssue.issueNumber,
url,
createdAt,
});
await deps.taskStore.recordActivity({
type: "task:updated",
taskId: task.id,
taskTitle: latestTask.title,
details: `Linked source issue ${sourceRepo.owner}/${sourceRepo.repo}#${sourceIssue.issueNumber}`,
metadata: {
type: "github-issue-source-linked",
repo: `${sourceRepo.owner}/${sourceRepo.repo}`,
number: sourceIssue.issueNumber,
htmlUrl: url,
},
});
return { created: false, reason: "source_issue_linked" };
}
}
const repo = resolvedTracking.repo;
if (!repo) {