@rage-against-the-pixel/unity-cli 1.8.2 → 2.0.0
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/README.md +128 -66
- package/dist/cli.js +239 -22
- package/dist/cli.js.map +1 -1
- package/dist/github-actions-ci.d.ts +21 -0
- package/dist/github-actions-ci.js +137 -0
- package/dist/github-actions-ci.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger-provider.d.ts +35 -0
- package/dist/logger-provider.js +48 -0
- package/dist/logger-provider.js.map +1 -0
- package/dist/logging.d.ts +65 -4
- package/dist/logging.js +712 -189
- package/dist/logging.js.map +1 -1
- package/dist/unity-editor.js +1 -0
- package/dist/unity-editor.js.map +1 -1
- package/dist/unity-hub.js +60 -17
- package/dist/unity-hub.js.map +1 -1
- package/dist/unity-logging.d.ts +23 -1
- package/dist/unity-logging.js +259 -12
- package/dist/unity-logging.js.map +1 -1
- package/dist/upm-cli.d.ts +80 -0
- package/dist/upm-cli.js +459 -0
- package/dist/upm-cli.js.map +1 -0
- package/dist/utilities.d.ts +49 -0
- package/dist/utilities.js +204 -12
- package/dist/utilities.js.map +1 -1
- package/dist/{utp/utp.d.ts → utp.d.ts} +21 -3
- package/dist/{utp/utp.js → utp.js} +25 -12
- package/dist/utp.js.map +1 -0
- package/package.json +12 -7
- package/dist/utp/utp.js.map +0 -1
package/dist/logging.js
CHANGED
|
@@ -1,40 +1,511 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.Logger = exports.LogLevel = void 0;
|
|
37
|
-
|
|
4
|
+
exports.mergeLogEntriesPreferringSeverity = mergeLogEntriesPreferringSeverity;
|
|
5
|
+
exports.testStatusFromState = testStatusFromState;
|
|
6
|
+
exports.utpToTestResultSummary = utpToTestResultSummary;
|
|
7
|
+
exports.buildTestResultsTableMarkdown = buildTestResultsTableMarkdown;
|
|
8
|
+
exports.buildUnitTestJobSummaryMarkdown = buildUnitTestJobSummaryMarkdown;
|
|
9
|
+
exports.truncateStringToUtf8ByteLength = truncateStringToUtf8ByteLength;
|
|
10
|
+
exports.stripSummaryNoiseFromLogMessage = stripSummaryNoiseFromLogMessage;
|
|
11
|
+
const utp_1 = require("./utp");
|
|
12
|
+
const github_actions_ci_1 = require("./github-actions-ci");
|
|
13
|
+
const logger_provider_1 = require("./logger-provider");
|
|
14
|
+
/** Severity order for display: Error first, then Warning, then Info. Undefined treats as Warning. */
|
|
15
|
+
function severityRank(s) {
|
|
16
|
+
if (s === utp_1.Severity.Error || s === utp_1.Severity.Exception || s === utp_1.Severity.Assert)
|
|
17
|
+
return 0;
|
|
18
|
+
if (s === utp_1.Severity.Warning || s === undefined)
|
|
19
|
+
return 1;
|
|
20
|
+
return 2; // Info
|
|
21
|
+
}
|
|
22
|
+
function dedupeKey(e) {
|
|
23
|
+
const msg = (e.message || '').trim();
|
|
24
|
+
const file = (e.file || e.fileName || '').replace(/\\/g, '/');
|
|
25
|
+
const line = e.line ?? e.lineNumber ?? 0;
|
|
26
|
+
return `${msg}\n${file}\n${line}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if the path looks absolute (Unix / or Windows X:/).
|
|
30
|
+
*/
|
|
31
|
+
function isAbsolutePath(file) {
|
|
32
|
+
const norm = file.replace(/\\/g, '/');
|
|
33
|
+
if (norm.startsWith('/'))
|
|
34
|
+
return true;
|
|
35
|
+
return /^[a-zA-Z]:\//.test(norm);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if the entry's file is under the project path (or entry has no file).
|
|
39
|
+
* Relative paths (e.g. Assets/..., Packages/...) are always kept so Unity UTP log/compiler
|
|
40
|
+
* entries with relative file paths still appear in the summary.
|
|
41
|
+
*/
|
|
42
|
+
function isEntryUnderProjectPath(e, projectPath) {
|
|
43
|
+
const file = (e.file || e.fileName || '').trim();
|
|
44
|
+
if (!file)
|
|
45
|
+
return true;
|
|
46
|
+
const normFile = file.replace(/\\/g, '/');
|
|
47
|
+
if (!isAbsolutePath(normFile))
|
|
48
|
+
return true;
|
|
49
|
+
const normProject = projectPath.replace(/\\/g, '/');
|
|
50
|
+
const base = normProject.endsWith('/') ? normProject : normProject + '/';
|
|
51
|
+
return normFile === normProject || normFile.startsWith(base);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns true if the entry's file looks like a Unity engine path (should be omitted when not using projectPath).
|
|
55
|
+
*/
|
|
56
|
+
function isUnityEnginePath(file) {
|
|
57
|
+
const norm = file.replace(/\\/g, '/');
|
|
58
|
+
if (UNITY_ENGINE_PATH_PREFIXES.some(p => norm.startsWith(p)))
|
|
59
|
+
return true;
|
|
60
|
+
if (norm.includes('/Runtime/') || norm.includes('\\Runtime\\'))
|
|
61
|
+
return true;
|
|
62
|
+
if (!norm.endsWith('.cpp'))
|
|
63
|
+
return false;
|
|
64
|
+
const underProject = norm.includes('/Assets/') || norm.includes('/Packages/') || norm.includes('/Library/PackageCache/');
|
|
65
|
+
return !underProject;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merges LogEntry/Compiler rows by message+file+line; on collision keeps the more severe entry.
|
|
69
|
+
* Exported for unit tests.
|
|
70
|
+
*/
|
|
71
|
+
function mergeLogEntriesPreferringSeverity(candidates) {
|
|
72
|
+
const byKey = new Map();
|
|
73
|
+
for (const e of candidates) {
|
|
74
|
+
const key = dedupeKey(e);
|
|
75
|
+
const existing = byKey.get(key);
|
|
76
|
+
if (!existing || severityRank(e.severity) < severityRank(existing.severity)) {
|
|
77
|
+
byKey.set(key, e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const merged = [...byKey.values()];
|
|
81
|
+
merged.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds one merged list from LogEntry and Compiler entries.
|
|
86
|
+
* Deduplicated by message+file+line (keeping worse severity on collision), sorted by severity.
|
|
87
|
+
*/
|
|
88
|
+
function buildMergedLogList(filtered) {
|
|
89
|
+
const candidates = filtered.filter(e => e.type === 'LogEntry' || e.type === 'Compiler');
|
|
90
|
+
return mergeLogEntriesPreferringSeverity(candidates);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Filters merged list to project-relevant entries only.
|
|
94
|
+
* When projectPath is set: keep entries with no file or file under projectPath.
|
|
95
|
+
* When projectPath is not set: exclude Unity engine paths only (keep PackageCache and project paths).
|
|
96
|
+
*/
|
|
97
|
+
function filterMergedByPath(merged, options) {
|
|
98
|
+
if (options?.projectPath != null && options.projectPath !== '') {
|
|
99
|
+
return merged.filter(e => isEntryUnderProjectPath(e, options.projectPath));
|
|
100
|
+
}
|
|
101
|
+
return merged.filter(e => {
|
|
102
|
+
const file = (e.file || e.fileName || '').trim();
|
|
103
|
+
if (!file)
|
|
104
|
+
return true;
|
|
105
|
+
return !isUnityEnginePath(file);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Groups merged log by severity for foldouts (Error, Warning, Info). Missing severity is grouped as Warning. */
|
|
109
|
+
function groupBySeverity(merged) {
|
|
110
|
+
const errorCritical = [];
|
|
111
|
+
const warning = [];
|
|
112
|
+
const info = [];
|
|
113
|
+
for (const e of merged) {
|
|
114
|
+
if (e.severity === utp_1.Severity.Error || e.severity === utp_1.Severity.Exception || e.severity === utp_1.Severity.Assert) {
|
|
115
|
+
errorCritical.push(e);
|
|
116
|
+
}
|
|
117
|
+
else if (e.severity === utp_1.Severity.Warning || e.severity === undefined) {
|
|
118
|
+
warning.push(e);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
info.push(e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { errorCritical, warning, info };
|
|
125
|
+
}
|
|
126
|
+
/** Maps UTPTestStatus.state to display status (Unity/NUnit-style: 0 Inconclusive, 1 Passed, 2 Failed, 3 Skipped). */
|
|
127
|
+
function testStatusFromState(state) {
|
|
128
|
+
switch (state) {
|
|
129
|
+
case 1: return '✅';
|
|
130
|
+
case 2: return '❌';
|
|
131
|
+
case 3: return '⏭️';
|
|
132
|
+
case 0:
|
|
133
|
+
default: return '◯';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Converts a single TestStatus UTP to TestResultSummary. Exported for CLI use. */
|
|
137
|
+
function utpToTestResultSummary(e) {
|
|
138
|
+
const state = e.state;
|
|
139
|
+
const durationMs = e.duration ?? (e.durationMicroseconds != null ? e.durationMicroseconds / 1000 : 0);
|
|
140
|
+
const description = (e.name || e.description || '-').trim();
|
|
141
|
+
const msg = (e.message || '').trim();
|
|
142
|
+
const summary = {
|
|
143
|
+
status: testStatusFromState(state),
|
|
144
|
+
durationMs,
|
|
145
|
+
description,
|
|
146
|
+
};
|
|
147
|
+
if (msg !== '') {
|
|
148
|
+
summary.message = msg;
|
|
149
|
+
}
|
|
150
|
+
const file = (e.file || e.fileName || '').trim();
|
|
151
|
+
const line = e.line ?? e.lineNumber;
|
|
152
|
+
if (file !== '') {
|
|
153
|
+
summary.file = file.replace(/\\/g, '/');
|
|
154
|
+
}
|
|
155
|
+
if (line !== undefined && line > 0) {
|
|
156
|
+
summary.line = line;
|
|
157
|
+
}
|
|
158
|
+
return summary;
|
|
159
|
+
}
|
|
160
|
+
/** Collects TestStatus entries from telemetry into TestResultSummary rows. */
|
|
161
|
+
function collectTestResults(filtered) {
|
|
162
|
+
return filtered.filter(e => e.type === 'TestStatus').map(utpToTestResultSummary);
|
|
163
|
+
}
|
|
164
|
+
function escapeMarkdownTableCell(value) {
|
|
165
|
+
return value
|
|
166
|
+
.replace(/\\/g, '\\\\')
|
|
167
|
+
.replace(/\|/g, '\\|');
|
|
168
|
+
}
|
|
169
|
+
/** Builds a markdown table string for test results (Status | Duration | Test). Exported for CLI use. */
|
|
170
|
+
function buildTestResultsTableMarkdown(testResults, byteLimit, prefix) {
|
|
171
|
+
if (testResults.length === 0)
|
|
172
|
+
return '';
|
|
173
|
+
const p = prefix ?? '';
|
|
174
|
+
let out = p + `### Test results\n\n`;
|
|
175
|
+
out += `| Status | Duration | Test |\n`;
|
|
176
|
+
out += `|--------|----------|------|\n`;
|
|
177
|
+
let shown = 0;
|
|
178
|
+
for (const row of testResults) {
|
|
179
|
+
const durationStr = row.durationMs >= 1000
|
|
180
|
+
? `${(row.durationMs / 1000).toFixed(1)}s`
|
|
181
|
+
: `${Math.round(row.durationMs)} ms`;
|
|
182
|
+
const rawDesc = row.description.length > 80 ? row.description.slice(0, 77) + '…' : row.description;
|
|
183
|
+
const desc = escapeMarkdownTableCell(rawDesc);
|
|
184
|
+
const line = `| ${escapeMarkdownTableCell(row.status)} | ${escapeMarkdownTableCell(durationStr)} | ${desc} |\n`;
|
|
185
|
+
if (Buffer.byteLength(out + line, 'utf8') > byteLimit)
|
|
186
|
+
break;
|
|
187
|
+
out += line;
|
|
188
|
+
shown++;
|
|
189
|
+
}
|
|
190
|
+
if (shown < testResults.length) {
|
|
191
|
+
out += `| … | … | … and ${testResults.length - shown} more |\n`;
|
|
192
|
+
}
|
|
193
|
+
out += `\n`;
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function summarizeTestOutcomes(testResults) {
|
|
197
|
+
let passed = 0;
|
|
198
|
+
let failed = 0;
|
|
199
|
+
let skipped = 0;
|
|
200
|
+
let inconclusive = 0;
|
|
201
|
+
let totalDurationMs = 0;
|
|
202
|
+
for (const t of testResults) {
|
|
203
|
+
totalDurationMs += t.durationMs;
|
|
204
|
+
switch (t.status) {
|
|
205
|
+
case '✅':
|
|
206
|
+
passed++;
|
|
207
|
+
break;
|
|
208
|
+
case '❌':
|
|
209
|
+
failed++;
|
|
210
|
+
break;
|
|
211
|
+
case '⏭️':
|
|
212
|
+
skipped++;
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
inconclusive++;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { passed, failed, skipped, inconclusive, totalDurationMs };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Rich unit-test markdown block used by workflow summary and stdout.
|
|
223
|
+
* Keeps byte-budget behavior and truncation hints.
|
|
224
|
+
*/
|
|
225
|
+
function buildUnitTestJobSummaryMarkdown(testResults, byteLimit, prefix) {
|
|
226
|
+
if (testResults.length === 0)
|
|
227
|
+
return '';
|
|
228
|
+
const p = prefix ?? '';
|
|
229
|
+
let out = p + '### Unit test results\n\n';
|
|
230
|
+
const counts = summarizeTestOutcomes(testResults);
|
|
231
|
+
const durationStr = counts.totalDurationMs >= 1000
|
|
232
|
+
? `${(counts.totalDurationMs / 1000).toFixed(1)}s`
|
|
233
|
+
: `${Math.round(counts.totalDurationMs)} ms`;
|
|
234
|
+
out += `**${testResults.length}** tests - **${counts.passed}** ✓, **${counts.failed}** ✗, **${counts.skipped}** skipped, **${counts.inconclusive}** inconclusive - **${durationStr}** total\n\n`;
|
|
235
|
+
out += '| Test | Result | Time | Message |\n';
|
|
236
|
+
out += '| --- | --- | --- | --- |\n';
|
|
237
|
+
const ordered = [...testResults].sort((a, b) => {
|
|
238
|
+
const aFail = a.status === '❌' ? 0 : 1;
|
|
239
|
+
const bFail = b.status === '❌' ? 0 : 1;
|
|
240
|
+
if (aFail !== bFail)
|
|
241
|
+
return aFail - bFail;
|
|
242
|
+
return b.durationMs - a.durationMs;
|
|
243
|
+
});
|
|
244
|
+
let shown = 0;
|
|
245
|
+
for (const row of ordered) {
|
|
246
|
+
const durationText = row.durationMs >= 1000 ? `${(row.durationMs / 1000).toFixed(1)}s` : `${Math.round(row.durationMs)} ms`;
|
|
247
|
+
const loc = row.file && row.line ? ` (${row.file}:${row.line})` : '';
|
|
248
|
+
const rawName = `${row.description}${loc}`;
|
|
249
|
+
const name = escapeMarkdownTableCell(rawName.length > 90 ? `${rawName.slice(0, 87)}…` : rawName);
|
|
250
|
+
const msgRaw = (row.message ?? '').replace(/\r?\n/g, ' ').trim();
|
|
251
|
+
const msg = escapeMarkdownTableCell(msgRaw.length > 120 ? `${msgRaw.slice(0, 117)}…` : msgRaw);
|
|
252
|
+
const line = `| ${name} | ${escapeMarkdownTableCell(row.status)} | ${escapeMarkdownTableCell(durationText)} | ${msg} |\n`;
|
|
253
|
+
if (Buffer.byteLength(out + line, 'utf8') > byteLimit)
|
|
254
|
+
break;
|
|
255
|
+
out += line;
|
|
256
|
+
shown++;
|
|
257
|
+
}
|
|
258
|
+
if (shown < ordered.length) {
|
|
259
|
+
out += `| … | … | … | … and ${ordered.length - shown} more |\n`;
|
|
260
|
+
}
|
|
261
|
+
out += '\n';
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function buildActionTimelineTableMarkdown(completedActions, byteLimit, prefix) {
|
|
265
|
+
if (completedActions.length === 0)
|
|
266
|
+
return { markdown: '', truncated: false };
|
|
267
|
+
const p = prefix ?? '';
|
|
268
|
+
let out = p + '| Status | Duration | Errors | Action |\n';
|
|
269
|
+
out += '| --- | --- | --- | --- |\n';
|
|
270
|
+
let shown = 0;
|
|
271
|
+
for (const a of completedActions) {
|
|
272
|
+
const durationMs = a.duration ?? (a.durationMicroseconds != null ? a.durationMicroseconds / 1000 : undefined);
|
|
273
|
+
const errCount = Array.isArray(a.errors) ? a.errors.length : 0;
|
|
274
|
+
const status = errCount > 0 ? '❌' : '✅';
|
|
275
|
+
const action = truncateStr(toSingleLineText(a.description || a.name || '-'), 120);
|
|
276
|
+
const row = `| ${escapeMarkdownTableCell(status)} | ${escapeMarkdownTableCell(formatDurationMsForSummary(durationMs))} | ${errCount} | ${escapeMarkdownTableCell(action)} |\n`;
|
|
277
|
+
if (Buffer.byteLength(out + row, 'utf8') > byteLimit)
|
|
278
|
+
break;
|
|
279
|
+
out += row;
|
|
280
|
+
shown++;
|
|
281
|
+
}
|
|
282
|
+
const truncated = shown < completedActions.length;
|
|
283
|
+
if (truncated) {
|
|
284
|
+
out += `| ... | ... | ... | ... and ${completedActions.length - shown} more actions |\n`;
|
|
285
|
+
}
|
|
286
|
+
out += '\n';
|
|
287
|
+
return { markdown: out, truncated };
|
|
288
|
+
}
|
|
289
|
+
function buildActionTimelineCodeblockMarkdown(completedActions, byteLimit, prefix) {
|
|
290
|
+
if (completedActions.length === 0)
|
|
291
|
+
return '';
|
|
292
|
+
const p = prefix ?? '';
|
|
293
|
+
let out = p + '```text\n';
|
|
294
|
+
let timelineShown = 0;
|
|
295
|
+
for (const a of completedActions) {
|
|
296
|
+
const durationMs = a.duration ?? (a.durationMicroseconds != null ? a.durationMicroseconds / 1000 : undefined);
|
|
297
|
+
const errCount = Array.isArray(a.errors) ? a.errors.length : 0;
|
|
298
|
+
const status = errCount > 0 ? '❌' : '✅';
|
|
299
|
+
const desc = toSingleLineText(a.description || a.name || '-');
|
|
300
|
+
const durationStr = formatDurationMsForSummary(durationMs);
|
|
301
|
+
const row = `${status} ${durationStr} ${errCount} - ${desc}\n`;
|
|
302
|
+
if (Buffer.byteLength(out + row, 'utf8') > byteLimit)
|
|
303
|
+
break;
|
|
304
|
+
out += row;
|
|
305
|
+
timelineShown++;
|
|
306
|
+
}
|
|
307
|
+
if (timelineShown < completedActions.length) {
|
|
308
|
+
out += `... and ${completedActions.length - timelineShown} more actions\n`;
|
|
309
|
+
}
|
|
310
|
+
out += '```\n\n';
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
function truncateStr(s, max) {
|
|
314
|
+
return s.length <= max ? s : s.slice(0, max) + '…';
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Truncates s to fit within maxBytes in UTF-8. If truncated, appends an ellipsis (…).
|
|
318
|
+
* If s already fits, returns s unchanged.
|
|
319
|
+
* Exported for unit tests.
|
|
320
|
+
*/
|
|
321
|
+
function truncateStringToUtf8ByteLength(s, maxBytes) {
|
|
322
|
+
if (maxBytes <= 0)
|
|
323
|
+
return '';
|
|
324
|
+
const ellipsis = '…';
|
|
325
|
+
const ellBytes = Buffer.byteLength(ellipsis, 'utf8');
|
|
326
|
+
if (Buffer.byteLength(s, 'utf8') <= maxBytes)
|
|
327
|
+
return s;
|
|
328
|
+
if (maxBytes <= ellBytes) {
|
|
329
|
+
let end = 0;
|
|
330
|
+
for (let i = 1; i <= s.length; i++) {
|
|
331
|
+
const sub = s.slice(0, i);
|
|
332
|
+
if (Buffer.byteLength(sub, 'utf8') > maxBytes)
|
|
333
|
+
break;
|
|
334
|
+
end = i;
|
|
335
|
+
}
|
|
336
|
+
return s.slice(0, end);
|
|
337
|
+
}
|
|
338
|
+
let low = 0;
|
|
339
|
+
let high = s.length;
|
|
340
|
+
while (low < high) {
|
|
341
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
342
|
+
const sub = s.slice(0, mid);
|
|
343
|
+
if (Buffer.byteLength(sub, 'utf8') + ellBytes <= maxBytes)
|
|
344
|
+
low = mid;
|
|
345
|
+
else
|
|
346
|
+
high = mid - 1;
|
|
347
|
+
}
|
|
348
|
+
return s.slice(0, low) + ellipsis;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Appends one formatted log line per entry, truncating each line only when it would exceed the
|
|
352
|
+
* remaining bytes in the workflow summary (byteLimit is total cap for the final string starting from out).
|
|
353
|
+
*/
|
|
354
|
+
function appendWorkflowSummaryLogLines(out, entries, byteLimit) {
|
|
355
|
+
let o = out;
|
|
356
|
+
let shown = 0;
|
|
357
|
+
const newline = '\n';
|
|
358
|
+
const nlBytes = Buffer.byteLength(newline, 'utf8');
|
|
359
|
+
for (let i = 0; i < entries.length; i++) {
|
|
360
|
+
const entry = entries[i];
|
|
361
|
+
if (entry === undefined) {
|
|
362
|
+
return { out: o, shown, omitted: entries.length - shown };
|
|
363
|
+
}
|
|
364
|
+
const room = byteLimit - Buffer.byteLength(o, 'utf8');
|
|
365
|
+
if (room < nlBytes) {
|
|
366
|
+
return { out: o, shown, omitted: entries.length - shown };
|
|
367
|
+
}
|
|
368
|
+
const rawLine = formatLogEntryLine(entry, Number.POSITIVE_INFINITY).replace(/\n$/, '');
|
|
369
|
+
const maxContentBytes = room - nlBytes;
|
|
370
|
+
const lineBody = Buffer.byteLength(rawLine, 'utf8') <= maxContentBytes
|
|
371
|
+
? rawLine
|
|
372
|
+
: truncateStringToUtf8ByteLength(rawLine, maxContentBytes);
|
|
373
|
+
o += lineBody + newline;
|
|
374
|
+
shown++;
|
|
375
|
+
}
|
|
376
|
+
return { out: o, shown, omitted: 0 };
|
|
377
|
+
}
|
|
378
|
+
function toSingleLineText(value) {
|
|
379
|
+
return value
|
|
380
|
+
.replace(/\r?\n+/g, ' ')
|
|
381
|
+
.replace(/\s+/g, ' ')
|
|
382
|
+
.trim();
|
|
383
|
+
}
|
|
384
|
+
function formatDurationMsForSummary(ms) {
|
|
385
|
+
if (ms === undefined || !Number.isFinite(ms)) {
|
|
386
|
+
return '-';
|
|
387
|
+
}
|
|
388
|
+
if (ms < 1000) {
|
|
389
|
+
return `${Math.round(ms)}ms`;
|
|
390
|
+
}
|
|
391
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
392
|
+
}
|
|
393
|
+
/** Unity/CI noise shown in logs; omit from workflow summary foldouts and counts. */
|
|
394
|
+
const SUMMARY_NOISE_ACCESS_TOKEN = 'Access token is unavailable; failed to update';
|
|
395
|
+
/**
|
|
396
|
+
* Removes known noise phrases from a log message for summary display.
|
|
397
|
+
* Exported for unit tests.
|
|
398
|
+
*/
|
|
399
|
+
function stripSummaryNoiseFromLogMessage(message) {
|
|
400
|
+
const flat = toSingleLineText(message);
|
|
401
|
+
if (!flat)
|
|
402
|
+
return '';
|
|
403
|
+
const pattern = SUMMARY_NOISE_ACCESS_TOKEN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
404
|
+
const out = flat.replace(new RegExp(pattern, 'gi'), ' ').replace(/\s+/g, ' ').trim();
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
function filterNoiseFromSummaryLogEntries(entries) {
|
|
408
|
+
const out = [];
|
|
409
|
+
for (const e of entries) {
|
|
410
|
+
const stripped = stripSummaryNoiseFromLogMessage(e.message || '');
|
|
411
|
+
if (stripped === '')
|
|
412
|
+
continue;
|
|
413
|
+
const originalFlat = toSingleLineText(e.message || '');
|
|
414
|
+
if (stripped !== originalFlat) {
|
|
415
|
+
out.push({ ...e, message: stripped });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
out.push(e);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
function renderBuildActionsFoldoutMarkdown(completedActions, maxBytes) {
|
|
424
|
+
const n = completedActions.length;
|
|
425
|
+
const open = `<details open><summary>Build actions (${n})</summary>\n\n`;
|
|
426
|
+
const close = `</details>\n\n`;
|
|
427
|
+
const overhead = Buffer.byteLength(open + close, 'utf8');
|
|
428
|
+
const innerBudget = Math.max(0, maxBytes - overhead);
|
|
429
|
+
const table = buildActionTimelineTableMarkdown(completedActions, innerBudget, '');
|
|
430
|
+
const inner = !table.truncated
|
|
431
|
+
? table.markdown
|
|
432
|
+
: buildActionTimelineCodeblockMarkdown(completedActions, innerBudget, '');
|
|
433
|
+
return open + inner + close;
|
|
434
|
+
}
|
|
435
|
+
/** Paths to treat as Unity engine (omit from summary when using heuristic filter). */
|
|
436
|
+
const UNITY_ENGINE_PATH_PREFIXES = [
|
|
437
|
+
'Runtime/',
|
|
438
|
+
'./Runtime/',
|
|
439
|
+
'Modules/',
|
|
440
|
+
'./Modules/',
|
|
441
|
+
];
|
|
442
|
+
/**
|
|
443
|
+
* Normalizes a log message for display by stripping a redundant file:line prefix
|
|
444
|
+
* when it matches the entry's file/line so the path appears only once.
|
|
445
|
+
* Returns the normalized message and optional column if present in the prefix.
|
|
446
|
+
*/
|
|
447
|
+
function normalizeMessageForDisplay(message, file, line) {
|
|
448
|
+
const trimmed = message.trim();
|
|
449
|
+
const normFile = file.replace(/\\/g, '/');
|
|
450
|
+
if (!normFile && line === undefined)
|
|
451
|
+
return { message: trimmed };
|
|
452
|
+
// path(line,col): e.g. Assets/File.cs(2,8): error ...
|
|
453
|
+
const parenColon = trimmed.match(/^(.+?)\((\d+),(\d+)\):\s*/);
|
|
454
|
+
if (parenColon && parenColon[1] != null && parenColon[2] != null && parenColon[3] != null) {
|
|
455
|
+
const fullMatch = parenColon[0];
|
|
456
|
+
const msgPath = parenColon[1].replace(/\\/g, '/');
|
|
457
|
+
const msgLine = parseInt(parenColon[2], 10);
|
|
458
|
+
const msgCol = parseInt(parenColon[3], 10);
|
|
459
|
+
const pathMatches = msgPath === normFile || normFile.endsWith(msgPath) || msgPath.endsWith(normFile);
|
|
460
|
+
if (pathMatches && (line === undefined || line === msgLine)) {
|
|
461
|
+
return { message: trimmed.slice(fullMatch.length).trim(), column: msgCol };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// path(line): e.g. Assets/File.cs(2): ...
|
|
465
|
+
const parenOnly = trimmed.match(/^(.+?)\((\d+)\):\s*/);
|
|
466
|
+
if (parenOnly && parenOnly[1] != null && parenOnly[2] != null) {
|
|
467
|
+
const fullMatch = parenOnly[0];
|
|
468
|
+
const msgPath = parenOnly[1].replace(/\\/g, '/');
|
|
469
|
+
const msgLine = parseInt(parenOnly[2], 10);
|
|
470
|
+
const pathMatches = msgPath === normFile || normFile.endsWith(msgPath) || msgPath.endsWith(normFile);
|
|
471
|
+
if (pathMatches && (line === undefined || line === msgLine)) {
|
|
472
|
+
return { message: trimmed.slice(fullMatch.length).trim() };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// path:line: e.g. path/to/file.cs:10:
|
|
476
|
+
const pathLineColon = trimmed.match(/^(.+?):(\d+):\s*/);
|
|
477
|
+
if (pathLineColon && pathLineColon[1] != null && pathLineColon[2] != null) {
|
|
478
|
+
const fullMatch = pathLineColon[0];
|
|
479
|
+
const msgPath = pathLineColon[1].replace(/\\/g, '/');
|
|
480
|
+
const msgLine = parseInt(pathLineColon[2], 10);
|
|
481
|
+
const pathMatches = msgPath === normFile || normFile.endsWith(msgPath) || msgPath.endsWith(normFile);
|
|
482
|
+
if (pathMatches && (line === undefined || line === msgLine)) {
|
|
483
|
+
return { message: trimmed.slice(fullMatch.length).trim() };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { message: trimmed };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* One line per entry: path(line,col): <message> or path(line): <message> when column is missing.
|
|
490
|
+
* When file/line are missing, outputs: - <message>.
|
|
491
|
+
*/
|
|
492
|
+
function formatLogEntryLine(e, maxMsgLen = Number.POSITIVE_INFINITY) {
|
|
493
|
+
const file = (e.file || e.fileName || '').replace(/\\/g, '/');
|
|
494
|
+
const line = e.line ?? e.lineNumber;
|
|
495
|
+
const hasLocation = file && (line !== undefined && line > 0);
|
|
496
|
+
const rawMsg = toSingleLineText(e.message || '');
|
|
497
|
+
const { message: normalizedMsg, column } = hasLocation
|
|
498
|
+
? normalizeMessageForDisplay(rawMsg, file, line)
|
|
499
|
+
: { message: rawMsg, column: undefined };
|
|
500
|
+
const msg = Number.isFinite(maxMsgLen) && maxMsgLen >= 0 && maxMsgLen < Number.POSITIVE_INFINITY
|
|
501
|
+
? truncateStr(normalizedMsg, maxMsgLen)
|
|
502
|
+
: normalizedMsg;
|
|
503
|
+
if (hasLocation) {
|
|
504
|
+
const loc = column !== undefined ? `${file}(${line},${column})` : `${file}(${line})`;
|
|
505
|
+
return `${loc}: ${msg}\n`;
|
|
506
|
+
}
|
|
507
|
+
return `${msg}\n`;
|
|
508
|
+
}
|
|
38
509
|
var LogLevel;
|
|
39
510
|
(function (LogLevel) {
|
|
40
511
|
LogLevel["DEBUG"] = "debug";
|
|
@@ -46,22 +517,16 @@ var LogLevel;
|
|
|
46
517
|
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
47
518
|
class Logger {
|
|
48
519
|
logLevel = LogLevel.INFO;
|
|
49
|
-
|
|
520
|
+
_provider;
|
|
50
521
|
static instance = new Logger();
|
|
51
522
|
constructor() {
|
|
523
|
+
this._provider = process.env.GITHUB_ACTIONS === 'true'
|
|
524
|
+
? new github_actions_ci_1.GitHubActionsLoggerProvider()
|
|
525
|
+
: new logger_provider_1.LocalCliLoggerProvider();
|
|
52
526
|
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
53
|
-
this._ci = 'GITHUB_ACTIONS';
|
|
54
527
|
this.logLevel = process.env.ACTIONS_STEP_DEBUG === 'true' ? LogLevel.DEBUG : LogLevel.CI;
|
|
55
528
|
}
|
|
56
529
|
}
|
|
57
|
-
printLine(message, lineColor, optionalParams = []) {
|
|
58
|
-
if (lineColor && lineColor.length > 0) {
|
|
59
|
-
process.stdout.write(`${lineColor}${message}\x1b[0m\n`, ...optionalParams);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
process.stdout.write(`${message}\n`, ...optionalParams);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
530
|
/**
|
|
66
531
|
* Logs a message to the console.
|
|
67
532
|
* @param level The log level for this message.
|
|
@@ -70,78 +535,24 @@ class Logger {
|
|
|
70
535
|
*/
|
|
71
536
|
log(level, message, optionalParams = []) {
|
|
72
537
|
if (this.shouldLog(level)) {
|
|
73
|
-
|
|
74
|
-
case 'GITHUB_ACTIONS': {
|
|
75
|
-
switch (level) {
|
|
76
|
-
case LogLevel.DEBUG: {
|
|
77
|
-
message.toString().split('\n').forEach((line) => {
|
|
78
|
-
process.stdout.write(`::debug::${line}\n`, ...optionalParams);
|
|
79
|
-
});
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
case LogLevel.CI:
|
|
83
|
-
case LogLevel.INFO: {
|
|
84
|
-
process.stdout.write(`${message}\n`, ...optionalParams);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
default: {
|
|
88
|
-
process.stdout.write(`::${level}::${message}\n`, ...optionalParams);
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
default: {
|
|
95
|
-
const stringColor = {
|
|
96
|
-
[LogLevel.DEBUG]: '\x1b[35m', // Purple
|
|
97
|
-
[LogLevel.INFO]: undefined, // No color / White
|
|
98
|
-
[LogLevel.CI]: undefined, // No color / White
|
|
99
|
-
[LogLevel.UTP]: undefined, // No color / White
|
|
100
|
-
[LogLevel.WARN]: '\x1b[33m', // Yellow
|
|
101
|
-
[LogLevel.ERROR]: '\x1b[31m', // Red
|
|
102
|
-
}[level] || undefined; // Default to no color / White
|
|
103
|
-
this.printLine(message, stringColor, optionalParams);
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
538
|
+
this._provider.log(level, message, optionalParams);
|
|
107
539
|
}
|
|
108
540
|
}
|
|
109
541
|
/**
|
|
110
542
|
* Starts a log group. In CI environments that support grouping, this will create a collapsible group.
|
|
111
543
|
*/
|
|
112
544
|
startGroup(message, optionalParams = [], logLevel = LogLevel.INFO) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// then print the rest of the lines inside the group in cyan color
|
|
117
|
-
const firstLine = message.toString().split('\n')[0];
|
|
118
|
-
const restLines = message.toString().split('\n').slice(1);
|
|
119
|
-
process.stdout.write(`::group::${firstLine}\n`, ...optionalParams);
|
|
120
|
-
restLines.forEach(line => {
|
|
121
|
-
this.printLine(line, '\x1b[36m', ...optionalParams);
|
|
122
|
-
});
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
default: {
|
|
126
|
-
// No grouping in standard console
|
|
127
|
-
this.log(logLevel, message, optionalParams);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
545
|
+
if (this._provider.isCi) {
|
|
546
|
+
this._provider.startGroup(message, optionalParams);
|
|
547
|
+
return;
|
|
130
548
|
}
|
|
549
|
+
this.log(logLevel, message, optionalParams);
|
|
131
550
|
}
|
|
132
551
|
/**
|
|
133
552
|
* Ends a log group. In CI environments that support grouping, this will end the current group.
|
|
134
553
|
*/
|
|
135
554
|
endGroup() {
|
|
136
|
-
|
|
137
|
-
case 'GITHUB_ACTIONS': {
|
|
138
|
-
process.stdout.write(`::endgroup::\n`);
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
default: {
|
|
142
|
-
break; // No grouping in standard console
|
|
143
|
-
}
|
|
144
|
-
}
|
|
555
|
+
this._provider.endGroup();
|
|
145
556
|
}
|
|
146
557
|
/**
|
|
147
558
|
* Logs a message with CI level.
|
|
@@ -175,59 +586,37 @@ class Logger {
|
|
|
175
586
|
* @param title The title of the annotation.
|
|
176
587
|
*/
|
|
177
588
|
annotate(logLevel, message, file, line, endLine, column, endColumn, title) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const parts = [];
|
|
190
|
-
const appendPart = (key, value) => {
|
|
191
|
-
if (value === undefined || value === null) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const stringValue = value.toString();
|
|
195
|
-
if (stringValue.length === 0) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
parts.push(`${key}=${this.escapeGitHubCommandValue(stringValue)}`);
|
|
199
|
-
};
|
|
200
|
-
appendPart('file', file);
|
|
201
|
-
if (line !== undefined && line > 0) {
|
|
202
|
-
appendPart('line', line);
|
|
203
|
-
}
|
|
204
|
-
if (endLine !== undefined && endLine > 0) {
|
|
205
|
-
appendPart('endLine', endLine);
|
|
206
|
-
}
|
|
207
|
-
if (column !== undefined && column > 0) {
|
|
208
|
-
appendPart('col', column);
|
|
209
|
-
}
|
|
210
|
-
if (endColumn !== undefined && endColumn > 0) {
|
|
211
|
-
appendPart('endColumn', endColumn);
|
|
212
|
-
}
|
|
213
|
-
appendPart('title', title);
|
|
214
|
-
const metadata = parts.length > 0 ? ` ${parts.join(',')}` : '';
|
|
215
|
-
annotation = `::${level}${metadata}::${this.escapeGitHubCommandValue(message)}`;
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
589
|
+
const level = {
|
|
590
|
+
[LogLevel.CI]: 'notice',
|
|
591
|
+
[LogLevel.INFO]: 'notice',
|
|
592
|
+
[LogLevel.DEBUG]: 'notice',
|
|
593
|
+
[LogLevel.UTP]: 'notice',
|
|
594
|
+
[LogLevel.WARN]: 'warning',
|
|
595
|
+
[LogLevel.ERROR]: 'error',
|
|
596
|
+
}[logLevel] ?? 'notice';
|
|
597
|
+
const options = {};
|
|
598
|
+
if (file !== undefined && file !== '') {
|
|
599
|
+
options.file = file;
|
|
218
600
|
}
|
|
219
|
-
if (
|
|
220
|
-
|
|
601
|
+
if (line !== undefined) {
|
|
602
|
+
options.line = line;
|
|
221
603
|
}
|
|
222
|
-
|
|
223
|
-
|
|
604
|
+
if (endLine !== undefined) {
|
|
605
|
+
options.endLine = endLine;
|
|
224
606
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.
|
|
230
|
-
|
|
607
|
+
if (column !== undefined) {
|
|
608
|
+
options.column = column;
|
|
609
|
+
}
|
|
610
|
+
if (endColumn !== undefined) {
|
|
611
|
+
options.endColumn = endColumn;
|
|
612
|
+
}
|
|
613
|
+
if (title !== undefined && title !== '') {
|
|
614
|
+
options.title = title;
|
|
615
|
+
}
|
|
616
|
+
const backendLevel = level === 'error'
|
|
617
|
+
? github_actions_ci_1.GitHubAnnotationLevel.Error
|
|
618
|
+
: (level === 'warning' ? github_actions_ci_1.GitHubAnnotationLevel.Warning : github_actions_ci_1.GitHubAnnotationLevel.Notice);
|
|
619
|
+
this._provider.annotate(backendLevel, message, options);
|
|
231
620
|
}
|
|
232
621
|
shouldLog(level) {
|
|
233
622
|
if (level === LogLevel.CI) {
|
|
@@ -241,12 +630,7 @@ class Logger {
|
|
|
241
630
|
* @param message The string to mask.
|
|
242
631
|
*/
|
|
243
632
|
CI_mask(message) {
|
|
244
|
-
|
|
245
|
-
case 'GITHUB_ACTIONS': {
|
|
246
|
-
process.stdout.write(`::add-mask::${message}\n`);
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
633
|
+
this._provider.mask(message);
|
|
250
634
|
}
|
|
251
635
|
/**
|
|
252
636
|
* Masks a credential value in CI environments before it appears in logs.
|
|
@@ -283,7 +667,8 @@ class Logger {
|
|
|
283
667
|
'config',
|
|
284
668
|
'organization',
|
|
285
669
|
'username',
|
|
286
|
-
'servicesConfig'
|
|
670
|
+
'servicesConfig',
|
|
671
|
+
'serviceaccountkey',
|
|
287
672
|
];
|
|
288
673
|
/**
|
|
289
674
|
* Scrubs sensitive information from an object for safe logging.
|
|
@@ -325,44 +710,182 @@ class Logger {
|
|
|
325
710
|
* @param value The value of the environment variable.
|
|
326
711
|
*/
|
|
327
712
|
CI_setEnvironmentVariable(name, value) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
713
|
+
this._provider.setEnvironmentVariable(name, value);
|
|
714
|
+
}
|
|
715
|
+
CI_setOutput(name, value) {
|
|
716
|
+
this._provider.setOutput(name, value);
|
|
717
|
+
}
|
|
718
|
+
static formatDurationMs(ms) {
|
|
719
|
+
if (ms === undefined || !Number.isFinite(ms)) {
|
|
720
|
+
return '-';
|
|
721
|
+
}
|
|
722
|
+
if (ms < 1000) {
|
|
723
|
+
return `${Math.round(ms)}ms`;
|
|
724
|
+
}
|
|
725
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
726
|
+
}
|
|
727
|
+
static truncateStr(s, max) {
|
|
728
|
+
return s.length <= max ? s : s.slice(0, max) + '…';
|
|
729
|
+
}
|
|
730
|
+
static truncateSummaryToByteLimit(summary, byteLimit) {
|
|
731
|
+
const footer = `\n***Summary truncated due to size limits.***\n`;
|
|
732
|
+
const footerSize = Buffer.byteLength(footer, 'utf8');
|
|
733
|
+
const lines = summary.split('\n');
|
|
734
|
+
let rebuilt = '';
|
|
735
|
+
for (const line of lines) {
|
|
736
|
+
const nextSize = Buffer.byteLength(rebuilt + line + '\n', 'utf8') + footerSize;
|
|
737
|
+
if (nextSize > byteLimit) {
|
|
336
738
|
break;
|
|
337
739
|
}
|
|
740
|
+
rebuilt += `${line}\n`;
|
|
338
741
|
}
|
|
742
|
+
return rebuilt + footer;
|
|
339
743
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Returns the markdown byte limit for a given output target.
|
|
746
|
+
* Workflow summary may be backend constrained; stdout is intentionally uncapped.
|
|
747
|
+
*/
|
|
748
|
+
getMarkdownByteLimit(target) {
|
|
749
|
+
return this._provider.getMarkdownByteLimit(target);
|
|
750
|
+
}
|
|
751
|
+
CI_appendWorkflowSummary(name, telemetry, options) {
|
|
752
|
+
if (telemetry.length === 0) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (this.getMarkdownByteLimit('workflow-summary') === Number.POSITIVE_INFINITY) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const excludedTypes = new Set(['MemoryLeaks', 'MemoryLeak']);
|
|
759
|
+
const filtered = telemetry.filter(entry => !excludedTypes.has(entry.type || ''));
|
|
760
|
+
if (filtered.length === 0) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const completedActions = filtered.filter(e => e.type === 'Action' && e.phase === 'End');
|
|
764
|
+
const testResults = collectTestResults(filtered);
|
|
765
|
+
const additional = options?.additionalLogEntries ?? [];
|
|
766
|
+
const merged = mergeLogEntriesPreferringSeverity([
|
|
767
|
+
...buildMergedLogList(filtered),
|
|
768
|
+
...additional.filter(e => e.type === 'LogEntry' || e.type === 'Compiler'),
|
|
769
|
+
]);
|
|
770
|
+
const pathFiltered = filterMergedByPath(merged, options);
|
|
771
|
+
const summaryLogs = filterNoiseFromSummaryLogEntries(pathFiltered);
|
|
772
|
+
const bySeverity = groupBySeverity(summaryLogs);
|
|
773
|
+
const limit = this.getMarkdownByteLimit('workflow-summary');
|
|
774
|
+
const builders = [
|
|
775
|
+
() => this.buildSummaryTimelineAndMergedLog(name, completedActions, bySeverity, testResults, limit),
|
|
776
|
+
() => this.buildSummaryCollapsibleWithMergedLog(name, completedActions, bySeverity, testResults, limit),
|
|
777
|
+
() => this.buildSummaryTimelineAndCounts(name, completedActions, summaryLogs.length, testResults, limit),
|
|
778
|
+
];
|
|
779
|
+
let summary = '';
|
|
780
|
+
for (const build of builders) {
|
|
781
|
+
summary = build();
|
|
782
|
+
if (Buffer.byteLength(summary, 'utf8') <= limit) {
|
|
349
783
|
break;
|
|
350
784
|
}
|
|
351
785
|
}
|
|
786
|
+
if (Buffer.byteLength(summary, 'utf8') > limit) {
|
|
787
|
+
summary = Logger.truncateSummaryToByteLimit(summary, limit);
|
|
788
|
+
}
|
|
789
|
+
this._provider.appendStepSummary(summary);
|
|
352
790
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Builds summary: stats + action table + unit-test block + severity foldouts.
|
|
793
|
+
*/
|
|
794
|
+
buildSummaryTimelineAndMergedLog(name, completedActions, bySeverity, testResults, byteLimit) {
|
|
795
|
+
let out = `## ${name} Summary\n\n`;
|
|
796
|
+
const totalDurationMs = completedActions.reduce((sum, a) => sum + (a.duration ?? (a.durationMicroseconds != null ? a.durationMicroseconds / 1000 : 0)), 0);
|
|
797
|
+
const totalSec = totalDurationMs / 1000;
|
|
798
|
+
const totalStr = totalSec >= 60 ? `${Math.round(totalSec / 60)}m ${Math.round(totalSec % 60)}s` : `${totalSec.toFixed(1)}s`;
|
|
799
|
+
out += `Errors: ${bySeverity.errorCritical.length}\n`;
|
|
800
|
+
out += `Warnings: ${bySeverity.warning.length}\n`;
|
|
801
|
+
out += `Total duration: ${totalStr}\n`;
|
|
802
|
+
out += `Actions: ${completedActions.length}\n`;
|
|
803
|
+
if (testResults.length > 0) {
|
|
804
|
+
out += `Tests: ${testResults.length}\n`;
|
|
805
|
+
}
|
|
806
|
+
out += '\n';
|
|
807
|
+
if (completedActions.length > 0) {
|
|
808
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
809
|
+
out += renderBuildActionsFoldoutMarkdown(completedActions, remaining);
|
|
810
|
+
}
|
|
811
|
+
if (testResults.length > 0) {
|
|
812
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
813
|
+
out += buildUnitTestJobSummaryMarkdown(testResults, remaining, '');
|
|
814
|
+
}
|
|
815
|
+
const limit = byteLimit;
|
|
816
|
+
const appendFoldout = (title, entries, dropSuffix, openByDefault) => {
|
|
817
|
+
if (entries.length === 0)
|
|
818
|
+
return;
|
|
819
|
+
const openAttr = openByDefault ? ' open' : '';
|
|
820
|
+
out += `<details${openAttr}><summary>${title} (${entries.length})</summary>\n\n`;
|
|
821
|
+
out += '```text\n';
|
|
822
|
+
const appended = appendWorkflowSummaryLogLines(out, entries, limit);
|
|
823
|
+
out = appended.out;
|
|
824
|
+
if (appended.omitted > 0) {
|
|
825
|
+
out += `... and ${appended.omitted} more ${dropSuffix}\n`;
|
|
364
826
|
}
|
|
827
|
+
out += '```\n\n';
|
|
828
|
+
out += `</details>\n\n`;
|
|
829
|
+
};
|
|
830
|
+
appendFoldout('Error', bySeverity.errorCritical, '(see annotations).', true);
|
|
831
|
+
appendFoldout('Warning', bySeverity.warning, '(truncated; see full log).');
|
|
832
|
+
appendFoldout('Info', bySeverity.info, '(truncated; see full log).');
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Builds summary with timeline in a <details> and merged log foldouts by severity.
|
|
837
|
+
* Used when primary builder would exceed size limit.
|
|
838
|
+
*/
|
|
839
|
+
buildSummaryCollapsibleWithMergedLog(name, completedActions, bySeverity, testResults, byteLimit) {
|
|
840
|
+
let out = `## ${name} Summary\n\n`;
|
|
841
|
+
if (completedActions.length > 0) {
|
|
842
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
843
|
+
out += renderBuildActionsFoldoutMarkdown(completedActions, remaining);
|
|
844
|
+
}
|
|
845
|
+
if (testResults.length > 0) {
|
|
846
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
847
|
+
out += buildUnitTestJobSummaryMarkdown(testResults, remaining, '');
|
|
848
|
+
}
|
|
849
|
+
const limit = byteLimit;
|
|
850
|
+
const appendFoldout = (title, entries, dropSuffix, openByDefault) => {
|
|
851
|
+
if (entries.length === 0)
|
|
852
|
+
return;
|
|
853
|
+
const openAttr = openByDefault ? ' open' : '';
|
|
854
|
+
out += `<details${openAttr}><summary>${title} (${entries.length})</summary>\n\n`;
|
|
855
|
+
out += '```text\n';
|
|
856
|
+
const appended = appendWorkflowSummaryLogLines(out, entries, limit);
|
|
857
|
+
out = appended.out;
|
|
858
|
+
if (appended.omitted > 0)
|
|
859
|
+
out += `... and ${appended.omitted} more ${dropSuffix}\n`;
|
|
860
|
+
out += '```\n\n';
|
|
861
|
+
out += `</details>\n\n`;
|
|
862
|
+
};
|
|
863
|
+
appendFoldout('Error', bySeverity.errorCritical, '(see annotations).', true);
|
|
864
|
+
appendFoldout('Warning', bySeverity.warning, '(truncated; see full log).');
|
|
865
|
+
appendFoldout('Info', bySeverity.info, '(truncated; see full log).');
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Fallback: list timeline (when actions exist) + unit-test block (when present) + compact count lines.
|
|
870
|
+
* Used when even collapsible summary would exceed 1 MB.
|
|
871
|
+
*/
|
|
872
|
+
buildSummaryTimelineAndCounts(name, completedActions, logCount, testResults, byteLimit) {
|
|
873
|
+
let out = `## ${name} Summary\n\n`;
|
|
874
|
+
if (completedActions.length > 0) {
|
|
875
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
876
|
+
out += renderBuildActionsFoldoutMarkdown(completedActions, remaining);
|
|
877
|
+
}
|
|
878
|
+
if (testResults.length > 0) {
|
|
879
|
+
const remaining = byteLimit - Buffer.byteLength(out, 'utf8');
|
|
880
|
+
out += buildUnitTestJobSummaryMarkdown(testResults, remaining, '');
|
|
881
|
+
}
|
|
882
|
+
out += `Log entries: ${logCount}\n`;
|
|
883
|
+
out += `Actions: ${completedActions.length}\n`;
|
|
884
|
+
if (testResults.length > 0) {
|
|
885
|
+
out += `Tests: ${testResults.length}\n`;
|
|
365
886
|
}
|
|
887
|
+
out += `\nSee annotations for details.\n`;
|
|
888
|
+
return out;
|
|
366
889
|
}
|
|
367
890
|
}
|
|
368
891
|
exports.Logger = Logger;
|