@quenty/nevermore-cli 4.50.1 → 4.52.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 +18 -0
- package/dist/commands/batch-command/batch-bundle-command.d.ts +15 -0
- package/dist/commands/batch-command/batch-bundle-command.d.ts.map +1 -0
- package/dist/commands/batch-command/batch-bundle-command.js +168 -0
- package/dist/commands/batch-command/batch-bundle-command.js.map +1 -0
- package/dist/commands/batch-command/batch-deploy-command.d.ts +2 -0
- package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-deploy-command.js +115 -16
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts +1 -0
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +109 -17
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/batch-command/index.d.ts.map +1 -1
- package/dist/commands/batch-command/index.js +2 -0
- package/dist/commands/batch-command/index.js.map +1 -1
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +7 -0
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/test-command/test-command.d.ts +1 -0
- package/dist/commands/test-command/test-command.d.ts.map +1 -1
- package/dist/commands/test-command/test-command.js +19 -3
- package/dist/commands/test-command/test-command.js.map +1 -1
- package/dist/commands/tools-command/ci-post-deploy-results.d.ts.map +1 -1
- package/dist/commands/tools-command/ci-post-deploy-results.js +6 -2
- package/dist/commands/tools-command/ci-post-deploy-results.js.map +1 -1
- package/dist/commands/tools-command/ci-post-test-results.d.ts +1 -0
- package/dist/commands/tools-command/ci-post-test-results.d.ts.map +1 -1
- package/dist/commands/tools-command/ci-post-test-results.js +12 -3
- package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
- package/dist/utils/batch/changed-packages-utils.d.ts +11 -0
- package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
- package/dist/utils/batch/changed-packages-utils.js +14 -0
- package/dist/utils/batch/changed-packages-utils.js.map +1 -1
- package/dist/utils/build/build.d.ts +7 -0
- package/dist/utils/build/build.d.ts.map +1 -1
- package/dist/utils/build/build.js.map +1 -1
- package/dist/utils/bundle/untrusted-tar.d.ts +28 -0
- package/dist/utils/bundle/untrusted-tar.d.ts.map +1 -0
- package/dist/utils/bundle/untrusted-tar.js +116 -0
- package/dist/utils/bundle/untrusted-tar.js.map +1 -0
- package/dist/utils/deploy/deploy-github-columns.d.ts +8 -1
- package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.d.ts +5 -0
- package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-metadata.js +36 -0
- package/dist/utils/deploy/deploy-metadata.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.test.js +35 -2
- package/dist/utils/deploy/deploy-metadata.test.js.map +1 -1
- package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts +38 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts.map +1 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.js +209 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.js.map +1 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts +2 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts.map +1 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.test.js +194 -0
- package/dist/utils/deploy/prebuilt-deploy-bundle.test.js.map +1 -0
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +3 -2
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +12 -0
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +19 -0
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +1 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +5 -0
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +11 -0
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/dist/utils/sourcemap/sourcemap-loader.d.ts +3 -3
- package/dist/utils/sourcemap/sourcemap-loader.d.ts.map +1 -1
- package/dist/utils/sourcemap/sourcemap-loader.js +6 -7
- package/dist/utils/sourcemap/sourcemap-loader.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.js +36 -0
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -1
- package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts +26 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.js +145 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.js.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts +2 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js +197 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js.map +1 -0
- package/dist/utils/testing/runner/test-runner.d.ts +8 -1
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +19 -6
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/package.json +7 -6
- package/src/commands/batch-command/batch-bundle-command.ts +236 -0
- package/src/commands/batch-command/batch-deploy-command.ts +160 -16
- package/src/commands/batch-command/batch-test-command.ts +162 -18
- package/src/commands/batch-command/index.ts +2 -0
- package/src/commands/deploy-command/index.ts +7 -0
- package/src/commands/test-command/test-command.ts +20 -3
- package/src/commands/tools-command/ci-post-deploy-results.ts +9 -2
- package/src/commands/tools-command/ci-post-test-results.ts +20 -3
- package/src/utils/batch/changed-packages-utils.ts +23 -0
- package/src/utils/build/build.ts +7 -0
- package/src/utils/bundle/untrusted-tar.ts +160 -0
- package/src/utils/deploy/deploy-github-columns.ts +8 -1
- package/src/utils/deploy/deploy-metadata.test.ts +50 -1
- package/src/utils/deploy/deploy-metadata.ts +39 -0
- package/src/utils/deploy/prebuilt-deploy-bundle.test.ts +265 -0
- package/src/utils/deploy/prebuilt-deploy-bundle.ts +297 -0
- package/src/utils/job-context/base-job-context.ts +4 -2
- package/src/utils/job-context/batch-script-job-context.ts +34 -3
- package/src/utils/open-cloud/open-cloud-client.test.ts +15 -0
- package/src/utils/open-cloud/open-cloud-client.ts +11 -0
- package/src/utils/sourcemap/sourcemap-loader.ts +7 -9
- package/src/utils/testing/batch-run.e2e.test.ts +42 -0
- package/src/utils/testing/runner/prebuilt-batch-bundle.test.ts +272 -0
- package/src/utils/testing/runner/prebuilt-batch-bundle.ts +204 -0
- package/src/utils/testing/runner/test-runner.ts +43 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,6 +11,8 @@ import { getApiKeyAsync } from '@quenty/nevermore-cli-helpers';
|
|
|
11
11
|
import { runBatchAsync } from '../../utils/batch/batch-runner.js';
|
|
12
12
|
import { renderBatchOutcome } from '../../utils/testing/parsers/batch-log-renderer.js';
|
|
13
13
|
import { emitDiagnostic } from '../../utils/testing/parsers/batch-log-parser.js';
|
|
14
|
+
import { type DeployTarget } from '@quenty/nevermore-deploy';
|
|
15
|
+
import { type BuildContext } from '@quenty/nevermore-template-helpers';
|
|
14
16
|
import { createBasePlaceResolver } from '../../utils/build/base-place-resolver-factory.js';
|
|
15
17
|
import {
|
|
16
18
|
type JobContext,
|
|
@@ -37,9 +39,13 @@ import {
|
|
|
37
39
|
createTestCommentConfig,
|
|
38
40
|
} from '../../utils/testing/reporting/index.js';
|
|
39
41
|
import {
|
|
42
|
+
executePreparedTestAsync,
|
|
40
43
|
runSingleTestAsync,
|
|
44
|
+
type SingleTestOptions,
|
|
41
45
|
type SingleTestResult,
|
|
42
46
|
} from '../../utils/testing/runner/test-runner.js';
|
|
47
|
+
import { type CombinedPlaceChunk } from '../../utils/testing/runner/combined-project-generator.js';
|
|
48
|
+
import { readPrebuiltBatchBundleAsync } from '../../utils/testing/runner/prebuilt-batch-bundle.js';
|
|
43
49
|
import { isCI } from '../../utils/nevermore-cli-utils.js';
|
|
44
50
|
|
|
45
51
|
interface BatchTestArgs extends NevermoreGlobalArgs {
|
|
@@ -56,6 +62,7 @@ interface BatchTestArgs extends NevermoreGlobalArgs {
|
|
|
56
62
|
batchUniverseId?: number;
|
|
57
63
|
maxPlaceMb?: number;
|
|
58
64
|
timeout?: number;
|
|
65
|
+
bundle?: string;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
export const batchTestCommand: CommandModule<
|
|
@@ -129,6 +136,14 @@ export const batchTestCommand: CommandModule<
|
|
|
129
136
|
'Max script execution time in seconds for the whole batch. Sent to the Open Cloud API so Roblox cancels server-side on overrun. Max 300s (API limit). (default: 300)',
|
|
130
137
|
type: 'number',
|
|
131
138
|
})
|
|
139
|
+
.option('bundle', {
|
|
140
|
+
describe:
|
|
141
|
+
'Run the places in a *.nevermore.tests.tar written by "batch bundle --test" instead of finding ' +
|
|
142
|
+
'and building packages. The bundle never chooses where it is uploaded: ' +
|
|
143
|
+
'that is --batch-place-id/--batch-universe-id, or else the test place this ' +
|
|
144
|
+
'checkout gives one of its packages.',
|
|
145
|
+
type: 'string',
|
|
146
|
+
})
|
|
132
147
|
// Accepted only to fail with a useful message. A batch runs one shared
|
|
133
148
|
// script across packages, so there is nothing for arbitrary Luau to mean
|
|
134
149
|
// here — but yargs strict mode would otherwise reject it with a bare
|
|
@@ -137,14 +152,30 @@ export const batchTestCommand: CommandModule<
|
|
|
137
152
|
hidden: true,
|
|
138
153
|
type: 'string',
|
|
139
154
|
})
|
|
155
|
+
.option('script-file', {
|
|
156
|
+
hidden: true,
|
|
157
|
+
type: 'string',
|
|
158
|
+
})
|
|
140
159
|
.check((args) => {
|
|
141
|
-
if (args.scriptText !== undefined) {
|
|
160
|
+
if (args.scriptText !== undefined || args.scriptFile !== undefined) {
|
|
142
161
|
throw new Error(
|
|
143
|
-
'--script-text
|
|
162
|
+
'--script-text and --script-file are not supported by "batch test", which runs one shared script for all packages.\n' +
|
|
144
163
|
'Run it against a single package instead:\n' +
|
|
145
164
|
' cd <package> && nevermore test --cloud --script-text \'print("hi")\''
|
|
146
165
|
);
|
|
147
166
|
}
|
|
167
|
+
if (args.bundle !== undefined) {
|
|
168
|
+
if (args.all || args.limit !== undefined) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'--bundle runs every package in the bundle; drop --all and --limit.'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (args.aggregated === false) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'--bundle runs an aggregated batch; drop --no-aggregated.'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
148
179
|
return true;
|
|
149
180
|
})
|
|
150
181
|
);
|
|
@@ -160,12 +191,38 @@ export const batchTestCommand: CommandModule<
|
|
|
160
191
|
};
|
|
161
192
|
|
|
162
193
|
async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
const prebuilt =
|
|
195
|
+
args.bundle !== undefined
|
|
196
|
+
? await _loadPrebuiltRunAsync(args, args.bundle)
|
|
197
|
+
: undefined;
|
|
198
|
+
let exitCode: number | undefined;
|
|
199
|
+
try {
|
|
200
|
+
exitCode = await _runPackagesAsync(args, prebuilt);
|
|
201
|
+
} finally {
|
|
202
|
+
await prebuilt?.buildContext.cleanupAsync();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Node.js fetch (undici) keeps TCP connections alive in a global pool,
|
|
206
|
+
// which prevents the event loop from draining. Explicit exit required.
|
|
207
|
+
if (exitCode !== undefined) {
|
|
208
|
+
process.exit(exitCode);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
166
211
|
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
async function _runPackagesAsync(
|
|
213
|
+
args: BatchTestArgs,
|
|
214
|
+
prebuilt: PrebuiltRun | undefined
|
|
215
|
+
): Promise<number | undefined> {
|
|
216
|
+
let packages: BatchTarget[];
|
|
217
|
+
if (prebuilt) {
|
|
218
|
+
packages = prebuilt.packages;
|
|
219
|
+
} else {
|
|
220
|
+
packages = args.all
|
|
221
|
+
? await discoverAllTestableBatchTargetsAsync()
|
|
222
|
+
: await discoverChangedTestableBatchTargetsAsync(args.base!);
|
|
223
|
+
if (args.limit && args.limit > 0) {
|
|
224
|
+
packages = packages.slice(0, args.limit);
|
|
225
|
+
}
|
|
169
226
|
}
|
|
170
227
|
|
|
171
228
|
if (packages.length === 0) {
|
|
@@ -173,7 +230,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
173
230
|
'No testable packages found. Packages need a deploy.nevermore.json with a "test" target.\n' +
|
|
174
231
|
'Run "nevermore deploy init" inside a package to set one up.'
|
|
175
232
|
);
|
|
176
|
-
return;
|
|
233
|
+
return undefined;
|
|
177
234
|
}
|
|
178
235
|
|
|
179
236
|
if (args.dryrun) {
|
|
@@ -181,7 +238,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
181
238
|
OutputHelper.info(
|
|
182
239
|
`[DRYRUN] Would test ${packages.length} packages: ${names}`
|
|
183
240
|
);
|
|
184
|
-
return;
|
|
241
|
+
return undefined;
|
|
185
242
|
}
|
|
186
243
|
|
|
187
244
|
const cloud = args.cloud ?? false;
|
|
@@ -273,8 +330,8 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
273
330
|
|
|
274
331
|
const batchContext = aggregated
|
|
275
332
|
? new BatchScriptJobContext(innerContext, packages, {
|
|
276
|
-
batchPlaceId: args.batchPlaceId,
|
|
277
|
-
batchUniverseId: args.batchUniverseId,
|
|
333
|
+
batchPlaceId: prebuilt?.target.placeId ?? args.batchPlaceId,
|
|
334
|
+
batchUniverseId: prebuilt?.target.universeId ?? args.batchUniverseId,
|
|
278
335
|
batchTimeoutMs: timeoutMs,
|
|
279
336
|
// Only an upload is size-limited, so a local run stays one place
|
|
280
337
|
// unless asked to split.
|
|
@@ -285,6 +342,10 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
285
342
|
? undefined
|
|
286
343
|
: Infinity,
|
|
287
344
|
reporter,
|
|
345
|
+
prebuilt: prebuilt && {
|
|
346
|
+
chunks: prebuilt.chunks,
|
|
347
|
+
buildContext: prebuilt.buildContext,
|
|
348
|
+
},
|
|
288
349
|
})
|
|
289
350
|
: undefined;
|
|
290
351
|
const context: JobContext = batchContext ?? innerContext;
|
|
@@ -326,7 +387,8 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
326
387
|
pkg,
|
|
327
388
|
context,
|
|
328
389
|
timeoutMs,
|
|
329
|
-
aggregated
|
|
390
|
+
aggregated,
|
|
391
|
+
prebuilt?.hasJestConfig.get(pkg.name)
|
|
330
392
|
);
|
|
331
393
|
|
|
332
394
|
return {
|
|
@@ -360,19 +422,17 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
360
422
|
}
|
|
361
423
|
|
|
362
424
|
await reporter.stopAsync();
|
|
363
|
-
|
|
364
|
-
// Node.js fetch (undici) keeps TCP connections alive in a global pool,
|
|
365
|
-
// which prevents the event loop from draining. Explicit exit required.
|
|
366
|
-
process.exit(exitCode);
|
|
425
|
+
return exitCode;
|
|
367
426
|
}
|
|
368
427
|
|
|
369
428
|
async function _runWithRetryAsync(
|
|
370
429
|
pkg: BatchTarget,
|
|
371
430
|
context: JobContext,
|
|
372
431
|
timeoutMs: number,
|
|
373
|
-
aggregated: boolean
|
|
432
|
+
aggregated: boolean,
|
|
433
|
+
prebuiltHasJestConfig: boolean | undefined
|
|
374
434
|
): Promise<SingleTestResult> {
|
|
375
|
-
const opts = {
|
|
435
|
+
const opts: SingleTestOptions = {
|
|
376
436
|
packagePath: pkg.path,
|
|
377
437
|
packageName: pkg.name,
|
|
378
438
|
timeoutMs,
|
|
@@ -382,11 +442,95 @@ async function _runWithRetryAsync(
|
|
|
382
442
|
suppressCountsProvenance: aggregated,
|
|
383
443
|
};
|
|
384
444
|
|
|
445
|
+
if (prebuiltHasJestConfig !== undefined) {
|
|
446
|
+
return _retryOnTransientAsync(pkg.name, async () =>
|
|
447
|
+
executePreparedTestAsync(
|
|
448
|
+
context,
|
|
449
|
+
{
|
|
450
|
+
builtPlace: await context.buildPlaceAsync(opts),
|
|
451
|
+
scriptContent: '',
|
|
452
|
+
hasJestConfig: prebuiltHasJestConfig,
|
|
453
|
+
},
|
|
454
|
+
opts
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
385
459
|
return _retryOnTransientAsync(pkg.name, () =>
|
|
386
460
|
runSingleTestAsync(context, opts)
|
|
387
461
|
);
|
|
388
462
|
}
|
|
389
463
|
|
|
464
|
+
interface PrebuiltRun {
|
|
465
|
+
target: DeployTarget;
|
|
466
|
+
packages: BatchTarget[];
|
|
467
|
+
chunks: CombinedPlaceChunk[];
|
|
468
|
+
hasJestConfig: Map<string, boolean>;
|
|
469
|
+
buildContext: BuildContext;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function _loadPrebuiltRunAsync(
|
|
473
|
+
args: BatchTestArgs,
|
|
474
|
+
bundleFile: string
|
|
475
|
+
): Promise<PrebuiltRun> {
|
|
476
|
+
const bundle = await readPrebuiltBatchBundleAsync(bundleFile);
|
|
477
|
+
let target: DeployTarget;
|
|
478
|
+
try {
|
|
479
|
+
target = await _resolvePrebuiltTargetAsync(
|
|
480
|
+
args,
|
|
481
|
+
new Set(bundle.packages.map((p) => p.name))
|
|
482
|
+
);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
await bundle.buildContext.cleanupAsync();
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
OutputHelper.info(
|
|
489
|
+
`Running ${bundle.packages.length} prebuilt package${
|
|
490
|
+
bundle.packages.length === 1 ? '' : 's'
|
|
491
|
+
} in ${bundle.chunks.length} place${
|
|
492
|
+
bundle.chunks.length === 1 ? '' : 's'
|
|
493
|
+
} on place ${target.placeId}.`
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
target,
|
|
498
|
+
packages: bundle.packages.map((p) => ({
|
|
499
|
+
name: p.name,
|
|
500
|
+
packageName: p.name,
|
|
501
|
+
path: process.cwd(),
|
|
502
|
+
target,
|
|
503
|
+
})),
|
|
504
|
+
chunks: bundle.chunks,
|
|
505
|
+
hasJestConfig: new Map(
|
|
506
|
+
bundle.packages.map((p) => [p.name, p.hasJestConfig])
|
|
507
|
+
),
|
|
508
|
+
buildContext: bundle.buildContext,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Where a bundle uploads to, picked the way a normal batch run picks it: the
|
|
514
|
+
* test place of one of its packages, read from this checkout rather than from
|
|
515
|
+
* the bundle.
|
|
516
|
+
*/
|
|
517
|
+
async function _resolvePrebuiltTargetAsync(
|
|
518
|
+
args: BatchTestArgs,
|
|
519
|
+
bundledNames: Set<string>
|
|
520
|
+
): Promise<DeployTarget> {
|
|
521
|
+
const known = await discoverAllTestableBatchTargetsAsync();
|
|
522
|
+
const base = known.find((t) => bundledNames.has(t.name)) ?? known[0];
|
|
523
|
+
const universeId = args.batchUniverseId ?? base?.target.universeId;
|
|
524
|
+
const placeId = args.batchPlaceId ?? base?.target.placeId;
|
|
525
|
+
if (!universeId || !placeId) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
'No test place to upload the bundle to: this checkout has no testable ' +
|
|
528
|
+
'package. Pass --batch-place-id and --batch-universe-id.'
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
return { universeId, placeId, project: '' };
|
|
532
|
+
}
|
|
533
|
+
|
|
390
534
|
/**
|
|
391
535
|
* Run something once more if it failed for a reason that tends not to repeat.
|
|
392
536
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Argv, CommandModule } from 'yargs';
|
|
2
2
|
import { NevermoreGlobalArgs } from '../../args/global-args.js';
|
|
3
|
+
import { batchBundleCommand } from './batch-bundle-command.js';
|
|
3
4
|
import { batchDeployCommand } from './batch-deploy-command.js';
|
|
4
5
|
import { batchTestCommand } from './batch-test-command.js';
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ export class BatchCommand<T> implements CommandModule<T, NevermoreGlobalArgs> {
|
|
|
8
9
|
public describe = 'Run operations across multiple packages';
|
|
9
10
|
|
|
10
11
|
public builder = (args: Argv<T>) => {
|
|
12
|
+
args.command(batchBundleCommand as any);
|
|
11
13
|
args.command(batchDeployCommand as any);
|
|
12
14
|
args.command(batchTestCommand as any);
|
|
13
15
|
return args as Argv<NevermoreGlobalArgs>;
|
|
@@ -711,6 +711,13 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
711
711
|
return {
|
|
712
712
|
packageName: buildTarget.name,
|
|
713
713
|
placeId: uploadedTarget.placeId,
|
|
714
|
+
universeId: uploadedTarget.universeId,
|
|
715
|
+
target: targetName,
|
|
716
|
+
version,
|
|
717
|
+
published: publish,
|
|
718
|
+
basePlaceVersion: builtPlace.basePlaceVersion,
|
|
719
|
+
commit: deployGitInfo.version,
|
|
720
|
+
branch: deployGitInfo.branch,
|
|
714
721
|
success: true,
|
|
715
722
|
logs: `${action} v${version}`,
|
|
716
723
|
progressSummary: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
import { Argv, CommandModule } from 'yargs';
|
|
3
4
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
@@ -36,6 +37,7 @@ export interface TestProjectArgs extends NevermoreGlobalArgs {
|
|
|
36
37
|
placeId?: number;
|
|
37
38
|
scriptTemplate?: string;
|
|
38
39
|
scriptText?: string;
|
|
40
|
+
scriptFile?: string;
|
|
39
41
|
output?: string;
|
|
40
42
|
timeout?: number;
|
|
41
43
|
}
|
|
@@ -58,7 +60,7 @@ export class TestProjectCommand<T>
|
|
|
58
60
|
});
|
|
59
61
|
args.option('logs', {
|
|
60
62
|
describe:
|
|
61
|
-
'Show execution logs. Defaults on with --script-text, whose output is the whole point of the run',
|
|
63
|
+
'Show execution logs. Defaults on with --script-text and --script-file, whose output is the whole point of the run',
|
|
62
64
|
type: 'boolean',
|
|
63
65
|
});
|
|
64
66
|
args.option('universe-id', {
|
|
@@ -78,6 +80,12 @@ export class TestProjectCommand<T>
|
|
|
78
80
|
describe:
|
|
79
81
|
'Luau code to execute directly instead of the configured script template',
|
|
80
82
|
type: 'string',
|
|
83
|
+
conflicts: 'script-file',
|
|
84
|
+
});
|
|
85
|
+
args.option('script-file', {
|
|
86
|
+
describe:
|
|
87
|
+
'Path to a Luau file to execute directly, like --script-text but without the command line length limit',
|
|
88
|
+
type: 'string',
|
|
81
89
|
});
|
|
82
90
|
args.option('output', {
|
|
83
91
|
describe: 'Write JSON results to this file',
|
|
@@ -98,7 +106,16 @@ export class TestProjectCommand<T>
|
|
|
98
106
|
// A probe run exists to show its output. Hiding it behind an opt-in flag
|
|
99
107
|
// made --script-text look broken enough that a workaround was written into
|
|
100
108
|
// the docs instead of a bug report.
|
|
101
|
-
|
|
109
|
+
let scriptText = args.scriptText;
|
|
110
|
+
if (args.scriptFile !== undefined) {
|
|
111
|
+
try {
|
|
112
|
+
scriptText = await fs.readFile(args.scriptFile, 'utf-8');
|
|
113
|
+
} catch {
|
|
114
|
+
OutputHelper.error(`Script file not found: ${args.scriptFile}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const showLogs = args.logs ?? scriptText !== undefined;
|
|
102
119
|
const useSpinner = process.stdout.isTTY && !args.verbose;
|
|
103
120
|
|
|
104
121
|
const reporter = new CompositeReporter(
|
|
@@ -168,7 +185,7 @@ export class TestProjectCommand<T>
|
|
|
168
185
|
placeId: args.placeId,
|
|
169
186
|
scriptTemplate: args.scriptTemplate,
|
|
170
187
|
},
|
|
171
|
-
scriptText
|
|
188
|
+
scriptText,
|
|
172
189
|
timeoutMs:
|
|
173
190
|
args.timeout !== undefined ? args.timeout * 1000 : undefined,
|
|
174
191
|
});
|
|
@@ -49,8 +49,8 @@ export const ciPostDeployResultsCommand: CommandModule<
|
|
|
49
49
|
.option('run-outcome', {
|
|
50
50
|
describe:
|
|
51
51
|
'Outcome of the deploy step (e.g. "success", "failure"). ' +
|
|
52
|
-
'When the results file is missing
|
|
53
|
-
'posts
|
|
52
|
+
'When the results file is missing, "success" posts a neutral comment, ' +
|
|
53
|
+
'"cancelled" posts nothing, and anything else posts a failure.',
|
|
54
54
|
type: 'string',
|
|
55
55
|
});
|
|
56
56
|
},
|
|
@@ -62,6 +62,13 @@ export const ciPostDeployResultsCommand: CommandModule<
|
|
|
62
62
|
} catch {
|
|
63
63
|
OutputHelper.warn(`Results file not found: ${args.input}`);
|
|
64
64
|
|
|
65
|
+
if (args.runOutcome === 'cancelled') {
|
|
66
|
+
OutputHelper.info(
|
|
67
|
+
'Deploy step was cancelled; leaving the PR comment to the run that replaced it.'
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
const reporters = _createGithubReporters(undefined);
|
|
66
73
|
|
|
67
74
|
if (args.runOutcome === 'success') {
|
|
@@ -27,6 +27,7 @@ type ErrorReporter = Reporter & {
|
|
|
27
27
|
interface CiPostTestResultsArgs extends NevermoreGlobalArgs {
|
|
28
28
|
input: string;
|
|
29
29
|
runOutcome?: string;
|
|
30
|
+
sourcemap?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** Create the standard set of GitHub reporters for posting results. */
|
|
@@ -58,8 +59,14 @@ export const ciPostTestResultsCommand: CommandModule<
|
|
|
58
59
|
.option('run-outcome', {
|
|
59
60
|
describe:
|
|
60
61
|
'Outcome of the test step (e.g. "success", "failure"). ' +
|
|
61
|
-
'When the results file is missing
|
|
62
|
-
'posts
|
|
62
|
+
'When the results file is missing, "success" posts a neutral comment, ' +
|
|
63
|
+
'"cancelled" posts nothing, and anything else posts a failure.',
|
|
64
|
+
type: 'string',
|
|
65
|
+
})
|
|
66
|
+
.option('sourcemap', {
|
|
67
|
+
describe:
|
|
68
|
+
'Sourcemap that test failure annotations resolve files against ' +
|
|
69
|
+
'(default: sourcemap.json in the current directory)',
|
|
63
70
|
type: 'string',
|
|
64
71
|
});
|
|
65
72
|
},
|
|
@@ -71,6 +78,13 @@ export const ciPostTestResultsCommand: CommandModule<
|
|
|
71
78
|
} catch {
|
|
72
79
|
OutputHelper.warn(`Results file not found: ${args.input}`);
|
|
73
80
|
|
|
81
|
+
if (args.runOutcome === 'cancelled') {
|
|
82
|
+
OutputHelper.info(
|
|
83
|
+
'Test step was cancelled; leaving the PR comment to the run that replaced it.'
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
const reporters = _createGithubReporters(undefined);
|
|
75
89
|
|
|
76
90
|
if (args.runOutcome === 'success') {
|
|
@@ -100,7 +114,10 @@ export const ciPostTestResultsCommand: CommandModule<
|
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
// Emit test failure annotations
|
|
103
|
-
const sourcemapResolver = tryLoadSourcemapResolver(
|
|
117
|
+
const sourcemapResolver = tryLoadSourcemapResolver(
|
|
118
|
+
process.cwd(),
|
|
119
|
+
args.sourcemap
|
|
120
|
+
);
|
|
104
121
|
const workspace = await tryLoadAnnotationWorkspaceAsync(process.cwd());
|
|
105
122
|
const parsedDiagnostics: Diagnostic[] = [];
|
|
106
123
|
for (const failure of state.getFailures()) {
|
|
@@ -126,6 +126,29 @@ export async function discoverChangedTestableBatchTargetsAsync(
|
|
|
126
126
|
return _requireScriptTemplate(flattenToBatchTargets(packages));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The places `batch deploy` deploys, and `batch bundle --deploy` builds for a
|
|
131
|
+
* later deploy. One function so the two can never pick different places: a
|
|
132
|
+
* fork's bundle must hold what a same-repo run would have deployed.
|
|
133
|
+
*/
|
|
134
|
+
export async function discoverDeployBatchTargetsAsync(options: {
|
|
135
|
+
targetName: string;
|
|
136
|
+
all?: boolean;
|
|
137
|
+
base: string;
|
|
138
|
+
limit?: number;
|
|
139
|
+
}): Promise<BatchTarget[]> {
|
|
140
|
+
const discovered = options.all
|
|
141
|
+
? await discoverAllTargetPackagesAsync(options.targetName)
|
|
142
|
+
: await discoverChangedTargetPackagesAsync(
|
|
143
|
+
options.base,
|
|
144
|
+
options.targetName
|
|
145
|
+
);
|
|
146
|
+
const batchTargets = flattenToBatchTargets(discovered);
|
|
147
|
+
return options.limit && options.limit > 0
|
|
148
|
+
? batchTargets.slice(0, options.limit)
|
|
149
|
+
: batchTargets;
|
|
150
|
+
}
|
|
151
|
+
|
|
129
152
|
/**
|
|
130
153
|
* Fan a list of TargetPackages out into BatchTargets — one per place. A
|
|
131
154
|
* single-place package produces one BatchTarget with the package name; a
|
package/src/utils/build/build.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface DeployOverrides {
|
|
|
10
10
|
placeId?: number;
|
|
11
11
|
scriptTemplate?: string;
|
|
12
12
|
placeFile?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Rojo project the base place merge grafts by, in place of the target's own.
|
|
15
|
+
* Set together with `placeFile` when the place was built from a different
|
|
16
|
+
* checkout, so the merge follows the project that built it.
|
|
17
|
+
*/
|
|
18
|
+
projectFile?: string;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface BuildPlaceOptions {
|
|
@@ -35,6 +41,7 @@ export interface BuildPlaceOptions {
|
|
|
35
41
|
overrides?: DeployOverrides;
|
|
36
42
|
reporter?: Reporter;
|
|
37
43
|
packageName?: string;
|
|
44
|
+
deferBasePlaceMerge?: boolean;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as tar from 'tar';
|
|
4
|
+
|
|
5
|
+
// Names reach the bot's PR comment, so a fork's bundle gets no Markdown links,
|
|
6
|
+
// images, URLs, tables or @mentions: only a leading scope `@`, and plain text
|
|
7
|
+
// for a multi-place target's place name ("pkg - Hub (Test)").
|
|
8
|
+
export const PACKAGE_NAME_PATTERN = /^@?[\p{L}\p{N} /()+.,'_~-]{1,200}$/u;
|
|
9
|
+
|
|
10
|
+
export type BundleKind = 'tests' | 'deploy';
|
|
11
|
+
|
|
12
|
+
const RUN_COMMANDS: Record<BundleKind, string> = {
|
|
13
|
+
tests: 'nevermore batch test --bundle <file>',
|
|
14
|
+
deploy: 'nevermore batch deploy --bundle <file>',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface UntrustedTarOptions {
|
|
18
|
+
kind: BundleKind;
|
|
19
|
+
/** The one entry read into memory rather than written to `dir`. */
|
|
20
|
+
manifestName: string;
|
|
21
|
+
maxManifestBytes: number;
|
|
22
|
+
/** Every other entry must pass this, by its exact path in the archive. */
|
|
23
|
+
isEntryAllowed: (name: string) => boolean;
|
|
24
|
+
maxEntries: number;
|
|
25
|
+
/** Message for an archive holding more than `maxEntries` entries. */
|
|
26
|
+
maxEntriesMessage: string;
|
|
27
|
+
/** Returns an error message when `size` is too large for this entry. */
|
|
28
|
+
checkEntrySize: (name: string, size: number) => string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UntrustedTarContents {
|
|
32
|
+
manifestText: string;
|
|
33
|
+
/** Size of each entry written into `dir`, keyed by its archive path. */
|
|
34
|
+
entrySizes: Map<string, number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read a bundle written by a fork's CI run into `dir`. Entries are read from the
|
|
39
|
+
* archive, never extracted by it: only regular files whose paths match
|
|
40
|
+
* `entryPattern` (or are the manifest) are accepted, so a path can never leave
|
|
41
|
+
* `dir` and a link can never be followed.
|
|
42
|
+
*/
|
|
43
|
+
export async function readUntrustedTarAsync(
|
|
44
|
+
file: string,
|
|
45
|
+
dir: string,
|
|
46
|
+
options: UntrustedTarOptions
|
|
47
|
+
): Promise<UntrustedTarContents> {
|
|
48
|
+
let manifestText: string | undefined;
|
|
49
|
+
const entrySizes = new Map<string, number>();
|
|
50
|
+
const writes: Promise<void>[] = [];
|
|
51
|
+
let failure: Error | undefined;
|
|
52
|
+
// Write errors are recorded rather than rejected: the promises are awaited
|
|
53
|
+
// only once parsing ends, and one rejecting before then is unhandled.
|
|
54
|
+
const fail = (err: Error) => {
|
|
55
|
+
failure ??= err;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parser = new tar.Parser({
|
|
59
|
+
strict: true,
|
|
60
|
+
onReadEntry: (entry) => {
|
|
61
|
+
const name = entry.path;
|
|
62
|
+
const reject = (message: string) => {
|
|
63
|
+
failure ??= new Error(message);
|
|
64
|
+
entry.resume();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (entry.type !== 'File' && entry.type !== 'OldFile') {
|
|
68
|
+
return reject(`Bundle entry ${name} is not a regular file`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (name === options.manifestName) {
|
|
72
|
+
if (
|
|
73
|
+
manifestText !== undefined ||
|
|
74
|
+
entry.size > options.maxManifestBytes
|
|
75
|
+
) {
|
|
76
|
+
return reject('Bundle has more than one manifest, or one too large');
|
|
77
|
+
}
|
|
78
|
+
const parts: Buffer[] = [];
|
|
79
|
+
entry.on('data', (part: Buffer) => parts.push(part));
|
|
80
|
+
entry.on('end', () => {
|
|
81
|
+
manifestText = Buffer.concat(parts).toString('utf-8');
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!options.isEntryAllowed(name) || entrySizes.has(name)) {
|
|
87
|
+
return reject(`Bundle has an unexpected entry: ${name}`);
|
|
88
|
+
}
|
|
89
|
+
if (entrySizes.size >= options.maxEntries) {
|
|
90
|
+
return reject(options.maxEntriesMessage);
|
|
91
|
+
}
|
|
92
|
+
const sizeError = options.checkEntrySize(name, entry.size);
|
|
93
|
+
if (sizeError) {
|
|
94
|
+
return reject(sizeError);
|
|
95
|
+
}
|
|
96
|
+
entrySizes.set(name, entry.size);
|
|
97
|
+
writes.push(
|
|
98
|
+
new Promise<void>((resolve) => {
|
|
99
|
+
const done = (err?: Error) => {
|
|
100
|
+
if (err) {
|
|
101
|
+
fail(err);
|
|
102
|
+
}
|
|
103
|
+
resolve();
|
|
104
|
+
};
|
|
105
|
+
const out = createWriteStream(path.join(dir, name));
|
|
106
|
+
out.on('finish', () => done());
|
|
107
|
+
out.on('error', (err) => {
|
|
108
|
+
done(err);
|
|
109
|
+
entry.resume();
|
|
110
|
+
});
|
|
111
|
+
entry.on('error', (err: unknown) =>
|
|
112
|
+
done(err instanceof Error ? err : new Error(String(err)))
|
|
113
|
+
);
|
|
114
|
+
entry.pipe(out);
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await new Promise<void>((resolve, rejectRead) => {
|
|
121
|
+
const input = createReadStream(file);
|
|
122
|
+
input.on('error', rejectRead);
|
|
123
|
+
parser.on('error', (err: Error) =>
|
|
124
|
+
rejectRead(new Error(`Bundle is not a valid tar file: ${err.message}`))
|
|
125
|
+
);
|
|
126
|
+
parser.on('end', () => resolve());
|
|
127
|
+
input.pipe(parser);
|
|
128
|
+
});
|
|
129
|
+
await Promise.all(writes);
|
|
130
|
+
|
|
131
|
+
_checkKind(manifestText, options.kind);
|
|
132
|
+
if (failure) {
|
|
133
|
+
throw failure;
|
|
134
|
+
}
|
|
135
|
+
if (manifestText === undefined) {
|
|
136
|
+
throw new Error(`Bundle is missing ${options.manifestName}`);
|
|
137
|
+
}
|
|
138
|
+
return { manifestText, entrySizes };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _checkKind(manifestText: string | undefined, expected: BundleKind) {
|
|
142
|
+
if (manifestText === undefined) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let kind: unknown;
|
|
146
|
+
try {
|
|
147
|
+
kind = (JSON.parse(manifestText) as { kind?: unknown })?.kind;
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (kind === undefined || kind === expected) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (kind === 'tests' || kind === 'deploy') {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`This is a ${kind} bundle, not a ${expected} bundle. Run it with "${RUN_COMMANDS[kind]}".`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Bundle is of an unknown kind (expected ${expected})`);
|
|
160
|
+
}
|
|
@@ -6,9 +6,16 @@ import {
|
|
|
6
6
|
summarizeError,
|
|
7
7
|
} from '@quenty/cli-output-helpers/reporting';
|
|
8
8
|
|
|
9
|
-
/** Deploy-specific result that extends the generic PackageResult with
|
|
9
|
+
/** Deploy-specific result that extends the generic PackageResult with what was deployed where. */
|
|
10
10
|
export interface BatchDeployResult extends PackageResult {
|
|
11
11
|
placeId: number;
|
|
12
|
+
universeId?: number;
|
|
13
|
+
target?: string;
|
|
14
|
+
version?: number;
|
|
15
|
+
published?: boolean;
|
|
16
|
+
basePlaceVersion?: number;
|
|
17
|
+
commit?: string;
|
|
18
|
+
branch?: string;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
/**
|