@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/commands/batch-command/batch-bundle-command.d.ts +15 -0
  3. package/dist/commands/batch-command/batch-bundle-command.d.ts.map +1 -0
  4. package/dist/commands/batch-command/batch-bundle-command.js +168 -0
  5. package/dist/commands/batch-command/batch-bundle-command.js.map +1 -0
  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 +1 -0
  11. package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
  12. package/dist/commands/batch-command/batch-test-command.js +109 -17
  13. package/dist/commands/batch-command/batch-test-command.js.map +1 -1
  14. package/dist/commands/batch-command/index.d.ts.map +1 -1
  15. package/dist/commands/batch-command/index.js +2 -0
  16. package/dist/commands/batch-command/index.js.map +1 -1
  17. package/dist/commands/deploy-command/index.d.ts.map +1 -1
  18. package/dist/commands/deploy-command/index.js +7 -0
  19. package/dist/commands/deploy-command/index.js.map +1 -1
  20. package/dist/commands/test-command/test-command.d.ts +1 -0
  21. package/dist/commands/test-command/test-command.d.ts.map +1 -1
  22. package/dist/commands/test-command/test-command.js +19 -3
  23. package/dist/commands/test-command/test-command.js.map +1 -1
  24. package/dist/commands/tools-command/ci-post-deploy-results.d.ts.map +1 -1
  25. package/dist/commands/tools-command/ci-post-deploy-results.js +6 -2
  26. package/dist/commands/tools-command/ci-post-deploy-results.js.map +1 -1
  27. package/dist/commands/tools-command/ci-post-test-results.d.ts +1 -0
  28. package/dist/commands/tools-command/ci-post-test-results.d.ts.map +1 -1
  29. package/dist/commands/tools-command/ci-post-test-results.js +12 -3
  30. package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
  31. package/dist/utils/batch/changed-packages-utils.d.ts +11 -0
  32. package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
  33. package/dist/utils/batch/changed-packages-utils.js +14 -0
  34. package/dist/utils/batch/changed-packages-utils.js.map +1 -1
  35. package/dist/utils/build/build.d.ts +7 -0
  36. package/dist/utils/build/build.d.ts.map +1 -1
  37. package/dist/utils/build/build.js.map +1 -1
  38. package/dist/utils/bundle/untrusted-tar.d.ts +28 -0
  39. package/dist/utils/bundle/untrusted-tar.d.ts.map +1 -0
  40. package/dist/utils/bundle/untrusted-tar.js +116 -0
  41. package/dist/utils/bundle/untrusted-tar.js.map +1 -0
  42. package/dist/utils/deploy/deploy-github-columns.d.ts +8 -1
  43. package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
  44. package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
  45. package/dist/utils/deploy/deploy-metadata.d.ts +5 -0
  46. package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
  47. package/dist/utils/deploy/deploy-metadata.js +36 -0
  48. package/dist/utils/deploy/deploy-metadata.js.map +1 -1
  49. package/dist/utils/deploy/deploy-metadata.test.js +35 -2
  50. package/dist/utils/deploy/deploy-metadata.test.js.map +1 -1
  51. package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts +38 -0
  52. package/dist/utils/deploy/prebuilt-deploy-bundle.d.ts.map +1 -0
  53. package/dist/utils/deploy/prebuilt-deploy-bundle.js +209 -0
  54. package/dist/utils/deploy/prebuilt-deploy-bundle.js.map +1 -0
  55. package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts +2 -0
  56. package/dist/utils/deploy/prebuilt-deploy-bundle.test.d.ts.map +1 -0
  57. package/dist/utils/deploy/prebuilt-deploy-bundle.test.js +194 -0
  58. package/dist/utils/deploy/prebuilt-deploy-bundle.test.js.map +1 -0
  59. package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
  60. package/dist/utils/job-context/base-job-context.js +3 -2
  61. package/dist/utils/job-context/base-job-context.js.map +1 -1
  62. package/dist/utils/job-context/batch-script-job-context.d.ts +12 -0
  63. package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
  64. package/dist/utils/job-context/batch-script-job-context.js +19 -0
  65. package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
  66. package/dist/utils/open-cloud/open-cloud-client.d.ts +1 -0
  67. package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
  68. package/dist/utils/open-cloud/open-cloud-client.js +5 -0
  69. package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
  70. package/dist/utils/open-cloud/open-cloud-client.test.js +11 -0
  71. package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
  72. package/dist/utils/sourcemap/sourcemap-loader.d.ts +3 -3
  73. package/dist/utils/sourcemap/sourcemap-loader.d.ts.map +1 -1
  74. package/dist/utils/sourcemap/sourcemap-loader.js +6 -7
  75. package/dist/utils/sourcemap/sourcemap-loader.js.map +1 -1
  76. package/dist/utils/testing/batch-run.e2e.test.js +36 -0
  77. package/dist/utils/testing/batch-run.e2e.test.js.map +1 -1
  78. package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts +26 -0
  79. package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts.map +1 -0
  80. package/dist/utils/testing/runner/prebuilt-batch-bundle.js +145 -0
  81. package/dist/utils/testing/runner/prebuilt-batch-bundle.js.map +1 -0
  82. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts +2 -0
  83. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts.map +1 -0
  84. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js +197 -0
  85. package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js.map +1 -0
  86. package/dist/utils/testing/runner/test-runner.d.ts +8 -1
  87. package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
  88. package/dist/utils/testing/runner/test-runner.js +19 -6
  89. package/dist/utils/testing/runner/test-runner.js.map +1 -1
  90. package/package.json +7 -6
  91. package/src/commands/batch-command/batch-bundle-command.ts +236 -0
  92. package/src/commands/batch-command/batch-deploy-command.ts +160 -16
  93. package/src/commands/batch-command/batch-test-command.ts +162 -18
  94. package/src/commands/batch-command/index.ts +2 -0
  95. package/src/commands/deploy-command/index.ts +7 -0
  96. package/src/commands/test-command/test-command.ts +20 -3
  97. package/src/commands/tools-command/ci-post-deploy-results.ts +9 -2
  98. package/src/commands/tools-command/ci-post-test-results.ts +20 -3
  99. package/src/utils/batch/changed-packages-utils.ts +23 -0
  100. package/src/utils/build/build.ts +7 -0
  101. package/src/utils/bundle/untrusted-tar.ts +160 -0
  102. package/src/utils/deploy/deploy-github-columns.ts +8 -1
  103. package/src/utils/deploy/deploy-metadata.test.ts +50 -1
  104. package/src/utils/deploy/deploy-metadata.ts +39 -0
  105. package/src/utils/deploy/prebuilt-deploy-bundle.test.ts +265 -0
  106. package/src/utils/deploy/prebuilt-deploy-bundle.ts +297 -0
  107. package/src/utils/job-context/base-job-context.ts +4 -2
  108. package/src/utils/job-context/batch-script-job-context.ts +34 -3
  109. package/src/utils/open-cloud/open-cloud-client.test.ts +15 -0
  110. package/src/utils/open-cloud/open-cloud-client.ts +11 -0
  111. package/src/utils/sourcemap/sourcemap-loader.ts +7 -9
  112. package/src/utils/testing/batch-run.e2e.test.ts +42 -0
  113. package/src/utils/testing/runner/prebuilt-batch-bundle.test.ts +272 -0
  114. package/src/utils/testing/runner/prebuilt-batch-bundle.ts +204 -0
  115. package/src/utils/testing/runner/test-runner.ts +43 -13
  116. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,236 @@
