@quenty/nevermore-cli 4.51.0 → 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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/commands/batch-command/batch-bundle-command.d.ts +3 -0
  3. package/dist/commands/batch-command/batch-bundle-command.d.ts.map +1 -1
  4. package/dist/commands/batch-command/batch-bundle-command.js +84 -12
  5. package/dist/commands/batch-command/batch-bundle-command.js.map +1 -1
  6. package/dist/commands/batch-command/batch-deploy-command.d.ts +2 -0
  7. package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
  8. package/dist/commands/batch-command/batch-deploy-command.js +115 -16
  9. package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
  10. package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
  11. package/dist/commands/batch-command/batch-test-command.js +7 -3
  12. package/dist/commands/batch-command/batch-test-command.js.map +1 -1
  13. package/dist/commands/deploy-command/index.d.ts.map +1 -1
  14. package/dist/commands/deploy-command/index.js +7 -0
  15. package/dist/commands/deploy-command/index.js.map +1 -1
  16. package/dist/commands/test-command/test-command.d.ts +1 -0
  17. package/dist/commands/test-command/test-command.d.ts.map +1 -1
  18. package/dist/commands/test-command/test-command.js +19 -3
  19. package/dist/commands/test-command/test-command.js.map +1 -1
  20. package/dist/commands/tools-command/ci-post-deploy-results.d.ts.map +1 -1
  21. package/dist/commands/tools-command/ci-post-deploy-results.js +6 -2
  22. package/dist/commands/tools-command/ci-post-deploy-results.js.map +1 -1
  23. package/dist/commands/tools-command/ci-post-test-results.d.ts.map +1 -1
  24. package/dist/commands/tools-command/ci-post-test-results.js +6 -2
  25. package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
  26. package/dist/utils/batch/changed-packages-utils.d.ts +11 -0
  27. package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
  28. package/dist/utils/batch/changed-packages-utils.js +14 -0
  29. package/dist/utils/batch/changed-packages-utils.js.map +1 -1
  30. package/dist/utils/build/build.d.ts +7 -0
  31. package/dist/utils/build/build.d.ts.map +1 -1
  32. package/dist/utils/build/build.js.map +1 -1
  33. package/dist/utils/bundle/untrusted-tar.d.ts +28 -0
  34. package/dist/utils/bundle/untrusted-tar.d.ts.map +1 -0
  35. package/dist/utils/bundle/untrusted-tar.js +116 -0
  36. package/dist/utils/bundle/untrusted-tar.js.map +1 -0
  37. package/dist/utils/deploy/deploy-github-columns.d.ts +8 -1
  38. package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
  39. package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
  40. package/dist/utils/deploy/deploy-metadata.d.ts +5 -0
  41. package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
  42. package/dist/utils/deploy/deploy-metadata.js +36 -0
  43. package/dist/utils/deploy/deploy-metadata.js.map +1 -1
  44. package/dist/utils/deploy/deploy-metadata.test.js +35 -2
  45. package/dist/utils/deploy/deploy-metadata.test.js.map +1 -1
  46. package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts +38 -0
  47. package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts.map +1 -0
  48. package/dist/utils/deploy/prebuilt-deploy-bundle.js +209 -0
  49. package/dist/utils/deploy/prebuilt-deploy-bundle.js.map +1 -0
  50. package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts +2 -0
  51. package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts.map +1 -0
  52. package/dist/utils/deploy/prebuilt-deploy-bundle.test.js +194 -0
  53. package/dist/utils/deploy/prebuilt-deploy-bundle.test.js.map +1 -0
  54. package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
  55. package/dist/utils/job-context/base-job-context.js +3 -2
  56. package/dist/utils/job-context/base-job-context.js.map +1 -1
  57. package/dist/utils/job-context/batch-script-job-context.d.ts +1 -1
  58. package/dist/utils/open-cloud/open-cloud-client.d.ts +1 -0
  59. package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
  60. package/dist/utils/open-cloud/open-cloud-client.js +5 -0
  61. package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
  62. package/dist/utils/open-cloud/open-cloud-client.test.js +11 -0
  63. package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
  64. package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts +1 -1
  65. package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts.map +1 -1
  66. package/dist/utils/testing/runner/prebuilt-batch-bundle.js +13 -68
  67. package/dist/utils/testing/runner/prebuilt-batch-bundle.js.map +1 -1
  68. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js +16 -0
  69. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js.map +1 -1
  70. package/package.json +3 -3
  71. package/src/commands/batch-command/batch-bundle-command.ts +112 -16
  72. package/src/commands/batch-command/batch-deploy-command.ts +160 -16
  73. package/src/commands/batch-command/batch-test-command.ts +7 -3
  74. package/src/commands/deploy-command/index.ts +7 -0
  75. package/src/commands/test-command/test-command.ts +20 -3
  76. package/src/commands/tools-command/ci-post-deploy-results.ts +9 -2
  77. package/src/commands/tools-command/ci-post-test-results.ts +9 -2
  78. package/src/utils/batch/changed-packages-utils.ts +23 -0
  79. package/src/utils/build/build.ts +7 -0
  80. package/src/utils/bundle/untrusted-tar.ts +160 -0
  81. package/src/utils/deploy/deploy-github-columns.ts +8 -1
  82. package/src/utils/deploy/deploy-metadata.test.ts +50 -1
  83. package/src/utils/deploy/deploy-metadata.ts +39 -0
  84. package/src/utils/deploy/prebuilt-deploy-bundle.test.ts +265 -0
  85. package/src/utils/deploy/prebuilt-deploy-bundle.ts +297 -0
  86. package/src/utils/job-context/base-job-context.ts +4 -2
  87. package/src/utils/job-context/batch-script-job-context.ts +1 -1
  88. package/src/utils/open-cloud/open-cloud-client.test.ts +15 -0
  89. package/src/utils/open-cloud/open-cloud-client.ts +11 -0
  90. package/src/utils/testing/runner/prebuilt-batch-bundle.test.ts +20 -0
  91. package/src/utils/testing/runner/prebuilt-batch-bundle.ts +20 -83
  92. package/tsconfig.tsbuildinfo +1 -1
