@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
|
@@ -42,6 +42,8 @@ class BatchDeployment implements Deployment {
|
|
|
42
42
|
) {}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type BatchBuild = Pick<CombinedProjectResult, 'chunks' | 'primaryTarget'>;
|
|
46
|
+
|
|
45
47
|
/** One combined place's upload and execution, each started at most once. */
|
|
46
48
|
interface ChunkRun {
|
|
47
49
|
chunk: CombinedPlaceChunk;
|
|
@@ -98,7 +100,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
98
100
|
private _reporter?: Reporter;
|
|
99
101
|
|
|
100
102
|
// Lazy-promise state
|
|
101
|
-
private _combinedBuildPromise?: Promise<
|
|
103
|
+
private _combinedBuildPromise?: Promise<BatchBuild>;
|
|
102
104
|
private _chunkRuns?: ChunkRun[];
|
|
103
105
|
private _executionPromise?: Promise<BatchExecutionOutcome>;
|
|
104
106
|
|
|
@@ -119,6 +121,12 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
119
121
|
*/
|
|
120
122
|
maxChunkBytes?: number;
|
|
121
123
|
reporter?: Reporter;
|
|
124
|
+
/**
|
|
125
|
+
* Places already built by `batch bundle --test`, run instead of building. They
|
|
126
|
+
* upload to `batchPlaceId` in `batchUniverseId`, which are then required.
|
|
127
|
+
* The context owns `buildContext` and cleans it up like its own build.
|
|
128
|
+
*/
|
|
129
|
+
prebuilt?: { chunks: CombinedPlaceChunk[]; buildContext: BuildContext };
|
|
122
130
|
}
|
|
123
131
|
) {
|
|
124
132
|
this._inner = inner;
|
|
@@ -130,6 +138,25 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
130
138
|
this._maxChunkBytes = options?.maxChunkBytes;
|
|
131
139
|
this._reporter = options?.reporter;
|
|
132
140
|
|
|
141
|
+
if (options?.prebuilt) {
|
|
142
|
+
if (!this._batchPlaceId || !this._batchUniverseId) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Prebuilt places need batchPlaceId and batchUniverseId to upload to'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const { chunks, buildContext } = options.prebuilt;
|
|
148
|
+
this._combinedBuildContext = buildContext;
|
|
149
|
+
this._chunkRuns = chunks.map((chunk) => ({ chunk }));
|
|
150
|
+
this._combinedBuildPromise = Promise.resolve({
|
|
151
|
+
chunks,
|
|
152
|
+
primaryTarget: {
|
|
153
|
+
placeId: this._batchPlaceId,
|
|
154
|
+
universeId: this._batchUniverseId,
|
|
155
|
+
project: '',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
const withBasePlace = batchTargets.filter((t) => t.target.basePlace);
|
|
134
161
|
if (withBasePlace.length > 0) {
|
|
135
162
|
OutputHelper.warn(
|
|
@@ -203,6 +230,10 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
203
230
|
return this._getBatchExecutionAsync();
|
|
204
231
|
}
|
|
205
232
|
|
|
233
|
+
async getBuiltChunksAsync(): Promise<CombinedPlaceChunk[]> {
|
|
234
|
+
return (await this._getChunkRunsAsync()).map((run) => run.chunk);
|
|
235
|
+
}
|
|
236
|
+
|
|
206
237
|
async getLogsAsync(deployment: Deployment): Promise<JobLogs> {
|
|
207
238
|
const batchDeployment = deployment as BatchDeployment;
|
|
208
239
|
const { results, stats } = await this._getBatchExecutionAsync();
|
|
@@ -249,14 +280,14 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
249
280
|
|
|
250
281
|
// ---- Lazy-promise internals ----
|
|
251
282
|
|
|
252
|
-
private _getCombinedBuildAsync(): Promise<
|
|
283
|
+
private _getCombinedBuildAsync(): Promise<BatchBuild> {
|
|
253
284
|
if (!this._combinedBuildPromise) {
|
|
254
285
|
this._combinedBuildPromise = this._doCombinedBuildAsync();
|
|
255
286
|
}
|
|
256
287
|
return this._combinedBuildPromise;
|
|
257
288
|
}
|
|
258
289
|
|
|
259
|
-
private async _doCombinedBuildAsync(): Promise<
|
|
290
|
+
private async _doCombinedBuildAsync(): Promise<BatchBuild> {
|
|
260
291
|
OutputHelper.verbose('Building combined batch place...');
|
|
261
292
|
|
|
262
293
|
// Set all packages to "waiting" — they're queued for building
|
|
@@ -733,6 +733,21 @@ describe('OpenCloudClient asset owner lookups', () => {
|
|
|
733
733
|
});
|
|
734
734
|
});
|
|
735
735
|
|
|
736
|
+
describe('OpenCloudClient.createExecutionTaskAsync', () => {
|
|
737
|
+
it('refuses a script over the 4 MB limit without sending it', async () => {
|
|
738
|
+
const fakeLimiter = { fetchAsync: vi.fn() };
|
|
739
|
+
const client = new OpenCloudClient({
|
|
740
|
+
apiKey: 'key',
|
|
741
|
+
rateLimiter: fakeLimiter as unknown as RateLimiter,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
await expect(
|
|
745
|
+
client.createExecutionTaskAsync(1, 2, 3, 'a'.repeat(4_000_001))
|
|
746
|
+
).rejects.toThrow('over the Open Cloud limit of 4 MB');
|
|
747
|
+
expect(fakeLimiter.fetchAsync).not.toHaveBeenCalled();
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
736
751
|
describe('getTaskReturnValues', () => {
|
|
737
752
|
function makeTask(overrides: Partial<LuauTask>): LuauTask {
|
|
738
753
|
return {
|
|
@@ -28,6 +28,8 @@ import { recompressPlace } from '../build/place-recompression.js';
|
|
|
28
28
|
*/
|
|
29
29
|
export const PLACE_UPLOAD_MAX_BYTES = 150 * 1024 * 1024;
|
|
30
30
|
|
|
31
|
+
export const LUAU_EXECUTION_SCRIPT_MAX_BYTES = 4_000_000;
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* One engine message, with the severity Open Cloud reported for it.
|
|
33
35
|
*
|
|
@@ -397,6 +399,15 @@ export class OpenCloudClient {
|
|
|
397
399
|
timeoutMs?: number,
|
|
398
400
|
onRateLimitWait?: (wait: RateLimitWait) => void
|
|
399
401
|
): Promise<LuauTask> {
|
|
402
|
+
const scriptBytes = Buffer.byteLength(script, 'utf-8');
|
|
403
|
+
if (scriptBytes > LUAU_EXECUTION_SCRIPT_MAX_BYTES) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`The script is ${_formatMegabytes(
|
|
406
|
+
scriptBytes
|
|
407
|
+
)}, over the Open Cloud limit of 4 MB for a Luau execution script`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
400
411
|
const apiKey = await this._resolveApiKeyAsync();
|
|
401
412
|
const url = `https://apis.roblox.com/cloud/v2/universes/${universeId}/places/${placeId}/versions/${placeVersion}/luau-execution-session-tasks`;
|
|
402
413
|
|
|
@@ -4,17 +4,16 @@ import type { SourcemapNode } from './sourcemap-types.js';
|
|
|
4
4
|
import { SourcemapResolver } from './sourcemap-resolver.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Try to load a `SourcemapResolver` from `sourcemap.json`
|
|
8
|
-
*
|
|
7
|
+
* Try to load a `SourcemapResolver` for `repoRoot`, from its `sourcemap.json`
|
|
8
|
+
* unless another file is given.
|
|
9
9
|
*
|
|
10
10
|
* Returns `undefined` if the file doesn't exist or can't be parsed — callers
|
|
11
11
|
* should fall back to heuristic resolution.
|
|
12
12
|
*/
|
|
13
13
|
export function tryLoadSourcemapResolver(
|
|
14
|
-
repoRoot: string
|
|
14
|
+
repoRoot: string,
|
|
15
|
+
sourcemapPath = path.join(repoRoot, 'sourcemap.json')
|
|
15
16
|
): SourcemapResolver | undefined {
|
|
16
|
-
const sourcemapPath = path.join(repoRoot, 'sourcemap.json');
|
|
17
|
-
|
|
18
17
|
let content: string;
|
|
19
18
|
try {
|
|
20
19
|
content = fs.readFileSync(sourcemapPath, 'utf-8');
|
|
@@ -22,12 +21,11 @@ export function tryLoadSourcemapResolver(
|
|
|
22
21
|
return undefined;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
// A fork's build supplies this file, so a malformed tree is not fatal.
|
|
26
25
|
try {
|
|
27
|
-
root = JSON.parse(content) as SourcemapNode;
|
|
26
|
+
const root = JSON.parse(content) as SourcemapNode;
|
|
27
|
+
return SourcemapResolver.fromSourcemap(root, repoRoot);
|
|
28
28
|
} catch {
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
return SourcemapResolver.fromSourcemap(root, repoRoot);
|
|
33
31
|
}
|
|
@@ -142,6 +142,7 @@ class FakeTransport implements JobContext {
|
|
|
142
142
|
public scriptContent?: string;
|
|
143
143
|
public scriptContents: string[] = [];
|
|
144
144
|
public deployedPaths: string[] = [];
|
|
145
|
+
public deployedTargets: unknown[] = [];
|
|
145
146
|
public deployCount = 0;
|
|
146
147
|
public runCount = 0;
|
|
147
148
|
public messagesFor?: (script: string) => TaskLogMessage[];
|
|
@@ -160,6 +161,7 @@ class FakeTransport implements JobContext {
|
|
|
160
161
|
): Promise<Deployment> {
|
|
161
162
|
this.deployCount++;
|
|
162
163
|
this.deployedPaths.push(options.builtPlace.rbxlPath);
|
|
164
|
+
this.deployedTargets.push(options.builtPlace.target);
|
|
163
165
|
return {};
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -506,4 +508,44 @@ describe('an aggregated batch run, end to end', () => {
|
|
|
506
508
|
])
|
|
507
509
|
);
|
|
508
510
|
});
|
|
511
|
+
|
|
512
|
+
it('runs prebuilt places on the target it was given, without building', async () => {
|
|
513
|
+
const generate = vi.mocked(
|
|
514
|
+
(await import('./runner/combined-project-generator.js'))
|
|
515
|
+
.generateCombinedProjectAsync
|
|
516
|
+
);
|
|
517
|
+
generate.mockClear();
|
|
518
|
+
const { targets } = createBatch();
|
|
519
|
+
const transport = new FakeTransport(TASK_MESSAGES);
|
|
520
|
+
const cleanupAsync = vi.fn(async () => {});
|
|
521
|
+
const batch = new BatchScriptJobContext(transport, targets, {
|
|
522
|
+
repoRoot: '/repo',
|
|
523
|
+
batchPlaceId: 900,
|
|
524
|
+
batchUniverseId: 901,
|
|
525
|
+
prebuilt: {
|
|
526
|
+
chunks: [
|
|
527
|
+
{
|
|
528
|
+
rbxlPath: '/bundle/place-1.rbxl',
|
|
529
|
+
slugMap: SLUG_MAP,
|
|
530
|
+
sizeBytes: 1024,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
buildContext: { cleanupAsync } as any,
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const alpha = await runPackageAsync(batch, '@quenty/alpha');
|
|
538
|
+
const beta = await runPackageAsync(batch, '@quenty/beta');
|
|
539
|
+
|
|
540
|
+
expect(generate).not.toHaveBeenCalled();
|
|
541
|
+
expect(transport.deployedPaths).toEqual(['/bundle/place-1.rbxl']);
|
|
542
|
+
expect(transport.deployedTargets).toEqual([
|
|
543
|
+
{ placeId: 900, universeId: 901, project: '' },
|
|
544
|
+
]);
|
|
545
|
+
expect(alpha.success).toBe(true);
|
|
546
|
+
expect(beta.success).toBe(false);
|
|
547
|
+
|
|
548
|
+
await batch.disposeAsync();
|
|
549
|
+
expect(cleanupAsync).toHaveBeenCalledOnce();
|
|
550
|
+
});
|
|
509
551
|
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { Header } from 'tar';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
type PrebuiltBatchBundle,
|
|
8
|
+
readPrebuiltBatchBundleAsync,
|
|
9
|
+
writePrebuiltBatchBundleAsync,
|
|
10
|
+
} from './prebuilt-batch-bundle.js';
|
|
11
|
+
|
|
12
|
+
interface RawEntry {
|
|
13
|
+
path: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
type?: 'File' | 'SymbolicLink' | 'Directory';
|
|
16
|
+
linkpath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rawTar(entries: RawEntry[]): Buffer {
|
|
20
|
+
const blocks: Buffer[] = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const body = Buffer.from(entry.body ?? '');
|
|
23
|
+
const header = new Header({
|
|
24
|
+
path: entry.path,
|
|
25
|
+
type: entry.type ?? 'File',
|
|
26
|
+
linkpath: entry.linkpath,
|
|
27
|
+
size: entry.type && entry.type !== 'File' ? 0 : body.length,
|
|
28
|
+
mode: 0o644,
|
|
29
|
+
mtime: new Date(0),
|
|
30
|
+
});
|
|
31
|
+
header.encode();
|
|
32
|
+
blocks.push(header.block!);
|
|
33
|
+
if (!entry.type || entry.type === 'File') {
|
|
34
|
+
const padded = Buffer.alloc(Math.ceil(body.length / 512) * 512);
|
|
35
|
+
body.copy(padded);
|
|
36
|
+
blocks.push(padded);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
blocks.push(Buffer.alloc(1024));
|
|
40
|
+
return Buffer.concat(blocks);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validManifest(): any {
|
|
44
|
+
return {
|
|
45
|
+
formatVersion: 1,
|
|
46
|
+
chunks: [
|
|
47
|
+
{
|
|
48
|
+
file: 'place-1.rbxl',
|
|
49
|
+
packages: [
|
|
50
|
+
{ name: '@quenty/rx', slug: 'rx', hasJestConfig: true },
|
|
51
|
+
{ name: '@quenty/maid', slug: 'maid', hasJestConfig: false },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('prebuilt batch bundle', () => {
|
|
59
|
+
let workDir: string;
|
|
60
|
+
const opened: PrebuiltBatchBundle[] = [];
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prebuilt-bundle-'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(async () => {
|
|
67
|
+
for (const bundle of opened.splice(0)) {
|
|
68
|
+
await bundle.buildContext.cleanupAsync();
|
|
69
|
+
}
|
|
70
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
async function readAsync(file: string) {
|
|
74
|
+
const bundle = await readPrebuiltBatchBundleAsync(file);
|
|
75
|
+
opened.push(bundle);
|
|
76
|
+
return bundle;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function writeRawAsync(entries: RawEntry[]): Promise<string> {
|
|
80
|
+
const file = path.join(workDir, 'raw.nevermore.tests.tar');
|
|
81
|
+
await fs.writeFile(file, rawTar(entries));
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeManifestAsync(manifest: any): Promise<string> {
|
|
86
|
+
return writeRawAsync([
|
|
87
|
+
{ path: 'manifest.json', body: JSON.stringify(manifest) },
|
|
88
|
+
{ path: 'place-1.rbxl', body: 'place bytes' },
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('round-trips places, slugs and jest.config flags through one file', async () => {
|
|
93
|
+
const source = path.join(workDir, 'source.rbxl');
|
|
94
|
+
await fs.writeFile(source, 'place bytes');
|
|
95
|
+
const file = path.join(workDir, 'out', 'pr-1.nevermore.tests.tar');
|
|
96
|
+
await writePrebuiltBatchBundleAsync(
|
|
97
|
+
file,
|
|
98
|
+
[
|
|
99
|
+
{
|
|
100
|
+
rbxlPath: source,
|
|
101
|
+
slugMap: new Map([
|
|
102
|
+
['@quenty/rx', 'rx'],
|
|
103
|
+
['@quenty/maid', 'maid'],
|
|
104
|
+
]),
|
|
105
|
+
sizeBytes: 11,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
new Map([['@quenty/rx', true]])
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const bundle = await readAsync(file);
|
|
112
|
+
|
|
113
|
+
expect(bundle.packages).toEqual([
|
|
114
|
+
{ name: '@quenty/rx', hasJestConfig: true },
|
|
115
|
+
{ name: '@quenty/maid', hasJestConfig: false },
|
|
116
|
+
]);
|
|
117
|
+
expect(bundle.chunks).toHaveLength(1);
|
|
118
|
+
expect(bundle.chunks[0]!.sizeBytes).toBe(11);
|
|
119
|
+
expect([...bundle.chunks[0]!.slugMap]).toEqual([
|
|
120
|
+
['@quenty/rx', 'rx'],
|
|
121
|
+
['@quenty/maid', 'maid'],
|
|
122
|
+
]);
|
|
123
|
+
expect(await fs.readFile(bundle.chunks[0]!.rbxlPath, 'utf-8')).toBe(
|
|
124
|
+
'place bytes'
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('reads an empty bundle as no packages', async () => {
|
|
129
|
+
const file = path.join(workDir, 'empty.nevermore.tests.tar');
|
|
130
|
+
await writePrebuiltBatchBundleAsync(file, [], new Map());
|
|
131
|
+
|
|
132
|
+
const bundle = await readAsync(file);
|
|
133
|
+
|
|
134
|
+
expect(bundle.packages).toEqual([]);
|
|
135
|
+
expect(bundle.chunks).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('removes the unpacked places on cleanup', async () => {
|
|
139
|
+
const bundle = await readPrebuiltBatchBundleAsync(
|
|
140
|
+
await writeManifestAsync(validManifest())
|
|
141
|
+
);
|
|
142
|
+
const placePath = bundle.chunks[0]!.rbxlPath;
|
|
143
|
+
|
|
144
|
+
await bundle.buildContext.cleanupAsync();
|
|
145
|
+
|
|
146
|
+
await expect(fs.stat(placePath)).rejects.toThrow();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('never writes outside its own directory for an entry that escapes', async () => {
|
|
150
|
+
const file = await writeRawAsync([
|
|
151
|
+
{ path: 'manifest.json', body: JSON.stringify(validManifest()) },
|
|
152
|
+
{ path: 'place-1.rbxl', body: 'place bytes' },
|
|
153
|
+
{ path: '../escaped.rbxl', body: 'evil' },
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
await expect(readAsync(file)).rejects.toThrow('unexpected entry');
|
|
157
|
+
await expect(
|
|
158
|
+
fs.stat(path.join(os.tmpdir(), 'escaped.rbxl'))
|
|
159
|
+
).rejects.toThrow();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('rejects a symlink entry', async () => {
|
|
163
|
+
const file = await writeRawAsync([
|
|
164
|
+
{ path: 'manifest.json', body: JSON.stringify(validManifest()) },
|
|
165
|
+
{ path: 'place-1.rbxl', type: 'SymbolicLink', linkpath: '/etc/passwd' },
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
await expect(readAsync(file)).rejects.toThrow('not a regular file');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('rejects a place the manifest does not list', async () => {
|
|
172
|
+
const file = await writeRawAsync([
|
|
173
|
+
{ path: 'manifest.json', body: JSON.stringify(validManifest()) },
|
|
174
|
+
{ path: 'place-1.rbxl', body: 'place bytes' },
|
|
175
|
+
{ path: 'place-2.rbxl', body: 'extra' },
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
await expect(readAsync(file)).rejects.toThrow('every place it holds');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('rejects a manifest that lists a place the bundle does not hold', async () => {
|
|
182
|
+
const manifest = validManifest();
|
|
183
|
+
manifest.chunks[0].file = 'place-2.rbxl';
|
|
184
|
+
|
|
185
|
+
await expect(readAsync(await writeManifestAsync(manifest))).rejects.toThrow(
|
|
186
|
+
'invalid place file'
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('rejects a file that is not a tar', async () => {
|
|
191
|
+
const file = path.join(workDir, 'junk.nevermore.tests.tar');
|
|
192
|
+
await fs.writeFile(file, 'not a tar at all');
|
|
193
|
+
|
|
194
|
+
await expect(readAsync(file)).rejects.toThrow();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('rejects a slug that could break out of the batch script', async () => {
|
|
198
|
+
const manifest = validManifest();
|
|
199
|
+
manifest.chunks[0].packages[0].slug = 'rx"]] print("hi") --';
|
|
200
|
+
|
|
201
|
+
await expect(readAsync(await writeManifestAsync(manifest))).rejects.toThrow(
|
|
202
|
+
'invalid slug'
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('accepts a multi-place target name with a free-form place name', async () => {
|
|
207
|
+
const manifest = validManifest();
|
|
208
|
+
manifest.chunks[0].packages[0].name = '@quenty/rx - Hub (Test) + Lobby é';
|
|
209
|
+
|
|
210
|
+
const bundle = await readAsync(await writeManifestAsync(manifest));
|
|
211
|
+
|
|
212
|
+
expect(bundle.packages[0]!.name).toBe('@quenty/rx - Hub (Test) + Lobby é');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it.each([
|
|
216
|
+
'@quenty/rx | injected',
|
|
217
|
+
'@quenty/rx<img>',
|
|
218
|
+
'@quenty/rx\nline',
|
|
219
|
+
'@quenty/rx [Approved](https://evil.example)',
|
|
220
|
+
'@quenty/rx ',
|
|
221
|
+
'@quenty/rx cc @someuser',
|
|
222
|
+
'@quenty/rx https://evil.example',
|
|
223
|
+
])(
|
|
224
|
+
'rejects a package name that could break the PR comment: %j',
|
|
225
|
+
async (name) => {
|
|
226
|
+
const manifest = validManifest();
|
|
227
|
+
manifest.chunks[0].packages[0].name = name;
|
|
228
|
+
|
|
229
|
+
await expect(
|
|
230
|
+
readAsync(await writeManifestAsync(manifest))
|
|
231
|
+
).rejects.toThrow('invalid package name');
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
it('rejects a package listed twice', async () => {
|
|
236
|
+
const manifest = validManifest();
|
|
237
|
+
manifest.chunks[0].packages[1].name = '@quenty/rx';
|
|
238
|
+
|
|
239
|
+
await expect(readAsync(await writeManifestAsync(manifest))).rejects.toThrow(
|
|
240
|
+
'more than once'
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('rejects an unknown format version', async () => {
|
|
245
|
+
const manifest = validManifest();
|
|
246
|
+
manifest.formatVersion = 2;
|
|
247
|
+
|
|
248
|
+
await expect(readAsync(await writeManifestAsync(manifest))).rejects.toThrow(
|
|
249
|
+
'Unsupported bundle format'
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('names the right command for a deploy bundle, before its unexpected entries', async () => {
|
|
254
|
+
const file = await writeRawAsync([
|
|
255
|
+
{
|
|
256
|
+
path: 'manifest.json',
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
formatVersion: 1,
|
|
259
|
+
kind: 'deploy',
|
|
260
|
+
target: 'integration',
|
|
261
|
+
places: [],
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
{ path: 'place-1.rbxl', body: 'place bytes' },
|
|
265
|
+
{ path: 'project-1.json', body: '{}' },
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
await expect(readAsync(file)).rejects.toThrow(
|
|
269
|
+
'This is a deploy bundle, not a tests bundle. Run it with "nevermore batch deploy --bundle <file>".'
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|