1
+ import * as path from 'path';
2
+ import { CommandModule } from 'yargs';
3
+ import { OutputHelper } from '@quenty/cli-output-helpers';
4
+ import { BaseReporter } from '@quenty/cli-output-helpers/reporting';
5
+ import { NevermoreGlobalArgs } from '../../args/global-args.js';
6
+ import {
7
+ BatchScriptJobContext,
8
+ LocalJobContext,
9
+ } from '../../utils/job-context/index.js';
10
+ import {
11
+ discoverAllTestableBatchTargetsAsync,
12
+ discoverChangedTestableBatchTargetsAsync,
13
+ discoverDeployBatchTargetsAsync,
14
+ } from '../../utils/batch/changed-packages-utils.js';
15
+ import { gatherGitDeployInfo } from '../../utils/deploy/deploy-metadata.js';
16
+ import {
17
+ DEPLOY_BUNDLE_FILE_SUFFIX,
18
+ type PrebuiltDeployPlace,
19
+ writePrebuiltDeployBundleAsync,
20
+ } from '../../utils/deploy/prebuilt-deploy-bundle.js';
21
+ import { readPackageVersionAsync } from '../../utils/nevermore-cli-utils.js';
22
+ import { prepareTestAsync } from '../../utils/testing/runner/test-runner.js';
23
+ import {
24
+ BUNDLE_FILE_SUFFIX,
25
+ writePrebuiltBatchBundleAsync,
26
+ } from '../../utils/testing/runner/prebuilt-batch-bundle.js';
27
+
28
+ interface BatchBundleArgs extends NevermoreGlobalArgs {
29
+ file: string;
30
+ test?: boolean;
31
+ deploy?: boolean;
32
+ target?: string;
33
+ base?: string;
34
+ all?: boolean;
35
+ limit?: number;
36
+ maxPlaceMb?: number;
37
+ }
38
+
39
+ export const batchBundleCommand: CommandModule<
40
+ NevermoreGlobalArgs,
41
+ BatchBundleArgs
42
+ > = {
43
+ command: 'bundle <file>',
44
+ describe:
45
+ 'Build what "batch test" or "batch deploy" would run into one bundle file, without an API key. ' +
46
+ 'Run it later with "batch test --bundle <file>" or "batch deploy --bundle <file>".',
47
+ builder: (yargs) => {
48
+ return yargs
49
+ .positional('file', {
50
+ describe: `File to write, conventionally <name>${BUNDLE_FILE_SUFFIX} or <name>${DEPLOY_BUNDLE_FILE_SUFFIX}`,
51
+ type: 'string',
52
+ demandOption: true,
53
+ })
54
+ .option('test', {
55
+ describe: 'Bundle the combined test places "batch test" would run',
56
+ type: 'boolean',
57
+ })
58
+ .option('deploy', {
59
+ describe:
60
+ 'Bundle the places "batch deploy" would deploy, without their base place, which the deploying run merges',
61
+ type: 'boolean',
62
+ })
63
+ .option('target', {
64
+ describe: 'Deploy target name in deploy.nevermore.json (--deploy only)',
65
+ type: 'string',
66
+ })
67
+ .option('all', {
68
+ describe: 'Bundle every package, not just changed ones',
69
+ type: 'boolean',
70
+ default: false,
71
+ })
72
+ .option('base', {
73
+ describe: 'Git ref to diff against for change detection',
74
+ type: 'string',
75
+ default: 'origin/main',
76
+ })
77
+ .option('limit', {
78
+ describe: 'Max number of packages to bundle (for debugging)',
79
+ type: 'number',
80
+ })
81
+ .option('max-place-mb', {
82
+ describe:
83
+ 'Size budget in MB for one combined place (--test only). Larger batches are split into several places. (default: 90, under the Open Cloud upload limit)',
84
+ type: 'number',
85
+ })
86
+ .check((args) => {
87
+ if (args.test === args.deploy || (!args.test && !args.deploy)) {
88
+ throw new Error('Pass exactly one of --test or --deploy.');
89
+ }
90
+ if (args.test && args.target !== undefined) {
91
+ throw new Error(
92
+ '--target picks a deploy target; it applies to --deploy only.'
93
+ );
94
+ }
95
+ if (args.deploy && args.maxPlaceMb !== undefined) {
96
+ throw new Error(
97
+ '--max-place-mb sizes combined test places; it applies to --test only.'
98
+ );
99
+ }
100
+ return true;
101
+ }) as any;
102
+ },
103
+ handler: async (args) => {
104
+ try {
105
+ if (args.deploy) {
106
+ await _writeDeployBundleAsync(args);
107
+ } else {
108
+ await _writeTestBundleAsync(args);
109
+ }
110
+ } catch (err) {
111
+ OutputHelper.error(err instanceof Error ? err.message : String(err));
112
+ process.exit(1);
113
+ }
114
+ },
115
+ };
116
+
117
+ async function _writeTestBundleAsync(args: BatchBundleArgs): Promise<void> {
118
+ let packages = args.all
119
+ ? await discoverAllTestableBatchTargetsAsync()
120
+ : await discoverChangedTestableBatchTargetsAsync(args.base!);
121
+ if (args.limit && args.limit > 0) {
122
+ packages = packages.slice(0, args.limit);
123
+ }
124
+
125
+ if (args.dryrun) {
126
+ OutputHelper.info(
127
+ `[DRYRUN] Would bundle ${packages.length} packages: ${packages
128
+ .map((p) => p.name)
129
+ .join(', ')}`
130
+ );
131
+ return;
132
+ }
133
+
134
+ if (packages.length === 0) {
135
+ OutputHelper.warn('No testable packages found; writing an empty bundle.');
136
+ await writePrebuiltBatchBundleAsync(args.file, [], new Map());
137
+ return;
138
+ }
139
+
140
+ // No client: building never uploads, so there is no key to need.
141
+ const batchContext = new BatchScriptJobContext(
142
+ new LocalJobContext(new BaseReporter()),
143
+ packages,
144
+ {
145
+ maxChunkBytes:
146
+ args.maxPlaceMb !== undefined
147
+ ? args.maxPlaceMb * 1024 * 1024
148
+ : undefined,
149
+ }
150
+ );
151
+
152
+ try {
153
+ // Prepared one at a time so every package's deploy-metadata stamp lands in
154
+ // the shared place before it is packed, and the last package's stamp wins.
155
+ // A live aggregated run stamps only after the shared place has uploaded,
156
+ // so its specs never see a stamp at all.
157
+ const hasJestConfig = new Map<string, boolean>();
158
+ for (const pkg of packages) {
159
+ const prepared = await prepareTestAsync(batchContext, {
160
+ packagePath: pkg.path,
161
+ packageName: pkg.name,
162
+ target: pkg.target,
163
+ });
164
+ hasJestConfig.set(pkg.name, prepared.hasJestConfig);
165
+ }
166
+
167
+ const chunks = await batchContext.getBuiltChunksAsync();
168
+ await writePrebuiltBatchBundleAsync(args.file, chunks, hasJestConfig);
169
+ OutputHelper.info(
170
+ `Wrote ${packages.length} package${packages.length === 1 ? '' : 's'} in ${
171
+ chunks.length
172
+ } place${chunks.length === 1 ? '' : 's'} to ${args.file}.`
173
+ );
174
+ } finally {
175
+ await batchContext.disposeAsync();
176
+ }
177
+ }
178
+
179
+ async function _writeDeployBundleAsync(args: BatchBundleArgs): Promise<void> {
180
+ const targetName = args.target ?? 'test';
181
+ const batchTargets = await discoverDeployBatchTargetsAsync({
182
+ targetName,
183
+ all: args.all,
184
+ base: args.base!,
185
+ limit: args.limit,
186
+ });
187
+
188
+ if (args.dryrun) {
189
+ OutputHelper.info(
190
+ `[DRYRUN] Would bundle ${batchTargets.length} places: ${batchTargets
191
+ .map((p) => p.name)
192
+ .join(', ')}`
193
+ );
194
+ return;
195
+ }
196
+
197
+ if (batchTargets.length === 0) {
198
+ OutputHelper.warn(
199
+ `No changed packages have a "${targetName}" target; writing an empty bundle.`
200
+ );
201
+ }
202
+
203
+ const context = new LocalJobContext(new BaseReporter());
204
+ try {
205
+ const places: PrebuiltDeployPlace[] = [];
206
+ for (const buildTarget of batchTargets) {
207
+ const built = await context.buildPlaceAsync({
208
+ target: buildTarget.target,
209
+ packagePath: buildTarget.path,
210
+ packageName: buildTarget.name,
211
+ outputFileName: 'deploy.rbxl',
212
+ deferBasePlaceMerge: true,
213
+ });
214
+ places.push({
215
+ name: buildTarget.name,
216
+ rbxlPath: built.rbxlPath,
217
+ projectPath: path.resolve(buildTarget.path, buildTarget.target.project),
218
+ packageVersion: await readPackageVersionAsync(buildTarget.path),
219
+ });
220
+ }
221
+
222
+ await writePrebuiltDeployBundleAsync(
223
+ args.file,
224
+ targetName,
225
+ gatherGitDeployInfo(),
226
+ places
227
+ );
228
+ OutputHelper.info(
229
+ `Wrote ${places.length} place${places.length === 1 ? '' : 's'} to ${
230
+ args.file
231
+ }.`
232
+ );
233
+ } finally {
234
+ await context.disposeAsync();
235
+ }
236
+ }
@@ -24,7 +24,13 @@ import {
24
24
  buildDeployMetadataAttributes,
25
25
  gatherGitDeployInfo,
26
26
  injectDeployMetadataAsync,
27
+ type GitDeployInfo,
27
28
  } from '../../utils/deploy/deploy-metadata.js';
