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);
|
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 linkGithubIssue = vi.fn();
|
||||||
const recordActivity = vi.fn();
|
const recordActivity = vi.fn();
|
||||||
|
|
||||||
@@ -383,6 +383,13 @@ describe("maybeCreateTrackingIssue", () => {
|
|||||||
sourceType: "github_import",
|
sourceType: "github_import",
|
||||||
title: "Imported issue follow-up",
|
title: "Imported issue follow-up",
|
||||||
description: "Short body",
|
description: "Short body",
|
||||||
|
sourceIssue: {
|
||||||
|
provider: "github",
|
||||||
|
repository: "upstream/repo",
|
||||||
|
externalIssueId: "123",
|
||||||
|
issueNumber: 123,
|
||||||
|
url: "https://github.com/upstream/repo/issues/123",
|
||||||
|
},
|
||||||
githubTracking: { enabled: true },
|
githubTracking: { enabled: true },
|
||||||
}), {
|
}), {
|
||||||
taskStore: { linkGithubIssue, recordActivity } as any,
|
taskStore: { linkGithubIssue, recordActivity } as any,
|
||||||
@@ -392,18 +399,89 @@ describe("maybeCreateTrackingIssue", () => {
|
|||||||
logger: console,
|
logger: console,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createIssueMock).toHaveBeenCalledWith(expect.objectContaining({
|
expect(result).toEqual({ created: false, reason: "source_issue_linked" });
|
||||||
owner: "o",
|
expect(createIssueMock).not.toHaveBeenCalled();
|
||||||
repo: "r",
|
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({
|
||||||
title: "[FN-1] Imported issue follow-up",
|
owner: "upstream",
|
||||||
body: "Fusion task: FN-1\n\nShort body",
|
repo: "repo",
|
||||||
|
number: 123,
|
||||||
|
url: "https://github.com/upstream/repo/issues/123",
|
||||||
}));
|
}));
|
||||||
expect(result).toMatchObject({
|
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
created: true,
|
metadata: expect.objectContaining({ type: "github-issue-source-linked", repo: "upstream/repo", number: 123 }),
|
||||||
issue: { owner: "o", repo: "r", number: 12, htmlUrl: "https://github.com/o/r/issues/12" },
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it("uses the AI summarizer when the title is missing and a summarizer model is configured", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AiServiceError,
|
AiServiceError,
|
||||||
MIN_DESCRIPTION_LENGTH,
|
MIN_DESCRIPTION_LENGTH,
|
||||||
|
parseRepoSlug,
|
||||||
resolveTaskGithubTracking,
|
resolveTaskGithubTracking,
|
||||||
summarizeTitle,
|
summarizeTitle,
|
||||||
type GlobalSettings,
|
type GlobalSettings,
|
||||||
@@ -152,6 +153,7 @@ export type MaybeCreateTrackingIssueReason =
|
|||||||
| "no_repo_configured"
|
| "no_repo_configured"
|
||||||
| "no_title_available"
|
| "no_title_available"
|
||||||
| "existing_issue_found"
|
| "existing_issue_found"
|
||||||
|
| "source_issue_linked"
|
||||||
| "github_error"
|
| "github_error"
|
||||||
| "auth_token_missing"
|
| "auth_token_missing"
|
||||||
| "auth_gh_not_installed"
|
| "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;
|
const repo = resolvedTracking.repo;
|
||||||
|
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
|
|||||||
Reference in New Issue
Block a user