@@ -122,7 +122,7 @@ export class BatchScriptJobContext implements JobContext {
122
122
  maxChunkBytes?: number;
123
123
  reporter?: Reporter;
124
124
  /**
125
- * Places already built by `batch bundle`, run instead of building. They
125
+ * Places already built by `batch bundle --test`, run instead of building. They
126
126
  * upload to `batchPlaceId` in `batchUniverseId`, which are then required.
127
127
  * The context owns `buildContext` and cleans it up like its own build.
128
128
  */
@@ -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
 
@@ -249,4 +249,24 @@ describe('prebuilt batch bundle', () => {
249
249
  'Unsupported bundle format'
250
250
  );
251
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
+ });
252
272
  });
@@ -1,13 +1,16 @@
1
1
  import * as fs from 'fs/promises';
2
- import { createReadStream, createWriteStream } from 'fs';
3
2
  import * as path from 'path';
4
3
  import * as tar from 'tar';
5
4
  import { BuildContext } from '@quenty/nevermore-template-helpers';
6
5
  import { type CombinedPlaceChunk } from './combined-project-generator.js';
7
6
  import { PLACE_UPLOAD_MAX_BYTES } from '../../open-cloud/open-cloud-client.js';
7
+ import {
8
+ PACKAGE_NAME_PATTERN,
9
+ readUntrustedTarAsync,
10
+ } from '../../bundle/untrusted-tar.js';
8
11
 
9
12
  /**
10
- * Places built by `nevermore batch bundle`, unpacked from a
13
+ * Places built by `nevermore batch bundle --test`, unpacked from a
11
14
  * `*.nevermore.tests.tar` and ready for `batch test --bundle` to run.
12
15
  */