29
+ import {
30
+ type PrebuiltDeployBundle,
31
+ type PrebuiltDeployPlace,
32
+ readPrebuiltDeployBundleAsync,
33
+ } from '../../utils/deploy/prebuilt-deploy-bundle.js';
28
34
  import { OpenCloudClient } from '../../utils/open-cloud/open-cloud-client.js';
29
35
  import {
30
36
  RateLimiter,
@@ -38,7 +44,7 @@ import {
38
44
  import {
39
45
  type BatchTarget,
40
46
  discoverAllTargetPackagesAsync,
41
- discoverChangedTargetPackagesAsync,
47
+ discoverDeployBatchTargetsAsync,
42
48
  flattenToBatchTargets,
43
49
  } from '../../utils/batch/changed-packages-utils.js';
44
50
  import {
@@ -77,6 +83,8 @@ interface BatchDeployArgs extends NevermoreGlobalArgs {
77
83
  watch?: string;
78
84
  watchShareApiKey?: boolean;
79
85
  watchUseGhAuth?: boolean;
86
+ bundle?: string;
87
+ bundleCommit?: string;
80
88
  }
81
89
 
82
90
  export const batchDeployCommand: CommandModule<
@@ -157,6 +165,36 @@ export const batchDeployCommand: CommandModule<
157
165
  'Off by default — the key is stored by the service.',
158
166
  type: 'boolean',
159
167
  default: false,
168
+ })
169
+ .option('bundle', {
170
+ describe:
171
+ 'Deploy the places in a *.nevermore.deploy.tar written by "batch bundle --deploy" instead of ' +
172
+ 'finding and building packages. The bundle only names places: where each one deploys, ' +
173
+ "and the base place it merges onto, come from this checkout's deploy.nevermore.json.",
174
+ type: 'string',
175
+ })
176
+ .option('bundle-commit', {
177
+ describe:
178
+ 'Fail unless the --bundle was built from this full commit sha, e.g. the PR head GitHub reports',
179
+ type: 'string',
180
+ })
181
+ .check((args) => {
182
+ if (args.bundleCommit !== undefined && args.bundle === undefined) {
183
+ throw new Error('--bundle-commit checks a --bundle; pass one.');
184
+ }
185
+ if (args.bundle !== undefined) {
186
+ if (args.all || args.limit !== undefined) {
187
+ throw new Error(
188
+ '--bundle deploys every place in the bundle; drop --all and --limit.'
189
+ );
190
+ }
191
+ if (args.watch !== undefined) {
192
+ throw new Error(
193
+ "--bundle deploys another checkout's build, which must not become a watch baseline; drop --watch."
194
+ );
195
+ }
196
+ }
197
+ return true;
160
198
  });
161
199
  },
