FN-7575: add release version lines to Fusion self-repo done comments
Extends GitHubIssueCommentService so task-close comments on issues in the runfusion/fusion repo itself append both a current-version and target-next-minor-release line, while comments on all other linked repos stay byte-for-byte unchanged.
- Add isFusionSelfRepo() and computeNextMinorVersion() helpers to github-issue-comment.ts
- Append "Current version: v{current}" and "Target release: v{next-minor}" lines only when the linked source issue's repo is runfusion/fusion (case-insensitive)
- Fall back silently (no version lines) when the resolved version is unparseable or the unresolved 0.0.0 sentinel
- Add changeset (minor) documenting the new behavior
- Update docs/settings-reference.md and docs/gitlab-parity-inventory.md
- Expand github-issue-comment.test.ts coverage for self-repo vs other-repo behavior and version edge cases
Files changed:
.changeset/fn-7575-release-version-comment.md | 7 +
docs/gitlab-parity-inventory.md | 2 +-
docs/settings-reference.md | 2 +-
packages/dashboard/src/__tests__/github-issue-comment.test.ts | 142 ++++++++++++++++++++-
packages/dashboard/src/github-issue-comment.ts | 69 +++++++++-
5 files changed, 212 insertions(+), 10 deletions(-)
Fusion-Task-Id: FN-7575
Fusion-Task-Lineage: b7cf7e6f-8d96-4442-8595-5d54ea911481
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7575-release-version-comment.md
Normal file
7
.changeset/fn-7575-release-version-comment.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Fusion self-repo issue-close comments now show current and target release versions.
|
||||
category: feature
|
||||
dev: GitHubIssueCommentService appends "Current version: v{current}" and "Target release: v{next-minor}" lines when the linked source issue is runfusion/fusion; other repos unchanged. Version resolved via getCliPackageVersion.
|
||||
@@ -41,7 +41,7 @@ Required included surfaces: issue import, linked issue tracking, completion comm
|
||||
| Post-create tracking hook | `registerGithubTrackingHook()` registers a universal post-create hook for dashboard, CLI, extension, mission, automation, delegation, routine, duplicate/refine, and subtask creation paths. | `github-tracking-hook.ts`; `docs/task-management.md` | GitLab tracking must register an equivalent universal post-create hook at the same process entrypoints, or a provider-neutral hook dispatcher that calls GitHub/GitLab handlers. It must be best-effort and non-blocking. |
|
||||
| Tracking title/body format | GitHub tracking issues use title `[FN-XXXX] Task title` and body prefix `Fusion task: FN-XXXX` with bounded plain-text summary. | `github-tracking.ts`; docs contract test | GitLab tracking issues should use the same title/body invariant unless later UX explicitly changes it. Keep body bounded and do not include local dashboard links. |
|
||||
| Tracking lifecycle comments | GitHub posts lifecycle comments on tracked issues for in-progress/done, with bounded plain text and optional merge metadata. | `github-tracking-comments.ts` | GitLab should post issue notes for the same lifecycle moments to linked GitLab tracking issues. Merge-request notes are only relevant for imported MR review tasks and must be designed separately. |
|
||||
| Completion comments on source issues | `githubCommentOnDone` and optional `githubCommentTemplate` post a completion comment to imported GitHub source issues when tasks enter done. | `github-issue-comment.ts`; settings | Add GitLab equivalents using project issue notes. The template placeholders should remain provider-neutral (`{taskId}`, `{taskTitle}`) or be explicitly duplicated with `gitlabCommentOnDone`/`gitlabCommentTemplate`. Group issue imports still comment through owning project issue notes. |
|
||||
| Completion comments on source issues | `githubCommentOnDone` and optional `githubCommentTemplate` post a completion comment to imported GitHub source issues when tasks enter done. On GitHub, when the linked source issue's repository is the Fusion self-repo (`runfusion/fusion`, case-insensitive), the comment additionally appends `Current version:` and `Target release:` lines (next-minor bump of the resolved `@runfusion/fusion` CLI package version); this enrichment is GitHub/Fusion-specific and intentionally out of scope for this GitLab parity pass. | `github-issue-comment.ts`; settings | Add GitLab equivalents using project issue notes. The template placeholders should remain provider-neutral (`{taskId}`, `{taskTitle}`) or be explicitly duplicated with `gitlabCommentOnDone`/`gitlabCommentTemplate`. Group issue imports still comment through owning project issue notes. A GitLab release-version enrichment equivalent is not implemented here — file a follow-up if needed. |
|
||||
| Auto-close imported source issues | `githubCloseSourceIssueOnDone` closes/reopens source-imported GitHub issues on task state transitions and startup reconciliation closes missed done tasks. | `github-source-issue-close.ts`; `github-tracking-reconciler.ts` | Add GitLab source issue close/reopen on imported project issues. Reconciliation must use stored GitLab project identity plus issue IID/global ID. Group issue rows cannot be closed through group endpoint; resolve owning project first. Do not auto-close or merge merge requests unless a later task explicitly adds an MR policy. |
|
||||
| Source issue close timestamp backfill | Command Center GitHub fixed counts can be made exact by `POST /api/git/github/backfill-source-issue-closed-at` in manual batches. | `GithubArea.tsx`; `register-git-github.ts`; `TaskSourceIssue.closedAt` | Add a GitLab manual backfill for imported GitLab source issues/MRs if analytics need exact close/merged timestamps. It must be an explicit operator action and never run during render-time analytics. |
|
||||
| Webhooks for linked state | `POST /api/github/webhooks` verifies GitHub App signatures and updates issue/PR/task badges from pull_request/issues/issue_comment events. | `github-webhooks.ts`; `register-git-github.ts` | GitLab webhook support should use GitLab project/group/system hooks as provider-specific signed signal ingestion. Badge/state update hooks for GitLab-linked issues/MRs are separate from Command Center Signals but can reuse signature-validation patterns. Document exact GitLab headers/secrets in the implementation task. |
|
||||
|
||||
@@ -604,7 +604,7 @@ Default notes:
|
||||
| `archiveAgentLogMode` | `"none" \| "compact" \| "full"` | `"compact"` | Agent log retention strategy for cold archive snapshots. |
|
||||
| `autoUpdatePrStatus` | `boolean` | `false` | Auto-refresh PR status badges. |
|
||||
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
|
||||
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. |
|
||||
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. When the linked source issue's repository is the Fusion self-repo (`runfusion/fusion`, case-insensitive), Fusion appends a `Current version: v<current>` line and a `Target release: v<nextMinor>` line (next-minor bump, patch reset to 0, e.g. `0.55.0` → `0.56.0`), resolved via the published `@runfusion/fusion` CLI package version. If that version is unresolved/unparseable, the base comment is posted with no version lines. Comments on every other repository are byte-for-byte unchanged. |
|
||||
| `githubCloseSourceIssueOnDone` | `boolean` | `false` | When enabled, source-imported GitHub issues are automatically closed with `state_reason: completed` when the Fusion task moves to `done`. A startup reconciliation sweep also closes missed open source issues on boot. |
|
||||
| `githubTrackingEnabledByDefault` | `boolean` | `false` | Project-level default for enabling issue tracking on ordinary new tasks. When this is false, the Quick Entry GitHub toggle is disabled until tracking is enabled in Settings. Imported GitHub issues still follow this default unless `githubLinkImportedIssuesToTracking` is enabled. |
|
||||
| `githubLinkImportedIssuesToTracking` | `boolean` | `false` | Project-scoped, import-only option. When enabled, GitHub issue imports from the dashboard, CLI, and extension tools persist `githubTracking: { enabled: true }` so Fusion adopts the imported source issue as the tracking issue without turning tracking on for ordinary new tasks. Duplicate/skipped imports do not create tasks or tracking metadata. |
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { DEFAULT_COMMENT_TEMPLATE, GitHubIssueCommentService } from "../github-issue-comment.js";
|
||||
import {
|
||||
computeNextMinorVersion,
|
||||
DEFAULT_COMMENT_TEMPLATE,
|
||||
GitHubIssueCommentService,
|
||||
isFusionSelfRepo,
|
||||
} from "../github-issue-comment.js";
|
||||
|
||||
const { mockCommentOnIssue } = vi.hoisted(() => ({
|
||||
mockCommentOnIssue: vi.fn(),
|
||||
@@ -30,6 +35,10 @@ class MockStore extends EventEmitter {
|
||||
setSettings(settings: Record<string, unknown>): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
getRootDir(): string {
|
||||
return "/tmp/github-issue-comment-test";
|
||||
}
|
||||
}
|
||||
|
||||
function createTask(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
@@ -49,17 +58,67 @@ async function flushAsync(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
describe("isFusionSelfRepo", () => {
|
||||
it("matches the canonical slug", () => {
|
||||
expect(isFusionSelfRepo("runfusion/fusion")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches case-insensitively and trims whitespace", () => {
|
||||
expect(isFusionSelfRepo("Runfusion/Fusion")).toBe(true);
|
||||
expect(isFusionSelfRepo(" runfusion/fusion ")).toBe(true);
|
||||
expect(isFusionSelfRepo("RUNFUSION/FUSION")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match other repos", () => {
|
||||
expect(isFusionSelfRepo("owner/repo")).toBe(false);
|
||||
expect(isFusionSelfRepo("runfusion/other")).toBe(false);
|
||||
expect(isFusionSelfRepo("other/fusion")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeNextMinorVersion", () => {
|
||||
it("bumps the minor version and resets patch to 0", () => {
|
||||
expect(computeNextMinorVersion("0.55.0")).toBe("0.56.0");
|
||||
});
|
||||
|
||||
it("resets patch to 0 for a non-zero patch", () => {
|
||||
expect(computeNextMinorVersion("1.2.9")).toBe("1.3.0");
|
||||
});
|
||||
|
||||
it("tolerates a leading v prefix", () => {
|
||||
expect(computeNextMinorVersion("v0.55.0")).toBe("0.56.0");
|
||||
});
|
||||
|
||||
it("ignores pre-release/build suffixes", () => {
|
||||
expect(computeNextMinorVersion("0.55.0-beta.1")).toBe("0.56.0");
|
||||
});
|
||||
|
||||
it("returns null for the unresolved 0.0.0 sentinel", () => {
|
||||
expect(computeNextMinorVersion("0.0.0")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for unparseable input", () => {
|
||||
expect(computeNextMinorVersion("not-a-version")).toBeNull();
|
||||
expect(computeNextMinorVersion("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubIssueCommentService", () => {
|
||||
let store: MockStore;
|
||||
let service: GitHubIssueCommentService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCommentOnIssue.mockResolvedValue(undefined);
|
||||
store = new MockStore({ githubCommentOnDone: true });
|
||||
service = new GitHubIssueCommentService(store as unknown as TaskStore, () => "ghp_test");
|
||||
service = new GitHubIssueCommentService(store as unknown as TaskStore, () => "ghp_test", () => "0.55.0");
|
||||
service.start();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.stop();
|
||||
});
|
||||
|
||||
it("does nothing when setting is disabled", async () => {
|
||||
store.setSettings({ githubCommentOnDone: false });
|
||||
|
||||
@@ -105,7 +164,7 @@ describe("GitHubIssueCommentService", () => {
|
||||
expect(mockCommentOnIssue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("posts comment when setting enabled and task moved to done", async () => {
|
||||
it("posts comment when setting enabled and task moved to done (non-self-repo, byte-for-byte unchanged)", async () => {
|
||||
mockCommentOnIssue.mockResolvedValue(undefined);
|
||||
|
||||
store.emit("task:moved", { task: createTask(), from: "in-progress", to: "done" });
|
||||
@@ -119,7 +178,7 @@ describe("GitHubIssueCommentService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses custom template with placeholder substitution", async () => {
|
||||
it("uses custom template with placeholder substitution for non-self-repo", async () => {
|
||||
store.setSettings({
|
||||
githubCommentOnDone: true,
|
||||
githubCommentTemplate: "Task {taskId}: {taskTitle} complete",
|
||||
@@ -177,6 +236,79 @@ describe("GitHubIssueCommentService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("appends current + target release version lines for the Fusion self-repo", async () => {
|
||||
store.emit("task:moved", {
|
||||
task: createTask({
|
||||
sourceIssue: { provider: "github", repository: "runfusion/fusion", issueNumber: 42 },
|
||||
}),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledWith(
|
||||
"runfusion",
|
||||
"fusion",
|
||||
42,
|
||||
"✅ Task FN-2623 (Imported task) has been completed and resolved.\n\nCurrent version: v0.55.0\nTarget release: v0.56.0",
|
||||
);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2623",
|
||||
"Posted GitHub issue completion comment",
|
||||
"runfusion/fusion#42",
|
||||
);
|
||||
});
|
||||
|
||||
it("appends release version lines for a case-insensitive self-repo match", async () => {
|
||||
store.emit("task:moved", {
|
||||
task: createTask({
|
||||
sourceIssue: { provider: "github", repository: "Runfusion/Fusion", issueNumber: 42 },
|
||||
}),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledWith(
|
||||
"Runfusion",
|
||||
"Fusion",
|
||||
42,
|
||||
"✅ Task FN-2623 (Imported task) has been completed and resolved.\n\nCurrent version: v0.55.0\nTarget release: v0.56.0",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the base comment with no version lines when the version is unresolved (0.0.0 sentinel)", async () => {
|
||||
const unresolvedService = new GitHubIssueCommentService(
|
||||
store as unknown as TaskStore,
|
||||
() => "ghp_test",
|
||||
() => "0.0.0",
|
||||
);
|
||||
unresolvedService.start();
|
||||
|
||||
store.emit("task:moved", {
|
||||
task: createTask({
|
||||
sourceIssue: { provider: "github", repository: "runfusion/fusion", issueNumber: 42 },
|
||||
}),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledWith(
|
||||
"runfusion",
|
||||
"fusion",
|
||||
42,
|
||||
"✅ Task FN-2623 (Imported task) has been completed and resolved.",
|
||||
);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2623",
|
||||
"Posted GitHub issue completion comment",
|
||||
"runfusion/fusion#42",
|
||||
);
|
||||
|
||||
unresolvedService.stop();
|
||||
});
|
||||
|
||||
it("stop unregisters listener", async () => {
|
||||
service.stop();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { GitHubClient } from "./github.js";
|
||||
import { getCliPackageVersion, isUnresolvedCliPackageVersion } from "./cli-package-version.js";
|
||||
|
||||
interface TaskMovedEvent {
|
||||
task: {
|
||||
@@ -16,17 +17,70 @@ interface TaskMovedEvent {
|
||||
|
||||
const DEFAULT_COMMENT_TEMPLATE = "✅ Task {taskId} ({taskTitle}) has been completed and resolved.";
|
||||
|
||||
/*
|
||||
* FNXC:GitHubIssueComment 2026-07-05-01:30:
|
||||
* Requirement: when a Fusion task's linked source GitHub issue lives in the
|
||||
* Fusion self-repo (`runfusion/fusion`, case-insensitive), the completion
|
||||
* comment posted on `done` must ALSO include both a "Current version:" line
|
||||
* and a "Target release:" line (the next-minor bump of the currently
|
||||
* published `@runfusion/fusion` version), so readers know which Fusion
|
||||
* release ships the fix. Every other linked repository's completion comment
|
||||
* must remain byte-for-byte identical to the pre-FN-7575 template output.
|
||||
* If the resolved version is unparseable/unresolved (the `0.0.0` sentinel),
|
||||
* fall back silently to the base comment with no version lines — never throw.
|
||||
*/
|
||||
const FUSION_SELF_REPO = "runfusion/fusion";
|
||||
|
||||
/** Case-insensitive, trimmed `owner/repo` slug comparison against the Fusion self-repo. */
|
||||
function isFusionSelfRepo(repository: string): boolean {
|
||||
return repository.trim().toLowerCase() === FUSION_SELF_REPO;
|
||||
}
|
||||
|
||||
/** `major.minor.patch` leading numeric semver shape; ignores any trailing prerelease/build metadata. */
|
||||
const SEMVER_PREFIX_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)/;
|
||||
|
||||
/**
|
||||
* Compute the next-minor release version (patch reset to 0) from a semver string,
|
||||
* e.g. `"0.55.0"` -> `"0.56.0"`, `"1.2.9"` -> `"1.3.0"`, `"v0.55.0"` -> `"0.56.0"`.
|
||||
* Returns `null` for the unresolved `"0.0.0"` sentinel or any unparseable input so
|
||||
* callers can skip appending version lines rather than emit garbage.
|
||||
*/
|
||||
function computeNextMinorVersion(current: string): string | null {
|
||||
if (isUnresolvedCliPackageVersion(current)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = SEMVER_PREFIX_PATTERN.exec(current.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const major = Number.parseInt(match[1] ?? "", 10);
|
||||
const minor = Number.parseInt(match[2] ?? "", 10);
|
||||
if (!Number.isFinite(major) || !Number.isFinite(minor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${major}.${minor + 1}.0`;
|
||||
}
|
||||
|
||||
export class GitHubIssueCommentService {
|
||||
private readonly store: TaskStore;
|
||||
private readonly getGitHubToken: () => string | undefined;
|
||||
private readonly getCurrentVersion: () => string;
|
||||
private readonly onTaskMoved = (event: TaskMovedEvent): void => {
|
||||
void this.handleTaskMoved(event);
|
||||
};
|
||||
private started = false;
|
||||
|
||||
constructor(store: TaskStore, getGitHubToken?: () => string | undefined) {
|
||||
constructor(
|
||||
store: TaskStore,
|
||||
getGitHubToken?: () => string | undefined,
|
||||
getCurrentVersion?: () => string,
|
||||
) {
|
||||
this.store = store;
|
||||
this.getGitHubToken = getGitHubToken ?? (() => process.env.GITHUB_TOKEN);
|
||||
this.getCurrentVersion = getCurrentVersion ?? (() => getCliPackageVersion(import.meta.url));
|
||||
}
|
||||
|
||||
start(): void {
|
||||
@@ -68,10 +122,19 @@ export class GitHubIssueCommentService {
|
||||
}
|
||||
|
||||
const template = settings.githubCommentTemplate || DEFAULT_COMMENT_TEMPLATE;
|
||||
const commentBody = template
|
||||
let commentBody = template
|
||||
.replaceAll("{taskId}", task.id)
|
||||
.replaceAll("{taskTitle}", task.title ?? "");
|
||||
|
||||
if (isFusionSelfRepo(sourceIssue.repository)) {
|
||||
const currentVersion = this.getCurrentVersion();
|
||||
const nextMinorVersion = computeNextMinorVersion(currentVersion);
|
||||
if (nextMinorVersion) {
|
||||
const currentLine = currentVersion.startsWith("v") ? currentVersion : `v${currentVersion}`;
|
||||
commentBody += `\n\nCurrent version: ${currentLine}\nTarget release: v${nextMinorVersion}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new GitHubClient(this.getGitHubToken());
|
||||
await client.commentOnIssue(owner, repo, sourceIssue.issueNumber, commentBody);
|
||||
@@ -91,4 +154,4 @@ export class GitHubIssueCommentService {
|
||||
}
|
||||
}
|
||||
|
||||
export { DEFAULT_COMMENT_TEMPLATE };
|
||||
export { DEFAULT_COMMENT_TEMPLATE, FUSION_SELF_REPO, isFusionSelfRepo, computeNextMinorVersion };
|
||||
|
||||
Reference in New Issue
Block a user