|
| 1 | +--- |
| 2 | +title: Deploying with the CLI |
| 3 | +sidebar_position: 3 |
| 4 | +--- |
| 5 | + |
| 6 | +# Deploying places with `nevermore deploy` |
| 7 | + |
| 8 | +`nevermore deploy` builds a Rojo project, uploads it to a Roblox place via the [Open Cloud API](https://create.roblox.com/docs/cloud), and (optionally) publishes the new version so players see it. `nevermore test` is built on the same pipeline, so everything that runs in a Roblox place goes through this command. |
| 9 | + |
| 10 | +This guide walks you from zero to your first uploaded place. For advanced features (merging with a Studio-authored base place, smoke tests, batch deploys, CI), see [Integration Testing](testing/integration-testing.md). |
| 11 | + |
| 12 | +:::tip New to Nevermore? |
| 13 | +Start with the [Intro](intro.md) for an overview and [Install](install.md) for setting up Node, Rojo, and the Nevermore CLI itself. This guide assumes the CLI is already on your `PATH`. |
| 14 | +::: |
| 15 | + |
| 16 | +## When should I use this? |
| 17 | + |
| 18 | +For most Roblox projects, especially solo work and small teams, you don't need this. Open Roblox Studio, click **Save to Roblox** or **Publish to Roblox**, and you're done. That's normally the right answer. |
| 19 | + |
| 20 | +You probably want `nevermore deploy` when: |
| 21 | + |
| 22 | +- Your code lives in a git repository and Studio is just where you preview it. Deploys should come from the same commit history that reviews and tests run against, not from whichever developer's Studio happens to be open. |
| 23 | +- More than one programmer ships to the same place. Studio's publish flow is last-writer-wins, and a CLI deploy from CI gives you one path from "merge to main" to "live in game", traceable to a specific commit. |
| 24 | +- You want one config to drive both tests and deploys. `nevermore deploy` and `nevermore test` both read `deploy.nevermore.json`, so the place you smoke-test against on every PR is configured exactly like the one you ship to. See [Test Infrastructure](testing/testing.md). |
| 25 | +- You want CI to gate releases. Batch deploys plug into PR checks, so a deploy only runs after lint, tests, and smoke tests pass, and shows up as a PR comment instead of an ad-hoc Studio session. |
| 26 | + |
| 27 | +If none of those apply, stick with Studio Publish. Come back when you outgrow it. |
| 28 | + |
| 29 | +## What deploy actually does |
| 30 | + |
| 31 | +When you run `nevermore deploy run`: |
| 32 | + |
| 33 | +1. Reads `deploy.nevermore.json` in your current directory and resolves the target you asked for (default: `test`). |
| 34 | +2. Runs `rojo build` on the target's `project` file to produce an `.rbxl` place file in a temp directory. |
| 35 | +3. Uploads the `.rbxl` to the configured `universeId` / `placeId` over Open Cloud. |
| 36 | +4. Saves the new version as a draft. If `--publish` is passed, it is also published as the live version. |
| 37 | + |
| 38 | +That's the whole pipeline. There are no deploy hooks or post-processing steps to register. |
| 39 | + |
| 40 | +## Prerequisites |
| 41 | + |
| 42 | +- [Node.js](https://nodejs.org/) v18+ and the Nevermore CLI installed. The [Install guide](install.md) walks through both. The short version is `npm install -g @quenty/nevermore-cli`, or use `npx nevermore ...` from any package that depends on it. |
| 43 | +- [Rojo](https://rojo.space/docs/v7/getting-started/installation/) v7+ on your `PATH`. |
| 44 | +- A Roblox universe and place you own. You can create both at [create.roblox.com/dashboard/creations](https://create.roblox.com/dashboard/creations). |
| 45 | +- A Roblox Open Cloud API key. See [Logging in](#logging-in) below. |
| 46 | + |
| 47 | +## Logging in |
| 48 | + |
| 49 | +`nevermore deploy` authenticates against Open Cloud with an API key. Create one at [create.roblox.com/dashboard/credentials](https://create.roblox.com/dashboard/credentials) and grant it these scopes for the universe you want to deploy to: |
| 50 | + |
| 51 | +| Scope | Used for | |
| 52 | +|-------|----------| |
| 53 | +| `universe-places:write` | Uploading new place versions | |
| 54 | +| `universe.place.luau-execution-session:write` | Running scripts (used by `nevermore test` and smoke tests) | |
| 55 | +| `universe.place.luau-execution-session:read` | Reading script execution results | |
| 56 | +| `legacy-asset:manage` | Downloading a [base place](testing/integration-testing.md#merging-with-an-existing-place-baseplace) (only needed if you use `basePlace`) | |
| 57 | + |
| 58 | +Save the key to your machine once: |
| 59 | + |
| 60 | +```bash |
| 61 | +nevermore login |
| 62 | +``` |
| 63 | + |
| 64 | +This stores the key at `~/.nevermore/credentials.json` (mode `0700`) after validating it against Open Cloud. Other useful flags: |
| 65 | + |
| 66 | +- `nevermore login --force` swaps the stored key. |
| 67 | +- `nevermore login --clear` removes it. |
| 68 | +- `nevermore login --status` shows what's loaded and re-validates it. |
| 69 | + |
| 70 | +### How the CLI finds your key |
| 71 | + |
| 72 | +The CLI resolves credentials in this order (first match wins): |
| 73 | + |
| 74 | +1. The `--api-key` CLI flag |
| 75 | +2. The `ROBLOX_OPEN_CLOUD_API_KEY` environment variable |
| 76 | +3. The `ROBLOX_UNIT_TEST_API_KEY` environment variable (kept for backwards compatibility) |
| 77 | +4. `~/.nevermore/credentials.json` (from `nevermore login`) |
| 78 | + |
| 79 | +In CI, set `ROBLOX_OPEN_CLOUD_API_KEY` as a secret. `nevermore login` is for local developer machines. |
| 80 | + |
| 81 | +## Setting up a package for deploy |
| 82 | + |
| 83 | +`deploy.nevermore.json` is the only file the CLI needs to know about. The fastest way to create one is the interactive `init` wizard. |
| 84 | + |
| 85 | +### `nevermore deploy init` |
| 86 | + |
| 87 | +From inside the directory you want to deploy from (a package under `src/`, a game under `games/`, or any directory with a `package.json`): |
| 88 | + |
| 89 | +```bash |
| 90 | +nevermore deploy init |
| 91 | +``` |
| 92 | + |
| 93 | +The wizard: |
| 94 | + |
| 95 | +- Detects a `test/default.project.json` if one exists and offers it as the default Rojo project. |
| 96 | +- Detects `test/scripts/Server/ServerMain.server.lua` (or `.luau`) and offers it as the default script template. |
| 97 | +- Walks up the filesystem looking for a parent `deploy.nevermore.json` with a `universeId` and reuses it. Once you have one game configured, sibling packages can inherit the universe automatically. |
| 98 | +- Lists every existing place in the universe so you can pick one, or offers to create a new place. |
| 99 | +- Prints the resulting config and asks you to confirm before writing it. |
| 100 | + |
| 101 | +#### Non-interactive setup |
| 102 | + |
| 103 | +Pass `--yes` to skip prompts. You must supply enough flags for the wizard to resolve everything without asking: |
| 104 | + |
| 105 | +```bash |
| 106 | +nevermore deploy init --yes \ |
| 107 | + --universe-id 12345 \ |
| 108 | + --place-id 67890 \ |
| 109 | + --project default.project.json \ |
| 110 | + --target test |
| 111 | +``` |
| 112 | + |
| 113 | +If you have the universe but no place yet, use `--create-place` to create one. Place creation is not exposed in Open Cloud, so this uses your `.ROBLOSECURITY` cookie instead. It only works on a machine that's logged in to Roblox. |
| 114 | + |
| 115 | +```bash |
| 116 | +nevermore deploy init --yes \ |
| 117 | + --universe-id 12345 \ |
| 118 | + --create-place \ |
| 119 | + --project default.project.json |
| 120 | +``` |
| 121 | + |
| 122 | +Other flags: |
| 123 | + |
| 124 | +| Flag | Description | |
| 125 | +|------|-------------| |
| 126 | +| `--target <name>` | Name of the target to create (default: `test`) | |
| 127 | +| `--script-template <path>` | Set the Luau script template that `nevermore test` will run | |
| 128 | +| `--force` | Overwrite an existing `deploy.nevermore.json` | |
| 129 | + |
| 130 | +### The `deploy.nevermore.json` schema |
| 131 | + |
| 132 | +```json |
| 133 | +{ |
| 134 | + "targets": { |
| 135 | + "test": { |
| 136 | + "universeId": 12345, |
| 137 | + "placeId": 67890, |
| 138 | + "project": "default.project.json", |
| 139 | + "scriptTemplate": "test/scripts/Server/ServerMain.server.lua", |
| 140 | + "basePlace": { |
| 141 | + "universeId": 12345, |
| 142 | + "placeId": 11111 |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +| Field | Required | Description | |
| 150 | +|-------|----------|-------------| |
| 151 | +| `targets` | yes | Map of target name to deploy config. Most packages start with a single `test` target. | |
| 152 | +| `targets.<name>.universeId` | yes | Roblox universe ID to deploy into. | |
| 153 | +| `targets.<name>.placeId` | yes | Roblox place ID. The build is uploaded here as a new version. | |
| 154 | +| `targets.<name>.project` | yes | Path to the Rojo project file, relative to the package directory. | |
| 155 | +| `targets.<name>.scriptTemplate` | no | Luau file `nevermore test` executes via Open Cloud after upload. Not used by `nevermore deploy` itself. | |
| 156 | +| `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). | |
| 157 | + |
| 158 | +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: |
| 159 | + |
| 160 | +```json |
| 161 | +{ |
| 162 | + "targets": { |
| 163 | + "test": { "universeId": 1, "placeId": 10, "project": "test/default.project.json" }, |
| 164 | + "production": { "universeId": 1, "placeId": 20, "project": "default.project.json" } |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +## Running a deploy |
| 170 | + |
| 171 | +From the directory containing `deploy.nevermore.json`: |
| 172 | + |
| 173 | +```bash |
| 174 | +# Build + upload to the default "test" target as a saved (draft) version |
| 175 | +nevermore deploy run |
| 176 | + |
| 177 | +# Same, but publish so the new version is live for players |
| 178 | +nevermore deploy run --publish |
| 179 | + |
| 180 | +# Deploy a specific target |
| 181 | +nevermore deploy run production --publish |
| 182 | +``` |
| 183 | + |
| 184 | +`nevermore deploy run` and the bare `nevermore deploy <target>` form are equivalent. `run` is the default subcommand. |
| 185 | + |
| 186 | +On success you'll see one of: |
| 187 | + |
| 188 | +``` |
| 189 | +Saved v42 — not yet live. |
| 190 | +Published v42 — live in game. |
| 191 | +``` |
| 192 | + |
| 193 | +A "saved" version is uploaded but not visible to players. You can publish it later from the Roblox dashboard, or re-run with `--publish` to publish a fresh build. That version number matches what you'll see on the place page. |
| 194 | + |
| 195 | +### Run flags |
| 196 | + |
| 197 | +| Flag | Description | |
| 198 | +|------|-------------| |
| 199 | +| `--publish` | Publish the new version (default: save only) | |
| 200 | +| `--api-key <key>` | Open Cloud API key (overrides credential lookup) | |
| 201 | +| `--universe-id <id>` | Override the target's `universeId` | |
| 202 | +| `--place-id <id>` | Override the target's `placeId` | |
| 203 | +| `--place-file <path>` | Skip the rojo build and upload an existing `.rbxl` instead | |
| 204 | +| `--output <path>` | Write a JSON record of the deploy result to this path | |
| 205 | + |
| 206 | +Global flags (available on every `nevermore` command): |
| 207 | + |
| 208 | +| Flag | Description | |
| 209 | +|------|-------------| |
| 210 | +| `--yes` | Non-interactive (fails fast instead of prompting) | |
| 211 | +| `--dryrun` | Print what would happen without doing it | |
| 212 | +| `--verbose` | Verbose logging (rojo output, upload details) | |
| 213 | + |
| 214 | +### Overriding the configured place |
| 215 | + |
| 216 | +`--universe-id` and `--place-id` let you redirect a single deploy without editing the config. This is useful when you want to push the same build to a personal staging place for a one-off test: |
| 217 | + |
| 218 | +```bash |
| 219 | +nevermore deploy run --universe-id 999 --place-id 8888 |
| 220 | +``` |
| 221 | + |
| 222 | +### Uploading a pre-built place |
| 223 | + |
| 224 | +If you already have a `.rbxl` (for example, one produced by `rojo build` in an upstream CI step), skip the rebuild: |
| 225 | + |
| 226 | +```bash |
| 227 | +nevermore deploy run --place-file ./build/my-place.rbxl |
| 228 | +``` |
| 229 | + |
| 230 | +The `project` field in `deploy.nevermore.json` is ignored when `--place-file` is set, but `universeId` and `placeId` are still required. |
| 231 | + |
| 232 | +## Batch deploys |
| 233 | + |
| 234 | +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. |
| 235 | + |
| 236 | +See [Integration Testing → Batch deploy](testing/integration-testing.md#batch-deploy) for the full flag list and CI usage. |
| 237 | + |
| 238 | +## Common workflows |
| 239 | + |
| 240 | +### First-time setup for a new game |
| 241 | + |
| 242 | +If you're starting from a clean directory, [`nevermore init`](install.md#fast-track-installing-via-npm-and-the-nevermore-cli-recommended) scaffolds a working Nevermore game template: a `default.project.json`, server/client entry scripts, and the default packages (`loader`, `servicebag`, `binder`, etc.). After that, `nevermore deploy init` only needs your universe and place IDs. |
| 243 | + |
| 244 | +```bash |
| 245 | +mkdir my-game && cd my-game |
| 246 | +nevermore init # scaffold a Nevermore game template |
| 247 | +nevermore deploy init # configure the deploy target (interactive) |
| 248 | +nevermore deploy run # first upload, draft only |
| 249 | +nevermore deploy run --publish # publish when ready |
| 250 | +``` |
| 251 | + |
| 252 | +The wizard auto-detects the project file `nevermore init` creates, so you only answer prompts for universe and place. See [Install](install.md) for the full breakdown of what `nevermore init` produces, and [Integration Testing → Setting up a new integration game](testing/integration-testing.md#setting-up-a-new-integration-game) if you're building a game inside the Nevermore/Raven monorepo instead. |
| 253 | + |
| 254 | +### Promoting a tested build to production |
| 255 | + |
| 256 | +If you have separate `test` and `production` targets, the common pattern is to run the test target on every PR and only deploy production from `main`: |
| 257 | + |
| 258 | +```bash |
| 259 | +# In CI on main |
| 260 | +nevermore deploy run production --publish |
| 261 | +``` |
| 262 | + |
| 263 | +There is no separate "promote" command. You deploy the same code to whichever target you want. |
| 264 | + |
| 265 | +### Debugging a failed deploy |
| 266 | + |
| 267 | +- Pass `--verbose` to see the rojo build output and the raw Open Cloud responses. |
| 268 | +- Pass `--dryrun` to confirm which target, universe, place, and project the CLI would use without uploading anything. |
| 269 | +- Check `nevermore login --status` if you suspect a credential problem. |
| 270 | +- Place uploads can fail with HTTP 403 when the API key is missing the `universe-places:write` scope for that specific universe. The scope is granted per-universe, not globally. |
| 271 | + |
| 272 | +## See also |
| 273 | + |
| 274 | +- [Intro](intro.md) — Why Nevermore, the major packages, and how the library is organized. |
| 275 | +- [Install](install.md) — Setting up Node, Rojo, the Nevermore CLI, and scaffolding a project with `nevermore init`. |
| 276 | +- [Integration Testing](testing/integration-testing.md) — `basePlace` merging, smoke tests, batch deploys, CI integration. |
| 277 | +- [Test Infrastructure](testing/testing.md) — How `nevermore test` reuses the deploy config to run Jest specs in Open Cloud. |
0 commit comments