FN-8227: link closed tracked issues to landing commits
Closed GitHub tracked issues now reliably include their landing commit link. - Re-read the authoritative task row before posting a Done tracking comment. - Preserve event-snapshot commit links when the task lookup fails or is absent. - Cover stale events, unavailable rows, no-op landings, and non-Done transitions. Files changed: .changeset/fn-8227-tracked-issue-commit-link.md | 7 ++ .../src/__tests__/github-tracking-comments.test.ts | 81 ++++++++++++++++++++++ packages/dashboard/src/github-tracking-comments.ts | 14 +++- 3 files changed, 100 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8227 Fusion-Task-Lineage: f497dbb8-da5c-4409-a8fd-836d6df573da Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8227-tracked-issue-commit-link.md
Normal file
7
.changeset/fn-8227-tracked-issue-commit-link.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Closed GitHub tracked issues now reliably link the landing commit.
|
||||
category: fix
|
||||
dev: GitHubTrackingCommentService re-reads the authoritative task via store.getTask before building the Done comment, so mergeDetails.commitSha present at closure time is linked even when the task:moved snapshot omitted it (autoMerge:false PR merges, no-op landings, recovery finalization). Falls back to the event snapshot on refetch failure.
|
||||
@@ -41,12 +41,15 @@ vi.mock("../cli-package-version.js", async (importOriginal) => ({
|
||||
|
||||
class MockStore extends EventEmitter {
|
||||
logEntry: Mock;
|
||||
getTask: Mock;
|
||||
getSettings: Mock;
|
||||
getGlobalSettingsStore: Mock;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logEntry = vi.fn().mockResolvedValue(undefined);
|
||||
// Null preserves the event snapshot unless a test supplies a newer authoritative row.
|
||||
this.getTask = vi.fn().mockResolvedValue(null);
|
||||
this.getSettings = vi.fn().mockResolvedValue({ githubAuthMode: "token", githubAuthToken: "ghp_test" });
|
||||
this.getGlobalSettingsStore = vi.fn(() => ({ getSettings: vi.fn().mockResolvedValue({}) }));
|
||||
}
|
||||
@@ -503,6 +506,84 @@ describe("GitHubTrackingCommentService", () => {
|
||||
expect(body).not.toContain("Current version");
|
||||
});
|
||||
|
||||
it("recovers a landing commit from the authoritative row when the done event is stale", async () => {
|
||||
service.start();
|
||||
const snapshot = createTask({ mergeDetails: { prNumber: 7 } });
|
||||
store.getTask.mockResolvedValueOnce(createTask({
|
||||
mergeDetails: { commitSha: "abcdef1234567890", prNumber: 7 },
|
||||
}));
|
||||
|
||||
store.emit("task:moved", { task: snapshot, from: "in-progress", to: "done" });
|
||||
await flushAsync();
|
||||
|
||||
expect(store.getTask).toHaveBeenCalledWith("FN-1");
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledTimes(1);
|
||||
expect(mockCommentOnIssue.mock.calls[0]?.[3]).toContain(
|
||||
"Commit: [abcdef1](https://github.com/owner/repo/commit/abcdef1234567890)",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an event-snapshot commit link when the authoritative row is unavailable", async () => {
|
||||
service.start();
|
||||
store.getTask.mockRejectedValueOnce(new Error("store unavailable"));
|
||||
|
||||
store.emit("task:moved", {
|
||||
task: createTask({ mergeDetails: { commitSha: "abcdef1234567890" } }),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledTimes(1);
|
||||
expect(mockCommentOnIssue.mock.calls[0]?.[3]).toContain(
|
||||
"Commit: [abcdef1](https://github.com/owner/repo/commit/abcdef1234567890)",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an event-snapshot commit link when the authoritative row is missing", async () => {
|
||||
service.start();
|
||||
store.getTask.mockResolvedValueOnce(null);
|
||||
|
||||
store.emit("task:moved", {
|
||||
task: createTask({ mergeDetails: { commitSha: "abcdef1234567890" } }),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledTimes(1);
|
||||
expect(mockCommentOnIssue.mock.calls[0]?.[3]).toContain(
|
||||
"Commit: [abcdef1](https://github.com/owner/repo/commit/abcdef1234567890)",
|
||||
);
|
||||
});
|
||||
|
||||
it("posts a no-op landing without a Commit line", async () => {
|
||||
service.start();
|
||||
store.getTask.mockResolvedValueOnce(createTask({ mergeDetails: { noOpMerge: true } }));
|
||||
|
||||
store.emit("task:moved", {
|
||||
task: createTask({ mergeDetails: { noOpMerge: true } }),
|
||||
from: "in-progress",
|
||||
to: "done",
|
||||
});
|
||||
await flushAsync();
|
||||
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledTimes(1);
|
||||
expect(mockCommentOnIssue.mock.calls[0]?.[3]).not.toContain("Commit:");
|
||||
});
|
||||
|
||||
it("does not refetch or duplicate a comment for in-progress and same-column transitions", async () => {
|
||||
service.start();
|
||||
|
||||
store.emit("task:moved", { task: createTask(), from: "todo", to: "in-progress" });
|
||||
store.emit("task:moved", { task: createTask(), from: "done", to: "done" });
|
||||
await flushAsync();
|
||||
|
||||
expect(store.getTask).not.toHaveBeenCalled();
|
||||
expect(mockCommentOnIssue).toHaveBeenCalledTimes(1);
|
||||
expect(mockCommentOnIssue.mock.calls[0]?.[3]).toContain("🚧 In progress");
|
||||
});
|
||||
|
||||
it("writes success logs", async () => {
|
||||
service.start();
|
||||
|
||||
|
||||
@@ -248,9 +248,19 @@ export class GitHubTrackingCommentService {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* FNXC:GitHubTrackingComments 2026-07-16-12:40:
|
||||
* A closed tracked issue must link its landing commit when one exists. The task:moved snapshot
|
||||
* can predate mergeDetails persistence on human PR, no-op, and recovery done paths, so re-read
|
||||
* the authoritative row before building the Done comment. Fall back to the snapshot when the
|
||||
* read fails so the comment is never dropped.
|
||||
*/
|
||||
const taskForComment = event.to === "done"
|
||||
? await this.store.getTask(event.task.id).catch(() => null) ?? event.task
|
||||
: event.task;
|
||||
const body = event.to === "done"
|
||||
? formatTrackingComment(event.task, event.to, { owner, repo })
|
||||
: formatTrackingComment(event.task, event.to);
|
||||
? formatTrackingComment(taskForComment, event.to, { owner, repo })
|
||||
: formatTrackingComment(taskForComment, event.to);
|
||||
|
||||
try {
|
||||
const projectSettings = await this.store.getSettings() as Pick<ProjectSettings, "githubAuthMode" | "githubAuthToken">;
|
||||
|
||||
Reference in New Issue
Block a user