@quenty/nevermore-cli 4.41.0 → 4.41.1
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/CHANGELOG.md +6 -0
- package/dist/commands/batch-command/batch-test-command.js +2 -0
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +5 -1
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +20 -2
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +16 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +80 -7
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.js +65 -0
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -0
- package/dist/utils/testing/reporting/test-github-columns.d.ts.map +1 -1
- package/dist/utils/testing/reporting/test-github-columns.js +1 -0
- package/dist/utils/testing/reporting/test-github-columns.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +2 -0
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +1 -0
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/package.json +6 -6
- package/src/commands/batch-command/batch-test-command.ts +2 -0
- package/src/utils/job-context/batch-script-job-context.ts +5 -1
- package/src/utils/open-cloud/open-cloud-client.ts +25 -2
- package/src/utils/testing/parsers/batch-log-parser.test.ts +85 -0
- package/src/utils/testing/parsers/batch-log-parser.ts +98 -7
- package/src/utils/testing/reporting/test-github-columns.ts +1 -0
- package/src/utils/testing/runner/test-runner.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/nevermore-cli",
|
|
3
|
-
"version": "4.41.
|
|
3
|
+
"version": "4.41.1",
|
|
4
4
|
"description": "CLI interface for Nevermore",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"Quenty"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@quenty/cli-output-helpers": "1.17.
|
|
29
|
-
"@quenty/nevermore-cli-helpers": "1.16.
|
|
30
|
-
"@quenty/nevermore-template-helpers": "1.18.
|
|
31
|
-
"@quenty/studio-bridge": "0.17.
|
|
28
|
+
"@quenty/cli-output-helpers": "1.17.1",
|
|
29
|
+
"@quenty/nevermore-cli-helpers": "1.16.1",
|
|
30
|
+
"@quenty/nevermore-template-helpers": "1.18.1",
|
|
31
|
+
"@quenty/studio-bridge": "0.17.1",
|
|
32
32
|
"execa": "^9.6.1",
|
|
33
33
|
"find-git-root": "^1.0.4",
|
|
34
34
|
"inquirer": "^13.2.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=16"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "35993999cfe6fbdfd44721a1835b6ee29602f057"
|
|
61
61
|
}
|
|
@@ -171,6 +171,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
171
171
|
showLogs: args.logs ?? false,
|
|
172
172
|
verbose: args.verbose,
|
|
173
173
|
actionVerb: 'Testing',
|
|
174
|
+
expectsTestCounts: true,
|
|
174
175
|
})
|
|
175
176
|
: new SpinnerReporter(state, {
|
|
176
177
|
showLogs: args.logs ?? false,
|
|
@@ -251,6 +252,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
251
252
|
progressSummary: result.testCounts
|
|
252
253
|
? { kind: 'test-counts' as const, ...result.testCounts }
|
|
253
254
|
: undefined,
|
|
255
|
+
error: result.error,
|
|
254
256
|
};
|
|
255
257
|
},
|
|
256
258
|
});
|
|
@@ -113,7 +113,11 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
113
113
|
return { success: false };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
success: result.success,
|
|
118
|
+
durationMs: result.durationMs,
|
|
119
|
+
errorMessage: result.error,
|
|
120
|
+
};
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
async getLogsAsync(deployment: Deployment): Promise<string> {
|
|
@@ -336,7 +336,9 @@ export class OpenCloudClient {
|
|
|
336
336
|
private async _fetchRawLogsAsync(taskPath: string): Promise<string> {
|
|
337
337
|
const apiKey = await this._resolveApiKeyAsync();
|
|
338
338
|
const messages: string[] = [];
|
|
339
|
+
const structured: { message: string; createTime: string }[] = [];
|
|
339
340
|
let pageToken: string | undefined;
|
|
341
|
+
let pagesFetched = 0;
|
|
340
342
|
|
|
341
343
|
do {
|
|
342
344
|
const url = new URL(`https://apis.roblox.com/cloud/v2/${taskPath}/logs`);
|
|
@@ -371,18 +373,39 @@ export class OpenCloudClient {
|
|
|
371
373
|
|
|
372
374
|
for (const entry of data.luauExecutionSessionTaskLogs ?? []) {
|
|
373
375
|
if (entry.structuredMessages?.length) {
|
|
376
|
+
// Keep createTime alongside the text: the API does not guarantee
|
|
377
|
+
// chronological order, and readers downstream assume it.
|
|
374
378
|
for (const msg of entry.structuredMessages) {
|
|
375
|
-
|
|
379
|
+
structured.push({
|
|
380
|
+
message: msg.message,
|
|
381
|
+
createTime: msg.createTime,
|
|
382
|
+
});
|
|
376
383
|
}
|
|
377
384
|
} else if (entry.messages?.length) {
|
|
378
385
|
messages.push(...entry.messages);
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
388
|
|
|
389
|
+
pagesFetched++;
|
|
382
390
|
pageToken = data.nextPageToken || undefined;
|
|
383
391
|
} while (pageToken);
|
|
384
392
|
|
|
385
|
-
|
|
393
|
+
// Out-of-order messages silently destroy marker-delimited parsing: a
|
|
394
|
+
// trailing summary that arrives early ends the scan before any of the
|
|
395
|
+
// output it summarizes has been read.
|
|
396
|
+
structured.sort((a, b) => a.createTime.localeCompare(b.createTime));
|
|
397
|
+
messages.push(...structured.map((m) => m.message));
|
|
398
|
+
|
|
399
|
+
const text = messages.join('\n');
|
|
400
|
+
|
|
401
|
+
// Log volume is the prime suspect when engine output goes missing between
|
|
402
|
+
// the task and the parser, and the parser cannot tell a short run from a
|
|
403
|
+
// truncated fetch. Record what arrived so a real run can settle it.
|
|
404
|
+
OutputHelper.verbose(
|
|
405
|
+
`[open-cloud] Task logs: ${pagesFetched} page(s), ${messages.length} messages, ${text.length} chars`
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
return text;
|
|
386
409
|
}
|
|
387
410
|
|
|
388
411
|
/**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { countTracebacks, parseBatchTestLogs } from './batch-log-parser.js';
|
|
4
|
+
|
|
5
|
+
const SLUG_MAP = new Map([['egghunt2026', 'egghunt2026']]);
|
|
6
|
+
|
|
7
|
+
function buildLogs(section: string): string {
|
|
8
|
+
return [
|
|
9
|
+
'===BATCH_TEST_BEGIN egghunt2026===',
|
|
10
|
+
section,
|
|
11
|
+
'===BATCH_TEST_END egghunt2026 PASS 1000===',
|
|
12
|
+
'===BATCH_TEST_SUMMARY===',
|
|
13
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('parseBatchTestLogs', () => {
|
|
18
|
+
it('reads test counts out of a package section', () => {
|
|
19
|
+
const logs = buildLogs('Tests: 275 passed, 275 total');
|
|
20
|
+
|
|
21
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
22
|
+
|
|
23
|
+
expect(result?.testCounts).toEqual({ passed: 275, failed: 0, total: 275 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('fails a pass it cannot read, rather than trusting the pcall', () => {
|
|
27
|
+
// The summary prints last, so it survives a log window that dropped the
|
|
28
|
+
// head. The pcall said success, but nothing here saw a test run.
|
|
29
|
+
const logs = [
|
|
30
|
+
'Tests: 275 passed, 275 total',
|
|
31
|
+
'===BATCH_TEST_END egghunt2026 PASS 1000===',
|
|
32
|
+
'===BATCH_TEST_SUMMARY===',
|
|
33
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
37
|
+
|
|
38
|
+
expect(result?.success).toBe(false);
|
|
39
|
+
expect(result?.error).toContain('No test output could be attributed');
|
|
40
|
+
expect(result?.testCounts).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('still shows output it could not attribute', () => {
|
|
44
|
+
// 330KB of real test output was being reported as "(no output)" because no
|
|
45
|
+
// BEGIN marker survived to delimit it.
|
|
46
|
+
const logs = [
|
|
47
|
+
'PlayerBadgeHelper: attempt to index nil',
|
|
48
|
+
'===BATCH_TEST_SUMMARY===',
|
|
49
|
+
'[{"slug":"egghunt2026","success":true,"durationMs":1000}]',
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
53
|
+
|
|
54
|
+
expect(result?.logs).toContain('PlayerBadgeHelper');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('counts tracebacks that jest reports as a clean pass', () => {
|
|
58
|
+
const logs = buildLogs(
|
|
59
|
+
[
|
|
60
|
+
'Tests: 155 passed, 155 total',
|
|
61
|
+
'PlayerBadgeHelper: attempt to index nil',
|
|
62
|
+
'Stack Begin',
|
|
63
|
+
"Script 'PlayerBadgeHelper', Line 365",
|
|
64
|
+
'Stack End',
|
|
65
|
+
].join('\n')
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const result = parseBatchTestLogs(logs, SLUG_MAP).get('egghunt2026');
|
|
69
|
+
|
|
70
|
+
expect(result?.testCounts?.failed).toBe(0);
|
|
71
|
+
expect(result?.tracebackCount).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('countTracebacks', () => {
|
|
76
|
+
it('counts each traceback separately', () => {
|
|
77
|
+
expect(countTracebacks('Stack Begin\nfoo\nStack End\nStack Begin\n')).toBe(
|
|
78
|
+
2
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('is zero for empty output', () => {
|
|
83
|
+
expect(countTracebacks('')).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -8,6 +8,14 @@ export interface BatchPackageResult {
|
|
|
8
8
|
/** Inner pcall execution time reported by the Luau batch runner. */
|
|
9
9
|
durationMs?: number;
|
|
10
10
|
testCounts?: ParsedTestCounts;
|
|
11
|
+
/** Why this package failed, when the failure is the parser's own verdict. */
|
|
12
|
+
error?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Luau tracebacks found in this package's section. Reported, not gated on:
|
|
15
|
+
* jest cannot count these, since a deferred-callback crash fires outside any
|
|
16
|
+
* test — often after the test that scheduled it already passed.
|
|
17
|
+
*/
|
|
18
|
+
tracebackCount: number;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
const BEGIN_MARKER = '===BATCH_TEST_BEGIN ';
|
|
@@ -55,6 +63,8 @@ export function parseBatchTestLogs(
|
|
|
55
63
|
let currentSlug: string | null = null;
|
|
56
64
|
let currentLines: string[] = [];
|
|
57
65
|
let summaryLineIndex = -1;
|
|
66
|
+
let beginMarkersSeen = 0;
|
|
67
|
+
let endMarkersSeen = 0;
|
|
58
68
|
|
|
59
69
|
// Matches "<slug> PASS|FAIL [<durationMs>]" — slugs have no whitespace.
|
|
60
70
|
const endInnerPattern = /^(\S+)\s+(?:PASS|FAIL)(?:\s+(\d+))?$/;
|
|
@@ -65,11 +75,13 @@ export function parseBatchTestLogs(
|
|
|
65
75
|
if (trimmed.startsWith(BEGIN_MARKER) && trimmed.endsWith('===')) {
|
|
66
76
|
currentSlug = trimmed.slice(BEGIN_MARKER.length, -3);
|
|
67
77
|
currentLines = [];
|
|
78
|
+
beginMarkersSeen++;
|
|
68
79
|
continue;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
if (trimmed.startsWith(END_MARKER) && trimmed.endsWith('===')) {
|
|
72
83
|
const inner = trimmed.slice(END_MARKER.length, -3);
|
|
84
|
+
endMarkersSeen++;
|
|
73
85
|
const match = endInnerPattern.exec(inner);
|
|
74
86
|
const endSlug = match ? match[1] : inner;
|
|
75
87
|
const durationStr = match?.[2];
|
|
@@ -144,26 +156,76 @@ export function parseBatchTestLogs(
|
|
|
144
156
|
);
|
|
145
157
|
}
|
|
146
158
|
|
|
159
|
+
// The summary is printed last, so it survives a log fetch that dropped the
|
|
160
|
+
// output above it. Summary present with no sections is exactly that case, and
|
|
161
|
+
// it is invisible to the check above — the results below would then be built
|
|
162
|
+
// from pcall exit status alone, knowing nothing about what actually ran.
|
|
163
|
+
const lostSectionOutput = logSections.size === 0 && summaryResults.size > 0;
|
|
164
|
+
if (lostSectionOutput) {
|
|
165
|
+
OutputHelper.warn(
|
|
166
|
+
`[batch-log-parser] Received ${rawLogs.length} chars of output but could not ` +
|
|
167
|
+
`attribute any of it to a package section ` +
|
|
168
|
+
`(${beginMarkersSeen} BEGIN, ${endMarkersSeen} END markers; summary at line ` +
|
|
169
|
+
`${summaryLineIndex} of ${lines.length}). ` +
|
|
170
|
+
'Test counts are not derived from unattributed output.'
|
|
171
|
+
);
|
|
172
|
+
// BEGIN is printed first and the summary last, so their positions separate
|
|
173
|
+
// the two ways this happens: a log window that dropped the head keeps the
|
|
174
|
+
// summary at the end, while reordered messages put it early.
|
|
175
|
+
OutputHelper.warn(
|
|
176
|
+
summaryLineIndex >= lines.length - 2
|
|
177
|
+
? '[batch-log-parser] Summary sits at the end of the log — consistent with the head being dropped by a log size/retention limit.'
|
|
178
|
+
: '[batch-log-parser] Summary sits early in the log — consistent with out-of-order messages ending the scan prematurely.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
// ── Pass 3: Combine log sections with summary results ──
|
|
148
183
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
184
|
+
// Output that arrived but could not be split into sections is still output.
|
|
185
|
+
// Reporting "(no output)" over it hides the very thing needed to diagnose the
|
|
186
|
+
// parse failure, so it is shown verbatim instead. Only safe to hand to a
|
|
187
|
+
// single package: with several, there is no way to say whose output this is.
|
|
188
|
+
const fallbackLogs = noOutputAtAll || lostSectionOutput ? rawLogs.trim() : '';
|
|
189
|
+
|
|
190
|
+
// The unattributed stream is the same text for every package, so it is
|
|
191
|
+
// attached once rather than repeated per package across a large log.
|
|
192
|
+
let unattributedClaimed = false;
|
|
152
193
|
|
|
153
194
|
for (const [packageName, slug] of slugMap) {
|
|
154
|
-
const
|
|
195
|
+
const attributedLogs = logSections.get(slug);
|
|
196
|
+
let sectionLogs = attributedLogs ?? '';
|
|
197
|
+
if (!attributedLogs && fallbackLogs && !unattributedClaimed) {
|
|
198
|
+
sectionLogs = fallbackLogs;
|
|
199
|
+
unattributedClaimed = true;
|
|
200
|
+
}
|
|
155
201
|
const summarySuccess = summaryResults.get(slug);
|
|
156
202
|
|
|
157
203
|
// The JSON summary (pcall result) is the primary success signal.
|
|
158
204
|
// Jest failure detection provides a secondary override.
|
|
205
|
+
//
|
|
206
|
+
// Gate on attributed output only. Unattributed output is shown but not
|
|
207
|
+
// judged: whatever broke attribution is reason enough not to trust which
|
|
208
|
+
// package a failure line belongs to.
|
|
159
209
|
let success = summarySuccess ?? false;
|
|
160
|
-
|
|
161
|
-
|
|
210
|
+
let error: string | undefined;
|
|
211
|
+
if (success && attributedLogs) {
|
|
212
|
+
const hasJestFailures = _hasJestFailuresInLogs(attributedLogs);
|
|
162
213
|
if (hasJestFailures) {
|
|
163
214
|
success = false;
|
|
215
|
+
error = 'Jest reported failing tests';
|
|
164
216
|
}
|
|
165
217
|
}
|
|
166
218
|
|
|
219
|
+
// A pass we cannot read is not a pass. The pcall only proves the script did
|
|
220
|
+
// not throw, which says nothing about whether any test ran or passed.
|
|
221
|
+
if (success && !attributedLogs) {
|
|
222
|
+
success = false;
|
|
223
|
+
error =
|
|
224
|
+
`No test output could be attributed to this package ` +
|
|
225
|
+
`(${rawLogs.length} chars received, ${beginMarkersSeen} BEGIN markers found). ` +
|
|
226
|
+
'Unattributed output follows.';
|
|
227
|
+
}
|
|
228
|
+
|
|
167
229
|
if (!success && !noOutputAtAll) {
|
|
168
230
|
console.error(
|
|
169
231
|
`[batch-log-parser] ${slug}: summarySuccess=${summarySuccess} hasLogs=${
|
|
@@ -172,7 +234,19 @@ export function parseBatchTestLogs(
|
|
|
172
234
|
);
|
|
173
235
|
}
|
|
174
236
|
|
|
175
|
-
|
|
237
|
+
// Deliberately not parsed from unattributed output: attribution is exactly
|
|
238
|
+
// what failed, so a count read from it could belong to anything. The logs
|
|
239
|
+
// are still shown, so the jest summary remains readable by eye.
|
|
240
|
+
const testCounts = attributedLogs
|
|
241
|
+
? parseTestCounts(attributedLogs)
|
|
242
|
+
: undefined;
|
|
243
|
+
const tracebackCount = countTracebacks(sectionLogs);
|
|
244
|
+
if (tracebackCount > 0) {
|
|
245
|
+
OutputHelper.warn(
|
|
246
|
+
`[batch-log-parser] ${slug}: ${tracebackCount} Luau traceback(s) in output. ` +
|
|
247
|
+
'Jest does not count these — a deferred-callback crash fires outside any test.'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
176
250
|
// Prefer the JSON summary (authoritative, immune to log reordering);
|
|
177
251
|
// fall back to the END-marker value if the summary was truncated.
|
|
178
252
|
const durationMs = summaryDurations.get(slug) ?? markerDurations.get(slug);
|
|
@@ -182,12 +256,29 @@ export function parseBatchTestLogs(
|
|
|
182
256
|
logs: sectionLogs,
|
|
183
257
|
durationMs,
|
|
184
258
|
testCounts,
|
|
259
|
+
tracebackCount,
|
|
260
|
+
error,
|
|
185
261
|
});
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
return results;
|
|
189
265
|
}
|
|
190
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Count Luau tracebacks in log output.
|
|
269
|
+
*
|
|
270
|
+
* Reported separately from test counts because jest never sees these: a crash in
|
|
271
|
+
* a deferred callback fires outside any test, so a run can print
|
|
272
|
+
* "Tests: 155 passed, 0 failed" while something is genuinely broken.
|
|
273
|
+
*/
|
|
274
|
+
export function countTracebacks(rawOutput: string): number {
|
|
275
|
+
if (!rawOutput) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const cleanLogs = OutputHelper.stripAnsi(rawOutput);
|
|
279
|
+
return cleanLogs.match(/Stack Begin\s/g)?.length ?? 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
191
282
|
/**
|
|
192
283
|
* Check specifically for Jest test suite/test failures in logs.
|
|
193
284
|
* Unlike parseTestLogs, this ignores "Stack Begin" runtime errors since
|
|
@@ -26,6 +26,8 @@ export interface SingleTestResult {
|
|
|
26
26
|
* non-aggregated cloud/local runs — callers should fall back to wall-clock.
|
|
27
27
|
*/
|
|
28
28
|
durationMs?: number;
|
|
29
|
+
/** Why the run failed, when the runner can say more than "it failed". */
|
|
30
|
+
error?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface SingleTestOptions {
|
|
@@ -109,6 +111,7 @@ export async function runSingleTestAsync(
|
|
|
109
111
|
logs: parsed.logs,
|
|
110
112
|
testCounts: parseTestCounts(parsed.logs),
|
|
111
113
|
durationMs: result.durationMs,
|
|
114
|
+
error: result.errorMessage,
|
|
112
115
|
};
|
|
113
116
|
} finally {
|
|
114
117
|
await context.releaseAsync(deployment);
|