162
200
  handler: async (args) => {
@@ -170,18 +208,58 @@ export const batchDeployCommand: CommandModule<
170
208
  };
171
209
 
172
210
  async function _runAsync(args: BatchDeployArgs): Promise<void> {
211
+ const bundle =
212
+ args.bundle !== undefined
213
+ ? await readPrebuiltDeployBundleAsync(args.bundle)
214
+ : undefined;
215
+ let exitCode: number;
216
+ try {
217
+ exitCode = await _runDeployAsync(args, bundle);
218
+ } finally {
219
+ await bundle?.buildContext.cleanupAsync();
220
+ }
221
+ process.exit(exitCode);
222
+ }
223
+
224
+ async function _runDeployAsync(
225
+ args: BatchDeployArgs,
226
+ bundle: PrebuiltDeployBundle | undefined
227
+ ): Promise<number> {
173
228
  const targetName = args.target ?? 'test';
174
229
  // Parsed up front so a typo'd duration fails before anything is deployed.
175
230
  const watchOption =
176
231
  args.watch == null ? undefined : parseWatchOption(args.watch);
177
232
 
178
- const discovered = args.all
179
- ? await discoverAllTargetPackagesAsync(targetName)
180
- : await discoverChangedTargetPackagesAsync(args.base!, targetName);
181
- let batchTargets = flattenToBatchTargets(discovered);
233
+ let batchTargets: BatchTarget[];
234
+ let prebuiltByName: Map<string, PrebuiltDeployPlace> | undefined;
235
+ if (
236
+ bundle &&
237
+ args.bundleCommit !== undefined &&
238
+ bundle.git.version !== args.bundleCommit
239
+ ) {
240
+ throw new Error(
241
+ `The bundle was built from ${bundle.git.version}, not ${args.bundleCommit}.`
242
+ );
243
+ }
244
+ if (bundle) {
245
+ ({ batchTargets, prebuiltByName } = await _resolveBundleTargetsAsync(
246
+ bundle,
247
+ targetName
248
+ ));
249
+ } else {
250
+ batchTargets = await discoverDeployBatchTargetsAsync({
251
+ targetName,
252
+ all: args.all,
253
+ base: args.base!,
254
+ limit: args.limit,
255
+ });
256
+ }
182
257
 
183
- if (args.limit && args.limit > 0) {
184
- batchTargets = batchTargets.slice(0, args.limit);
258
+ if (batchTargets.length === 0 && bundle) {
259
+ OutputHelper.warn(
260
+ `The bundle holds no places; nothing changed in a "${targetName}" target.`
261
+ );
262
+ return 0;
185
263
  }
186
264
 
187
265
  if (batchTargets.length === 0) {
@@ -217,7 +295,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
217
295
  dryrun: args.dryrun,
218
296
  });
219
297
  }
