@probelabs/visor 0.1.181 → 0.1.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults/code-talk.yaml +80 -14
- package/defaults/engineer.yaml +33 -15
- package/defaults/skills/code-explorer.yaml +5 -0
- package/dist/agent-protocol/a2a-frontend.d.ts +10 -0
- package/dist/agent-protocol/a2a-frontend.d.ts.map +1 -1
- package/dist/agent-protocol/task-evaluator.d.ts +52 -0
- package/dist/agent-protocol/task-evaluator.d.ts.map +1 -0
- package/dist/agent-protocol/task-store.d.ts +5 -3
- package/dist/agent-protocol/task-store.d.ts.map +1 -1
- package/dist/agent-protocol/tasks-cli-handler.d.ts.map +1 -1
- package/dist/agent-protocol/tasks-tui.d.ts +34 -0
- package/dist/agent-protocol/tasks-tui.d.ts.map +1 -0
- package/dist/agent-protocol/trace-serializer.d.ts +90 -0
- package/dist/agent-protocol/trace-serializer.d.ts.map +1 -0
- package/dist/agent-protocol/track-execution.d.ts +2 -0
- package/dist/agent-protocol/track-execution.d.ts.map +1 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/defaults/code-talk.yaml +80 -14
- package/dist/defaults/engineer.yaml +33 -15
- package/dist/defaults/skills/code-explorer.yaml +5 -0
- package/dist/docs/commands.md +57 -14
- package/dist/docs/configuration.md +2 -0
- package/dist/docs/guides/graceful-restart.md +178 -0
- package/dist/docs/observability.md +69 -0
- package/dist/docs/production-deployment.md +17 -0
- package/dist/email/polling-runner.d.ts +4 -0
- package/dist/email/polling-runner.d.ts.map +1 -1
- package/dist/generated/config-schema.d.ts +70 -6
- package/dist/generated/config-schema.d.ts.map +1 -1
- package/dist/generated/config-schema.json +73 -6
- package/dist/index.js +5006 -886
- package/dist/output/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
- package/dist/{traces/run-2026-03-17T13-59-10-403Z.ndjson → output/traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
- package/dist/providers/mcp-custom-sse-server.d.ts +4 -0
- package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
- package/dist/runners/graceful-restart.d.ts +46 -0
- package/dist/runners/graceful-restart.d.ts.map +1 -0
- package/dist/runners/mcp-server-runner.d.ts +12 -0
- package/dist/runners/mcp-server-runner.d.ts.map +1 -1
- package/dist/runners/runner-factory.d.ts.map +1 -1
- package/dist/runners/runner-host.d.ts +12 -0
- package/dist/runners/runner-host.d.ts.map +1 -1
- package/dist/runners/runner.d.ts +12 -0
- package/dist/runners/runner.d.ts.map +1 -1
- package/dist/sdk/{a2a-frontend-IWOUJOIZ.mjs → a2a-frontend-4LP3MLTS.mjs} +47 -5
- package/dist/sdk/a2a-frontend-4LP3MLTS.mjs.map +1 -0
- package/dist/sdk/a2a-frontend-5J3UNFY4.mjs +1718 -0
- package/dist/sdk/a2a-frontend-5J3UNFY4.mjs.map +1 -0
- package/dist/sdk/{a2a-frontend-BDACLGMA.mjs → a2a-frontend-MU5EO2HZ.mjs} +35 -1
- package/dist/sdk/a2a-frontend-MU5EO2HZ.mjs.map +1 -0
- package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs → check-provider-registry-MHXQGUNN.mjs} +7 -7
- package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs → check-provider-registry-RRWCXSTG.mjs} +3 -3
- package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs → check-provider-registry-Y33CRFVD.mjs} +7 -7
- package/dist/sdk/{chunk-DGIH6EX3.mjs → chunk-4AXAVXG5.mjs} +151 -281
- package/dist/sdk/chunk-4AXAVXG5.mjs.map +1 -0
- package/dist/sdk/{chunk-VMVIM4JB.mjs → chunk-4I3TJ7UJ.mjs} +37 -7
- package/dist/sdk/chunk-4I3TJ7UJ.mjs.map +1 -0
- package/dist/sdk/{chunk-VXC2XNQJ.mjs → chunk-5J3DNRF7.mjs} +3 -3
- package/dist/sdk/{chunk-7YZSSO4X.mjs → chunk-6DPPP7LD.mjs} +10 -10
- package/dist/sdk/chunk-7ERVRLDV.mjs +296 -0
- package/dist/sdk/chunk-7ERVRLDV.mjs.map +1 -0
- package/dist/sdk/{chunk-4DVP6KVC.mjs → chunk-7Z2WHX2J.mjs} +71 -30
- package/dist/sdk/chunk-7Z2WHX2J.mjs.map +1 -0
- package/dist/sdk/chunk-ANUT54HW.mjs +1502 -0
- package/dist/sdk/chunk-ANUT54HW.mjs.map +1 -0
- package/dist/sdk/{chunk-J73GEFPT.mjs → chunk-DHETLQIX.mjs} +2 -2
- package/dist/sdk/{chunk-QGBASDYP.mjs → chunk-JCOSKBMP.mjs} +71 -30
- package/dist/sdk/chunk-JCOSKBMP.mjs.map +1 -0
- package/dist/sdk/chunk-MK7ONH47.mjs +739 -0
- package/dist/sdk/chunk-MK7ONH47.mjs.map +1 -0
- package/dist/sdk/chunk-QXT47ZHR.mjs +390 -0
- package/dist/sdk/chunk-QXT47ZHR.mjs.map +1 -0
- package/dist/sdk/chunk-V75NEIXL.mjs +296 -0
- package/dist/sdk/chunk-V75NEIXL.mjs.map +1 -0
- package/dist/sdk/chunk-ZOF5QT6U.mjs +5943 -0
- package/dist/sdk/chunk-ZOF5QT6U.mjs.map +1 -0
- package/dist/sdk/{config-TSA5FUOM.mjs → config-2STD74CJ.mjs} +2 -2
- package/dist/sdk/config-JE4HKTWW.mjs +16 -0
- package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs → failure-condition-evaluator-5DZYMCGW.mjs} +4 -4
- package/dist/sdk/failure-condition-evaluator-R6DCDJAV.mjs +18 -0
- package/dist/sdk/{github-frontend-3SDFCCKI.mjs → github-frontend-3PSCKPAJ.mjs} +4 -4
- package/dist/sdk/github-frontend-L3F5JXPJ.mjs +1394 -0
- package/dist/sdk/github-frontend-L3F5JXPJ.mjs.map +1 -0
- package/dist/sdk/{host-QE4L7UXE.mjs → host-54CHV2LW.mjs} +3 -3
- package/dist/sdk/{host-VBBSLUWG.mjs → host-WAU6CT42.mjs} +3 -3
- package/dist/sdk/{host-CVH2CSHM.mjs → host-X5ZZCEWN.mjs} +2 -2
- package/dist/sdk/{routing-YVMTKFDZ.mjs → routing-CVQT4KHX.mjs} +5 -5
- package/dist/sdk/routing-EBAE5SSO.mjs +26 -0
- package/dist/sdk/{schedule-tool-Z5VG67JK.mjs → schedule-tool-POY3CDZL.mjs} +7 -7
- package/dist/sdk/{schedule-tool-ADUXTCY7.mjs → schedule-tool-R2OAATUS.mjs} +7 -7
- package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs → schedule-tool-Z6QYL2B3.mjs} +3 -3
- package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs → schedule-tool-handler-J4NUETJ6.mjs} +3 -3
- package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs → schedule-tool-handler-JMAKHPI7.mjs} +7 -7
- package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs → schedule-tool-handler-MWFUIQKR.mjs} +7 -7
- package/dist/sdk/sdk.d.mts +33 -0
- package/dist/sdk/sdk.d.ts +33 -0
- package/dist/sdk/sdk.js +2058 -342
- package/dist/sdk/sdk.js.map +1 -1
- package/dist/sdk/sdk.mjs +6 -6
- package/dist/sdk/task-evaluator-HLNXKKVV.mjs +1278 -0
- package/dist/sdk/task-evaluator-HLNXKKVV.mjs.map +1 -0
- package/dist/sdk/{trace-helpers-KXDOJWBL.mjs → trace-helpers-HL5FBX65.mjs} +3 -3
- package/dist/sdk/trace-helpers-WJXYVV4S.mjs +29 -0
- package/dist/sdk/trace-helpers-WJXYVV4S.mjs.map +1 -0
- package/dist/sdk/trace-reader-ZY77OFNM.mjs +266 -0
- package/dist/sdk/trace-reader-ZY77OFNM.mjs.map +1 -0
- package/dist/sdk/track-execution-MKIQXP2C.mjs +136 -0
- package/dist/sdk/track-execution-MKIQXP2C.mjs.map +1 -0
- package/dist/sdk/track-execution-YUXQ6WQH.mjs +136 -0
- package/dist/sdk/track-execution-YUXQ6WQH.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs → workflow-check-provider-SE5I7EMA.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-SE5I7EMA.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs → workflow-check-provider-VKYGI5GK.mjs} +3 -3
- package/dist/sdk/workflow-check-provider-VKYGI5GK.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs → workflow-check-provider-YDGZRI3Z.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-YDGZRI3Z.mjs.map +1 -0
- package/dist/slack/socket-runner.d.ts +12 -0
- package/dist/slack/socket-runner.d.ts.map +1 -1
- package/dist/teams/webhook-runner.d.ts +4 -0
- package/dist/teams/webhook-runner.d.ts.map +1 -1
- package/dist/telegram/polling-runner.d.ts +2 -0
- package/dist/telegram/polling-runner.d.ts.map +1 -1
- package/dist/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
- package/dist/{output/traces/run-2026-03-17T13-59-10-403Z.ndjson → traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/whatsapp/webhook-runner.d.ts +4 -0
- package/dist/whatsapp/webhook-runner.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/sdk/a2a-frontend-BDACLGMA.mjs.map +0 -1
- package/dist/sdk/a2a-frontend-IWOUJOIZ.mjs.map +0 -1
- package/dist/sdk/chunk-4DVP6KVC.mjs.map +0 -1
- package/dist/sdk/chunk-DGIH6EX3.mjs.map +0 -1
- package/dist/sdk/chunk-QGBASDYP.mjs.map +0 -1
- package/dist/sdk/chunk-VMVIM4JB.mjs.map +0 -1
- /package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs.map → check-provider-registry-MHXQGUNN.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs.map → check-provider-registry-RRWCXSTG.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs.map → check-provider-registry-Y33CRFVD.mjs.map} +0 -0
- /package/dist/sdk/{chunk-VXC2XNQJ.mjs.map → chunk-5J3DNRF7.mjs.map} +0 -0
- /package/dist/sdk/{chunk-7YZSSO4X.mjs.map → chunk-6DPPP7LD.mjs.map} +0 -0
- /package/dist/sdk/{chunk-J73GEFPT.mjs.map → chunk-DHETLQIX.mjs.map} +0 -0
- /package/dist/sdk/{config-TSA5FUOM.mjs.map → config-2STD74CJ.mjs.map} +0 -0
- /package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs.map → config-JE4HKTWW.mjs.map} +0 -0
- /package/dist/sdk/{routing-YVMTKFDZ.mjs.map → failure-condition-evaluator-5DZYMCGW.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-ADUXTCY7.mjs.map → failure-condition-evaluator-R6DCDJAV.mjs.map} +0 -0
- /package/dist/sdk/{github-frontend-3SDFCCKI.mjs.map → github-frontend-3PSCKPAJ.mjs.map} +0 -0
- /package/dist/sdk/{host-CVH2CSHM.mjs.map → host-54CHV2LW.mjs.map} +0 -0
- /package/dist/sdk/{host-QE4L7UXE.mjs.map → host-WAU6CT42.mjs.map} +0 -0
- /package/dist/sdk/{host-VBBSLUWG.mjs.map → host-X5ZZCEWN.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-Z5VG67JK.mjs.map → routing-CVQT4KHX.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs.map → routing-EBAE5SSO.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs.map → schedule-tool-POY3CDZL.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs.map → schedule-tool-R2OAATUS.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs.map → schedule-tool-Z6QYL2B3.mjs.map} +0 -0
- /package/dist/sdk/{trace-helpers-KXDOJWBL.mjs.map → schedule-tool-handler-J4NUETJ6.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs.map → schedule-tool-handler-JMAKHPI7.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs.map → schedule-tool-handler-MWFUIQKR.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs.map → trace-helpers-HL5FBX65.mjs.map} +0 -0
|
@@ -0,0 +1,1394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractTextFromJson,
|
|
3
|
+
init_json_text_extractor
|
|
4
|
+
} from "./chunk-H5BOW5CR.mjs";
|
|
5
|
+
import {
|
|
6
|
+
failure_condition_evaluator_exports,
|
|
7
|
+
init_failure_condition_evaluator
|
|
8
|
+
} from "./chunk-DHETLQIX.mjs";
|
|
9
|
+
import "./chunk-7ERVRLDV.mjs";
|
|
10
|
+
import {
|
|
11
|
+
generateShortHumanId,
|
|
12
|
+
init_human_id
|
|
13
|
+
} from "./chunk-QXT47ZHR.mjs";
|
|
14
|
+
import "./chunk-34QX63WK.mjs";
|
|
15
|
+
import "./chunk-25IC7KXZ.mjs";
|
|
16
|
+
import "./chunk-LW3INISN.mjs";
|
|
17
|
+
import "./chunk-UFHOIB3R.mjs";
|
|
18
|
+
import {
|
|
19
|
+
init_logger,
|
|
20
|
+
logger
|
|
21
|
+
} from "./chunk-FT3I25QV.mjs";
|
|
22
|
+
import "./chunk-UCMJJ3IM.mjs";
|
|
23
|
+
import {
|
|
24
|
+
__esm,
|
|
25
|
+
__export,
|
|
26
|
+
__toCommonJS
|
|
27
|
+
} from "./chunk-J7LXIPZS.mjs";
|
|
28
|
+
|
|
29
|
+
// src/footer.ts
|
|
30
|
+
function generateFooter(options = {}) {
|
|
31
|
+
const { includeMetadata, includeSeparator = true } = options;
|
|
32
|
+
const parts = [];
|
|
33
|
+
if (includeSeparator) {
|
|
34
|
+
parts.push("---");
|
|
35
|
+
parts.push("");
|
|
36
|
+
}
|
|
37
|
+
parts.push(
|
|
38
|
+
"*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*"
|
|
39
|
+
);
|
|
40
|
+
if (includeMetadata) {
|
|
41
|
+
const { lastUpdated, triggeredBy, commitSha } = includeMetadata;
|
|
42
|
+
const commitInfo = commitSha ? ` | Commit: ${commitSha.substring(0, 7)}` : "";
|
|
43
|
+
parts.push("");
|
|
44
|
+
parts.push(`*Last updated: ${lastUpdated} | Triggered by: ${triggeredBy}${commitInfo}*`);
|
|
45
|
+
}
|
|
46
|
+
parts.push("");
|
|
47
|
+
parts.push("\u{1F4A1} **TIP:** You can chat with Visor using `/visor ask <your question>`");
|
|
48
|
+
return parts.join("\n");
|
|
49
|
+
}
|
|
50
|
+
var init_footer = __esm({
|
|
51
|
+
"src/footer.ts"() {
|
|
52
|
+
"use strict";
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/github-check-service.ts
|
|
57
|
+
var github_check_service_exports = {};
|
|
58
|
+
__export(github_check_service_exports, {
|
|
59
|
+
GitHubCheckService: () => GitHubCheckService
|
|
60
|
+
});
|
|
61
|
+
var GitHubCheckService;
|
|
62
|
+
var init_github_check_service = __esm({
|
|
63
|
+
"src/github-check-service.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
init_footer();
|
|
66
|
+
GitHubCheckService = class {
|
|
67
|
+
octokit;
|
|
68
|
+
maxAnnotations = 50;
|
|
69
|
+
// GitHub API limit
|
|
70
|
+
constructor(octokit) {
|
|
71
|
+
this.octokit = octokit;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a new check run in queued status
|
|
75
|
+
* M4: Includes engine_mode metadata in summary
|
|
76
|
+
*/
|
|
77
|
+
async createCheckRun(options, summary) {
|
|
78
|
+
try {
|
|
79
|
+
const enhancedSummary = summary && options.engine_mode ? {
|
|
80
|
+
...summary,
|
|
81
|
+
summary: `${summary.summary}
|
|
82
|
+
|
|
83
|
+
_Engine: ${options.engine_mode}_`
|
|
84
|
+
} : summary;
|
|
85
|
+
const response = await this.octokit.rest.checks.create({
|
|
86
|
+
owner: options.owner,
|
|
87
|
+
repo: options.repo,
|
|
88
|
+
name: options.name,
|
|
89
|
+
head_sha: options.head_sha,
|
|
90
|
+
status: "queued",
|
|
91
|
+
details_url: options.details_url,
|
|
92
|
+
external_id: options.external_id,
|
|
93
|
+
output: enhancedSummary ? {
|
|
94
|
+
title: enhancedSummary.title,
|
|
95
|
+
summary: enhancedSummary.summary,
|
|
96
|
+
text: enhancedSummary.text
|
|
97
|
+
} : void 0
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
id: response.data.id,
|
|
101
|
+
url: response.data.html_url || ""
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Failed to create check run: ${error instanceof Error ? error.message : String(error)}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Update check run to in_progress status
|
|
111
|
+
*/
|
|
112
|
+
async updateCheckRunInProgress(owner, repo, check_run_id, summary) {
|
|
113
|
+
try {
|
|
114
|
+
await this.octokit.rest.checks.update({
|
|
115
|
+
owner,
|
|
116
|
+
repo,
|
|
117
|
+
check_run_id,
|
|
118
|
+
status: "in_progress",
|
|
119
|
+
output: summary ? {
|
|
120
|
+
title: summary.title,
|
|
121
|
+
summary: summary.summary,
|
|
122
|
+
text: summary.text
|
|
123
|
+
} : void 0
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Failed to update check run to in_progress: ${error instanceof Error ? error.message : String(error)}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Complete a check run with results based on failure conditions
|
|
133
|
+
*/
|
|
134
|
+
async completeCheckRun(owner, repo, check_run_id, checkName, failureResults, reviewIssues = [], executionError, filesChangedInCommit, prNumber, currentCommitSha) {
|
|
135
|
+
try {
|
|
136
|
+
if (prNumber && currentCommitSha) {
|
|
137
|
+
await this.clearOldAnnotations(
|
|
138
|
+
owner,
|
|
139
|
+
repo,
|
|
140
|
+
prNumber,
|
|
141
|
+
checkName,
|
|
142
|
+
currentCommitSha,
|
|
143
|
+
check_run_id
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const { conclusion, summary } = this.determineCheckRunConclusion(
|
|
147
|
+
checkName,
|
|
148
|
+
failureResults,
|
|
149
|
+
reviewIssues,
|
|
150
|
+
executionError
|
|
151
|
+
);
|
|
152
|
+
let filteredIssues = reviewIssues.filter(
|
|
153
|
+
(issue) => !(issue.file === "system" && issue.line === 0)
|
|
154
|
+
);
|
|
155
|
+
if (filesChangedInCommit && filesChangedInCommit.length > 0) {
|
|
156
|
+
filteredIssues = filteredIssues.filter(
|
|
157
|
+
(issue) => filesChangedInCommit.some((changedFile) => issue.file === changedFile)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const annotations = this.convertIssuesToAnnotations(filteredIssues);
|
|
161
|
+
await this.octokit.rest.checks.update({
|
|
162
|
+
owner,
|
|
163
|
+
repo,
|
|
164
|
+
check_run_id,
|
|
165
|
+
status: "completed",
|
|
166
|
+
conclusion,
|
|
167
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
168
|
+
output: {
|
|
169
|
+
title: summary.title,
|
|
170
|
+
summary: summary.summary,
|
|
171
|
+
text: summary.text,
|
|
172
|
+
annotations: annotations.slice(0, this.maxAnnotations)
|
|
173
|
+
// GitHub limit
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Failed to complete check run: ${error instanceof Error ? error.message : String(error)}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Determine check run conclusion based on failure conditions and issues
|
|
184
|
+
*/
|
|
185
|
+
determineCheckRunConclusion(checkName, failureResults, reviewIssues, executionError) {
|
|
186
|
+
if (executionError) {
|
|
187
|
+
return {
|
|
188
|
+
conclusion: "failure",
|
|
189
|
+
summary: {
|
|
190
|
+
title: "\u274C Check Execution Failed",
|
|
191
|
+
summary: `The ${checkName} check failed to execute properly.`,
|
|
192
|
+
text: `**Error:** ${executionError}
|
|
193
|
+
|
|
194
|
+
Please check your configuration and try again.`
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const failedConditions = failureResults.filter((result) => result.failed);
|
|
199
|
+
const criticalIssues = reviewIssues.filter((issue) => issue.severity === "critical").length;
|
|
200
|
+
const errorIssues = reviewIssues.filter((issue) => issue.severity === "error").length;
|
|
201
|
+
const warningIssues = reviewIssues.filter((issue) => issue.severity === "warning").length;
|
|
202
|
+
const totalIssues = reviewIssues.length;
|
|
203
|
+
let conclusion;
|
|
204
|
+
let title;
|
|
205
|
+
let summaryText;
|
|
206
|
+
let details;
|
|
207
|
+
if (failedConditions.length > 0) {
|
|
208
|
+
conclusion = "failure";
|
|
209
|
+
title = "\u{1F6A8} Check Failed";
|
|
210
|
+
summaryText = `${checkName} check failed because fail_if condition was met.`;
|
|
211
|
+
details = this.formatCheckDetails(failureResults, reviewIssues, {
|
|
212
|
+
failedConditions: failedConditions.length,
|
|
213
|
+
warningConditions: 0,
|
|
214
|
+
criticalIssues,
|
|
215
|
+
errorIssues,
|
|
216
|
+
warningIssues,
|
|
217
|
+
totalIssues
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
conclusion = "success";
|
|
221
|
+
if (criticalIssues > 0 || errorIssues > 0) {
|
|
222
|
+
title = "\u2705 Check Passed (Issues Found)";
|
|
223
|
+
summaryText = `${checkName} check passed. Found ${criticalIssues} critical and ${errorIssues} error issues, but fail_if condition was not met.`;
|
|
224
|
+
} else if (warningIssues > 0) {
|
|
225
|
+
title = "\u2705 Check Passed (Warnings Found)";
|
|
226
|
+
summaryText = `${checkName} check passed. Found ${warningIssues} warning${warningIssues === 1 ? "" : "s"}, but fail_if condition was not met.`;
|
|
227
|
+
} else {
|
|
228
|
+
title = "\u2705 Check Passed";
|
|
229
|
+
summaryText = `${checkName} check completed successfully with no issues found.`;
|
|
230
|
+
}
|
|
231
|
+
details = this.formatCheckDetails(failureResults, reviewIssues, {
|
|
232
|
+
failedConditions: 0,
|
|
233
|
+
warningConditions: 0,
|
|
234
|
+
criticalIssues,
|
|
235
|
+
errorIssues,
|
|
236
|
+
warningIssues,
|
|
237
|
+
totalIssues
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
conclusion,
|
|
242
|
+
summary: {
|
|
243
|
+
title,
|
|
244
|
+
summary: summaryText,
|
|
245
|
+
text: details
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Format detailed check results for the check run summary
|
|
251
|
+
*/
|
|
252
|
+
formatCheckDetails(failureResults, reviewIssues, counts) {
|
|
253
|
+
const sections = [];
|
|
254
|
+
sections.push("## \u{1F4CA} Summary");
|
|
255
|
+
sections.push(`- **Total Issues:** ${counts.totalIssues}`);
|
|
256
|
+
if (counts.criticalIssues > 0) {
|
|
257
|
+
sections.push(`- **Critical Issues:** ${counts.criticalIssues}`);
|
|
258
|
+
}
|
|
259
|
+
if (counts.errorIssues > 0) {
|
|
260
|
+
sections.push(`- **Error Issues:** ${counts.errorIssues}`);
|
|
261
|
+
}
|
|
262
|
+
if (counts.warningIssues > 0) {
|
|
263
|
+
sections.push(`- **Warning Issues:** ${counts.warningIssues}`);
|
|
264
|
+
}
|
|
265
|
+
sections.push("");
|
|
266
|
+
if (failureResults.length > 0) {
|
|
267
|
+
sections.push("## \u{1F50D} Failure Condition Results");
|
|
268
|
+
const failedConditions = failureResults.filter((result) => result.failed);
|
|
269
|
+
const passedConditions = failureResults.filter((result) => !result.failed);
|
|
270
|
+
if (failedConditions.length > 0) {
|
|
271
|
+
sections.push("### Failed Conditions");
|
|
272
|
+
failedConditions.forEach((condition) => {
|
|
273
|
+
sections.push(
|
|
274
|
+
`- **${condition.conditionName}**: ${condition.message || condition.expression}`
|
|
275
|
+
);
|
|
276
|
+
if (condition.severity) {
|
|
277
|
+
const icon = this.getSeverityEmoji(condition.severity);
|
|
278
|
+
sections.push(` - Severity: ${icon} ${condition.severity}`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
sections.push("");
|
|
282
|
+
}
|
|
283
|
+
if (passedConditions.length > 0) {
|
|
284
|
+
sections.push("### Passed Conditions");
|
|
285
|
+
passedConditions.forEach((condition) => {
|
|
286
|
+
sections.push(
|
|
287
|
+
`- **${condition.conditionName}**: ${condition.message || "Condition passed"}`
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
sections.push("");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (reviewIssues.length > 0) {
|
|
294
|
+
const issuesByCategory = this.groupIssuesByCategory(reviewIssues);
|
|
295
|
+
sections.push("## Issues by Category");
|
|
296
|
+
Object.entries(issuesByCategory).forEach(([category, issues]) => {
|
|
297
|
+
if (issues.length > 0) {
|
|
298
|
+
sections.push(
|
|
299
|
+
`### ${category.charAt(0).toUpperCase() + category.slice(1)} (${issues.length})`
|
|
300
|
+
);
|
|
301
|
+
const displayIssues = issues.slice(0, 5);
|
|
302
|
+
displayIssues.forEach((issue) => {
|
|
303
|
+
const severityIcon = this.getSeverityEmoji(issue.severity);
|
|
304
|
+
sections.push(`- ${severityIcon} **${issue.file}:${issue.line}** - ${issue.message}`);
|
|
305
|
+
});
|
|
306
|
+
if (issues.length > 5) {
|
|
307
|
+
sections.push(`- *...and ${issues.length - 5} more ${category} issues*`);
|
|
308
|
+
}
|
|
309
|
+
sections.push("");
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
sections.push("");
|
|
314
|
+
sections.push(generateFooter());
|
|
315
|
+
return sections.join("\n");
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Convert review issues to GitHub check run annotations
|
|
319
|
+
*/
|
|
320
|
+
convertIssuesToAnnotations(reviewIssues) {
|
|
321
|
+
return reviewIssues.slice(0, this.maxAnnotations).map((issue) => ({
|
|
322
|
+
path: issue.file,
|
|
323
|
+
start_line: issue.line,
|
|
324
|
+
end_line: issue.endLine || issue.line,
|
|
325
|
+
annotation_level: this.mapSeverityToAnnotationLevel(issue.severity),
|
|
326
|
+
message: issue.message,
|
|
327
|
+
title: `${issue.category} Issue`,
|
|
328
|
+
raw_details: issue.suggestion || void 0
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Map Visor issue severity to GitHub annotation level
|
|
333
|
+
*/
|
|
334
|
+
mapSeverityToAnnotationLevel(severity) {
|
|
335
|
+
switch (severity) {
|
|
336
|
+
case "critical":
|
|
337
|
+
case "error":
|
|
338
|
+
return "failure";
|
|
339
|
+
case "warning":
|
|
340
|
+
return "warning";
|
|
341
|
+
case "info":
|
|
342
|
+
default:
|
|
343
|
+
return "notice";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Group issues by category
|
|
348
|
+
*/
|
|
349
|
+
groupIssuesByCategory(issues) {
|
|
350
|
+
const grouped = {};
|
|
351
|
+
issues.forEach((issue) => {
|
|
352
|
+
const category = issue.category || "general";
|
|
353
|
+
if (!grouped[category]) {
|
|
354
|
+
grouped[category] = [];
|
|
355
|
+
}
|
|
356
|
+
grouped[category].push(issue);
|
|
357
|
+
});
|
|
358
|
+
return grouped;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get emoji for issue severity (allowed; step/category emojis are removed)
|
|
362
|
+
*/
|
|
363
|
+
getSeverityEmoji(severity) {
|
|
364
|
+
const iconMap = {
|
|
365
|
+
critical: "\u{1F6A8}",
|
|
366
|
+
error: "\u274C",
|
|
367
|
+
warning: "\u26A0\uFE0F",
|
|
368
|
+
info: "\u2139\uFE0F"
|
|
369
|
+
};
|
|
370
|
+
return iconMap[String(severity || "").toLowerCase()] || "";
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create multiple check runs for different checks with failure condition support
|
|
374
|
+
*/
|
|
375
|
+
async createMultipleCheckRuns(options, checkResults) {
|
|
376
|
+
const results = [];
|
|
377
|
+
for (const checkResult of checkResults) {
|
|
378
|
+
try {
|
|
379
|
+
const checkRun = await this.createCheckRun({
|
|
380
|
+
...options,
|
|
381
|
+
name: `Visor: ${checkResult.checkName}`,
|
|
382
|
+
external_id: `visor-${checkResult.checkName}-${options.head_sha.substring(0, 7)}`
|
|
383
|
+
});
|
|
384
|
+
await this.updateCheckRunInProgress(options.owner, options.repo, checkRun.id, {
|
|
385
|
+
title: `Running ${checkResult.checkName} check...`,
|
|
386
|
+
summary: `Analyzing code with ${checkResult.checkName} check using AI.`
|
|
387
|
+
});
|
|
388
|
+
await this.completeCheckRun(
|
|
389
|
+
options.owner,
|
|
390
|
+
options.repo,
|
|
391
|
+
checkRun.id,
|
|
392
|
+
checkResult.checkName,
|
|
393
|
+
checkResult.failureResults,
|
|
394
|
+
checkResult.reviewIssues,
|
|
395
|
+
checkResult.executionError
|
|
396
|
+
);
|
|
397
|
+
results.push({
|
|
398
|
+
checkName: checkResult.checkName,
|
|
399
|
+
id: checkRun.id,
|
|
400
|
+
url: checkRun.url
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(`Failed to create check run for ${checkResult.checkName}:`, error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get check runs for a specific commit
|
|
410
|
+
*/
|
|
411
|
+
async getCheckRuns(owner, repo, ref) {
|
|
412
|
+
try {
|
|
413
|
+
const response = await this.octokit.rest.checks.listForRef({
|
|
414
|
+
owner,
|
|
415
|
+
repo,
|
|
416
|
+
ref,
|
|
417
|
+
filter: "all"
|
|
418
|
+
});
|
|
419
|
+
return response.data.check_runs.filter((check) => check.name.startsWith("Visor:")).map((check) => ({
|
|
420
|
+
id: check.id,
|
|
421
|
+
name: check.name,
|
|
422
|
+
status: check.status,
|
|
423
|
+
conclusion: check.conclusion
|
|
424
|
+
}));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Failed to get check runs: ${error instanceof Error ? error.message : String(error)}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get check runs for a specific commit SHA
|
|
433
|
+
* Returns all check runs with the given name on this commit
|
|
434
|
+
*/
|
|
435
|
+
async getCheckRunsForCommit(owner, repo, commitSha, checkName) {
|
|
436
|
+
try {
|
|
437
|
+
const checksResponse = await this.octokit.rest.checks.listForRef({
|
|
438
|
+
owner,
|
|
439
|
+
repo,
|
|
440
|
+
ref: commitSha,
|
|
441
|
+
check_name: `Visor: ${checkName}`
|
|
442
|
+
});
|
|
443
|
+
return checksResponse.data.check_runs.map((check) => ({
|
|
444
|
+
id: check.id,
|
|
445
|
+
head_sha: commitSha
|
|
446
|
+
}));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Failed to get check runs for commit ${commitSha}: ${error instanceof Error ? error.message : String(error)}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Clear annotations from old check runs on the current commit
|
|
455
|
+
* This prevents annotation accumulation when a check runs multiple times on the same commit
|
|
456
|
+
* (e.g., force push, re-running checks)
|
|
457
|
+
*/
|
|
458
|
+
async clearOldAnnotations(owner, repo, prNumber, checkName, currentCommitSha, currentCheckRunId) {
|
|
459
|
+
try {
|
|
460
|
+
const allCheckRuns = await this.getCheckRunsForCommit(
|
|
461
|
+
owner,
|
|
462
|
+
repo,
|
|
463
|
+
currentCommitSha,
|
|
464
|
+
checkName
|
|
465
|
+
);
|
|
466
|
+
const oldRuns = allCheckRuns.filter((run) => run.id !== currentCheckRunId);
|
|
467
|
+
if (oldRuns.length === 0) {
|
|
468
|
+
console.debug(`No old check runs to clear for ${checkName} on commit ${currentCommitSha}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
console.debug(
|
|
472
|
+
`Clearing ${oldRuns.length} old check run(s) for ${checkName} on commit ${currentCommitSha.substring(0, 7)} (keeping current run ${currentCheckRunId})`
|
|
473
|
+
);
|
|
474
|
+
for (const run of oldRuns) {
|
|
475
|
+
try {
|
|
476
|
+
await this.octokit.rest.checks.update({
|
|
477
|
+
owner,
|
|
478
|
+
repo,
|
|
479
|
+
check_run_id: run.id,
|
|
480
|
+
output: {
|
|
481
|
+
title: "Outdated",
|
|
482
|
+
summary: "This check has been superseded by a newer run.",
|
|
483
|
+
annotations: []
|
|
484
|
+
// Clear annotations
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
console.debug(`\u2713 Cleared annotations from check run ${run.id}`);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.debug(`Could not clear annotations for check run ${run.id}:`, error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.warn("Failed to clear old annotations:", error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// src/github-comments.ts
|
|
501
|
+
var github_comments_exports = {};
|
|
502
|
+
__export(github_comments_exports, {
|
|
503
|
+
CommentManager: () => CommentManager
|
|
504
|
+
});
|
|
505
|
+
var CommentManager;
|
|
506
|
+
var init_github_comments = __esm({
|
|
507
|
+
"src/github-comments.ts"() {
|
|
508
|
+
"use strict";
|
|
509
|
+
init_human_id();
|
|
510
|
+
init_logger();
|
|
511
|
+
init_footer();
|
|
512
|
+
CommentManager = class {
|
|
513
|
+
octokit;
|
|
514
|
+
retryConfig;
|
|
515
|
+
// Serial write queue: chains all updateOrCreateComment calls so only one
|
|
516
|
+
// GitHub comment write is in-flight at a time within a job.
|
|
517
|
+
_writeQueue = Promise.resolve();
|
|
518
|
+
constructor(octokit, retryConfig) {
|
|
519
|
+
this.octokit = octokit;
|
|
520
|
+
this.retryConfig = {
|
|
521
|
+
maxRetries: 3,
|
|
522
|
+
baseDelay: 1e3,
|
|
523
|
+
maxDelay: 1e4,
|
|
524
|
+
backoffFactor: 2,
|
|
525
|
+
...retryConfig
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Find existing Visor comment by comment ID marker
|
|
530
|
+
*/
|
|
531
|
+
async findVisorComment(owner, repo, prNumber, commentId) {
|
|
532
|
+
try {
|
|
533
|
+
const comments = await this.octokit.rest.issues.listComments({
|
|
534
|
+
owner,
|
|
535
|
+
repo,
|
|
536
|
+
issue_number: prNumber,
|
|
537
|
+
per_page: 100
|
|
538
|
+
// GitHub default max
|
|
539
|
+
});
|
|
540
|
+
for (const comment of comments.data) {
|
|
541
|
+
if (comment.body && this.isVisorComment(comment.body, commentId)) {
|
|
542
|
+
return comment;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (this.isRateLimitError(
|
|
548
|
+
error
|
|
549
|
+
)) {
|
|
550
|
+
await this.handleRateLimit(error);
|
|
551
|
+
return this.findVisorComment(owner, repo, prNumber, commentId);
|
|
552
|
+
}
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Update existing comment or create new one with collision detection
|
|
558
|
+
*/
|
|
559
|
+
async updateOrCreateComment(owner, repo, prNumber, content, options = {}) {
|
|
560
|
+
return new Promise((resolve, reject) => {
|
|
561
|
+
this._writeQueue = this._writeQueue.then(() => this._doUpdateOrCreate(owner, repo, prNumber, content, options)).then(resolve, reject);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
async _doUpdateOrCreate(owner, repo, prNumber, content, options = {}) {
|
|
565
|
+
const {
|
|
566
|
+
commentId = this.generateCommentId(),
|
|
567
|
+
triggeredBy = "unknown",
|
|
568
|
+
allowConcurrentUpdates = false,
|
|
569
|
+
commitSha,
|
|
570
|
+
cachedGithubCommentId
|
|
571
|
+
} = options;
|
|
572
|
+
return this.withRetry(async () => {
|
|
573
|
+
let existingComment = await this.findVisorComment(owner, repo, prNumber, commentId);
|
|
574
|
+
if (!existingComment && cachedGithubCommentId) {
|
|
575
|
+
try {
|
|
576
|
+
const cachedComment = await this.octokit.rest.issues.getComment({
|
|
577
|
+
owner,
|
|
578
|
+
repo,
|
|
579
|
+
comment_id: cachedGithubCommentId
|
|
580
|
+
});
|
|
581
|
+
if (cachedComment.data && this.isVisorComment(cachedComment.data.body || "", commentId)) {
|
|
582
|
+
existingComment = cachedComment.data;
|
|
583
|
+
logger.debug(
|
|
584
|
+
`[github-comments] Found comment via cached ID ${cachedGithubCommentId} (not visible in listComments yet)`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
} catch (_e) {
|
|
588
|
+
logger.debug(
|
|
589
|
+
`[github-comments] Cached comment ${cachedGithubCommentId} not found, will create new`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const formattedContent = this.formatCommentWithMetadata(content, {
|
|
594
|
+
commentId,
|
|
595
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
596
|
+
triggeredBy,
|
|
597
|
+
commitSha
|
|
598
|
+
});
|
|
599
|
+
if (existingComment) {
|
|
600
|
+
if (!allowConcurrentUpdates) {
|
|
601
|
+
const currentComment = await this.octokit.rest.issues.getComment({
|
|
602
|
+
owner,
|
|
603
|
+
repo,
|
|
604
|
+
comment_id: existingComment.id
|
|
605
|
+
});
|
|
606
|
+
if (currentComment.data.updated_at !== existingComment.updated_at) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Comment collision detected for comment ${commentId}. Another process may have updated it.`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const updatedComment = await this.octokit.rest.issues.updateComment({
|
|
613
|
+
owner,
|
|
614
|
+
repo,
|
|
615
|
+
comment_id: existingComment.id,
|
|
616
|
+
body: formattedContent
|
|
617
|
+
});
|
|
618
|
+
logger.info(
|
|
619
|
+
`\u2705 Successfully updated comment (ID: ${commentId}, GitHub ID: ${existingComment.id}) on PR #${prNumber} in ${owner}/${repo}`
|
|
620
|
+
);
|
|
621
|
+
return updatedComment.data;
|
|
622
|
+
} else {
|
|
623
|
+
const newComment = await this.octokit.rest.issues.createComment({
|
|
624
|
+
owner,
|
|
625
|
+
repo,
|
|
626
|
+
issue_number: prNumber,
|
|
627
|
+
body: formattedContent
|
|
628
|
+
});
|
|
629
|
+
logger.info(
|
|
630
|
+
`\u2705 Successfully created comment (ID: ${commentId}, GitHub ID: ${newComment.data.id}) on PR #${prNumber} in ${owner}/${repo}`
|
|
631
|
+
);
|
|
632
|
+
return newComment.data;
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Format comment content with metadata markers
|
|
638
|
+
*/
|
|
639
|
+
formatCommentWithMetadata(content, metadata) {
|
|
640
|
+
const { commentId, lastUpdated, triggeredBy, commitSha } = metadata;
|
|
641
|
+
const footer = generateFooter({
|
|
642
|
+
includeMetadata: {
|
|
643
|
+
lastUpdated,
|
|
644
|
+
triggeredBy,
|
|
645
|
+
commitSha
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
return `<!-- visor-comment-id:${commentId} -->
|
|
649
|
+
${content}
|
|
650
|
+
|
|
651
|
+
${footer}
|
|
652
|
+
<!-- /visor-comment-id:${commentId} -->`;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Create collapsible sections for comment content
|
|
656
|
+
*/
|
|
657
|
+
createCollapsibleSection(title, content, isExpanded = false) {
|
|
658
|
+
const openAttribute = isExpanded ? " open" : "";
|
|
659
|
+
return `<details${openAttribute}>
|
|
660
|
+
<summary>${title}</summary>
|
|
661
|
+
|
|
662
|
+
${content}
|
|
663
|
+
|
|
664
|
+
</details>`;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Group review results by check type with collapsible sections
|
|
668
|
+
*/
|
|
669
|
+
formatGroupedResults(results, groupBy = "check") {
|
|
670
|
+
const grouped = this.groupResults(results, groupBy);
|
|
671
|
+
const sections = [];
|
|
672
|
+
for (const [groupKey, items] of Object.entries(grouped)) {
|
|
673
|
+
const totalScore = items.reduce((sum, item) => sum + (item.score || 0), 0) / items.length;
|
|
674
|
+
const totalIssues = items.reduce((sum, item) => sum + (item.issuesFound || 0), 0);
|
|
675
|
+
const title = this.formatGroupTitle(groupKey, totalScore, totalIssues);
|
|
676
|
+
const sectionContent = items.map((item) => item.content).join("\n\n");
|
|
677
|
+
sections.push(this.createCollapsibleSection(title, sectionContent, totalIssues > 0));
|
|
678
|
+
}
|
|
679
|
+
return sections.join("\n\n");
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Generate unique comment ID
|
|
683
|
+
*/
|
|
684
|
+
generateCommentId() {
|
|
685
|
+
return generateShortHumanId();
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Check if comment is a Visor comment
|
|
689
|
+
*/
|
|
690
|
+
isVisorComment(body, commentId) {
|
|
691
|
+
if (commentId) {
|
|
692
|
+
if (body.includes(`visor-comment-id:${commentId} `) || body.includes(`visor-comment-id:${commentId} -->`)) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
if (commentId.startsWith("pr-review-") && body.includes("visor-review-")) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
return body.includes("visor-comment-id:") && body.includes("<!-- /visor-comment-id:") || body.includes("visor-review-");
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Extract comment ID from comment body
|
|
704
|
+
*/
|
|
705
|
+
extractCommentId(body) {
|
|
706
|
+
const match = body.match(/visor-comment-id:([a-f0-9-]+)/);
|
|
707
|
+
return match ? match[1] : null;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Handle rate limiting with exponential backoff
|
|
711
|
+
*/
|
|
712
|
+
async handleRateLimit(error) {
|
|
713
|
+
const resetTime = error.response?.headers?.["x-ratelimit-reset"];
|
|
714
|
+
if (resetTime) {
|
|
715
|
+
const resetDate = new Date(parseInt(resetTime) * 1e3);
|
|
716
|
+
const waitTime = Math.max(resetDate.getTime() - Date.now(), this.retryConfig.baseDelay);
|
|
717
|
+
console.log(`Rate limit exceeded. Waiting ${Math.round(waitTime / 1e3)}s until reset...`);
|
|
718
|
+
await this.sleep(Math.min(waitTime, this.retryConfig.maxDelay));
|
|
719
|
+
} else {
|
|
720
|
+
await this.sleep(this.retryConfig.baseDelay);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Check if error is a rate limit error
|
|
725
|
+
*/
|
|
726
|
+
isRateLimitError(error) {
|
|
727
|
+
return error.status === 403 && (error.response?.data?.message?.includes("rate limit") ?? false);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Check if error should not be retried (auth errors, not found, etc.)
|
|
731
|
+
*/
|
|
732
|
+
isNonRetryableError(error) {
|
|
733
|
+
const nonRetryableStatuses = [401, 404, 422];
|
|
734
|
+
const status = error.status || error.response?.status;
|
|
735
|
+
if (status === 403) {
|
|
736
|
+
return !this.isRateLimitError(error);
|
|
737
|
+
}
|
|
738
|
+
return status !== void 0 && nonRetryableStatuses.includes(status);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Retry wrapper with exponential backoff
|
|
742
|
+
*/
|
|
743
|
+
async withRetry(operation) {
|
|
744
|
+
let lastError = new Error("Unknown error");
|
|
745
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
746
|
+
try {
|
|
747
|
+
return await operation();
|
|
748
|
+
} catch (error) {
|
|
749
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
750
|
+
if (attempt === this.retryConfig.maxRetries) {
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
if (this.isRateLimitError(
|
|
754
|
+
error
|
|
755
|
+
)) {
|
|
756
|
+
await this.handleRateLimit(error);
|
|
757
|
+
} else if (this.isNonRetryableError(error)) {
|
|
758
|
+
throw error;
|
|
759
|
+
} else {
|
|
760
|
+
const computed = this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffFactor, attempt);
|
|
761
|
+
const delay = computed > this.retryConfig.maxDelay ? Math.max(0, this.retryConfig.maxDelay - 1) : computed;
|
|
762
|
+
await this.sleep(delay);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
throw lastError;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Sleep utility
|
|
770
|
+
*/
|
|
771
|
+
sleep(ms) {
|
|
772
|
+
return new Promise((resolve) => {
|
|
773
|
+
const t = setTimeout(resolve, ms);
|
|
774
|
+
if (typeof t.unref === "function") {
|
|
775
|
+
try {
|
|
776
|
+
t.unref();
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Group results by specified criteria
|
|
784
|
+
*/
|
|
785
|
+
groupResults(results, groupBy) {
|
|
786
|
+
const grouped = {};
|
|
787
|
+
for (const result of results) {
|
|
788
|
+
const key = groupBy === "check" ? result.checkType : this.getSeverityGroup(result.score);
|
|
789
|
+
if (!grouped[key]) {
|
|
790
|
+
grouped[key] = [];
|
|
791
|
+
}
|
|
792
|
+
grouped[key].push(result);
|
|
793
|
+
}
|
|
794
|
+
return grouped;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Get severity group based on score
|
|
798
|
+
*/
|
|
799
|
+
getSeverityGroup(score) {
|
|
800
|
+
if (!score) return "Unknown";
|
|
801
|
+
if (score >= 90) return "Excellent";
|
|
802
|
+
if (score >= 75) return "Good";
|
|
803
|
+
if (score >= 50) return "Needs Improvement";
|
|
804
|
+
return "Critical Issues";
|
|
805
|
+
}
|
|
806
|
+
// Emoji helper removed: plain titles are used in group headers
|
|
807
|
+
/**
|
|
808
|
+
* Format group title with score and issue count
|
|
809
|
+
*/
|
|
810
|
+
formatGroupTitle(groupKey, score, issuesFound) {
|
|
811
|
+
const formattedScore = Math.round(score);
|
|
812
|
+
return `${groupKey} Review (Score: ${formattedScore}/100)${issuesFound > 0 ? ` - ${issuesFound} issues found` : ""}`;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// src/frontends/github-frontend.ts
|
|
819
|
+
var GitHubFrontend;
|
|
820
|
+
var init_github_frontend = __esm({
|
|
821
|
+
"src/frontends/github-frontend.ts"() {
|
|
822
|
+
init_logger();
|
|
823
|
+
init_json_text_extractor();
|
|
824
|
+
GitHubFrontend = class {
|
|
825
|
+
name = "github";
|
|
826
|
+
subs = [];
|
|
827
|
+
checkRunIds = /* @__PURE__ */ new Map();
|
|
828
|
+
revision = 0;
|
|
829
|
+
cachedCommentId;
|
|
830
|
+
// legacy single-thread id (kept for compatibility)
|
|
831
|
+
// Group → (checkId → SectionState)
|
|
832
|
+
stepStatusByGroup = /* @__PURE__ */ new Map();
|
|
833
|
+
// Debounce/coalescing state
|
|
834
|
+
debounceMs = 400;
|
|
835
|
+
maxWaitMs = 2e3;
|
|
836
|
+
_timer = null;
|
|
837
|
+
_lastFlush = 0;
|
|
838
|
+
_pendingIds = /* @__PURE__ */ new Set();
|
|
839
|
+
// Mutex for serializing comment updates per group
|
|
840
|
+
updateLocks = /* @__PURE__ */ new Map();
|
|
841
|
+
minUpdateDelayMs = 1e3;
|
|
842
|
+
// Minimum delay between updates (public for testing)
|
|
843
|
+
// Cache of created GitHub comment IDs per group to handle API eventual consistency
|
|
844
|
+
createdCommentGithubIds = /* @__PURE__ */ new Map();
|
|
845
|
+
_stopped = false;
|
|
846
|
+
start(ctx) {
|
|
847
|
+
const log = ctx.logger;
|
|
848
|
+
const bus = ctx.eventBus;
|
|
849
|
+
const octokit = ctx.octokit;
|
|
850
|
+
const repo = ctx.run.repo;
|
|
851
|
+
const pr = ctx.run.pr;
|
|
852
|
+
const headSha = ctx.run.headSha;
|
|
853
|
+
const canPostComments = !!(octokit && repo && pr);
|
|
854
|
+
const canPostChecks = !!(octokit && repo && pr && headSha);
|
|
855
|
+
const svc = canPostChecks ? new (init_github_check_service(), __toCommonJS(github_check_service_exports)).GitHubCheckService(octokit) : null;
|
|
856
|
+
const CommentManager2 = (init_github_comments(), __toCommonJS(github_comments_exports)).CommentManager;
|
|
857
|
+
const comments = canPostComments ? new CommentManager2(octokit) : null;
|
|
858
|
+
const threadKey = repo && pr && headSha ? `${repo.owner}/${repo.name}#${pr}@${(headSha || "").substring(0, 7)}` : ctx.run.runId;
|
|
859
|
+
this.cachedCommentId = `visor-thread-${threadKey}`;
|
|
860
|
+
this.subs.push(
|
|
861
|
+
bus.on("CheckScheduled", async (env) => {
|
|
862
|
+
const ev = env && env.payload || env;
|
|
863
|
+
try {
|
|
864
|
+
if (!canPostChecks || !svc) return;
|
|
865
|
+
if (this.checkRunIds.has(ev.checkId)) return;
|
|
866
|
+
const group = this.getGroupForCheck(ctx, ev.checkId);
|
|
867
|
+
this.upsertSectionState(group, ev.checkId, {
|
|
868
|
+
status: "queued",
|
|
869
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
870
|
+
});
|
|
871
|
+
const res = await svc.createCheckRun(
|
|
872
|
+
{
|
|
873
|
+
owner: repo.owner,
|
|
874
|
+
repo: repo.name,
|
|
875
|
+
head_sha: headSha,
|
|
876
|
+
name: `Visor: ${ev.checkId}`,
|
|
877
|
+
external_id: `visor:${ctx.run.runId}:${ev.checkId}`,
|
|
878
|
+
engine_mode: "state-machine"
|
|
879
|
+
},
|
|
880
|
+
{ title: `${ev.checkId}`, summary: "Queued" }
|
|
881
|
+
);
|
|
882
|
+
this.checkRunIds.set(ev.checkId, res.id);
|
|
883
|
+
} catch (e) {
|
|
884
|
+
log.warn(
|
|
885
|
+
`[github-frontend] createCheckRun failed for ${ev.checkId}: ${e instanceof Error ? e.message : e}`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
);
|
|
890
|
+
this.subs.push(
|
|
891
|
+
bus.on("CheckCompleted", async (env) => {
|
|
892
|
+
const ev = env && env.payload || env;
|
|
893
|
+
try {
|
|
894
|
+
if (canPostChecks && svc && this.checkRunIds.has(ev.checkId)) {
|
|
895
|
+
const id = this.checkRunIds.get(ev.checkId);
|
|
896
|
+
const issues = Array.isArray(ev.result?.issues) ? ev.result.issues : [];
|
|
897
|
+
const failureResults = await this.evaluateFailureResults(ctx, ev.checkId, ev.result);
|
|
898
|
+
await svc.completeCheckRun(
|
|
899
|
+
repo.owner,
|
|
900
|
+
repo.name,
|
|
901
|
+
id,
|
|
902
|
+
ev.checkId,
|
|
903
|
+
failureResults,
|
|
904
|
+
issues,
|
|
905
|
+
void 0,
|
|
906
|
+
void 0,
|
|
907
|
+
pr,
|
|
908
|
+
headSha
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (canPostComments && comments) {
|
|
912
|
+
const count = Array.isArray(ev.result?.issues) ? ev.result.issues.length : 0;
|
|
913
|
+
const failureResults = await this.evaluateFailureResults(ctx, ev.checkId, ev.result);
|
|
914
|
+
const failed = Array.isArray(failureResults) ? failureResults.some((r) => r && r.failed) : false;
|
|
915
|
+
const group = this.getGroupForCheck(ctx, ev.checkId);
|
|
916
|
+
const rawContent = ev?.result?.content;
|
|
917
|
+
const extractedContent = extractTextFromJson(rawContent) ?? extractTextFromJson(ev?.result?.output);
|
|
918
|
+
this.upsertSectionState(group, ev.checkId, {
|
|
919
|
+
status: "completed",
|
|
920
|
+
conclusion: failed ? "failure" : "success",
|
|
921
|
+
issues: count,
|
|
922
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
923
|
+
content: extractedContent
|
|
924
|
+
});
|
|
925
|
+
await this.updateGroupedComment(ctx, comments, group, ev.checkId);
|
|
926
|
+
}
|
|
927
|
+
} catch (e) {
|
|
928
|
+
log.warn(
|
|
929
|
+
`[github-frontend] handle CheckCompleted failed: ${e instanceof Error ? e.message : e}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
);
|
|
934
|
+
this.subs.push(
|
|
935
|
+
bus.on("CheckErrored", async (env) => {
|
|
936
|
+
const ev = env && env.payload || env;
|
|
937
|
+
try {
|
|
938
|
+
if (canPostChecks && svc && this.checkRunIds.has(ev.checkId)) {
|
|
939
|
+
const id = this.checkRunIds.get(ev.checkId);
|
|
940
|
+
await svc.completeCheckRun(
|
|
941
|
+
repo.owner,
|
|
942
|
+
repo.name,
|
|
943
|
+
id,
|
|
944
|
+
ev.checkId,
|
|
945
|
+
[],
|
|
946
|
+
[],
|
|
947
|
+
ev.error?.message || "Execution error",
|
|
948
|
+
void 0,
|
|
949
|
+
pr,
|
|
950
|
+
headSha
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (canPostComments && comments) {
|
|
954
|
+
const group = this.getGroupForCheck(ctx, ev.checkId);
|
|
955
|
+
this.upsertSectionState(group, ev.checkId, {
|
|
956
|
+
status: "errored",
|
|
957
|
+
conclusion: "failure",
|
|
958
|
+
issues: 0,
|
|
959
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
960
|
+
error: ev.error?.message || "Execution error"
|
|
961
|
+
});
|
|
962
|
+
await this.updateGroupedComment(ctx, comments, group, ev.checkId);
|
|
963
|
+
}
|
|
964
|
+
} catch (e) {
|
|
965
|
+
log.warn(
|
|
966
|
+
`[github-frontend] handle CheckErrored failed: ${e instanceof Error ? e.message : e}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
this.subs.push(
|
|
972
|
+
bus.on("StateTransition", async (env) => {
|
|
973
|
+
const ev = env && env.payload || env;
|
|
974
|
+
try {
|
|
975
|
+
if (ev.to === "Completed" || ev.to === "Error") {
|
|
976
|
+
if (canPostComments && comments) {
|
|
977
|
+
for (const group of this.stepStatusByGroup.keys()) {
|
|
978
|
+
await this.updateGroupedComment(ctx, comments, group);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
} catch (e) {
|
|
983
|
+
log.warn(
|
|
984
|
+
`[github-frontend] handle StateTransition failed: ${e instanceof Error ? e.message : e}`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
})
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
async stop() {
|
|
991
|
+
this._stopped = true;
|
|
992
|
+
for (const s of this.subs) s.unsubscribe();
|
|
993
|
+
this.subs = [];
|
|
994
|
+
if (this._timer) {
|
|
995
|
+
clearTimeout(this._timer);
|
|
996
|
+
this._timer = null;
|
|
997
|
+
}
|
|
998
|
+
this._pendingIds.clear();
|
|
999
|
+
const pending = Array.from(this.updateLocks.values());
|
|
1000
|
+
if (pending.length > 0) {
|
|
1001
|
+
await Promise.allSettled(pending);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async buildFullBody(ctx, group) {
|
|
1005
|
+
const header = this.renderThreadHeader(ctx, group);
|
|
1006
|
+
const sections = this.renderSections(ctx, group);
|
|
1007
|
+
return `${header}
|
|
1008
|
+
|
|
1009
|
+
${sections}
|
|
1010
|
+
|
|
1011
|
+
<!-- visor:thread-end key="${this.threadKeyFor(ctx)}" -->`;
|
|
1012
|
+
}
|
|
1013
|
+
threadKeyFor(ctx) {
|
|
1014
|
+
const r = ctx.run;
|
|
1015
|
+
return r.repo && r.pr && r.headSha ? `${r.repo.owner}/${r.repo.name}#${r.pr}@${(r.headSha || "").substring(0, 7)}` : r.runId;
|
|
1016
|
+
}
|
|
1017
|
+
renderThreadHeader(ctx, group) {
|
|
1018
|
+
const header = {
|
|
1019
|
+
key: this.threadKeyFor(ctx),
|
|
1020
|
+
runId: ctx.run.runId,
|
|
1021
|
+
workflowId: ctx.run.workflowId,
|
|
1022
|
+
revision: this.revision,
|
|
1023
|
+
group,
|
|
1024
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1025
|
+
};
|
|
1026
|
+
return `<!-- visor:thread=${JSON.stringify(header)} -->`;
|
|
1027
|
+
}
|
|
1028
|
+
renderSections(ctx, group) {
|
|
1029
|
+
const lines = [];
|
|
1030
|
+
const groupMap = this.stepStatusByGroup.get(group) || /* @__PURE__ */ new Map();
|
|
1031
|
+
for (const [checkId, st] of groupMap.entries()) {
|
|
1032
|
+
const start = `<!-- visor:section=${JSON.stringify({ id: checkId, revision: this.revision })} -->`;
|
|
1033
|
+
const end = `<!-- visor:section-end id="${checkId}" -->`;
|
|
1034
|
+
const body = st.content && st.content.toString().trim().length > 0 ? st.content.toString().trim() : "";
|
|
1035
|
+
lines.push(`${start}
|
|
1036
|
+
${body}
|
|
1037
|
+
${end}`);
|
|
1038
|
+
}
|
|
1039
|
+
return lines.join("\\n\\n");
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Acquires a mutex lock for the given group and executes the update.
|
|
1043
|
+
* This ensures only one comment update happens at a time per group,
|
|
1044
|
+
* preventing race conditions where updates overwrite each other.
|
|
1045
|
+
*
|
|
1046
|
+
* Uses a proper queue-based mutex: each new caller chains onto the previous
|
|
1047
|
+
* lock, ensuring strict serialization even when multiple callers wait
|
|
1048
|
+
* simultaneously.
|
|
1049
|
+
*/
|
|
1050
|
+
async updateGroupedComment(ctx, comments, group, changedIds) {
|
|
1051
|
+
const existingLock = this.updateLocks.get(group);
|
|
1052
|
+
let resolveLock;
|
|
1053
|
+
const ourLock = new Promise((resolve) => {
|
|
1054
|
+
resolveLock = resolve;
|
|
1055
|
+
});
|
|
1056
|
+
this.updateLocks.set(group, ourLock);
|
|
1057
|
+
try {
|
|
1058
|
+
if (existingLock) {
|
|
1059
|
+
logger.info(
|
|
1060
|
+
`[github-frontend] Comment update for group "${group}" queued, waiting for previous update to finish...`
|
|
1061
|
+
);
|
|
1062
|
+
const queuedAt = Date.now();
|
|
1063
|
+
const reminder = setInterval(() => {
|
|
1064
|
+
const waited = Math.round((Date.now() - queuedAt) / 1e3);
|
|
1065
|
+
logger.info(
|
|
1066
|
+
`[github-frontend] Comment update for group "${group}" still queued (${waited}s).`
|
|
1067
|
+
);
|
|
1068
|
+
}, 1e4);
|
|
1069
|
+
try {
|
|
1070
|
+
await existingLock;
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
logger.warn(
|
|
1073
|
+
`[github-frontend] Previous update for group ${group} failed: ${error instanceof Error ? error.message : error}`
|
|
1074
|
+
);
|
|
1075
|
+
} finally {
|
|
1076
|
+
clearInterval(reminder);
|
|
1077
|
+
const waitedMs = Date.now() - queuedAt;
|
|
1078
|
+
if (waitedMs > 100) {
|
|
1079
|
+
logger.info(
|
|
1080
|
+
`[github-frontend] Comment update for group "${group}" dequeued after ${Math.round(waitedMs / 1e3)}s.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
await this.performGroupedCommentUpdate(ctx, comments, group, changedIds);
|
|
1086
|
+
} finally {
|
|
1087
|
+
if (this.updateLocks.get(group) === ourLock) {
|
|
1088
|
+
this.updateLocks.delete(group);
|
|
1089
|
+
}
|
|
1090
|
+
resolveLock();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Performs the actual comment update with delay enforcement.
|
|
1095
|
+
*/
|
|
1096
|
+
async performGroupedCommentUpdate(ctx, comments, group, changedIds) {
|
|
1097
|
+
try {
|
|
1098
|
+
if (this._stopped) return;
|
|
1099
|
+
if (!ctx.run.repo || !ctx.run.pr) return;
|
|
1100
|
+
const config = ctx.config;
|
|
1101
|
+
const prCommentEnabled = config?.output?.pr_comment?.enabled !== false;
|
|
1102
|
+
if (!prCommentEnabled) {
|
|
1103
|
+
logger.debug(
|
|
1104
|
+
`[github-frontend] PR comments disabled in config, skipping comment for group: ${group}`
|
|
1105
|
+
);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const timeSinceLastFlush = Date.now() - this._lastFlush;
|
|
1109
|
+
if (this._lastFlush > 0 && timeSinceLastFlush < this.minUpdateDelayMs) {
|
|
1110
|
+
const delay = this.minUpdateDelayMs - timeSinceLastFlush;
|
|
1111
|
+
logger.debug(
|
|
1112
|
+
`[github-frontend] Waiting ${delay}ms before next update to prevent rate limiting`
|
|
1113
|
+
);
|
|
1114
|
+
await this.sleep(delay);
|
|
1115
|
+
}
|
|
1116
|
+
this.revision++;
|
|
1117
|
+
const commentId = this.commentIdForGroup(ctx, group);
|
|
1118
|
+
const mergedBody = await this.mergeIntoExistingBody(ctx, comments, group, changedIds);
|
|
1119
|
+
const cachedGithubId = this.createdCommentGithubIds.get(commentId);
|
|
1120
|
+
const result = await comments.updateOrCreateComment(
|
|
1121
|
+
ctx.run.repo.owner,
|
|
1122
|
+
ctx.run.repo.name,
|
|
1123
|
+
ctx.run.pr,
|
|
1124
|
+
mergedBody,
|
|
1125
|
+
{
|
|
1126
|
+
commentId,
|
|
1127
|
+
triggeredBy: this.deriveTriggeredBy(ctx),
|
|
1128
|
+
commitSha: ctx.run.headSha,
|
|
1129
|
+
// Pass the cached GitHub comment ID if available
|
|
1130
|
+
cachedGithubCommentId: cachedGithubId
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
if (result && result.id) {
|
|
1134
|
+
this.createdCommentGithubIds.set(commentId, result.id);
|
|
1135
|
+
}
|
|
1136
|
+
this._lastFlush = Date.now();
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
logger.debug(
|
|
1139
|
+
`[github-frontend] updateGroupedComment failed: ${e instanceof Error ? e.message : e}`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
deriveTriggeredBy(ctx) {
|
|
1144
|
+
const ev = ctx.run.event || "";
|
|
1145
|
+
const actor = ctx.run.actor;
|
|
1146
|
+
const commentEvents = /* @__PURE__ */ new Set([
|
|
1147
|
+
"issue_comment",
|
|
1148
|
+
"issue_comment_created",
|
|
1149
|
+
"pr_comment",
|
|
1150
|
+
"comment",
|
|
1151
|
+
"pull_request_review_comment"
|
|
1152
|
+
]);
|
|
1153
|
+
if (commentEvents.has(ev) && actor) return actor;
|
|
1154
|
+
if (ev) return ev;
|
|
1155
|
+
return actor || "unknown";
|
|
1156
|
+
}
|
|
1157
|
+
async mergeIntoExistingBody(ctx, comments, group, changedIds) {
|
|
1158
|
+
const repo = ctx.run.repo;
|
|
1159
|
+
const pr = ctx.run.pr;
|
|
1160
|
+
const existing = await comments.findVisorComment(
|
|
1161
|
+
repo.owner,
|
|
1162
|
+
repo.name,
|
|
1163
|
+
pr,
|
|
1164
|
+
this.commentIdForGroup(ctx, group)
|
|
1165
|
+
);
|
|
1166
|
+
if (!existing || !existing.body) return this.buildFullBody(ctx, group);
|
|
1167
|
+
const body = String(existing.body);
|
|
1168
|
+
const doc = this.parseSections(body);
|
|
1169
|
+
doc.header = {
|
|
1170
|
+
...doc.header || {},
|
|
1171
|
+
key: this.threadKeyFor(ctx),
|
|
1172
|
+
revision: this.revision,
|
|
1173
|
+
group
|
|
1174
|
+
};
|
|
1175
|
+
if (changedIds) {
|
|
1176
|
+
const ids = Array.isArray(changedIds) ? changedIds : [changedIds];
|
|
1177
|
+
const fresh = this.renderSections(ctx, group);
|
|
1178
|
+
for (const id of ids) {
|
|
1179
|
+
const block = this.extractSectionById(fresh, id);
|
|
1180
|
+
if (block) doc.sections.set(id, block);
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
const fresh = this.renderSections(ctx, group);
|
|
1184
|
+
const map = this.stepStatusByGroup.get(group) || /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const [checkId] of map.entries()) {
|
|
1186
|
+
if (!doc.sections.has(checkId)) {
|
|
1187
|
+
const block = this.extractSectionById(fresh, checkId);
|
|
1188
|
+
if (block) doc.sections.set(checkId, block);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return this.serializeSections(doc);
|
|
1193
|
+
}
|
|
1194
|
+
parseSections(body) {
|
|
1195
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1196
|
+
const headerRe = /<!--\s*visor:thread=(\{[\s\S]*?\})\s*-->/m;
|
|
1197
|
+
const startRe = /<!--\s*visor:section=(\{[\s\S]*?\})\s*-->/g;
|
|
1198
|
+
const endRe = /<!--\s*visor:section-end\s+id=\"([^\"]+)\"\s*-->/g;
|
|
1199
|
+
const safePick = (obj, allowed) => {
|
|
1200
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return void 0;
|
|
1201
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1202
|
+
for (const [k, t] of Object.entries(allowed)) {
|
|
1203
|
+
if (Object.prototype.hasOwnProperty.call(obj, k)) {
|
|
1204
|
+
const v = obj[k];
|
|
1205
|
+
if (t === "string" && typeof v === "string") out[k] = v;
|
|
1206
|
+
else if (t === "number" && typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return out;
|
|
1210
|
+
};
|
|
1211
|
+
const safeParse = (text) => {
|
|
1212
|
+
try {
|
|
1213
|
+
return JSON.parse(text);
|
|
1214
|
+
} catch {
|
|
1215
|
+
return void 0;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
let header;
|
|
1219
|
+
try {
|
|
1220
|
+
const h = headerRe.exec(body);
|
|
1221
|
+
if (h) {
|
|
1222
|
+
const parsed = safeParse(h[1]);
|
|
1223
|
+
const picked = safePick(parsed, {
|
|
1224
|
+
key: "string",
|
|
1225
|
+
runId: "string",
|
|
1226
|
+
workflowId: "string",
|
|
1227
|
+
revision: "number",
|
|
1228
|
+
group: "string",
|
|
1229
|
+
generatedAt: "string"
|
|
1230
|
+
});
|
|
1231
|
+
header = picked;
|
|
1232
|
+
}
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
let cursor = 0;
|
|
1236
|
+
while (true) {
|
|
1237
|
+
const s = startRe.exec(body);
|
|
1238
|
+
if (!s) break;
|
|
1239
|
+
const metaRaw = safeParse(s[1]);
|
|
1240
|
+
const meta = safePick(metaRaw, { id: "string", revision: "number" }) || { id: "" };
|
|
1241
|
+
const startIdx = startRe.lastIndex;
|
|
1242
|
+
endRe.lastIndex = startIdx;
|
|
1243
|
+
const e = endRe.exec(body);
|
|
1244
|
+
if (!e) break;
|
|
1245
|
+
const id = typeof meta.id === "string" && meta.id ? String(meta.id) : String(e[1]);
|
|
1246
|
+
const content = body.substring(startIdx, e.index).trim();
|
|
1247
|
+
const block = `<!-- visor:section=${JSON.stringify(meta)} -->
|
|
1248
|
+
${content}
|
|
1249
|
+
<!-- visor:section-end id="${id}" -->`;
|
|
1250
|
+
sections.set(id, block);
|
|
1251
|
+
cursor = endRe.lastIndex;
|
|
1252
|
+
startRe.lastIndex = cursor;
|
|
1253
|
+
}
|
|
1254
|
+
return { header, sections };
|
|
1255
|
+
}
|
|
1256
|
+
serializeSections(doc) {
|
|
1257
|
+
const header = `<!-- visor:thread=${JSON.stringify({ ...doc.header || {}, generatedAt: (/* @__PURE__ */ new Date()).toISOString() })} -->`;
|
|
1258
|
+
const blocks = Array.from(doc.sections.values()).join("\n\n");
|
|
1259
|
+
const key = doc.header && doc.header.key || "";
|
|
1260
|
+
return `${header}
|
|
1261
|
+
|
|
1262
|
+
${blocks}
|
|
1263
|
+
|
|
1264
|
+
<!-- visor:thread-end key="${key}" -->`;
|
|
1265
|
+
}
|
|
1266
|
+
extractSectionById(rendered, id) {
|
|
1267
|
+
const rx = new RegExp(
|
|
1268
|
+
`<!--\\s*visor:section=(\\{[\\s\\S]*?\\})\\s*-->[\\s\\S]*?<!--\\s*visor:section-end\\s+id=\\"${this.escapeRegExp(id)}\\"\\s*-->`,
|
|
1269
|
+
"m"
|
|
1270
|
+
);
|
|
1271
|
+
const m = rx.exec(rendered);
|
|
1272
|
+
return m ? m[0] : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
escapeRegExp(s) {
|
|
1275
|
+
return s.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
1276
|
+
}
|
|
1277
|
+
getGroupForCheck(ctx, checkId) {
|
|
1278
|
+
try {
|
|
1279
|
+
const cfg = ctx.config || {};
|
|
1280
|
+
const g = cfg?.checks?.[checkId]?.group || cfg?.steps?.[checkId]?.group;
|
|
1281
|
+
if (typeof g === "string" && g.trim().length > 0) return g;
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
return "review";
|
|
1285
|
+
}
|
|
1286
|
+
upsertSectionState(group, checkId, patch) {
|
|
1287
|
+
let groupMap = this.stepStatusByGroup.get(group);
|
|
1288
|
+
if (!groupMap) {
|
|
1289
|
+
groupMap = /* @__PURE__ */ new Map();
|
|
1290
|
+
this.stepStatusByGroup.set(group, groupMap);
|
|
1291
|
+
}
|
|
1292
|
+
const prev = groupMap.get(checkId) || { status: "queued", lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1293
|
+
groupMap.set(checkId, { ...prev, ...patch });
|
|
1294
|
+
}
|
|
1295
|
+
commentIdForGroup(ctx, group) {
|
|
1296
|
+
if (group === "dynamic") {
|
|
1297
|
+
return `visor-thread-dynamic-${ctx.run.runId}`;
|
|
1298
|
+
}
|
|
1299
|
+
const r = ctx.run;
|
|
1300
|
+
const base = r.repo && r.pr ? `${r.repo.owner}/${r.repo.name}#${r.pr}` : r.runId;
|
|
1301
|
+
return `visor-thread-${group}-${base}`;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Compute failure condition results for a completed check so Check Runs map to the
|
|
1305
|
+
* correct GitHub conclusion. This mirrors the engine's evaluation for fail_if.
|
|
1306
|
+
*/
|
|
1307
|
+
async evaluateFailureResults(ctx, checkId, result) {
|
|
1308
|
+
try {
|
|
1309
|
+
const config = ctx.config || {};
|
|
1310
|
+
const checks = config && config.checks || {};
|
|
1311
|
+
const checkCfg = checks[checkId] || {};
|
|
1312
|
+
const checkSchema = typeof checkCfg.schema === "string" ? checkCfg.schema : "code-review";
|
|
1313
|
+
const checkGroup = checkCfg.group || "default";
|
|
1314
|
+
const { FailureConditionEvaluator } = (init_failure_condition_evaluator(), __toCommonJS(failure_condition_evaluator_exports));
|
|
1315
|
+
const evaluator = new FailureConditionEvaluator();
|
|
1316
|
+
const reviewSummary = { issues: Array.isArray(result?.issues) ? result.issues : [] };
|
|
1317
|
+
const failures = [];
|
|
1318
|
+
if (config.fail_if) {
|
|
1319
|
+
const failed = await evaluator.evaluateSimpleCondition(
|
|
1320
|
+
checkId,
|
|
1321
|
+
checkSchema,
|
|
1322
|
+
checkGroup,
|
|
1323
|
+
reviewSummary,
|
|
1324
|
+
config.fail_if
|
|
1325
|
+
);
|
|
1326
|
+
failures.push({
|
|
1327
|
+
conditionName: "global_fail_if",
|
|
1328
|
+
failed,
|
|
1329
|
+
expression: config.fail_if,
|
|
1330
|
+
severity: "error",
|
|
1331
|
+
haltExecution: false
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (checkCfg.fail_if) {
|
|
1335
|
+
const failed = await evaluator.evaluateSimpleCondition(
|
|
1336
|
+
checkId,
|
|
1337
|
+
checkSchema,
|
|
1338
|
+
checkGroup,
|
|
1339
|
+
reviewSummary,
|
|
1340
|
+
checkCfg.fail_if
|
|
1341
|
+
);
|
|
1342
|
+
failures.push({
|
|
1343
|
+
conditionName: `${checkId}_fail_if`,
|
|
1344
|
+
failed,
|
|
1345
|
+
expression: checkCfg.fail_if,
|
|
1346
|
+
severity: "error",
|
|
1347
|
+
haltExecution: false
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
return failures;
|
|
1351
|
+
} catch {
|
|
1352
|
+
return [];
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
// Debounce helpers
|
|
1356
|
+
scheduleUpdate(ctx, comments, group, id) {
|
|
1357
|
+
if (id) this._pendingIds.add(id);
|
|
1358
|
+
const now = Date.now();
|
|
1359
|
+
const since = now - this._lastFlush;
|
|
1360
|
+
const remaining = this.maxWaitMs - since;
|
|
1361
|
+
if (this._timer) clearTimeout(this._timer);
|
|
1362
|
+
const wait = Math.max(0, Math.min(this.debounceMs, remaining));
|
|
1363
|
+
this._timer = setTimeout(async () => {
|
|
1364
|
+
const ids = Array.from(this._pendingIds);
|
|
1365
|
+
this._pendingIds.clear();
|
|
1366
|
+
this._timer = null;
|
|
1367
|
+
await this.updateGroupedComment(ctx, comments, group, ids.length > 0 ? ids : void 0);
|
|
1368
|
+
this._lastFlush = Date.now();
|
|
1369
|
+
}, wait);
|
|
1370
|
+
}
|
|
1371
|
+
async flushNow(ctx, comments, group) {
|
|
1372
|
+
if (this._timer) {
|
|
1373
|
+
clearTimeout(this._timer);
|
|
1374
|
+
this._timer = null;
|
|
1375
|
+
}
|
|
1376
|
+
const ids = Array.from(this._pendingIds);
|
|
1377
|
+
this._pendingIds.clear();
|
|
1378
|
+
await this.updateGroupedComment(ctx, comments, group, ids.length > 0 ? ids : void 0);
|
|
1379
|
+
this._lastFlush = Date.now();
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Sleep utility for enforcing delays
|
|
1383
|
+
*/
|
|
1384
|
+
sleep(ms) {
|
|
1385
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
init_github_frontend();
|
|
1391
|
+
export {
|
|
1392
|
+
GitHubFrontend
|
|
1393
|
+
};
|
|
1394
|
+
//# sourceMappingURL=github-frontend-L3F5JXPJ.mjs.map
|