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:
5
.changeset/fn-5835-source-issue-tracking.md
Normal file
5
.changeset/fn-5835-source-issue-tracking.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Reuse imported GitHub source issues as task tracking links when GitHub tracking is enabled, instead of creating a duplicate issue. Tasks imported from GitHub now link their existing `sourceIssue` (when valid) as `githubTracking.issue` with no GitHub auth or issue creation call required.
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user