Skip to content

Commit 2f265c4

Browse files
authored
feat(nevermore-cli): pin and promote base place versions in deploy config (#734)
* feat(nevermore-cli): pin base place versions in deploy config Deploys that use a basePlace always downloaded the latest published version of that place, so a broken Studio edit to a base place shipped on the next deploy even when the code hadn't changed. Add an optional basePlace.version pin to deploy.nevermore.json. When set, the deploy downloads exactly that version instead of the latest, making deploys reproducible. Omitting it keeps the previous latest-pull behavior, so existing configs are unaffected. Add `nevermore deploy version upgrade [target]` to bump the pins: it resolves each basePlace's current latest published version via the Open Cloud Assets API (reusing the legacy-asset:manage scope), prints an old -> new table, and writes the new versions back to the config after a confirmation prompt. Supports --dryrun and --yes, and scopes to a single target when named. Base places shared across targets are resolved once. * feat(nevermore-cli): add `deploy version promote` to copy pins between targets Promoting a validated build usually means shipping the exact base-place versions a demo/staging target was verified against, not re-pinning to whatever is newest. Add `nevermore deploy version promote <from> <to>`, which copies basePlace version pins from one target onto another (e.g. production-demo -> production). Places are matched by base place id rather than name, so the same source content lines up even when targets name their places differently. Destination places with no matching pin are left untouched and reported, and an inconsistent source (one base place pinned to two versions) is rejected. It's a pure edit of deploy.nevermore.json with no network calls, gated behind the same --dryrun / confirmation flow as `version upgrade`.
1 parent a3fa04d commit 2f265c4

10 files changed

Lines changed: 799 additions & 12 deletions

File tree

docs/cli.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,14 @@ nevermore test --cloud --script-text 'print("hi")' # run arbitrary Luau to de
157157

158158
### `nevermore deploy`
159159

160-
Builds a Rojo project and uploads it to a Roblox place. `deploy` has two subcommands; `run` is the default, so `nevermore deploy` and `nevermore deploy <target>` both deploy. Full walkthrough in [Deploying with the CLI](deploy.md).
160+
Builds a Rojo project and uploads it to a Roblox place. `run` is the default subcommand, so `nevermore deploy` and `nevermore deploy <target>` both deploy. Full walkthrough in [Deploying with the CLI](deploy.md).
161161

162162
```bash
163-
nevermore deploy init # create deploy.nevermore.json (interactive)
164-
nevermore deploy run # build + upload, saved (not live)
165-
nevermore deploy run --publish # build + upload and publish live
166-
nevermore deploy production --publish # 'run' is implied; deploy the 'production' target
163+
nevermore deploy init # create deploy.nevermore.json (interactive)
164+
nevermore deploy run # build + upload, saved (not live)
165+
nevermore deploy run --publish # build + upload and publish live
166+
nevermore deploy production --publish # 'run' is implied; deploy the 'production' target
167+
nevermore deploy version upgrade # re-pin base place versions to latest
167168
```
168169

169170
**`nevermore deploy init`** — writes a `deploy.nevermore.json` for the current package.
@@ -188,6 +189,16 @@ nevermore deploy production --publish # 'run' is implied; deploy the 'productio
188189
| `--output <file>` | Write JSON results to a file. |
189190
| `--logs` | Show build/upload logs even on success. |
190191

192+
**`nevermore deploy version upgrade [target]`** — re-pins every `basePlace` in `deploy.nevermore.json` to its current latest published version, so deploys pull a fixed, git-tracked base place instead of whatever is live. Without a target it walks every target. See [Pinning base place versions](deploy.md#pinning-base-place-versions).
193+
194+
| Flag | Description |
195+
|------|-------------|
196+
| `--dryrun` | Print the old → new version table without writing. |
197+
| `--yes` | Skip the confirmation prompt (for scripting/CI). |
198+
| `--api-key <key>` | Open Cloud API key. Otherwise resolved from login/env. |
199+
200+
**`nevermore deploy version promote <from> <to>`** — copies base-place version pins from one target to another (e.g. promote validated `production-demo` pins to `production`). Places are matched by base place id, so content lines up even when the targets name their places differently. Pure config edit — no network. Supports `--dryrun` and `--yes`. See [Promoting pins between targets](deploy.md#promoting-pins-between-targets).
201+
191202
### `nevermore batch`
192203

193204
Runs `test` or `deploy` across many packages at once, using git change detection so PRs only touch what changed. Scans the pnpm workspace for packages with a matching deploy target. See [Test Infrastructure](testing/testing.md) and [Deploying](deploy.md) for how targets are configured.

docs/deploy.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Other flags:
155155
| `targets.<name>.project` | yes | Path to the Rojo project file, relative to the package directory. |
156156
| `targets.<name>.scriptTemplate` | no | Luau file `nevermore test` executes via Open Cloud after upload. Not used by `nevermore deploy` itself. |
157157
| `targets.<name>.basePlace` | no | Universe/place to download and merge with the rojo build before uploading. See [Merging with an existing place](testing/integration-testing.md#merging-with-an-existing-place-baseplace). |
158+
| `targets.<name>.basePlace.version` | no | Pin the base place to a specific published version instead of pulling the latest. See [Pinning base place versions](#pinning-base-place-versions). |
158159

159160
You can declare any number of targets. A common setup is one `test` target for CI and a separate `production` or `staging` target for live deploys:
160161

@@ -230,6 +231,66 @@ nevermore deploy run --place-file ./build/my-place.rbxl
230231

231232
The `project` field in `deploy.nevermore.json` is ignored when `--place-file` is set, but `universeId` and `placeId` are still required.
232233

234+
## Pinning base place versions
235+
236+
If a target uses a [`basePlace`](testing/integration-testing.md#merging-with-an-existing-place-baseplace), `nevermore deploy` downloads that place and merges your rojo build into it. By default it pulls **the latest published version** of the base place — so a broken Studio edit to the base place ships on the very next deploy, even when your code hasn't changed.
237+
238+
To make deploys reproducible, pin the base place to a specific version with an optional `version` field:
239+
240+
```json
241+
{
242+
"targets": {
243+
"production": {
244+
"universeId": 12345,
245+
"placeId": 67890,
246+
"project": "default.project.json",
247+
"basePlace": {
248+
"universeId": 12345,
249+
"placeId": 11111,
250+
"version": 42
251+
}
252+
}
253+
}
254+
}
255+
```
256+
257+
With `version` set, the deploy downloads exactly that version of the base place. Omit it to keep pulling the latest (the previous behaviour — nothing changes for configs that don't opt in).
258+
259+
### Bumping the pin
260+
261+
When you actually want to roll base places forward, run:
262+
263+
```bash
264+
# Re-pin every basePlace in the config to its current latest published version
265+
nevermore deploy version upgrade
266+
267+
# Only upgrade one target
268+
nevermore deploy version upgrade production
269+
270+
# Preview the change set without writing
271+
nevermore deploy version upgrade --dryrun
272+
```
273+
274+
`upgrade` walks every `basePlace` in `deploy.nevermore.json` (or just the named target), resolves each place's current latest published version, prints an old → new table, and — after a confirmation prompt — writes the new `version` values back into the file. Base places shared by several targets are resolved once. Pass `--yes` to skip the prompt (for scripting), or `--dryrun` to preview only.
275+
276+
Commit the updated `deploy.nevermore.json`, then deploy as usual. This gives you a reviewable, git-tracked record of exactly which base-place content each deploy shipped.
277+
278+
Resolving the latest version uses the same `legacy-asset:manage` scope already required for `basePlace` downloads, so no extra credentials are needed.
279+
280+
### Promoting pins between targets
281+
282+
Once you've validated a target — say a `production-demo` universe — you usually want to ship those exact same base-place versions to `production`, not re-pin to whatever is newest. `promote` copies the pins across:
283+
284+
```bash
285+
# Copy every base place pin from production-demo onto production
286+
nevermore deploy version promote production-demo production
287+
288+
# Preview without writing
289+
nevermore deploy version promote production-demo production --dryrun
290+
```
291+
292+
Places are matched by **base place id**, not by name, so the same source content lines up even when the two targets name their places differently (e.g. a demo `chapter6` and a prod `chapter8` that share one base place). Places in the destination with no matching pin in the source are left untouched and reported. This is a pure edit of `deploy.nevermore.json` — no network calls — so it's safe to run offline and review as a diff.
293+
233294
## Batch deploys
234295

235296
If you want to deploy every game affected by a code change (for example, on every PR), use `nevermore batch deploy` instead. It scans the pnpm workspace for packages with a matching deploy target, uses `pnpm ls --filter` to figure out which ones changed since `origin/main`, and runs them in parallel.

docs/testing/integration-testing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ Add `basePlace` to your deploy target:
218218

219219
The `basePlace` place is the source of truth for Studio-authored content. The target `placeId` is where the merged result gets uploaded. These can be in the same universe or different ones.
220220

221+
By default the CLI downloads the **latest published version** of the base place on every deploy. To make deploys reproducible — so a broken Studio edit can't leak into a deploy — pin a specific version with an optional `basePlace.version`, and bump it deliberately with `nevermore deploy version upgrade`. See [Pinning base place versions](../deploy.md#pinning-base-place-versions).
222+
221223
### How the merge works
222224

223225
The merge is driven by your rojo project file. Each entry in the tree is treated as one of two kinds:

tools/nevermore-cli/src/commands/deploy-command/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import {
3636
resolveDeployTargetPlaces,
3737
} from '../../utils/build/deploy-config.js';
3838
import { handleInitAsync } from './deploy-init.js';
39+
import {
40+
handleVersionUpgradeAsync,
41+
handleVersionPromoteAsync,
42+
type VersionPromoteArgs,
43+
} from './version-command.js';
3944
import { selectTargetAsync } from './select-target.js';
4045

4146
const MULTI_PLACE_CONCURRENCY = 10;
@@ -109,6 +114,72 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
109114
}
110115
);
111116

117+
args.command(
118+
'version',
119+
'Manage pinned base place versions in deploy.nevermore.json',
120+
(yargs) => {
121+
return yargs
122+
.command(
123+
'upgrade [target]',
124+
'Re-pin every basePlace to its latest published version',
125+
(upgradeYargs) => {
126+
return upgradeYargs
127+
.positional('target', {
128+
describe:
129+
'Only upgrade this target (default: all targets in the config)',
130+
type: 'string',
131+
})
132+
.option('api-key', {
133+
describe: 'Roblox Open Cloud API key',
134+
type: 'string',
135+
});
136+
},
137+
async (upgradeArgs) => {
138+
try {
139+
await handleVersionUpgradeAsync(
140+
upgradeArgs as unknown as DeployArgs
141+
);
142+
} catch (err) {
143+
OutputHelper.error(
144+
err instanceof Error ? err.message : String(err)
145+
);
146+
process.exit(1);
147+
}
148+
}
149+
)
150+
.command(
151+
'promote <from> <to>',
152+
'Promote base place version pins from one target to another',
153+
(promoteYargs) => {
154+
return promoteYargs
155+
.positional('from', {
156+
describe:
157+
'Target to promote pins from (e.g. production-demo)',
158+
type: 'string',
159+
})
160+
.positional('to', {
161+
describe: 'Target to promote pins to (e.g. production)',
162+
type: 'string',
163+
});
164+
},
165+
async (promoteArgs) => {
166+
try {
167+
await handleVersionPromoteAsync(
168+
promoteArgs as unknown as VersionPromoteArgs
169+
);
170+
} catch (err) {
171+
OutputHelper.error(
172+
err instanceof Error ? err.message : String(err)
173+
);
174+
process.exit(1);
175+
}
176+
}
177+
)
178+
.demandCommand(1, 'Specify a version action, e.g. "upgrade".');
179+
},
180+
() => {}
181+
);
182+
112183
args.command(
113184
['run [target]', '$0 [target]'],
114185
'Deploy a target from deploy.nevermore.json',

0 commit comments

Comments
 (0)