feat(FN-4271): create tracking issue for github-imported tasks
Adds a tracking issue mechanism for GitHub-imported tasks, creating an internal Fusion task to track the import lifecycle. The TaskCard component gains visibility into tracking status, backed by new tests in both TaskCard and GitHub tracking modules. Fusion-Task-Id: FN-4271
This commit is contained in:
@@ -326,6 +326,21 @@ function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | und
|
||||
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||
}
|
||||
|
||||
function parseGithubIssueUrl(url?: string): { owner: string; repo: string; number: number } | null {
|
||||
if (!url) return null;
|
||||
const match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:$|[/?#])/i);
|
||||
if (!match) return null;
|
||||
|
||||
const issueNumber = Number(match[3]);
|
||||
if (!Number.isInteger(issueNumber) || issueNumber <= 0) return null;
|
||||
|
||||
return {
|
||||
owner: match[1],
|
||||
repo: match[2],
|
||||
number: issueNumber,
|
||||
};
|
||||
}
|
||||
|
||||
function areTaskWorkflowResultsEqual(previous?: Task["workflowStepResults"], next?: Task["workflowStepResults"]): boolean {
|
||||
if (!previous && !next) return true;
|
||||
if (!previous || !next) return false;
|
||||
@@ -740,12 +755,30 @@ function TaskCardComponent({
|
||||
const hasGithubTrackingLink = Boolean(githubTrackedIssue);
|
||||
const hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
|
||||
const isGitHubImportedTask = task.sourceType === "github_import";
|
||||
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||
const sourceIssueFromUrl = useMemo(() => parseGithubIssueUrl(sourceIssueUrl), [sourceIssueUrl]);
|
||||
const issueInfoFromUrl = useMemo(() => parseGithubIssueUrl(task.issueInfo?.url), [task.issueInfo?.url]);
|
||||
const issueInfoOwner = task.issueInfo?.owner ?? issueInfoFromUrl?.owner;
|
||||
const issueInfoRepo = task.issueInfo?.repo ?? issueInfoFromUrl?.repo;
|
||||
const hasMatchingIssueInfoBadge = Boolean(
|
||||
task.issueInfo
|
||||
&& githubTrackedIssue
|
||||
&& task.issueInfo.number === githubTrackedIssue.number
|
||||
&& issueInfoOwner === githubTrackedIssue.owner
|
||||
&& issueInfoRepo === githubTrackedIssue.repo,
|
||||
);
|
||||
const hasMatchingSourceIssue = Boolean(
|
||||
sourceIssueFromUrl
|
||||
&& githubTrackedIssue
|
||||
&& sourceIssueFromUrl.number === githubTrackedIssue.number
|
||||
&& sourceIssueFromUrl.owner === githubTrackedIssue.owner
|
||||
&& sourceIssueFromUrl.repo === githubTrackedIssue.repo,
|
||||
);
|
||||
const showTrackingIndicator = hasGithubTrackingLink
|
||||
&& !isGitHubImportedTask
|
||||
&& !(task.issueInfo && task.issueInfo.number === githubTrackedIssue?.number);
|
||||
&& !hasMatchingIssueInfoBadge
|
||||
&& !hasMatchingSourceIssue;
|
||||
const branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
|
||||
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
|
||||
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||
const isAgentCreated = isAgentCreatedTask(task);
|
||||
const sourceAgentName = getSourceAgentName(task);
|
||||
const agentCreatedTitle = sourceAgentName ? `Created by agent: ${sourceAgentName}` : "Created by agent";
|
||||
@@ -1630,6 +1663,7 @@ function TaskCardComponent({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ProviderIcon provider="github" size="sm" />
|
||||
<span>{`#${githubTrackedIssue.number}`}</span>
|
||||
</a>
|
||||
)}
|
||||
{timeIndicator && (
|
||||
|
||||
@@ -1062,7 +1062,33 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByRole("link", { name: /Linked GitHub issue/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render a GitHub tracking link for github_import tasks", () => {
|
||||
it("renders a GitHub tracking link for github_import tasks when the tracking issue is distinct from source", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
column: "todo",
|
||||
sourceType: "github_import",
|
||||
sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/42" },
|
||||
githubTracking: {
|
||||
issue: {
|
||||
owner: "other",
|
||||
repo: "tracking",
|
||||
number: 99,
|
||||
url: "https://github.com/other/tracking/issues/99",
|
||||
createdAt: "2026-05-12T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("link", { name: "Linked GitHub issue #99" })).toBeDefined();
|
||||
expect(screen.getByLabelText("Imported from GitHub")).toBeDefined();
|
||||
});
|
||||
|
||||
it("deduplicates the tracking link when github_import tracking issue matches source owner/repo/number", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
|
||||
@@ -225,6 +225,37 @@ describe("maybeCreateTrackingIssue", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("creates issue for github_import tasks when tracking is explicitly enabled", async () => {
|
||||
const linkGithubIssue = vi.fn();
|
||||
const recordActivity = vi.fn();
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({
|
||||
sourceType: "github_import",
|
||||
title: "Imported issue follow-up",
|
||||
description: "Short body",
|
||||
githubTracking: { enabled: true },
|
||||
}), {
|
||||
taskStore: { linkGithubIssue, recordActivity } as any,
|
||||
projectSettings: {},
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
rootDir,
|
||||
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).toMatchObject({
|
||||
created: true,
|
||||
issue: { owner: "o", repo: "r", number: 12, htmlUrl: "https://github.com/o/r/issues/12" },
|
||||
});
|
||||
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({ owner: "o", repo: "r", number: 12 }));
|
||||
expect(recordActivity).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the AI summarizer when the title is missing and a summarizer model is configured", async () => {
|
||||
const linkGithubIssue = vi.fn();
|
||||
const recordActivity = vi.fn();
|
||||
|
||||
@@ -142,7 +142,6 @@ export interface MaybeCreateTrackingIssueDeps {
|
||||
export type MaybeCreateTrackingIssueReason =
|
||||
| "tracking_disabled"
|
||||
| "issue_already_linked"
|
||||
| "github_import_source"
|
||||
| "no_repo_configured"
|
||||
| "no_title_available"
|
||||
| "github_error"
|
||||
@@ -193,10 +192,6 @@ export async function maybeCreateTrackingIssue(
|
||||
return { created: false, reason: "issue_already_linked" };
|
||||
}
|
||||
|
||||
if (task.sourceType === "github_import") {
|
||||
return { created: false, reason: "github_import_source" };
|
||||
}
|
||||
|
||||
const repo = resolvedTracking.repo;
|
||||
|
||||
if (!repo) {
|
||||
|
||||
Reference in New Issue
Block a user