@quenty/nevermore-cli 4.37.0 → 4.38.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 (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/commands/deploy-command/index.d.ts.map +1 -1
  3. package/dist/commands/deploy-command/index.js +43 -0
  4. package/dist/commands/deploy-command/index.js.map +1 -1
  5. package/dist/commands/deploy-command/version-command.d.ts +21 -0
  6. package/dist/commands/deploy-command/version-command.d.ts.map +1 -0
  7. package/dist/commands/deploy-command/version-command.js +206 -0
  8. package/dist/commands/deploy-command/version-command.js.map +1 -0
  9. package/dist/utils/build/deploy-config.d.ts +8 -0
  10. package/dist/utils/build/deploy-config.d.ts.map +1 -1
  11. package/dist/utils/build/deploy-config.js +5 -0
  12. package/dist/utils/build/deploy-config.js.map +1 -1
  13. package/dist/utils/build/deploy-config.test.js +86 -1
  14. package/dist/utils/build/deploy-config.test.js.map +1 -1
  15. package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
  16. package/dist/utils/job-context/base-job-context.js +4 -2
  17. package/dist/utils/job-context/base-job-context.js.map +1 -1
  18. package/dist/utils/open-cloud/open-cloud-client.d.ts +12 -1
  19. package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
  20. package/dist/utils/open-cloud/open-cloud-client.js +49 -4
  21. package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
  22. package/dist/utils/open-cloud/open-cloud-client.test.js +89 -0
  23. package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/commands/deploy-command/index.ts +71 -0
  26. package/src/commands/deploy-command/version-command.ts +311 -0
  27. package/src/utils/build/deploy-config.test.ts +98 -0
  28. package/src/utils/build/deploy-config.ts +17 -0
  29. package/src/utils/job-context/base-job-context.ts +7 -2
  30. package/src/utils/open-cloud/open-cloud-client.test.ts +132 -0
  31. package/src/utils/open-cloud/open-cloud-client.ts +84 -5
  32. package/tsconfig.tsbuildinfo +1 -1
@@ -390,17 +390,26 @@ export class OpenCloudClient {
390
390
  * Requires the `legacy-asset:manage` scope on the API key.
391
391
  *
392
392
  * Two-step process: fetch a temporary CDN URL, then download the binary.
393
+ *
394
+ * Pass `version` to fetch a specific published version instead of the latest.
395
+ * This is how deploys pin a `basePlace` so a broken Studio edit can't leak in.
393
396
  */
394
397
  async downloadPlaceAsync(
395
398
  universeId: number,
396
399
  placeId: number,
397
- onProgress?: (transferredBytes: number, totalBytes: number) => void
400
+ onProgress?: (transferredBytes: number, totalBytes: number) => void,
401
+ version?: number
398
402
  ): Promise<Buffer> {
399
403
  const apiKey = await this._resolveApiKeyAsync();
400
- const url = `https://apis.roblox.com/asset-delivery-api/v1/assetId/${placeId}`;
404
+ const url =
405
+ version != null
406
+ ? `https://apis.roblox.com/asset-delivery-api/v1/assetId/${placeId}/version/${version}`
407
+ : `https://apis.roblox.com/asset-delivery-api/v1/assetId/${placeId}`;
401
408
 
402
409
  OutputHelper.verbose(
403
- `Downloading base place from https://www.roblox.com/games/${placeId}/place ...`
410
+ `Downloading base place${
411
+ version != null ? ` v${version}` : ''
412
+ } from https://www.roblox.com/games/${placeId}/place ...`
404
413
  );
405
414
 
406
415
  const response = await this._rateLimiter.fetchAsync(url, {
@@ -432,14 +441,84 @@ export class OpenCloudClient {
432
441
  );
433
442
  }
434
443
 
435
- const data = (await response.json()) as { location: string };
444
+ // The Asset Delivery API reports a missing asset/version as HTTP 200 with an
445
+ // `errors` array rather than an error status, so a bad `version` pin lands
446
+ // here instead of the block above.
447
+ const data = (await response.json()) as {
448
+ location?: string;
449
+ errors?: Array<{ code?: number; message?: string }>;
450
+ };
436
451
  if (!data.location) {
437
- throw new Error('Download place failed: no CDN location in response');
452
+ const apiMessage = data.errors?.[0]?.message;
453
+ if (version != null) {
454
+ throw new Error(
455
+ `Base place ${placeId} has no version ${version}` +
456
+ (apiMessage ? ` (${apiMessage})` : '') +
457
+ `. Run "nevermore deploy version upgrade" to re-pin, or check the pinned version in deploy.nevermore.json.`
458
+ );
459
+ }
460
+ throw new Error(
461
+ `Download place failed: no CDN location in response` +
462
+ (apiMessage ? `: ${apiMessage}` : '')
463
+ );
438
464
  }
439
465
 
440
466
  OutputHelper.verbose('Fetching place binary from CDN...');
441
467
  return _fetchCdnBinaryAsync(data.location, onProgress);
442
468
  }
469
+
470
+ /**
471
+ * Resolve the current latest published version number of a place, via the
472
+ * Open Cloud Assets API (`asset:read` scope; `legacy-asset:manage` also
473
+ * grants it). The returned number is the same value the Asset Delivery API's
474
+ * `/version/{n}` route expects, so it can be written straight into a
475
+ * `basePlace.version` pin.
476
+ */
477
+ async getLatestPlaceVersionAsync(
478
+ universeId: number,
479
+ placeId: number
480
+ ): Promise<number> {
481
+ const apiKey = await this._resolveApiKeyAsync();
482
+ const url = `https://apis.roblox.com/assets/v1/assets/${placeId}`;
483
+
484
+ const response = await this._rateLimiter.fetchAsync(url, {
485
+ method: 'GET',
486
+ headers: {
487
+ 'x-api-key': apiKey,
488
+ },
489
+ });
490
+
491
+ if (!response.ok) {
492
+ const text = await response.text();
493
+ if (response.status === 401 || response.status === 403) {
494
+ throw new Error(
495
+ formatAuthError(
496
+ 'Read place version',
497
+ 'asset:read',
498
+ universeId,
499
+ placeId,
500
+ response.status,
501
+ response.statusText,
502
+ text,
503
+ 'GET',
504
+ url
505
+ )
506
+ );
507
+ }
508
+ throw new Error(
509
+ `Read place version failed: ${response.status} ${response.statusText}: ${text}`
510
+ );
511
+ }
512
+
513
+ const data = (await response.json()) as { revisionId?: string };
514
+ const version = Number(data.revisionId);
515
+ if (!data.revisionId || !Number.isFinite(version)) {
516
+ throw new Error(
517
+ `Read place version failed: no revisionId for place ${placeId}`
518
+ );
519
+ }
520
+ return version;
521
+ }
443
522
  }
444
523
 
445
524
  const CDN_MAX_ATTEMPTS = 4;