220
- return;
298
+ return 0;
221
299
  }
222
300
 
223
301
  if (args.dryrun) {
@@ -247,7 +325,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
247
325
  dryrun: true,
248
326
  });
249
327
  }
250
- return;
328
+ return 0;
251
329
  }
252
330
 
253
331
  const concurrency = args.concurrency ?? 3;
@@ -306,7 +384,8 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
306
384
  const context = new CloudJobContext(reporter, client, basePlaceResolver);
307
385
 
308
386
  // Gathered once so every deployed package shares one timestamp and commit.
309
- const deployGitInfo = gatherGitDeployInfo();
387
+ // A bundle's places carry the commit that built them, not this checkout's.
388
+ const deployGitInfo: GitDeployInfo = bundle?.git ?? gatherGitDeployInfo();
310
389
  const deployTimestamp = new Date().toISOString();
311
390
 
312
391
  await reporter.startAsync();
@@ -319,11 +398,18 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
319
398
  reporter,
320
399
  bufferOutput: isGrouped,
321
400
  executeAsync: async (buildTarget, pkgReporter) => {
401
+ const prebuilt = prebuiltByName?.get(buildTarget.name);
322
402
  const builtPlace = await context.buildPlaceAsync({
323
403
  target: buildTarget.target,
324
404
  outputFileName: publish ? 'publish.rbxl' : 'deploy.rbxl',
325
405
  packagePath: buildTarget.path,
326
406
  packageName: buildTarget.name,
407
+ overrides: prebuilt
408
+ ? {
409
+ placeFile: prebuilt.rbxlPath,
410
+ projectFile: prebuilt.projectPath,
411
+ }
412
+ : undefined,
327
413
  });
328
414
 
329
415
  const injected = await injectDeployMetadataAsync(
@@ -336,7 +422,9 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
336
422
  timestamp: deployTimestamp,
337
423
  universeId: buildTarget.target.universeId,
338
424
  placeId: buildTarget.target.placeId,
339
- packageVersion: await readPackageVersionAsync(buildTarget.path),
425
+ packageVersion: prebuilt
426
+ ? prebuilt.packageVersion
427
+ : await readPackageVersionAsync(buildTarget.path),
340
428
  basePlaceId: buildTarget.target.basePlace?.placeId,
341
429
  basePlaceVersion: builtPlace.basePlaceVersion,
342
430
  },
@@ -360,6 +448,18 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
360
448
  // Eagerly release build artifacts after upload
361
449
  await context.releaseBuiltPlaceAsync(builtPlace);
362
450
 
451
+ const deployed = {
452
+ packageName: buildTarget.name,
453
+ placeId: buildTarget.target.placeId,
454
+ universeId: buildTarget.target.universeId,
455
+ target: targetName,
456
+ version,
457
+ published: publish,
458
+ basePlaceVersion: builtPlace.basePlaceVersion,
459
+ commit: deployGitInfo.version,
460
+ branch: deployGitInfo.branch,
461
+ };
462
+
363
463
  let logs: string;
364
464
  if (args.smokeTest && buildTarget.target.basePlace) {
365
465
  OutputHelper.verbose('Running post-deploy smoke test...');
@@ -374,8 +474,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
374
474
  logs = smokeResult.logs;
375
475
  if (!smokeResult.success) {
376
476
  return {
377
- packageName: buildTarget.name,
378
- placeId: buildTarget.target.placeId,
477
+ ...deployed,
379
478
  success: false,
380
479
  logs: _annotateSmokeTestFailure(logs),
381
480
  failureLabel: 'SMOKE TEST FAILED',
@@ -387,8 +486,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
387
486
  }
388
487
 
389
488
  return {
390
- packageName: buildTarget.name,
391
- placeId: buildTarget.target.placeId,
489
+ ...deployed,
392
490
  success: true,
393
491
  logs,
394
492
  progressSummary: {
@@ -425,7 +523,53 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
425
523
  }
426
524
 
427
525
  await reporter.stopAsync();
428
- process.exit(exitCode);
526
+ return exitCode;
527
+ }
528
+
529
+ /**
530
+ * Match each bundled place to this checkout's target of the same name.
531
+ *
532
+ * The bundle is untrusted, so it only gets to say which places it built. Place
533
+ * ids, universe ids and the base place all come from this checkout, which is
534
+ * what a same-repo PR's run would have used. A place this checkout does not
535
+ * know — a fork adding a new game — has nowhere trusted to go, so it fails
536
+ * rather than deploying to ids the fork chose.
537
+ */
538
+ async function _resolveBundleTargetsAsync(
539
+ bundle: PrebuiltDeployBundle,
540
+ targetName: string
541
+ ): Promise<{
542
+ batchTargets: BatchTarget[];
543
+ prebuiltByName: Map<string, PrebuiltDeployPlace>;
544
+ }> {
545
+ if (bundle.target !== targetName) {
546
+ throw new Error(
547
+ `The bundle was built for the "${bundle.target}" target, not "${targetName}". ` +
548
+ `Pass --target ${bundle.target}.`
549
+ );
550
+ }
551
+
552
+ const known = new Map(
553
+ flattenToBatchTargets(await discoverAllTargetPackagesAsync(targetName)).map(
554
+ (t) => [t.name, t]
555
+ )
556
+ );
557
+ const unknown = bundle.places
558
+ .map((p) => p.name)
559
+ .filter((name) => !known.has(name));
560
+ if (unknown.length > 0) {
561
+ throw new Error(
562
+ `The bundle holds places this checkout has no "${targetName}" target for: ` +
563
+ `${unknown.join(
564
+ ', '
565
+ )}. A new place deploys once its deploy.nevermore.json is on this branch.`
566
+ );
567
+ }
568
+
569
+ return {
570
+ batchTargets: bundle.places.map((p) => known.get(p.name)!),
571
+ prebuiltByName: new Map(bundle.places.map((p) => [p.name, p])),
572
+ };
429
573
  }
430
574
 
431
575
  /**