13
16
  export interface PrebuiltBatchBundle {
@@ -24,6 +27,7 @@ export interface PrebuiltBatchPackage {
24
27
 
25
28
  interface ManifestFile {
26
29
  formatVersion: number;
30
+ kind?: 'tests';
27
31
  chunks: Array<{
28
32
  file: string;
29
33
  packages: Array<{ name: string; slug: string; hasJestConfig: boolean }>;
@@ -39,10 +43,6 @@ const MAX_CHUNKS = 32;
39
43
  const MAX_PACKAGES = 1000;
40
44
  const MAX_MANIFEST_BYTES = 1024 * 1024;
41
45
  const CHUNK_FILE_PATTERN = /^place-\d{1,3}\.rbxl$/;
42
- // Names reach the bot's PR comment, so a fork's bundle gets no Markdown links,
43
- // images, URLs, tables or @mentions: only a leading scope `@`, and plain text
44
- // for a multi-place target's place name ("pkg - Hub (Test)").
45
- const PACKAGE_NAME_PATTERN = /^@?[\p{L}\p{N} /()+.,'_~-]{1,200}$/u;
46
46
  const SLUG_PATTERN = /^[A-Za-z0-9_.-]{1,100}$/;
47
47
 
48
48
  export async function writePrebuiltBatchBundleAsync(
@@ -55,6 +55,7 @@ export async function writePrebuiltBatchBundleAsync(
55
55
  try {
56
56
  const manifest: ManifestFile = {
57
57
  formatVersion: FORMAT_VERSION,
58
+ kind: 'tests',
58
59
  chunks: [],
59
60
  };
60
61
  for (const [index, chunk] of chunks.entries()) {
@@ -107,7 +108,19 @@ export async function readPrebuiltBatchBundleAsync(
107
108
  });
108
109
  const dir = buildContext.buildDir;
109
110
  try {
110
- const { manifestText, placeSizes } = await _unpackAsync(file, dir);
111
+ const { manifestText, entrySizes: placeSizes } =
112
+ await readUntrustedTarAsync(file, dir, {
113
+ kind: 'tests',
114
+ manifestName: MANIFEST_FILE_NAME,
115
+ maxManifestBytes: MAX_MANIFEST_BYTES,
116
+ isEntryAllowed: (name) => CHUNK_FILE_PATTERN.test(name),
117
+ maxEntries: MAX_CHUNKS,
118
+ maxEntriesMessage: `Bundle must hold at most ${MAX_CHUNKS} places`,
119
+ checkEntrySize: (name, size) =>
120
+ size > PLACE_UPLOAD_MAX_BYTES
121
+ ? `${name} is over the place upload limit`
122
+ : undefined,
123
+ });
111
124
  return { ..._parseManifest(manifestText, dir, placeSizes), buildContext };
112
125
  } catch (err) {
113
126
  await buildContext.cleanupAsync();
@@ -115,82 +128,6 @@ export async function readPrebuiltBatchBundleAsync(
115
128
  }
116
129
  }
117
130
 
118
- async function _unpackAsync(
119
- file: string,
120
- dir: string
121
- ): Promise<{ manifestText: string; placeSizes: Map<string, number> }> {
122
- let manifestText: string | undefined;
123
- const placeSizes = new Map<string, number>();
124
- const writes: Promise<void>[] = [];
125
- let failure: Error | undefined;
126
-
127
- const parser = new tar.Parser({
128
- strict: true,
129
- onReadEntry: (entry) => {
130
- const name = entry.path;
131
- const reject = (message: string) => {
132
- failure ??= new Error(message);
133
- entry.resume();
134
- };
135
-
136
- if (entry.type !== 'File' && entry.type !== 'OldFile') {
137
- return reject(`Bundle entry ${name} is not a regular file`);
138
- }
139
-
140
- if (name === MANIFEST_FILE_NAME) {
141
- if (manifestText !== undefined || entry.size > MAX_MANIFEST_BYTES) {
142
- return reject('Bundle has more than one manifest, or one too large');
143
- }
144
- const parts: Buffer[] = [];
145
- entry.on('data', (part: Buffer) => parts.push(part));
146
- entry.on('end', () => {
147
- manifestText = Buffer.concat(parts).toString('utf-8');
148
- });
149
- return;
150
- }
151
-
152
- if (!CHUNK_FILE_PATTERN.test(name) || placeSizes.has(name)) {
153
- return reject(`Bundle has an unexpected entry: ${name}`);
154
- }
155
- if (placeSizes.size >= MAX_CHUNKS) {
156
- return reject(`Bundle must hold at most ${MAX_CHUNKS} places`);
157
- }
158
- if (entry.size > PLACE_UPLOAD_MAX_BYTES) {
159
- return reject(`${name} is over the place upload limit`);
160
- }
161
- placeSizes.set(name, entry.size);
162
- writes.push(
163
- new Promise<void>((resolve, rejectWrite) => {
164
- const out = createWriteStream(path.join(dir, name));
165
- out.on('finish', () => resolve());
166
- out.on('error', rejectWrite);
167
- entry.on('error', rejectWrite);
168
- entry.pipe(out);
169
- })
170
- );
171
- },
172
- });
173
-
174
- await new Promise<void>((resolve, rejectRead) => {
175
- const input = createReadStream(file);
176
- input.on('error', rejectRead);
177
- parser.on('error', (err: Error) =>
178
- rejectRead(new Error(`Bundle is not a valid tar file: ${err.message}`))
179
- );
180
- parser.on('end', () => resolve());
181
- input.pipe(parser);
182
- });
183
- await Promise.all(writes);
184
-
185
- if (failure) {
186
- throw failure;
187
- }
188
- if (manifestText === undefined) {
189
- throw new Error(`Bundle is missing ${MANIFEST_FILE_NAME}`);
190
- }
191
- return { manifestText, placeSizes };
192
- }
193
-
194
131
  function _parseManifest(
195
132
  manifestText: string,
196
133
  dir: string,