@quenty/nevermore-cli 4.43.4 → 4.45.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/CHANGELOG.md +10 -0
- package/build-scripts/{combine-test-places.luau → combine-test-places.lua} +3 -3
- package/build-scripts/{transform-inject-deploy-metadata.luau → transform-inject-deploy-metadata.lua} +3 -3
- package/dist/commands/batch-command/batch-deploy-command.js +4 -4
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +76 -19
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.js +7 -1
- package/dist/commands/deploy-command/deploy-init-utils.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init.js +24 -7
- package/dist/commands/deploy-command/deploy-init.js.map +1 -1
- package/dist/commands/deploy-command/index.js +1 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.d.ts +12 -2
- package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.js +11 -4
- package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.js +1 -1
- package/dist/utils/deploy/deploy-metadata.js.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts +2 -2
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +1 -1
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +37 -2
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +59 -22
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.d.ts +2 -2
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +10 -5
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/cloud-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/cloud-job-context.test.js +91 -0
- package/dist/utils/job-context/cloud-job-context.test.js.map +1 -0
- package/dist/utils/job-context/job-context.d.ts +70 -2
- package/dist/utils/job-context/job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.d.ts +2 -2
- package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.js +4 -2
- package/dist/utils/job-context/local-job-context.js.map +1 -1
- package/dist/utils/job-context/local-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/local-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/local-job-context.test.js +68 -0
- package/dist/utils/job-context/local-job-context.test.js.map +1 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts +50 -7
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +49 -13
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +125 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts +22 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts.map +1 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js +202 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js.map +1 -0
- package/dist/utils/package-manager-utils.d.ts.map +1 -1
- package/dist/utils/package-manager-utils.js +32 -1
- package/dist/utils/package-manager-utils.js.map +1 -1
- package/dist/utils/package-manager-utils.test.js +25 -0
- package/dist/utils/package-manager-utils.test.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.d.ts +17 -0
- package/dist/utils/testing/batch-run.e2e.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-run.e2e.test.js +343 -0
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts +11 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.js +38 -0
- package/dist/utils/testing/batch-test-runner-template.test.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.d.ts +40 -0
- package/dist/utils/testing/log-fetch-stats.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.js +18 -0
- package/dist/utils/testing/log-fetch-stats.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts +2 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.js +41 -0
- package/dist/utils/testing/log-fetch-stats.test.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +126 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +440 -103
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js +494 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts +79 -0
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js +241 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js +202 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js.map +1 -0
- package/dist/utils/testing/runner/combined-project-generator.js +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +24 -0
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +166 -6
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.test.js +228 -2
- package/dist/utils/testing/runner/test-runner.test.js.map +1 -1
- package/dist/utils/testing/structured-test-results.d.ts +77 -0
- package/dist/utils/testing/structured-test-results.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.js +140 -0
- package/dist/utils/testing/structured-test-results.js.map +1 -0
- package/dist/utils/testing/structured-test-results.test.d.ts +7 -0
- package/dist/utils/testing/structured-test-results.test.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.test.js +179 -0
- package/dist/utils/testing/structured-test-results.test.js.map +1 -0
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +4 -4
- package/src/commands/batch-command/batch-test-command.ts +94 -19
- package/src/commands/deploy-command/deploy-init-utils.ts +7 -1
- package/src/commands/deploy-command/deploy-init.ts +41 -11
- package/src/commands/deploy-command/index.ts +1 -1
- package/src/utils/deploy/deploy-github-columns.ts +14 -4
- package/src/utils/deploy/deploy-metadata.ts +1 -1
- package/src/utils/job-context/base-job-context.ts +3 -2
- package/src/utils/job-context/batch-script-job-context.ts +98 -17
- package/src/utils/job-context/cloud-job-context.test.ts +112 -0
- package/src/utils/job-context/cloud-job-context.ts +12 -6
- package/src/utils/job-context/job-context.ts +71 -2
- package/src/utils/job-context/local-job-context.test.ts +89 -0
- package/src/utils/job-context/local-job-context.ts +7 -3
- package/src/utils/open-cloud/open-cloud-client.test.ts +155 -1
- package/src/utils/open-cloud/open-cloud-client.ts +94 -18
- package/src/utils/open-cloud/open-cloud-logs.characterization.test.ts +277 -0
- package/src/utils/package-manager-utils.test.ts +27 -0
- package/src/utils/package-manager-utils.ts +34 -1
- package/src/utils/testing/batch-run.e2e.test.ts +425 -0
- package/src/utils/testing/batch-test-runner-template.test.ts +62 -0
- package/src/utils/testing/log-fetch-stats.test.ts +48 -0
- package/src/utils/testing/log-fetch-stats.ts +55 -0
- package/src/utils/testing/parsers/batch-log-parser.test.ts +660 -1
- package/src/utils/testing/parsers/batch-log-parser.ts +721 -116
- package/src/utils/testing/parsers/batch-log-renderer.test.ts +330 -0
- package/src/utils/testing/parsers/batch-log-renderer.ts +329 -0
- package/src/utils/testing/runner/combined-project-generator.ts +1 -1
- package/src/utils/testing/runner/test-runner.test.ts +332 -2
- package/src/utils/testing/runner/test-runner.ts +231 -9
- package/src/utils/testing/structured-test-results.test.ts +267 -0
- package/src/utils/testing/structured-test-results.ts +199 -0
- package/templates/batch-test-runner.lua +242 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/templates/batch-test-runner.luau +0 -137
- /package/build-scripts/{smoke-test-server.luau → smoke-test-server.lua} +0 -0
- /package/build-scripts/{transform-rojo-merge-place.luau → transform-rojo-merge-place.lua} +0 -0
|
@@ -23,13 +23,20 @@ import {
|
|
|
23
23
|
detectTargetNameAsync,
|
|
24
24
|
} from './deploy-init-utils.js';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Stands in for the place ID in a `--dryrun` preview. A dryrun never creates a
|
|
28
|
+
* place, so there is no real ID to render — and the preview is never written.
|
|
29
|
+
*/
|
|
30
|
+
const DRYRUN_PLACE_ID = '<would-be-created>';
|
|
31
|
+
|
|
26
32
|
interface InitState {
|
|
27
33
|
packagePath: string;
|
|
28
34
|
packageName: string;
|
|
29
35
|
placeName: string;
|
|
30
36
|
targetName: string;
|
|
31
37
|
universeId?: number;
|
|
32
|
-
|
|
38
|
+
/** Resolved place ID, or {@link DRYRUN_PLACE_ID} on a dryrun. */
|
|
39
|
+
placeId?: number | typeof DRYRUN_PLACE_ID;
|
|
33
40
|
project?: string;
|
|
34
41
|
scriptTemplate?: string;
|
|
35
42
|
mode: 'auto' | 'interactive' | 'non-interactive';
|
|
@@ -111,9 +118,8 @@ async function resolveNonInteractive(
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
if (args.createPlace && state.universeId && !state.placeId) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
cookie,
|
|
121
|
+
state.placeId = await createPlaceAsync(
|
|
122
|
+
args,
|
|
117
123
|
state.universeId,
|
|
118
124
|
state.placeName
|
|
119
125
|
);
|
|
@@ -133,7 +139,7 @@ async function resolveInteractiveAsync(
|
|
|
133
139
|
await promptModeAsync(state);
|
|
134
140
|
await ensureUniverseIdAsync(args, state); // Resolves targetName before script prompt
|
|
135
141
|
await ensureProjectAndScriptAsync(args, state);
|
|
136
|
-
await ensurePromptPlaceIdAsync(state); // This modifies the cloud, so is last
|
|
142
|
+
await ensurePromptPlaceIdAsync(args, state); // This modifies the cloud, so is last
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
function isTestTarget(state: InitState): boolean {
|
|
@@ -232,15 +238,17 @@ async function ensureUniverseIdAsync(
|
|
|
232
238
|
state.universeId = answers.universeId ?? state.universeId;
|
|
233
239
|
}
|
|
234
240
|
|
|
235
|
-
async function ensurePromptPlaceIdAsync(
|
|
241
|
+
async function ensurePromptPlaceIdAsync(
|
|
242
|
+
args: DeployArgs,
|
|
243
|
+
state: InitState
|
|
244
|
+
): Promise<void> {
|
|
236
245
|
if (!state.universeId) {
|
|
237
246
|
throw new Error('Universe ID is required to set up place ID');
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
cookie,
|
|
250
|
+
state.placeId = await createPlaceAsync(
|
|
251
|
+
args,
|
|
244
252
|
state.universeId,
|
|
245
253
|
state.placeName
|
|
246
254
|
);
|
|
@@ -255,6 +263,27 @@ async function ensurePromptPlaceIdAsync(state: InitState): Promise<void> {
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Creates the place the target will point at. On a dryrun nothing is created
|
|
268
|
+
* and nothing is fetched — not even the cookie — so the caller gets
|
|
269
|
+
* {@link DRYRUN_PLACE_ID} to render instead.
|
|
270
|
+
*/
|
|
271
|
+
async function createPlaceAsync(
|
|
272
|
+
args: DeployArgs,
|
|
273
|
+
universeId: number,
|
|
274
|
+
placeName: string
|
|
275
|
+
): Promise<number | typeof DRYRUN_PLACE_ID> {
|
|
276
|
+
if (args.dryrun) {
|
|
277
|
+
OutputHelper.info(
|
|
278
|
+
`[DRYRUN] Would create place '${placeName}' in universe ${universeId}`
|
|
279
|
+
);
|
|
280
|
+
return DRYRUN_PLACE_ID;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const cookie = await getRobloxCookieAsync();
|
|
284
|
+
return await createPlaceInUniverseAsync(cookie, universeId, placeName);
|
|
285
|
+
}
|
|
286
|
+
|
|
258
287
|
async function ensureProjectAndScriptAsync(
|
|
259
288
|
args: DeployArgs,
|
|
260
289
|
state: InitState
|
|
@@ -320,7 +349,8 @@ async function writeConfig(
|
|
|
320
349
|
targets: {
|
|
321
350
|
[state.targetName]: {
|
|
322
351
|
universeId: state.universeId!,
|
|
323
|
-
|
|
352
|
+
// On a dryrun this is DRYRUN_PLACE_ID, which only ever gets printed.
|
|
353
|
+
placeId: state.placeId as number,
|
|
324
354
|
project: state.project!.replace(/\\/g, '/'),
|
|
325
355
|
...(state.scriptTemplate
|
|
326
356
|
? { scriptTemplate: state.scriptTemplate.replace(/\\/g, '/') }
|
|
@@ -332,7 +362,7 @@ async function writeConfig(
|
|
|
332
362
|
const configJson = JSON.stringify(config, null, 2);
|
|
333
363
|
|
|
334
364
|
if (args.dryrun) {
|
|
335
|
-
OutputHelper.info('[DRYRUN]
|
|
365
|
+
OutputHelper.info('[DRYRUN] Would write deploy.nevermore.json:');
|
|
336
366
|
console.log(configJson);
|
|
337
367
|
return;
|
|
338
368
|
}
|
|
@@ -559,7 +559,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
559
559
|
reporters.push(
|
|
560
560
|
new GithubCommentTableReporter(
|
|
561
561
|
state,
|
|
562
|
-
createDeployCommentConfig(),
|
|
562
|
+
createDeployCommentConfig(deployLabels),
|
|
563
563
|
isMultiPlace ? MULTI_PLACE_CONCURRENCY : 1
|
|
564
564
|
)
|
|
565
565
|
);
|
|
@@ -11,16 +11,26 @@ export interface BatchDeployResult extends PackageResult {
|
|
|
11
11
|
placeId: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Shared config for the deploy results GitHub comment reporter.
|
|
16
|
+
*
|
|
17
|
+
* Takes the labels rather than fixing them, because a publish and a deploy are
|
|
18
|
+
* different words and the command already knows which it is doing. Without
|
|
19
|
+
* this the comment said "Failed" for a run whose terminal output said "DEPLOY
|
|
20
|
+
* FAILED" — the same run, described two ways.
|
|
21
|
+
*/
|
|
22
|
+
export function createDeployCommentConfig(labels?: {
|
|
23
|
+
successLabel?: string;
|
|
24
|
+
failureLabel?: string;
|
|
25
|
+
}): GithubCommentTableConfig {
|
|
16
26
|
return {
|
|
17
27
|
heading: 'Deploy Results',
|
|
18
28
|
commentMarker: '<!-- nevermore-deploy-results -->',
|
|
19
29
|
sectionId: 'deploy',
|
|
20
30
|
extraColumns: [createErrorColumn(), createTryItColumn()],
|
|
21
31
|
errorHeading: 'Deploy Results',
|
|
22
|
-
successLabel: 'Deployed',
|
|
23
|
-
failureLabel: '
|
|
32
|
+
successLabel: labels?.successLabel ?? 'Deployed',
|
|
33
|
+
failureLabel: labels?.failureLabel ?? 'DEPLOY FAILED',
|
|
24
34
|
summaryVerb: 'deployed',
|
|
25
35
|
};
|
|
26
36
|
}
|
|
@@ -25,7 +25,7 @@ export type DeployMetadataAttributes = Record<
|
|
|
25
25
|
const INJECT_SCRIPT_PATH = resolvePackagePath(
|
|
26
26
|
import.meta.url,
|
|
27
27
|
'build-scripts',
|
|
28
|
-
'transform-inject-deploy-metadata.
|
|
28
|
+
'transform-inject-deploy-metadata.lua'
|
|
29
29
|
);
|
|
30
30
|
|
|
31
31
|
function _git(args: string[]): string | undefined {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type DeployPlaceOptions,
|
|
18
18
|
type RunScriptOptions,
|
|
19
19
|
type ScriptRunResult,
|
|
20
|
+
type JobLogs,
|
|
20
21
|
} from './job-context.js';
|
|
21
22
|
import { type OpenCloudClient } from '../open-cloud/open-cloud-client.js';
|
|
22
23
|
import { type BasePlaceResolver } from '@quenty/nevermore-deploy';
|
|
@@ -24,7 +25,7 @@ import { type BasePlaceResolver } from '@quenty/nevermore-deploy';
|
|
|
24
25
|
const MERGE_SCRIPT_PATH = resolvePackagePath(
|
|
25
26
|
import.meta.url,
|
|
26
27
|
'build-scripts',
|
|
27
|
-
'transform-rojo-merge-place.
|
|
28
|
+
'transform-rojo-merge-place.lua'
|
|
28
29
|
);
|
|
29
30
|
|
|
30
31
|
interface SharedRojoBuild {
|
|
@@ -300,7 +301,7 @@ export abstract class BaseJobContext implements JobContext {
|
|
|
300
301
|
deployment: Deployment,
|
|
301
302
|
options: RunScriptOptions
|
|
302
303
|
): Promise<ScriptRunResult>;
|
|
303
|
-
abstract getLogsAsync(deployment: Deployment): Promise<
|
|
304
|
+
abstract getLogsAsync(deployment: Deployment): Promise<JobLogs>;
|
|
304
305
|
abstract releaseAsync(deployment: Deployment): Promise<void>;
|
|
305
306
|
|
|
306
307
|
async disposeAsync(): Promise<void> {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type JobContext,
|
|
13
13
|
type RunScriptOptions,
|
|
14
14
|
type ScriptRunResult,
|
|
15
|
+
type JobLogs,
|
|
15
16
|
} from './job-context.js';
|
|
16
17
|
import { type BuildPlaceOptions } from '../build/build.js';
|
|
17
18
|
import { type BatchTarget } from '../batch/changed-packages-utils.js';
|
|
@@ -21,9 +22,15 @@ import {
|
|
|
21
22
|
generateCombinedProjectAsync,
|
|
22
23
|
} from '../testing/runner/combined-project-generator.js';
|
|
23
24
|
import {
|
|
25
|
+
type BatchDiagnosticLevel,
|
|
24
26
|
type BatchPackageResult,
|
|
25
27
|
parseBatchTestLogs,
|
|
26
28
|
} from '../testing/parsers/batch-log-parser.js';
|
|
29
|
+
import { type LogFetchStats } from '../testing/log-fetch-stats.js';
|
|
30
|
+
import {
|
|
31
|
+
toRenderableLines,
|
|
32
|
+
type RenderableLogLine,
|
|
33
|
+
} from '../testing/parsers/batch-log-renderer.js';
|
|
27
34
|
|
|
28
35
|
/** Per-package deployment handle. Wraps a shared inner deployment. */
|
|
29
36
|
class BatchDeployment implements Deployment {
|
|
@@ -50,6 +57,28 @@ interface CombinedBuildState {
|
|
|
50
57
|
* package configured with one — those belong in `nevermore batch test
|
|
51
58
|
* --no-aggregated`, or in an integration deploy.
|
|
52
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* Everything one aggregated batch produced, as data.
|
|
62
|
+
*
|
|
63
|
+
* The execution used to parse, render and print from in here. Printing from a
|
|
64
|
+
* job context put presentation in the transport, bypassed whatever reporter the
|
|
65
|
+
* command had chosen — output written straight to the console is invisible to a
|
|
66
|
+
* SpinnerReporter's capture — and left the whole path untestable without spying
|
|
67
|
+
* on `console.log`. The context returns this instead, and the command layer
|
|
68
|
+
* decides what to show.
|
|
69
|
+
*/
|
|
70
|
+
export interface BatchExecutionOutcome {
|
|
71
|
+
results: Map<string, BatchPackageResult>;
|
|
72
|
+
/** The run's output, typed by severity and not yet rendered. */
|
|
73
|
+
lines: RenderableLogLine[];
|
|
74
|
+
/** What the fetch had to do to collect it; absent for a transport that cannot say. */
|
|
75
|
+
stats?: LogFetchStats;
|
|
76
|
+
/** What reading the log turned up, to report after the log itself. */
|
|
77
|
+
diagnostics: Array<{ level: BatchDiagnosticLevel; message: string }>;
|
|
78
|
+
/** Section slug to package name, for whoever titles the sections. */
|
|
79
|
+
slugToPackage: Map<string, string>;
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
export class BatchScriptJobContext implements JobContext {
|
|
54
83
|
private _inner: JobContext;
|
|
55
84
|
private _batchTargets: BatchTarget[];
|
|
@@ -62,7 +91,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
62
91
|
// Lazy-promise state
|
|
63
92
|
private _combinedBuildPromise?: Promise<CombinedBuildState>;
|
|
64
93
|
private _deployPromise?: Promise<Deployment>;
|
|
65
|
-
private _executionPromise?: Promise<
|
|
94
|
+
private _executionPromise?: Promise<BatchExecutionOutcome>;
|
|
66
95
|
|
|
67
96
|
// State for cleanup
|
|
68
97
|
private _combinedBuildContext?: BuildContext;
|
|
@@ -124,8 +153,8 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
124
153
|
options: RunScriptOptions
|
|
125
154
|
): Promise<ScriptRunResult> {
|
|
126
155
|
const batchDeployment = deployment as BatchDeployment;
|
|
127
|
-
const
|
|
128
|
-
const result =
|
|
156
|
+
const { results } = await this._getBatchExecutionAsync();
|
|
157
|
+
const result = results.get(batchDeployment.packageName);
|
|
129
158
|
|
|
130
159
|
if (!result) {
|
|
131
160
|
return { success: false };
|
|
@@ -135,15 +164,39 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
135
164
|
success: result.success,
|
|
136
165
|
durationMs: result.durationMs,
|
|
137
166
|
errorMessage: result.error,
|
|
167
|
+
// returnValues stays absent: one execution covers every package, so
|
|
168
|
+
// nothing it returns belongs to any single one of them. The batch runner
|
|
169
|
+
// folds each package's own results into the batch summary, which the log
|
|
170
|
+
// parser has already split back out — so they are handed over decoded
|
|
171
|
+
// rather than left for the caller to find in a return value that is not
|
|
172
|
+
// this package's.
|
|
173
|
+
testResults: result.testResults,
|
|
174
|
+
logsLost: result.logsLost,
|
|
138
175
|
};
|
|
139
176
|
}
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Run the batch if it has not run, and hand back everything it produced.
|
|
180
|
+
*
|
|
181
|
+
* Public so the command layer can render the run once, before the
|
|
182
|
+
* per-package loop asks for results — which is also what keeps the shared
|
|
183
|
+
* build and upload out of any one package's async context.
|
|
184
|
+
*/
|
|
185
|
+
async getExecutionOutcomeAsync(): Promise<BatchExecutionOutcome> {
|
|
186
|
+
return this._getBatchExecutionAsync();
|
|
187
|
+
}
|
|
145
188
|
|
|
146
|
-
|
|
189
|
+
async getLogsAsync(deployment: Deployment): Promise<JobLogs> {
|
|
190
|
+
const batchDeployment = deployment as BatchDeployment;
|
|
191
|
+
const { results, stats } = await this._getBatchExecutionAsync();
|
|
192
|
+
const result = results.get(batchDeployment.packageName);
|
|
193
|
+
|
|
194
|
+
// The text is this package's section; the stats describe the one fetch that
|
|
195
|
+
// pulled every section, which is what happened to any package whose own
|
|
196
|
+
// section went missing from it. Messages stay empty: severity is per
|
|
197
|
+
// message across the whole window, and slicing it per package would need
|
|
198
|
+
// the section boundaries the parser already resolved.
|
|
199
|
+
return { text: result?.logs ?? '', messages: [], stats };
|
|
147
200
|
}
|
|
148
201
|
|
|
149
202
|
async releaseAsync(_deployment: Deployment): Promise<void> {
|
|
@@ -257,7 +310,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
257
310
|
return deployment;
|
|
258
311
|
}
|
|
259
312
|
|
|
260
|
-
private _getBatchExecutionAsync(): Promise<
|
|
313
|
+
private _getBatchExecutionAsync(): Promise<BatchExecutionOutcome> {
|
|
261
314
|
if (!this._executionPromise) {
|
|
262
315
|
this._executionPromise = this._doBatchExecutionAsync();
|
|
263
316
|
// On rejection, clear the promise so that a retry re-triggers the batch
|
|
@@ -268,9 +321,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
268
321
|
return this._executionPromise;
|
|
269
322
|
}
|
|
270
323
|
|
|
271
|
-
private async _doBatchExecutionAsync(): Promise<
|
|
272
|
-
Map<string, BatchPackageResult>
|
|
273
|
-
> {
|
|
324
|
+
private async _doBatchExecutionAsync(): Promise<BatchExecutionOutcome> {
|
|
274
325
|
const buildState = await this._getCombinedBuildAsync();
|
|
275
326
|
const deployment = await this._getSharedDeploymentAsync();
|
|
276
327
|
const { slugMap } = buildState.combinedResult;
|
|
@@ -278,7 +329,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
278
329
|
// Build the batch Luau script from the template
|
|
279
330
|
const templatePath = resolveTemplatePath(
|
|
280
331
|
import.meta.url,
|
|
281
|
-
'batch-test-runner.
|
|
332
|
+
'batch-test-runner.lua'
|
|
282
333
|
);
|
|
283
334
|
const template = await fs.readFile(templatePath, 'utf-8');
|
|
284
335
|
|
|
@@ -300,8 +351,9 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
300
351
|
timeoutMs: this._batchTimeoutMs,
|
|
301
352
|
});
|
|
302
353
|
|
|
303
|
-
// Fetch the combined logs
|
|
304
|
-
const
|
|
354
|
+
// Fetch the combined logs, with the severity and fetch that produced them.
|
|
355
|
+
const logs = await this._inner.getLogsAsync(deployment);
|
|
356
|
+
const rawLogs = logs.text;
|
|
305
357
|
|
|
306
358
|
if (!result.success) {
|
|
307
359
|
const stateInfo = result.taskState ? ` (state: ${result.taskState})` : '';
|
|
@@ -319,7 +371,36 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
319
371
|
OutputHelper.verbose(`Raw batch logs:\n${rawLogs || '(empty)'}`);
|
|
320
372
|
}
|
|
321
373
|
|
|
322
|
-
// Parse into per-package results
|
|
323
|
-
|
|
374
|
+
// Parse into per-package results. Diagnostics are collected rather than
|
|
375
|
+
// printed: they describe the log window, so they belong after it, and that
|
|
376
|
+
// is the caller's ordering to make.
|
|
377
|
+
const diagnostics: Array<{ level: BatchDiagnosticLevel; message: string }> =
|
|
378
|
+
[];
|
|
379
|
+
const results = parseBatchTestLogs(rawLogs, slugMap, {
|
|
380
|
+
logFetchStats: logs.stats,
|
|
381
|
+
// The batch script returns the same summary it prints. Handing both over
|
|
382
|
+
// lets the parser read the copy that cannot arrive truncated, and fall
|
|
383
|
+
// back to the printed one on a transport that carries no return channel.
|
|
384
|
+
returnValues: result.returnValues,
|
|
385
|
+
onDiagnostic: (level, message) => diagnostics.push({ level, message }),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const slugToPackage = new Map<string, string>();
|
|
389
|
+
for (const [packageName, slug] of slugMap) {
|
|
390
|
+
slugToPackage.set(slug, packageName);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Typed messages when the transport carries severity; otherwise the joined
|
|
394
|
+
// text, which renders uncolored rather than guessed at.
|
|
395
|
+
const messages =
|
|
396
|
+
logs.messages.length > 0 ? logs.messages : [{ message: rawLogs }];
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
results,
|
|
400
|
+
lines: toRenderableLines(messages),
|
|
401
|
+
stats: logs.stats,
|
|
402
|
+
diagnostics,
|
|
403
|
+
slugToPackage,
|
|
404
|
+
};
|
|
324
405
|
}
|
|
325
406
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for CloudJobContext.runScriptAsync — validates what a finished
|
|
3
|
+
* Open Cloud task reports back as a ScriptRunResult, in particular that a task
|
|
4
|
+
* which produced no output stays distinguishable from one that returned nothing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
8
|
+
import { type Reporter } from '@quenty/cli-output-helpers/reporting';
|
|
9
|
+
import { CloudJobContext } from './cloud-job-context.js';
|
|
10
|
+
import {
|
|
11
|
+
type LuauTask,
|
|
12
|
+
type OpenCloudClient,
|
|
13
|
+
} from '../open-cloud/open-cloud-client.js';
|
|
14
|
+
import { type Deployment } from './job-context.js';
|
|
15
|
+
|
|
16
|
+
function createReporter(): Reporter {
|
|
17
|
+
return {
|
|
18
|
+
onPackagePhaseChange: vi.fn(),
|
|
19
|
+
onPackageProgressUpdate: vi.fn(),
|
|
20
|
+
onPackageStart: vi.fn(),
|
|
21
|
+
onPackageResult: vi.fn(),
|
|
22
|
+
} as unknown as Reporter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A client whose task completes as `completedTask`. The real deployment handle
|
|
27
|
+
* is private to the context, so the test passes the three fields runScriptAsync
|
|
28
|
+
* reads off it.
|
|
29
|
+
*/
|
|
30
|
+
function createContext(completedTask: Partial<LuauTask>) {
|
|
31
|
+
const task = {
|
|
32
|
+
path: 'universes/1/places/2/versions/3/luau-execution-session-tasks/4',
|
|
33
|
+
createTime: '2026-01-01T00:00:00Z',
|
|
34
|
+
updateTime: '2026-01-01T00:01:00Z',
|
|
35
|
+
user: 'users/1',
|
|
36
|
+
state: 'COMPLETE',
|
|
37
|
+
script: 'return 1',
|
|
38
|
+
...completedTask,
|
|
39
|
+
} as LuauTask;
|
|
40
|
+
|
|
41
|
+
const client = {
|
|
42
|
+
createExecutionTaskAsync: vi.fn(async () => task),
|
|
43
|
+
pollTaskCompletionAsync: vi.fn(async () => task),
|
|
44
|
+
} as unknown as OpenCloudClient;
|
|
45
|
+
|
|
46
|
+
const context = new CloudJobContext(createReporter(), client);
|
|
47
|
+
const deployment = {
|
|
48
|
+
universeId: 1,
|
|
49
|
+
placeId: 2,
|
|
50
|
+
version: 3,
|
|
51
|
+
} as unknown as Deployment;
|
|
52
|
+
|
|
53
|
+
return { context, deployment };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('CloudJobContext.runScriptAsync', () => {
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reports the values the script returned', async () => {
|
|
62
|
+
// Fake timers keep the client-side timeout race from leaving a live timer
|
|
63
|
+
// behind; nothing in the run itself waits on one.
|
|
64
|
+
vi.useFakeTimers();
|
|
65
|
+
const { context, deployment } = createContext({
|
|
66
|
+
state: 'COMPLETE',
|
|
67
|
+
output: { results: [{ slug: 'maid', counts: { passed: 1014 } }] },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await context.runScriptAsync(deployment, {
|
|
71
|
+
scriptContent: 'return results',
|
|
72
|
+
packageName: 'maid',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.success).toBe(true);
|
|
76
|
+
expect(result.returnValues).toEqual([
|
|
77
|
+
{ slug: 'maid', counts: { passed: 1014 } },
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('reports an empty result when the task returned nothing', async () => {
|
|
82
|
+
vi.useFakeTimers();
|
|
83
|
+
const { context, deployment } = createContext({
|
|
84
|
+
state: 'COMPLETE',
|
|
85
|
+
output: {},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = await context.runScriptAsync(deployment, {
|
|
89
|
+
scriptContent: 'print("hi")',
|
|
90
|
+
packageName: 'maid',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result.returnValues).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('leaves returnValues absent when a failed task carried no output', async () => {
|
|
97
|
+
vi.useFakeTimers();
|
|
98
|
+
const { context, deployment } = createContext({
|
|
99
|
+
state: 'FAILED',
|
|
100
|
+
output: undefined,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await context.runScriptAsync(deployment, {
|
|
104
|
+
scriptContent: 'return huge',
|
|
105
|
+
packageName: 'maid',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
expect(result.taskState).toBe('FAILED');
|
|
110
|
+
expect(result.returnValues).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Reporter } from '@quenty/cli-output-helpers/reporting';
|
|
2
2
|
import {
|
|
3
|
+
getTaskReturnValues,
|
|
3
4
|
type LuauTask,
|
|
4
5
|
type OpenCloudClient,
|
|
5
6
|
} from '../open-cloud/open-cloud-client.js';
|
|
@@ -8,6 +9,7 @@ import { buildPlaceNameAsync, timeoutAsync } from '../nevermore-cli-utils.js';
|
|
|
8
9
|
import {
|
|
9
10
|
type Deployment,
|
|
10
11
|
type DeployPlaceOptions,
|
|
12
|
+
type JobLogs,
|
|
11
13
|
type RunScriptOptions,
|
|
12
14
|
type ScriptRunResult,
|
|
13
15
|
} from './job-context.js';
|
|
@@ -134,27 +136,31 @@ export class CloudJobContext extends BaseJobContext {
|
|
|
134
136
|
success: completedTask.state === 'COMPLETE',
|
|
135
137
|
taskState: completedTask.state,
|
|
136
138
|
errorMessage,
|
|
139
|
+
returnValues: getTaskReturnValues(completedTask),
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
async getLogsAsync(deployment: Deployment): Promise<
|
|
143
|
+
async getLogsAsync(deployment: Deployment): Promise<JobLogs> {
|
|
141
144
|
const cloudDeployment = deployment as CloudDeployment;
|
|
142
145
|
|
|
143
146
|
if (!cloudDeployment.taskPath) {
|
|
144
147
|
throw new Error('No task has been run yet');
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
const
|
|
150
|
+
const fetched = await this._openCloudClient!.getRawTaskLogsAsync(
|
|
148
151
|
cloudDeployment.taskPath
|
|
149
152
|
);
|
|
150
153
|
|
|
151
154
|
if (cloudDeployment.taskState && cloudDeployment.taskState !== 'COMPLETE') {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.join('\n')
|
|
155
|
+
const note = `Task ended with state: ${cloudDeployment.taskState}`;
|
|
156
|
+
return {
|
|
157
|
+
text: [fetched.text, note].filter(Boolean).join('\n'),
|
|
158
|
+
messages: [...fetched.messages, { message: note }],
|
|
159
|
+
stats: fetched.stats,
|
|
160
|
+
};
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
return
|
|
163
|
+
return fetched;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
async releaseAsync(_deployment: Deployment): Promise<void> {
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { type BuildPlaceOptions, type BuiltPlace } from '../build/build.js';
|
|
2
|
+
import { type StructuredTestResults } from '../testing/structured-test-results.js';
|
|
3
|
+
import { type LogFetchStats } from '../testing/log-fetch-stats.js';
|
|
4
|
+
import { type TaskLogMessage } from '../open-cloud/open-cloud-client.js';
|
|
5
|
+
|
|
6
|
+
/** A run's output, with the severity and the fetch that produced it. */
|
|
7
|
+
export interface JobLogs {
|
|
8
|
+
text: string;
|
|
9
|
+
/**
|
|
10
|
+
* The same output split into typed messages, when the transport reports
|
|
11
|
+
* severity. Empty for one that does not — a local run reads its logs off the
|
|
12
|
+
* Studio bridge, which says only what was printed.
|
|
13
|
+
*/
|
|
14
|
+
messages: TaskLogMessage[];
|
|
15
|
+
/**
|
|
16
|
+
* What collecting them took, when the transport can say. Absent rather than
|
|
17
|
+
* zeroed, so "no fetch to measure" cannot read as "a fetch that found
|
|
18
|
+
* nothing".
|
|
19
|
+
*/
|
|
20
|
+
stats?: LogFetchStats;
|
|
21
|
+
}
|
|
2
22
|
|
|
3
23
|
export type { BuiltPlace } from '../build/build.js';
|
|
4
24
|
|
|
@@ -28,6 +48,48 @@ export interface ScriptRunResult {
|
|
|
28
48
|
taskState?: string;
|
|
29
49
|
/** Error message from the execution backend, if any. */
|
|
30
50
|
errorMessage?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Everything the executed script returned, in order — the structured channel
|
|
53
|
+
* out of a run, as opposed to its printed output. Engine logs are truncated
|
|
54
|
+
* by Open Cloud on long runs, so anything a caller must read back exactly
|
|
55
|
+
* belongs here rather than in the log text.
|
|
56
|
+
*
|
|
57
|
+
* `undefined` means the transport never delivered a return channel: a cloud
|
|
58
|
+
* task that ended without an `output` (a FAILED task carries none, and an
|
|
59
|
+
* oversize return value fails the task rather than truncating the value), a
|
|
60
|
+
* bridge run that timed out or disconnected, or a context that does not
|
|
61
|
+
* carry return values at all. That is deliberately distinct from `[]`, which
|
|
62
|
+
* means the script ran and returned nothing — a caller that needs the value
|
|
63
|
+
* can fall back to parsing logs in the first case but not the second.
|
|
64
|
+
*
|
|
65
|
+
* Values are JSON-shaped, but the two transports spell exotic Luau types
|
|
66
|
+
* differently: Open Cloud auto-serializes them, while the Studio bridge
|
|
67
|
+
* marshals them into `{ type, value }` wrappers (`SerializedReturnValue`).
|
|
68
|
+
* Plain tables of strings, numbers and booleans come back identically on
|
|
69
|
+
* both, so structured results should stay inside that subset.
|
|
70
|
+
*/
|
|
71
|
+
returnValues?: unknown[];
|
|
72
|
+
/**
|
|
73
|
+
* The run's structured test results, when the context resolved them itself
|
|
74
|
+
* rather than leaving them in `returnValues` for the caller to decode.
|
|
75
|
+
*
|
|
76
|
+
* Only aggregated batch mode sets this: one execution covers every package, so
|
|
77
|
+
* its return value belongs to no single one of them and the per-package
|
|
78
|
+
* results have to be recovered from the batch summary first. A context that
|
|
79
|
+
* sets this has also reported where the counts came from, so the caller does
|
|
80
|
+
* not report it a second time.
|
|
81
|
+
*/
|
|
82
|
+
testResults?: StructuredTestResults;
|
|
83
|
+
/**
|
|
84
|
+
* True when this run's verdict rests on returned results alone, its log
|
|
85
|
+
* output having been dropped before it reached us.
|
|
86
|
+
*
|
|
87
|
+
* A narrower verdict than an ordinary one: the counts are exact, but with no
|
|
88
|
+
* text there is nothing to check for tracebacks and nothing to read when
|
|
89
|
+
* something did go wrong. Reported so it can be shown as such rather than as
|
|
90
|
+
* an ordinary pass.
|
|
91
|
+
*/
|
|
92
|
+
logsLost?: boolean;
|
|
31
93
|
}
|
|
32
94
|
|
|
33
95
|
/**
|
|
@@ -50,8 +112,15 @@ export interface JobContext {
|
|
|
50
112
|
options: RunScriptOptions
|
|
51
113
|
): Promise<ScriptRunResult>;
|
|
52
114
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Everything the most recent script execution printed.
|
|
117
|
+
*
|
|
118
|
+
* One value rather than a log plus two optional side-channels for its
|
|
119
|
+
* severity and its fetch: they all come from the same fetch, so returning
|
|
120
|
+
* them together removes both the chance of asking a context that does not
|
|
121
|
+
* implement one and the chance of handing an accessor the wrong handle.
|
|
122
|
+
*/
|
|
123
|
+
getLogsAsync(deployment: Deployment): Promise<JobLogs>;
|
|
55
124
|
|
|
56
125
|
/** Release a single deployment (stop bridge / clear task metadata). */
|
|
57
126
|
releaseAsync(deployment: Deployment): Promise<void>;
|