139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import type { GlobalSettings, ProjectSettings, TaskStore } from "@fusion/core";
|
|
import { resolveGithubTrackingAuth } from "./github-auth.js";
|
|
import { GitHubClient } from "./github.js";
|
|
import { delay, isTransientGitHubError } from "./github-tracking-state.js";
|
|
|
|
type Column = "triage" | "todo" | "in-progress" | "in-review" | "done" | "archived";
|
|
|
|
interface TaskMovedEvent {
|
|
task: {
|
|
id: string;
|
|
sourceIssue?: {
|
|
provider?: string;
|
|
repository?: string;
|
|
issueNumber?: number;
|
|
};
|
|
};
|
|
from: Column;
|
|
to: Column;
|
|
}
|
|
|
|
export class GitHubSourceIssueCloseService {
|
|
private readonly defaultStore: TaskStore;
|
|
private readonly listeners = new Map<TaskStore, { onTaskMoved: (event: TaskMovedEvent) => void }>();
|
|
private started = false;
|
|
|
|
constructor(store: TaskStore) {
|
|
this.defaultStore = store;
|
|
}
|
|
|
|
start(): void {
|
|
if (this.started) return;
|
|
this.started = true;
|
|
this.attach(this.defaultStore);
|
|
}
|
|
|
|
stop(): void {
|
|
if (!this.started) return;
|
|
this.started = false;
|
|
for (const store of this.listeners.keys()) {
|
|
this.detach(store);
|
|
}
|
|
}
|
|
|
|
attach(store: TaskStore): void {
|
|
if (this.listeners.has(store)) {
|
|
return;
|
|
}
|
|
|
|
const onTaskMoved = (event: TaskMovedEvent): void => {
|
|
void this.handleTaskMoved(store, event);
|
|
};
|
|
this.listeners.set(store, { onTaskMoved });
|
|
|
|
if (this.started) {
|
|
store.on("task:moved", onTaskMoved);
|
|
}
|
|
}
|
|
|
|
detach(store: TaskStore): void {
|
|
const handlers = this.listeners.get(store);
|
|
if (!handlers) {
|
|
return;
|
|
}
|
|
store.off("task:moved", handlers.onTaskMoved);
|
|
this.listeners.delete(store);
|
|
}
|
|
|
|
private async handleTaskMoved(store: TaskStore, event: TaskMovedEvent): Promise<void> {
|
|
if (event.to !== "done" || event.from === "done") {
|
|
return;
|
|
}
|
|
|
|
const settings = ((await store.getSettings()) ?? {}) as Pick<ProjectSettings, "githubCloseSourceIssueOnDone" | "githubAuthMode" | "githubAuthToken">;
|
|
if (settings.githubCloseSourceIssueOnDone !== true) {
|
|
return;
|
|
}
|
|
|
|
const sourceIssue = event.task.sourceIssue;
|
|
if (!sourceIssue || sourceIssue.provider !== "github") {
|
|
return;
|
|
}
|
|
|
|
const repository = sourceIssue.repository ?? "";
|
|
const [owner, repo] = repository.split("/");
|
|
const issueNumber = sourceIssue.issueNumber;
|
|
if (!owner || !repo || !Number.isInteger(issueNumber)) {
|
|
await store.logEntry(
|
|
event.task.id,
|
|
"Failed to close linked GitHub source issue",
|
|
`Invalid GitHub source issue metadata: ${repository}#${String(issueNumber)}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const issueNumberValue = issueNumber as number;
|
|
|
|
try {
|
|
const globalSettings = (await store.getGlobalSettingsStore?.()?.getSettings?.() ?? {}) as Pick<GlobalSettings, never>;
|
|
const resolution = resolveGithubTrackingAuth({ projectSettings: settings, globalSettings });
|
|
if (!resolution.ok) {
|
|
await store.logEntry(event.task.id, "Skipped closing GitHub source issue", resolution.message);
|
|
return;
|
|
}
|
|
|
|
const client = resolution.auth.mode === "token"
|
|
? new GitHubClient({ token: resolution.auth.token, forceMode: "token" })
|
|
: new GitHubClient({ forceMode: "gh-cli" });
|
|
|
|
const existing = await client.getIssue(owner, repo, issueNumberValue);
|
|
if (!existing || existing.state === "closed") {
|
|
await store.logEntry(event.task.id, "Skipped closing GitHub source issue - issue not found or already closed", `${owner}/${repo}#${issueNumberValue}`);
|
|
return;
|
|
}
|
|
|
|
const closeIssue = async () => {
|
|
await client.setIssueState(owner, repo, issueNumberValue, "closed", "completed");
|
|
};
|
|
|
|
try {
|
|
await closeIssue();
|
|
} catch (error) {
|
|
if (!isTransientGitHubError(error)) {
|
|
throw error;
|
|
}
|
|
await delay(25);
|
|
await closeIssue();
|
|
}
|
|
|
|
await store.logEntry(event.task.id, "Closed linked GitHub source issue", `${owner}/${repo}#${issueNumberValue}`);
|
|
} catch (error) {
|
|
await store.logEntry(
|
|
event.task.id,
|
|
"Failed to close linked GitHub source issue",
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
}
|
|
}
|
|
}
|