@rage-against-the-pixel/unity-cli 1.8.3 → 3.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 +264 -34
- 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 +89 -4
- package/dist/logging.js +780 -186
- package/dist/logging.js.map +1 -1
- package/dist/unity-editor.d.ts +12 -0
- package/dist/unity-editor.js +75 -16
- package/dist/unity-editor.js.map +1 -1
- package/dist/unity-hub.d.ts +21 -1
- package/dist/unity-hub.js +197 -90
- 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 +86 -0
- package/dist/upm-cli.js +469 -0
- package/dist/upm-cli.js.map +1 -0
- package/dist/utilities.d.ts +52 -0
- package/dist/utilities.js +195 -13
- 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 +20 -9
- 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,79 @@ class Logger {
|
|
|
241
630
|
* @param message The string to mask.
|
|
242
631
|
*/
|
|
243
632
|
CI_mask(message) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
633
|
+
this._provider.mask(message);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Masks a credential value in CI environments before it appears in logs.
|
|
637
|
+
* This is a convenience wrapper around CI_mask for credential values.
|
|
638
|
+
* @param value The credential value to mask.
|
|
639
|
+
*/
|
|
640
|
+
maskCredential(value) {
|
|
641
|
+
if (value && value.length > 0) {
|
|
642
|
+
this.CI_mask(value);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Logs command-line options with sensitive information scrubbed.
|
|
647
|
+
* Automatically removes passwords, tokens, emails, and other credentials from the output.
|
|
648
|
+
* @param options The options object to log (typically from commander.js).
|
|
649
|
+
* @param optionalParams Additional parameters to log.
|
|
650
|
+
*/
|
|
651
|
+
debugOptions(options, ...optionalParams) {
|
|
652
|
+
// Avoid expensive scrubbing and stringification when debug logging is disabled.
|
|
653
|
+
if (this.logLevel !== LogLevel.DEBUG) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const scrubbed = this.scrubSensitiveData(options);
|
|
657
|
+
this.debug(JSON.stringify(scrubbed), ...optionalParams);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* List of sensitive option keys that should be scrubbed from debug output.
|
|
661
|
+
*/
|
|
662
|
+
SENSITIVE_KEYS = [
|
|
663
|
+
'password',
|
|
664
|
+
'email',
|
|
665
|
+
'serial',
|
|
666
|
+
'token',
|
|
667
|
+
'config',
|
|
668
|
+
'organization',
|
|
669
|
+
'username',
|
|
670
|
+
'servicesConfig',
|
|
671
|
+
'serviceaccountkey',
|
|
672
|
+
];
|
|
673
|
+
/**
|
|
674
|
+
* Scrubs sensitive information from an object for safe logging.
|
|
675
|
+
* Creates a deep clone of the object and replaces sensitive values with [REDACTED].
|
|
676
|
+
* @param obj The object to scrub (typically command-line options).
|
|
677
|
+
* @returns A new object with sensitive values replaced.
|
|
678
|
+
*/
|
|
679
|
+
scrubSensitiveData(obj) {
|
|
680
|
+
if (obj === null || obj === undefined) {
|
|
681
|
+
return obj;
|
|
682
|
+
}
|
|
683
|
+
if (typeof obj !== 'object') {
|
|
684
|
+
return obj;
|
|
685
|
+
}
|
|
686
|
+
if (Array.isArray(obj)) {
|
|
687
|
+
return obj.map((item) => this.scrubSensitiveData(item));
|
|
688
|
+
}
|
|
689
|
+
const scrubbedObj = {};
|
|
690
|
+
for (const key in obj) {
|
|
691
|
+
if (obj.hasOwnProperty(key)) {
|
|
692
|
+
const lowerKey = key.toLowerCase();
|
|
693
|
+
const isSensitive = this.SENSITIVE_KEYS.some(sensitiveKey => lowerKey.includes(sensitiveKey.toLowerCase()));
|
|
694
|
+
if (isSensitive) {
|
|
695
|
+
scrubbedObj[key] = '[REDACTED]';
|
|
696
|
+
}
|
|
697
|
+
else if (typeof obj[key] === 'object') {
|
|
698
|
+
scrubbedObj[key] = this.scrubSensitiveData(obj[key]);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
scrubbedObj[key] = obj[key];
|
|
702
|
+
}
|
|
248
703
|
}
|
|
249
704
|
}
|
|
705
|
+
return scrubbedObj;
|
|
250
706
|
}
|
|
251
707
|
/**
|
|
252
708
|
* Sets an environment variable in CI environments that support it.
|
|
@@ -254,44 +710,182 @@ class Logger {
|
|
|
254
710
|
* @param value The value of the environment variable.
|
|
255
711
|
*/
|
|
256
712
|
CI_setEnvironmentVariable(name, value) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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) {
|
|
265
738
|
break;
|
|
266
739
|
}
|
|
740
|
+
rebuilt += `${line}\n`;
|
|
267
741
|
}
|
|
742
|
+
return rebuilt + footer;
|
|
268
743
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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) {
|
|
278
783
|
break;
|
|
279
784
|
}
|
|
280
785
|
}
|
|
786
|
+
if (Buffer.byteLength(summary, 'utf8') > limit) {
|
|
787
|
+
summary = Logger.truncateSummaryToByteLimit(summary, limit);
|
|
788
|
+
}
|
|
789
|
+
this._provider.appendStepSummary(summary);
|
|
281
790
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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`;
|
|
293
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`;
|
|
294
886
|
}
|
|
887
|
+
out += `\nSee annotations for details.\n`;
|
|
888
|
+
return out;
|
|
295
889
|
}
|
|
296
890
|
}
|
|
297
891
|
exports.Logger = Logger;
|