feat(FN-3870): wire GitHub tracking issue creation
Implements GitHub tracking issue creation (FN-3870) for the dashboard, wiring new routes for planning subtasks and task workflows, adding a dedicated `github-tracking` module with issue creation and auth fallback logic, and updating the main `github.ts` with enhanced capabilities — backed by compreh Fusion-Task-Id: FN-3870
This commit is contained in:
137
packages/dashboard/src/__tests__/github-tracking.test.ts
Normal file
137
packages/dashboard/src/__tests__/github-tracking.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { maybeCreateTrackingIssue } from "../github-tracking.js";
|
||||
|
||||
function buildTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "FN-1",
|
||||
description: "desc",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as Task;
|
||||
}
|
||||
|
||||
describe("maybeCreateTrackingIssue", () => {
|
||||
it("returns tracking_disabled when not enabled", async () => {
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ githubTracking: { enabled: false } }), {
|
||||
taskStore: {} as any,
|
||||
githubClient: {} as any,
|
||||
projectSettings: {},
|
||||
globalSettings: {},
|
||||
});
|
||||
expect(result).toEqual({ created: false, reason: "tracking_disabled" });
|
||||
});
|
||||
|
||||
it("returns issue_already_linked when issue already exists", async () => {
|
||||
const result = await maybeCreateTrackingIssue(buildTask({
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
issue: { owner: "o", repo: "r", number: 1, url: "https://github.com/o/r/issues/1", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
},
|
||||
}), {
|
||||
taskStore: {} as any,
|
||||
githubClient: {} as any,
|
||||
projectSettings: {},
|
||||
globalSettings: {},
|
||||
});
|
||||
expect(result).toEqual({ created: false, reason: "issue_already_linked" });
|
||||
});
|
||||
|
||||
it("returns github_import_source for imported tasks", async () => {
|
||||
const result = await maybeCreateTrackingIssue(buildTask({
|
||||
githubTracking: { enabled: true },
|
||||
sourceType: "github_import",
|
||||
}), {
|
||||
taskStore: {} as any,
|
||||
githubClient: {} as any,
|
||||
projectSettings: {},
|
||||
globalSettings: {},
|
||||
});
|
||||
expect(result).toEqual({ created: false, reason: "github_import_source" });
|
||||
});
|
||||
|
||||
it("prefers task repo override over project/global defaults", async () => {
|
||||
const createIssue = vi.fn().mockResolvedValue({
|
||||
owner: "task-owner",
|
||||
repo: "task-repo",
|
||||
number: 11,
|
||||
htmlUrl: "https://github.com/task-owner/task-repo/issues/11",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
await maybeCreateTrackingIssue(buildTask({
|
||||
title: "Test",
|
||||
githubTracking: { enabled: true, repoOverride: "task-owner/task-repo" },
|
||||
}), {
|
||||
taskStore: { linkGithubIssue: vi.fn(), recordActivity: vi.fn() } as any,
|
||||
githubClient: { createIssue } as any,
|
||||
projectSettings: { githubTrackingDefaultRepo: "project-owner/project-repo" } as any,
|
||||
globalSettings: { githubTrackingDefaultRepo: "global-owner/global-repo" } as any,
|
||||
});
|
||||
|
||||
expect(createIssue).toHaveBeenCalledWith(expect.objectContaining({ owner: "task-owner", repo: "task-repo" }));
|
||||
});
|
||||
|
||||
it("creates issue, links metadata, and records activity", async () => {
|
||||
const createIssue = vi.fn().mockResolvedValue({
|
||||
owner: "o",
|
||||
repo: "r",
|
||||
number: 12,
|
||||
htmlUrl: "https://github.com/o/r/issues/12",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const linkGithubIssue = vi.fn();
|
||||
const recordActivity = vi.fn();
|
||||
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ title: "Test", githubTracking: { enabled: true } }), {
|
||||
taskStore: { linkGithubIssue, recordActivity } as any,
|
||||
githubClient: { createIssue } as any,
|
||||
projectSettings: {},
|
||||
globalSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
logger: console,
|
||||
});
|
||||
|
||||
expect(result.created).toBe(true);
|
||||
expect(createIssue).toHaveBeenCalledTimes(1);
|
||||
expect(linkGithubIssue).toHaveBeenCalledWith("FN-1", expect.objectContaining({ owner: "o", repo: "r", number: 12 }));
|
||||
expect(recordActivity).toHaveBeenCalledWith(expect.objectContaining({
|
||||
metadata: expect.objectContaining({ type: "github-issue-created", repo: "o/r", number: 12 }),
|
||||
}));
|
||||
});
|
||||
|
||||
it("returns no_repo_configured and records activity", async () => {
|
||||
const recordActivity = vi.fn();
|
||||
const warn = vi.fn();
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ githubTracking: { enabled: true } }), {
|
||||
taskStore: { recordActivity } as any,
|
||||
githubClient: {} as any,
|
||||
projectSettings: {},
|
||||
globalSettings: {},
|
||||
logger: { warn, info: vi.fn() },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false, reason: "no_repo_configured" });
|
||||
expect(recordActivity).toHaveBeenCalledTimes(1);
|
||||
expect(warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("swallows github errors", async () => {
|
||||
const warn = vi.fn();
|
||||
const result = await maybeCreateTrackingIssue(buildTask({ githubTracking: { enabled: true } }), {
|
||||
taskStore: { recordActivity: vi.fn() } as any,
|
||||
githubClient: { createIssue: vi.fn().mockRejectedValue(new Error("boom")) } as any,
|
||||
projectSettings: { githubTrackingDefaultRepo: "o/r" } as any,
|
||||
globalSettings: {},
|
||||
logger: { warn, info: vi.fn() },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false, reason: "github_error" });
|
||||
expect(warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -65,6 +65,63 @@ describe("GitHubClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIssue", () => {
|
||||
it("uses gh path when authenticated", async () => {
|
||||
mockRunGhJsonAsync.mockResolvedValue({ url: "https://github.com/o/r/issues/8", number: 8, createdAt: "2026-01-02T00:00:00Z" } as any);
|
||||
const issue = await client.createIssue({ owner: "o", repo: "r", title: "t", body: "b", labels: ["bug"] });
|
||||
expect(mockRunGhJsonAsync).toHaveBeenCalled();
|
||||
expect(issue.number).toBe(8);
|
||||
});
|
||||
|
||||
it("falls back to API when gh path fails and token is configured", async () => {
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
mockRunGhJsonAsync.mockRejectedValue(new Error("gh failed"));
|
||||
const fetchSpy = vi.spyOn(global, "fetch" as any).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => ({ number: 7, html_url: "https://github.com/o/r/issues/7", created_at: "2026-01-01T00:00:00Z" }),
|
||||
} as any);
|
||||
|
||||
const issue = await clientWithToken.createIssue({ owner: "o", repo: "r", title: "t", body: "b" });
|
||||
expect(issue).toEqual({ owner: "o", repo: "r", number: 7, htmlUrl: "https://github.com/o/r/issues/7", createdAt: "2026-01-01T00:00:00Z" });
|
||||
expect(mockRunGhJsonAsync).toHaveBeenCalled();
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("uses API path when gh auth is unavailable and token is configured", async () => {
|
||||
mockIsGhAvailable.mockReturnValue(false);
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
const fetchSpy = vi.spyOn(global, "fetch" as any).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => ({ number: 9, html_url: "https://github.com/o/r/issues/9", created_at: "2026-01-03T00:00:00Z" }),
|
||||
} as any);
|
||||
|
||||
const issue = await clientWithToken.createIssue({ owner: "o", repo: "r", title: "t", body: "b" });
|
||||
expect(issue.number).toBe(9);
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("throws when gh auth unavailable and no token provided", async () => {
|
||||
mockIsGhAvailable.mockReturnValue(false);
|
||||
await expect(client.createIssue({ owner: "o", repo: "r", title: "t", body: "b" })).rejects.toThrow("GitHub CLI (gh) is not available");
|
||||
});
|
||||
|
||||
it("surfaces 422 API failures with cause", async () => {
|
||||
mockIsGhAvailable.mockReturnValue(false);
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
vi.spyOn(global, "fetch" as any).mockResolvedValue({ ok: false, status: 422, statusText: "Unprocessable", json: async () => ({ message: "Validation Failed" }) } as any);
|
||||
await expect(clientWithToken.createIssue({ owner: "o", repo: "r", title: "t", body: "b" })).rejects.toThrow("Failed to create GitHub issue");
|
||||
});
|
||||
|
||||
it("surfaces 404 API failures", async () => {
|
||||
mockIsGhAvailable.mockReturnValue(false);
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
vi.spyOn(global, "fetch" as any).mockResolvedValue({ ok: false, status: 404, statusText: "Not Found", json: async () => ({ message: "Not Found" }) } as any);
|
||||
await expect(clientWithToken.createIssue({ owner: "o", repo: "r", title: "t", body: "b" })).rejects.toThrow("Failed to create GitHub issue");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPr", () => {
|
||||
const mockPrParams: CreatePrParams = {
|
||||
owner: "test-owner",
|
||||
|
||||
88
packages/dashboard/src/github-tracking.ts
Normal file
88
packages/dashboard/src/github-tracking.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { GlobalSettings, ProjectSettings, Task, TaskStore } from "@fusion/core";
|
||||
import type { CreatedIssue } from "./github.js";
|
||||
import type { GitHubClient } from "./github.js";
|
||||
|
||||
export interface MaybeCreateTrackingIssueDeps {
|
||||
taskStore: TaskStore;
|
||||
githubClient: GitHubClient;
|
||||
projectSettings: ProjectSettings;
|
||||
globalSettings: GlobalSettings;
|
||||
logger?: Pick<Console, "warn" | "info">;
|
||||
}
|
||||
|
||||
function parseRepo(value: string | undefined): { owner: string; repo: string } | null {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
const [owner, repo, ...rest] = trimmed.split("/");
|
||||
if (!owner || !repo || rest.length > 0) return null;
|
||||
return { owner, repo };
|
||||
}
|
||||
|
||||
export async function maybeCreateTrackingIssue(
|
||||
task: Task,
|
||||
deps: MaybeCreateTrackingIssueDeps,
|
||||
): Promise<{ created: false; reason: string } | { created: true; issue: CreatedIssue }> {
|
||||
const tracking = task.githubTracking;
|
||||
if (tracking?.enabled !== true) {
|
||||
return { created: false, reason: "tracking_disabled" };
|
||||
}
|
||||
|
||||
if (tracking.issue) {
|
||||
return { created: false, reason: "issue_already_linked" };
|
||||
}
|
||||
|
||||
if (task.sourceType === "github_import") {
|
||||
return { created: false, reason: "github_import_source" };
|
||||
}
|
||||
|
||||
const repo =
|
||||
parseRepo(tracking.repoOverride) ??
|
||||
parseRepo(deps.projectSettings.githubTrackingDefaultRepo) ??
|
||||
parseRepo(deps.globalSettings.githubTrackingDefaultRepo);
|
||||
|
||||
if (!repo) {
|
||||
deps.logger?.warn?.(`[github-tracking] No repo configured for ${task.id}`);
|
||||
await deps.taskStore.recordActivity({
|
||||
type: "task:updated",
|
||||
taskId: task.id,
|
||||
taskTitle: task.title,
|
||||
details: "GitHub tracking issue not created: no repository configured",
|
||||
metadata: { type: "github-tracking-no-repo" },
|
||||
});
|
||||
return { created: false, reason: "no_repo_configured" };
|
||||
}
|
||||
|
||||
const title = `[${task.id}] ${task.title ?? task.description.slice(0, 80)}`;
|
||||
const body = `Tracking issue for ${task.id}.\n\n_Summary placeholder — populated by FN-3871._`;
|
||||
|
||||
try {
|
||||
const issue = await deps.githubClient.createIssue({ owner: repo.owner, repo: repo.repo, title, body });
|
||||
|
||||
await deps.taskStore.linkGithubIssue(task.id, {
|
||||
owner: repo.owner,
|
||||
repo: repo.repo,
|
||||
number: issue.number,
|
||||
url: issue.htmlUrl,
|
||||
createdAt: issue.createdAt,
|
||||
});
|
||||
|
||||
await deps.taskStore.recordActivity({
|
||||
type: "task:updated",
|
||||
taskId: task.id,
|
||||
taskTitle: task.title,
|
||||
details: `Linked tracking issue ${repo.owner}/${repo.repo}#${issue.number}`,
|
||||
metadata: {
|
||||
type: "github-issue-created",
|
||||
repo: `${repo.owner}/${repo.repo}`,
|
||||
number: issue.number,
|
||||
htmlUrl: issue.htmlUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return { created: true, issue };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
deps.logger?.warn?.(`[github-tracking] Failed to create issue for ${task.id} in ${repo.owner}/${repo.repo}: ${message}`);
|
||||
return { created: false, reason: "github_error" };
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,22 @@ export interface CreatePrParams {
|
||||
base?: string;
|
||||
}
|
||||
|
||||
export interface CreateIssueParams {
|
||||
owner: string;
|
||||
repo: string;
|
||||
title: string;
|
||||
body: string;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export interface CreatedIssue {
|
||||
owner: string;
|
||||
repo: string;
|
||||
number: number;
|
||||
htmlUrl: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface PrComment {
|
||||
id: number;
|
||||
body: string;
|
||||
@@ -403,7 +419,7 @@ export class GitHubClient {
|
||||
throw new Error(getGhErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fall back to REST API
|
||||
if (this.token) {
|
||||
return this.createPrWithApi(params);
|
||||
@@ -411,6 +427,85 @@ export class GitHubClient {
|
||||
throw new Error("GitHub CLI (gh) is not available or not authenticated, and no GITHUB_TOKEN provided. Run 'gh auth login' or set GITHUB_TOKEN.");
|
||||
}
|
||||
|
||||
async createIssue(params: CreateIssueParams): Promise<CreatedIssue> {
|
||||
if (this.hasGhAuth()) {
|
||||
try {
|
||||
return await this.createIssueWithGh(params);
|
||||
} catch (error) {
|
||||
if (this.token) {
|
||||
try {
|
||||
return await this.createIssueWithApi(params);
|
||||
} catch (apiError) {
|
||||
throw new Error(`Failed to create GitHub issue in ${params.owner}/${params.repo}`, { cause: apiError });
|
||||
}
|
||||
}
|
||||
throw new Error(`Failed to create GitHub issue in ${params.owner}/${params.repo}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.token) {
|
||||
try {
|
||||
return await this.createIssueWithApi(params);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create GitHub issue in ${params.owner}/${params.repo}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("GitHub CLI (gh) is not available or not authenticated, and no GITHUB_TOKEN provided. Run 'gh auth login' or set GITHUB_TOKEN.");
|
||||
}
|
||||
|
||||
private async createIssueWithGh(params: CreateIssueParams): Promise<CreatedIssue> {
|
||||
const issue = await runGhJsonAsync<{ url: string; number: number; createdAt: string }>([
|
||||
"issue",
|
||||
"create",
|
||||
"--repo",
|
||||
`${params.owner}/${params.repo}`,
|
||||
"--title",
|
||||
params.title,
|
||||
"--body",
|
||||
params.body,
|
||||
...(params.labels && params.labels.length > 0 ? ["--label", params.labels.join(",")] : []),
|
||||
"--json",
|
||||
"url,number,createdAt",
|
||||
]);
|
||||
|
||||
return {
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
number: issue.number,
|
||||
htmlUrl: issue.url,
|
||||
createdAt: issue.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
private async createIssueWithApi(params: CreateIssueParams): Promise<CreatedIssue> {
|
||||
const url = `${this.baseUrl}/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/issues`;
|
||||
const result = await this.fetchThrottled<{
|
||||
number: number;
|
||||
html_url: string;
|
||||
created_at: string;
|
||||
}>(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: params.title,
|
||||
body: params.body,
|
||||
labels: params.labels,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.error ?? "GitHub API error");
|
||||
}
|
||||
|
||||
return {
|
||||
owner: params.owner,
|
||||
repo: params.repo,
|
||||
number: result.data.number,
|
||||
htmlUrl: result.data.html_url,
|
||||
createdAt: result.data.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
private createPrWithGh(params: CreatePrParams): PrInfo {
|
||||
const { owner: paramOwner, repo: paramRepo, title, body, head, base } = params;
|
||||
const { owner, repo } = this.resolveRepo(paramOwner, paramRepo);
|
||||
|
||||
@@ -11,7 +11,8 @@ export {
|
||||
type RuntimeLogSink,
|
||||
} from "./runtime-logger.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 PrMergeStatus, type PrCheckStatus, type ReviewDecision, type MergePrParams, type FindPrParams } from "./github.js";
|
||||
export { GitHubClient, isPrMergeReady, 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 { rateLimit, RATE_LIMITS, type RateLimitOptions } from "./rate-limit.js";
|
||||
export { GitHubPollingService, type GitHubPollingServiceOptions, type TaskWatchInput, type WatchedBadgeType } from "./github-poll.js";
|
||||
export { GitHubIssueCommentService, DEFAULT_COMMENT_TEMPLATE } from "./github-issue-comment.js";
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
} from "@fusion/core";
|
||||
import type { ServerOptions } from "./server.js";
|
||||
import { verifyWebhookSignature } from "./github-webhooks.js";
|
||||
import { GitHubClient } from "./github.js";
|
||||
import { maybeCreateTrackingIssue } from "./github-tracking.js";
|
||||
import { AiSessionStore, SESSION_CLEANUP_DEFAULT_MAX_AGE_MS } from "./ai-session-store.js";
|
||||
import { getSession as getPlanningSession, cleanupSession as cleanupPlanningSession } from "./planning.js";
|
||||
import { getSubtaskSession, cleanupSubtaskSession } from "./subtask-breakdown.js";
|
||||
@@ -4793,6 +4795,27 @@ async function executeAiPromptStep(
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeCreateTaskTrackingIssue(taskStore: TaskStore, task: Task): Promise<void> {
|
||||
const projectSettings = await taskStore.getSettings();
|
||||
const globalSettings = (await taskStore.getGlobalSettingsStore?.()?.getSettings?.()) ?? {};
|
||||
const authMode = projectSettings.githubAuthMode;
|
||||
const token = authMode === "token"
|
||||
? projectSettings.githubAuthToken
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
await maybeCreateTrackingIssue(task, {
|
||||
taskStore,
|
||||
githubClient: new GitHubClient(token),
|
||||
projectSettings,
|
||||
globalSettings,
|
||||
logger: console,
|
||||
});
|
||||
} catch {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCreateTaskStep(
|
||||
step: import("@fusion/core").AutomationStep,
|
||||
startedAt: string,
|
||||
@@ -4823,6 +4846,7 @@ async function executeCreateTaskStep(
|
||||
sourceMetadata: { stepId: step.id },
|
||||
},
|
||||
});
|
||||
await maybeCreateTaskTrackingIssue(taskStore, task);
|
||||
|
||||
return {
|
||||
stepId: step.id,
|
||||
|
||||
@@ -7,11 +7,34 @@ import {
|
||||
type TaskStore,
|
||||
} from "@fusion/core";
|
||||
import { ApiError, badRequest, notFound, rateLimited } from "../api-error.js";
|
||||
import { GitHubClient } from "../github.js";
|
||||
import { maybeCreateTrackingIssue } from "../github-tracking.js";
|
||||
import { writeSSEEvent, type SessionBufferedEvent } from "../sse-buffer.js";
|
||||
import type { AiSessionStore } from "../ai-session-store.js";
|
||||
import type { ApiRoutesContext } from "./types.js";
|
||||
import { derivePerTaskBranch, resolveBranchAssignmentContext, resolveBranchSelection } from "./branch-selection.js";
|
||||
|
||||
async function maybeCreateTaskTrackingIssue(taskStore: TaskStore, task: import("@fusion/core").Task): Promise<void> {
|
||||
const projectSettings = await taskStore.getSettings();
|
||||
const globalSettings = (await taskStore.getGlobalSettingsStore?.()?.getSettings?.()) ?? {};
|
||||
const authMode = projectSettings.githubAuthMode;
|
||||
const token = authMode === "token"
|
||||
? projectSettings.githubAuthToken
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
await maybeCreateTrackingIssue(task, {
|
||||
taskStore,
|
||||
githubClient: new GitHubClient(token),
|
||||
projectSettings,
|
||||
globalSettings,
|
||||
logger: console,
|
||||
});
|
||||
} catch {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
interface PlanningSubtaskRouteDeps {
|
||||
store: TaskStore;
|
||||
aiSessionStore?: AiSessionStore;
|
||||
@@ -244,6 +267,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
baseBranch: resolvedBaseBranch,
|
||||
branchContext: planningBranchContext,
|
||||
});
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, task);
|
||||
|
||||
tempIdToTaskId.set(item.tempId, task.id);
|
||||
createdTasks.push(task);
|
||||
@@ -1076,6 +1100,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
branch: resolvedBranch,
|
||||
baseBranch: resolvedBaseBranch,
|
||||
});
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, task);
|
||||
|
||||
// Update task with suggested size if provided
|
||||
if (summary.suggestedSize) {
|
||||
@@ -1240,6 +1265,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
baseBranch: resolvedBaseBranch,
|
||||
branchContext: planningBranchContext,
|
||||
});
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, task);
|
||||
|
||||
tempIdToTaskId.set(item.id, task.id);
|
||||
createdTasks.push(task);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getCurrentRepo,
|
||||
} from "@fusion/core";
|
||||
import { GitHubClient } from "../github.js";
|
||||
import { maybeCreateTrackingIssue } from "../github-tracking.js";
|
||||
import { parseGitHubBadgeUrl } from "./register-git-github.js";
|
||||
import { planTaskWorktreePath } from "@fusion/engine";
|
||||
import { ApiError, badRequest, conflict, notFound } from "../api-error.js";
|
||||
@@ -96,6 +97,27 @@ async function buildDirectTaskReviewData(task: Task, store: TaskStore): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
async function maybeCreateTaskTrackingIssue(taskStore: TaskStore, task: Task, optionsToken?: string): Promise<void> {
|
||||
const projectSettings = await taskStore.getSettings();
|
||||
const globalSettings = (await taskStore.getGlobalSettingsStore?.()?.getSettings?.()) ?? {};
|
||||
const authMode = projectSettings.githubAuthMode;
|
||||
const token = authMode === "token"
|
||||
? projectSettings.githubAuthToken ?? optionsToken
|
||||
: optionsToken;
|
||||
|
||||
try {
|
||||
await maybeCreateTrackingIssue(task, {
|
||||
taskStore,
|
||||
githubClient: new GitHubClient(token),
|
||||
projectSettings,
|
||||
globalSettings,
|
||||
logger: console,
|
||||
});
|
||||
} catch {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
interface TaskWorkflowRouteDeps {
|
||||
runtimeLogger: { error: (message: string, data?: Record<string, unknown>) => void; warn: (message: string, data?: Record<string, unknown>) => void };
|
||||
upload: { single: (name: string) => unknown };
|
||||
@@ -309,6 +331,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
createInput,
|
||||
{ onSummarize, settings: { autoSummarizeTitles: settings.autoSummarizeTitles } },
|
||||
);
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, task, options?.githubToken);
|
||||
res.status(201).json(task);
|
||||
return;
|
||||
}
|
||||
@@ -343,6 +366,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
createdTask = await scopedStore.createTaskWithReservedId(createInput, {
|
||||
taskId: reservation.taskId,
|
||||
});
|
||||
await maybeCreateTaskTrackingIssue(scopedStore, createdTask, options?.githubToken);
|
||||
|
||||
const replicatedPayload = buildMeshReplicatedTaskCreatePayload({
|
||||
taskId: createdTask.id,
|
||||
|
||||
Reference in New Issue
Block a user