Files
fusion/packages/dashboard/src/gitlab-lifecycle.ts
gsxdsm 34be333a2a FN-7426: add GitLab lifecycle automation
Add GitLab lifecycle automation for comments and source item state changes.

- Add GitLab REST helpers for posting notes and closing or reopening issues and merge requests.
- Register GitLab source-comment, tracking-comment, tracking-state, and source-close services with multi-project lifecycle wiring.
- Add project settings, documentation, tests, and a changeset for GitLab completion comments and auto-close behavior.

Files changed:
 .changeset/fn-7426-gitlab-lifecycle.md             |  7 ++
 docs/cli-reference.md                              |  2 +-
 docs/dashboard-guide.md                            |  3 +-
 docs/settings-reference.md                         |  5 +-
 packages/core/src/settings-schema.ts               |  3 +
 packages/core/src/types.ts                         | 10 +++
 .../src/__tests__/gitlab-issue-comment.test.ts     | 37 +++++++++
 .../__tests__/gitlab-source-issue-close.test.ts    | 29 +++++++
 .../src/__tests__/gitlab-tracking-comments.test.ts | 31 +++++++
 .../src/__tests__/gitlab-tracking-state.test.ts    | 42 ++++++++++
 packages/dashboard/src/__tests__/gitlab.test.ts    | 18 ++++
 .../register-git-github.gitlab-lifecycle.test.ts   | 54 ++++++++++++
 packages/dashboard/src/gitlab-issue-comment.ts     | 62 ++++++++++++++
 packages/dashboard/src/gitlab-lifecycle.ts         | 58 +++++++++++++
 .../dashboard/src/gitlab-source-issue-close.ts     | 45 ++++++++++
 packages/dashboard/src/gitlab-tracking-comments.ts | 77 +++++++++++++++++
 packages/dashboard/src/gitlab-tracking-state.ts    | 97 ++++++++++++++++++++++
 packages/dashboard/src/gitlab.ts                   | 30 +++++++
 .../dashboard/src/routes/register-git-github.ts    | 23 +++++
 19 files changed, 630 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-7426

Fusion-Task-Lineage: a46bd11f-96ef-4291-ab41-829007fa057a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:34 -07:00

59 lines
3.0 KiB
TypeScript

import type { GlobalSettings, ProjectSettings, Task, TaskGitLabTrackedItem, TaskStore } from "@fusion/core";
import { GitLabClient } from "./gitlab.js";
import { resolveGitlabAuth } from "./gitlab-auth.js";
export type GitLabLifecycleTarget = {
kind: "project_issue" | "group_issue" | "merge_request";
project: string | number;
iid: number;
label: string;
url?: string;
};
export async function resolveGitLabClient(store: TaskStore): Promise<{ ok: true; client: GitLabClient } | { ok: false; message: string }> {
const projectSettings = await store.getSettings() as ProjectSettings;
const globalSettings = (await store.getGlobalSettingsStore?.()?.getSettings?.() ?? {}) as Partial<GlobalSettings>;
const resolution = resolveGitlabAuth({ projectSettings, globalSettings });
if (!resolution.ok) return { ok: false, message: resolution.message };
return { ok: true, client: new GitLabClient(resolution.auth) };
}
export function resolveGitLabTarget(task: Pick<Task, "gitlabTracking" | "sourceIssue" | "source">): GitLabLifecycleTarget | null {
const item = task.gitlabTracking?.item;
if (item) return resolveGitLabTargetFromItem(item);
const meta = task.source?.sourceMetadata && typeof task.source.sourceMetadata === "object"
? task.source.sourceMetadata as Record<string, unknown>
: undefined;
if (task.sourceIssue?.provider !== "gitlab" || !meta) return null;
const kind = meta.resourceType === "merge_request" ? "merge_request" : meta.resourceType === "group_issue" ? "group_issue" : "project_issue";
const iid = typeof meta.iid === "number" ? meta.iid : task.sourceIssue.issueNumber;
const project = typeof meta.projectId === "number" ? meta.projectId : typeof meta.projectPath === "string" ? meta.projectPath : task.sourceIssue.repository;
if (!Number.isInteger(iid) || !project) return null;
return { kind, project, iid, label: formatGitLabTargetLabel(kind, project, iid), url: task.sourceIssue.url };
}
export function resolveGitLabTargetFromItem(item: TaskGitLabTrackedItem): GitLabLifecycleTarget | null {
const project = item.projectId ?? item.projectPath;
if (!project || !Number.isInteger(item.iid)) return null;
return { kind: item.kind, project, iid: item.iid, label: formatGitLabTargetLabel(item.kind, project, item.iid), url: item.url };
}
export function formatGitLabTargetLabel(kind: GitLabLifecycleTarget["kind"], project: string | number, iid: number): string {
const marker = kind === "merge_request" ? "!" : "#";
return `${String(project)}${marker}${iid}`;
}
export async function safeLogGitLabEntry(store: TaskStore, taskId: string, message: string, details: string): Promise<void> {
try {
await store.logEntry(taskId, message, details);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes(`Task ${taskId} not found`)) {
console.warn(`[gitlab-lifecycle] Unable to write log entry for deleted task ${taskId}: ${message}`);
return;
}
throw error;
}
}