From 4411b564d4e11c9adb11162654c5d7dfbd9f55d2 Mon Sep 17 00:00:00 2001 From: stain lu <109842185+stainlu@users.noreply.github.com> Date: Thu, 7 May 2026 06:49:21 +0800 Subject: [PATCH] fix: link repair targets to source repo Resolve repair dashboard shorthand targets through the source repository, preserving explicit pull request URLs and inferring PR links for repair/merge actions.\n\nThanks @stainlu. --- scripts/repair-dashboard.mjs | 32 +++++++++++++++-- test/repair-dashboard.test.mjs | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 test/repair-dashboard.test.mjs diff --git a/scripts/repair-dashboard.mjs b/scripts/repair-dashboard.mjs index 1b25874af6..ae5851ca8b 100644 --- a/scripts/repair-dashboard.mjs +++ b/scripts/repair-dashboard.mjs @@ -194,23 +194,49 @@ function runLink(record) { return record.run_url ? link(record.run_id ?? "run", record.run_url) : "_none_"; } -function targetLink(action) { +function targetLink(record, action) { const target = String(action.target ?? ""); const match = target.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/(issues|pull)\/(\d+)/); if (match) return link(`#${match[3]}`, target); + const shorthand = target.match(/^#(\d+)$/); + const repo = String(record.repo ?? ""); + if (shorthand && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) { + const explicitUrl = githubItemUrlForNumber(action.url, shorthand[1]); + if (explicitUrl) return link(target, explicitUrl); + const segment = repairActionTargetsPullRequest(action) ? "pull" : "issues"; + return link(target, `https://github.com/${repo}/${segment}/${shorthand[1]}`); + } return target ? link(target, target) : ""; } +function githubItemUrlForNumber(value, number) { + const url = String(value ?? ""); + const match = url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/(?:issues|pull)\/(\d+)$/); + return match?.[1] === number ? url : ""; +} + +function repairActionTargetsPullRequest(action) { + const actionName = String(action.action ?? ""); + const classification = String(action.classification ?? ""); + return ( + actionName.startsWith("merge_") || + actionName.includes("automerge") || + actionName.includes("repair_contributor_branch") || + classification === "canonical" || + classification === "fix_pr" + ); +} + function inspectionRow(row) { return `| ${clusterLink(row.record)} | ${tableCell(row.state)} | ${truncate(row.reason, 150)} | ${clusterLink(row.record)} | ${runLink(row.record)} |`; } function fixRow(row) { const action = row.action; - return `| ${clusterLink(row.record)} | ${tableCell(action.status)} | ${targetLink(action)} | ${tableCell(action.branch ?? action.pr ?? "")} | ${truncate(action.reason, 150)} | ${runLink(row.record)} |`; + return `| ${clusterLink(row.record)} | ${tableCell(action.status)} | ${targetLink(row.record, action)} | ${tableCell(action.branch ?? action.pr ?? "")} | ${truncate(action.reason, 150)} | ${runLink(row.record)} |`; } function closeRow(row) { const action = row.action; - return `| ${targetLink(action)} | ${tableCell(action.action)} | ${truncate(action.title ?? "")} | ${formatTimestamp(action.closed_at ?? action.merged_at ?? row.record.published_at)} | ${clusterLink(row.record)} | ${clusterLink(row.record)} | ${runLink(row.record)} |`; + return `| ${targetLink(row.record, action)} | ${tableCell(action.action)} | ${truncate(action.title ?? "")} | ${formatTimestamp(action.closed_at ?? action.merged_at ?? row.record.published_at)} | ${clusterLink(row.record)} | ${clusterLink(row.record)} | ${runLink(row.record)} |`; } diff --git a/test/repair-dashboard.test.mjs b/test/repair-dashboard.test.mjs new file mode 100644 index 0000000000..fcfb4354f6 --- /dev/null +++ b/test/repair-dashboard.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { renderRepairDashboard } from "../scripts/repair-dashboard.mjs"; + +test("repair dashboard links shorthand targets through the source repo", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawsweeper-state-repair-")); + const runsDir = path.join(root, "results", "runs"); + fs.mkdirSync(runsDir, { recursive: true }); + fs.writeFileSync( + path.join(runsDir, "run.json"), + JSON.stringify({ + repo: "openclaw/openclaw", + cluster_id: "repair-target-links", + run_id: "12345", + run_url: "https://github.com/openclaw/clawsweeper/actions/runs/12345", + workflow_conclusion: "success", + published_at: "2026-05-02T19:00:00.000Z", + fix_actions: [ + { + target: "#789", + status: "blocked", + reason: "needs inspection", + }, + { + target: "#901", + action: "repair_contributor_branch", + status: "blocked", + reason: "needs branch repair", + url: "https://github.com/openclaw/openclaw/pull/901", + }, + ], + apply_actions: [ + { + target: "#123", + action: "close_duplicate", + status: "executed", + title: "duplicate report", + closed_at: "2026-05-02T19:01:00.000Z", + }, + { + target: "https://github.com/openclaw/openclaw/pull/456", + action: "close_superseded", + status: "executed", + title: "superseded PR", + closed_at: "2026-05-02T19:00:00.000Z", + }, + ], + }), + "utf8", + ); + + const dashboard = renderRepairDashboard(root); + + assert.match(dashboard, /\[#123\]\(https:\/\/github\.com\/openclaw\/openclaw\/issues\/123\)/); + assert.match(dashboard, /\[#789\]\(https:\/\/github\.com\/openclaw\/openclaw\/issues\/789\)/); + assert.match(dashboard, /\[#456\]\(https:\/\/github\.com\/openclaw\/openclaw\/pull\/456\)/); + assert.match(dashboard, /\[#901\]\(https:\/\/github\.com\/openclaw\/openclaw\/pull\/901\)/); + assert.doesNotMatch(dashboard, /\[#123\]\(#123\)/); + assert.doesNotMatch(dashboard, /\[#789\]\(#789\)/); +});