@viraatdas/rudder 0.7.28 → 0.7.30

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.
package/dist/cloud.js CHANGED
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { currentBranch, currentCommit, findRepoRoot } from "./git.js";
6
6
  import { cloudAuthPath } from "./state.js";
7
- import { ensureDir, expandHome, newRunId, nowIso, pathExists, promptSecret, readJson, runCommand, shortenHome, writeJson, } from "./util.js";
7
+ import { ensureDir, commandExists, expandHome, newRunId, nowIso, pathExists, promptText, promptSelect, promptSecret, readJson, runCommand, shortenHome, shellQuote, writeJson, } from "./util.js";
8
8
  const DEFAULT_LOGIN_INTERVAL_MS = 2000;
9
9
  const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
10
10
  const DEFAULT_CLOUD_URL = "https://mpd2pmnpep.us-east-1.awsapprunner.com";
@@ -81,11 +81,16 @@ export async function runCloudCommand(command, args, options = {}) {
81
81
  case "login":
82
82
  await login(options);
83
83
  return;
84
+ case "launch":
85
+ await launch(rest, options, "task");
86
+ return;
84
87
  case "sail":
85
88
  await launch(rest, options);
86
89
  return;
87
- case "launch":
88
- await launch(rest, options, "task");
90
+ case "vm":
91
+ case "byoc":
92
+ case "byo-vm":
93
+ await launch(rest, options, "task", "byo-vm");
89
94
  return;
90
95
  case "list":
91
96
  case "ls":
@@ -94,6 +99,9 @@ export async function runCloudCommand(command, args, options = {}) {
94
99
  case "onload":
95
100
  await onload(rest, options);
96
101
  return;
102
+ case "bootstrap":
103
+ await bootstrap(rest, options);
104
+ return;
97
105
  case "pause":
98
106
  await mutateSail("pause", rest, options);
99
107
  return;
@@ -106,6 +114,28 @@ export async function runCloudCommand(command, args, options = {}) {
106
114
  case "setup-google":
107
115
  await setupOAuthProvider("google", rest, options);
108
116
  return;
117
+ case "setup-byoc":
118
+ await setupByoc(rest, options);
119
+ return;
120
+ case "setup-vm":
121
+ await setupByoc(rest, options);
122
+ return;
123
+ case "setup-fly":
124
+ await configureDefaultRuntime("fly", options);
125
+ return;
126
+ case "setup":
127
+ if (rest[0] === "byoc" || rest[0] === "vm" || rest[0] === "byo-vm") {
128
+ await setupByoc(rest.slice(1), options);
129
+ return;
130
+ }
131
+ if (rest[0] === "fly") {
132
+ await configureDefaultRuntime("fly", options);
133
+ return;
134
+ }
135
+ throw new Error("Usage: rudder cloud setup byoc | rudder cloud setup fly");
136
+ case "runtime":
137
+ await runtime(rest, options);
138
+ return;
109
139
  default:
110
140
  await launch(command === "sail" ? args : [subcommand, ...rest], options);
111
141
  return;
@@ -248,10 +278,15 @@ async function githubOAuthRequest(url, body) {
248
278
  return parsed;
249
279
  }
250
280
  async function saveCloudLogin(client, login, token, options, source) {
281
+ const previous = await loadCloudAuth();
282
+ const previousRuntime = previous?.cloudUrl === client.baseUrl ? parseCloudRuntime(previous.defaultRuntime) : undefined;
283
+ const previousByocHost = previous?.cloudUrl === client.baseUrl ? previous.byocSshHost : undefined;
251
284
  await saveCloudAuth({
252
285
  version: 1,
253
286
  token,
254
287
  cloudUrl: client.baseUrl,
288
+ defaultRuntime: previousRuntime,
289
+ byocSshHost: previousByocHost,
255
290
  accountId: login.accountId,
256
291
  email: login.email,
257
292
  expiresAt: login.expiresAt ?? (login.expiresIn ? new Date(Date.now() + login.expiresIn * 1000).toISOString() : undefined),
@@ -271,16 +306,15 @@ async function saveCloudLogin(client, login, token, options, source) {
271
306
  console.log(`Logged in to ${client.baseUrl}${login.email ? ` as ${login.email}` : ""} via ${source}.`);
272
307
  }
273
308
  }
274
- async function launch(args, options, mode = "name") {
309
+ async function launch(args, options, mode = "name", explicitRuntime) {
275
310
  const raw = args.join(" ").trim();
276
- const name = mode === "task"
277
- ? cloudNameFromTask(raw)
278
- : raw || randomCloudName();
279
- const task = mode === "task" ? raw : "";
280
311
  const repoRoot = findRepoRoot();
281
312
  const snapshot = await createSnapshot(repoRoot, options.homePaths ?? []);
282
313
  try {
283
314
  const client = await cloudClient({ requireToken: true });
315
+ const runtime = await selectedCloudRuntime(explicitRuntime);
316
+ const task = mode === "task" || runtime === "byo-vm" ? raw : "";
317
+ const name = task ? cloudNameFromTask(task) : raw || randomCloudName();
284
318
  const body = {
285
319
  repoName: path.basename(repoRoot),
286
320
  name,
@@ -291,6 +325,9 @@ async function launch(args, options, mode = "name") {
291
325
  manifest: snapshot.manifest,
292
326
  },
293
327
  };
328
+ if (runtime !== "fly") {
329
+ body.runtime = runtime;
330
+ }
294
331
  if (task) {
295
332
  body.task = task;
296
333
  }
@@ -298,7 +335,7 @@ async function launch(args, options, mode = "name") {
298
335
  method: "POST",
299
336
  body,
300
337
  });
301
- printResult(result, options);
338
+ await printResult(result, options);
302
339
  }
303
340
  finally {
304
341
  await fsp.rm(snapshot.tempDir, { recursive: true, force: true });
@@ -321,12 +358,14 @@ async function onload(args, options) {
321
358
  const snapshot = await createSnapshot(snapshotRoot, options.homePaths ?? []);
322
359
  try {
323
360
  const client = await cloudClient({ requireToken: true });
361
+ const runtime = await selectedCloudRuntime();
324
362
  const result = await client.request("/api/rudder/sail/onload", {
325
363
  method: "POST",
326
364
  body: {
327
365
  runId,
328
366
  repoName: path.basename(repoRoot),
329
367
  run: runRecord ?? null,
368
+ ...(runtime !== "fly" ? { runtime } : {}),
330
369
  snapshot: {
331
370
  name: path.basename(snapshot.archivePath),
332
371
  contentType: "application/gzip",
@@ -335,7 +374,7 @@ async function onload(args, options) {
335
374
  },
336
375
  },
337
376
  });
338
- printResult(result, options);
377
+ await printResult(result, options);
339
378
  }
340
379
  finally {
341
380
  await fsp.rm(snapshot.tempDir, { recursive: true, force: true });
@@ -344,7 +383,19 @@ async function onload(args, options) {
344
383
  async function listSails(options) {
345
384
  const client = await cloudClient({ requireToken: true });
346
385
  const result = await client.request("/api/rudder/sail", { method: "GET" });
347
- printResult(result, options);
386
+ await printResult(result, options);
387
+ }
388
+ async function bootstrap(args, options) {
389
+ const sailId = args[0];
390
+ if (!sailId) {
391
+ throw new Error("Missing sail id. Usage: rudder cloud bootstrap <id>");
392
+ }
393
+ const client = await cloudClient({ requireToken: true });
394
+ const result = await client.request(`/api/rudder/sail/${encodeURIComponent(sailId)}/bootstrap`, {
395
+ method: "POST",
396
+ body: {},
397
+ });
398
+ await printResult(result, options);
348
399
  }
349
400
  async function mutateSail(action, args, options) {
350
401
  const sailId = args[0];
@@ -356,7 +407,7 @@ async function mutateSail(action, args, options) {
356
407
  method: "POST",
357
408
  body: args.length > 1 ? { args: args.slice(1) } : {},
358
409
  });
359
- printResult(result, options);
410
+ await printResult(result, options);
360
411
  }
361
412
  async function setupOAuthProvider(provider, args, options) {
362
413
  const envPrefix = provider === "github" ? "RUDDER_GITHUB" : "RUDDER_GOOGLE";
@@ -379,7 +430,236 @@ async function setupOAuthProvider(provider, args, options) {
379
430
  clientSecret,
380
431
  },
381
432
  });
382
- printResult(result, options);
433
+ await printResult(result, options);
434
+ }
435
+ async function setupByoc(args, options) {
436
+ const sshConfigPath = path.join(os.homedir(), ".ssh", "config");
437
+ const configuredHosts = await listSshConfigHosts(sshConfigPath);
438
+ const host = (options.sshHost ?? args.join(" ").trim()) || await chooseByocHost(configuredHosts);
439
+ if (!host) {
440
+ throw new Error([
441
+ "Missing BYOC SSH host.",
442
+ "Add your workstation/server to ~/.ssh/config, then run:",
443
+ "",
444
+ " rudder cloud setup-byoc <ssh-host>",
445
+ "",
446
+ "Example ~/.ssh/config:",
447
+ " Host rudder-workstation",
448
+ " HostName 203.0.113.10",
449
+ " User ubuntu",
450
+ " IdentityFile ~/.ssh/id_ed25519",
451
+ "",
452
+ configuredHosts.length
453
+ ? `Detected SSH hosts: ${configuredHosts.slice(0, 12).join(", ")}`
454
+ : `No usable hosts found in ${shortenHome(sshConfigPath)}.`,
455
+ ].join("\n"));
456
+ }
457
+ const configMentionsHost = configuredHosts.includes(host) || await sshConfigMentions(sshConfigPath, host);
458
+ const diagnostics = await checkByocHost(host);
459
+ await configureDefaultRuntime("byo-vm", options, host);
460
+ if (options.json) {
461
+ return;
462
+ }
463
+ if (!configMentionsHost) {
464
+ console.log(`\nNote: ${shortenHome(sshConfigPath)} does not appear to define Host ${host}.`);
465
+ console.log("Rudder can still use it if SSH resolves it, but a ~/.ssh/config entry is recommended:");
466
+ console.log(` Host ${host}`);
467
+ console.log(" HostName <server-ip-or-dns>");
468
+ console.log(" User <user>");
469
+ console.log(" IdentityFile ~/.ssh/<private-key>");
470
+ }
471
+ if (diagnostics.ok) {
472
+ console.log(`SSH check passed for ${host}. Docker is available on the BYOC host.`);
473
+ }
474
+ else {
475
+ console.log(`\nSSH check did not fully pass for ${host}: ${diagnostics.message}`);
476
+ console.log("Fix SSH/Docker before launching, or run the printed Docker command manually on that host.");
477
+ }
478
+ }
479
+ async function chooseByocHost(hosts) {
480
+ if (hosts.length === 0) {
481
+ return await promptText("SSH host from ~/.ssh/config");
482
+ }
483
+ return await promptSelect("Choose a BYOC SSH host from ~/.ssh/config", hosts.slice(0, 24).map((host) => ({ value: host, label: host })), hosts[0]);
484
+ }
485
+ async function configureDefaultRuntime(runtime, options, byocSshHost) {
486
+ const client = await cloudClient({ requireToken: true });
487
+ const state = await loadCloudAuth();
488
+ if (!state || state.cloudUrl !== client.baseUrl) {
489
+ throw new Error("Not logged in to this Rudder Cloud control plane. Run `rudder login` first.");
490
+ }
491
+ await saveCloudAuth({
492
+ ...state,
493
+ defaultRuntime: runtime,
494
+ byocSshHost: runtime === "byo-vm" ? byocSshHost ?? state.byocSshHost : undefined,
495
+ updatedAt: nowIso(),
496
+ });
497
+ const result = {
498
+ ok: true,
499
+ cloudUrl: client.baseUrl,
500
+ defaultRuntime: runtime,
501
+ };
502
+ const savedByocHost = byocSshHost ?? state.byocSshHost;
503
+ if (runtime === "byo-vm" && savedByocHost) {
504
+ result.byocSshHost = savedByocHost;
505
+ }
506
+ const envRuntime = envCloudRuntime();
507
+ if (envRuntime) {
508
+ result.envOverride = envRuntime;
509
+ }
510
+ if (options.json) {
511
+ printJson(result);
512
+ return;
513
+ }
514
+ console.log(`Rudder Cloud runtime set to ${runtime}.`);
515
+ if (runtime === "byo-vm") {
516
+ const host = byocSshHost ?? state.byocSshHost;
517
+ console.log("Future `rudder cloud <task>` and `/sail <task>` launches will prepare a BYOC worker instead of creating a Fly Machine.");
518
+ console.log(host
519
+ ? `Rudder will try to start the worker over SSH on ${host}.`
520
+ : "Run `rudder cloud setup-byoc <ssh-host>` to let Rudder start workers over SSH.");
521
+ }
522
+ else {
523
+ console.log("Future `rudder cloud <task>` and `/sail <task>` launches will create Fly Machines.");
524
+ }
525
+ if (envRuntime) {
526
+ console.log(`RUDDER_CLOUD_RUNTIME=${envRuntime} is set and will override this saved default.`);
527
+ }
528
+ }
529
+ async function runtime(args, options) {
530
+ const next = args[0] ? parseCloudRuntime(args[0]) : undefined;
531
+ if (args[0] && !next) {
532
+ throw new Error("Runtime must be `fly`, `byoc`, or `byo-vm`.");
533
+ }
534
+ if (next) {
535
+ await configureDefaultRuntime(next, options);
536
+ return;
537
+ }
538
+ const client = await cloudClient({ requireToken: true });
539
+ const current = await selectedCloudRuntime();
540
+ const state = await loadCloudAuth();
541
+ const savedRuntime = parseCloudRuntime(state?.defaultRuntime);
542
+ const result = {
543
+ cloudUrl: client.baseUrl,
544
+ runtime: current,
545
+ };
546
+ const envRuntime = envCloudRuntime();
547
+ if (state?.cloudUrl === client.baseUrl && savedRuntime) {
548
+ result.savedDefaultRuntime = savedRuntime;
549
+ }
550
+ if (state?.cloudUrl === client.baseUrl && state.byocSshHost) {
551
+ result.byocSshHost = state.byocSshHost;
552
+ }
553
+ if (envRuntime) {
554
+ result.envOverride = envRuntime;
555
+ }
556
+ if (options.json) {
557
+ printJson(result);
558
+ }
559
+ else {
560
+ console.log(`Rudder Cloud runtime: ${current}`);
561
+ if (envRuntime) {
562
+ console.log(`Set by RUDDER_CLOUD_RUNTIME=${envRuntime}.`);
563
+ }
564
+ else if (state?.cloudUrl === client.baseUrl && savedRuntime) {
565
+ console.log("Set in local Rudder Cloud config.");
566
+ }
567
+ else {
568
+ console.log("Using default Fly Machines runtime.");
569
+ }
570
+ if (state?.cloudUrl === client.baseUrl && state.byocSshHost) {
571
+ console.log(`BYOC SSH host: ${state.byocSshHost}`);
572
+ }
573
+ }
574
+ }
575
+ async function sshConfigMentions(configPath, host) {
576
+ const text = await fsp.readFile(configPath, "utf8").catch(() => "");
577
+ if (!text.trim()) {
578
+ return false;
579
+ }
580
+ const target = host.toLowerCase();
581
+ for (const line of text.split(/\r?\n/)) {
582
+ const trimmed = line.trim();
583
+ if (!trimmed || trimmed.startsWith("#")) {
584
+ continue;
585
+ }
586
+ const match = /^Host\s+(.+)$/i.exec(trimmed);
587
+ if (!match) {
588
+ continue;
589
+ }
590
+ const patterns = match[1].split(/\s+/).map((part) => part.toLowerCase());
591
+ if (patterns.includes(target)) {
592
+ return true;
593
+ }
594
+ }
595
+ return false;
596
+ }
597
+ async function listSshConfigHosts(configPath) {
598
+ const text = await fsp.readFile(configPath, "utf8").catch(() => "");
599
+ const hosts = [];
600
+ const seen = new Set();
601
+ for (const line of text.split(/\r?\n/)) {
602
+ const trimmed = line.trim();
603
+ if (!trimmed || trimmed.startsWith("#")) {
604
+ continue;
605
+ }
606
+ const match = /^Host\s+(.+)$/i.exec(trimmed);
607
+ if (!match) {
608
+ continue;
609
+ }
610
+ for (const host of match[1].split(/\s+/)) {
611
+ if (!host || host.includes("*") || host.includes("?") || host.startsWith("!")) {
612
+ continue;
613
+ }
614
+ if (seen.has(host)) {
615
+ continue;
616
+ }
617
+ seen.add(host);
618
+ hosts.push(host);
619
+ }
620
+ }
621
+ return hosts;
622
+ }
623
+ async function checkByocHost(host) {
624
+ if (!commandExists("ssh")) {
625
+ return { ok: false, message: "ssh is not installed or not on PATH" };
626
+ }
627
+ const result = await runCommand("ssh", [
628
+ "-o",
629
+ "BatchMode=yes",
630
+ "-o",
631
+ "ConnectTimeout=8",
632
+ host,
633
+ "command -v docker >/dev/null && docker info >/dev/null 2>&1",
634
+ ], { allowFailure: true });
635
+ if (result.code === 0) {
636
+ return { ok: true, message: "ok" };
637
+ }
638
+ const detail = (result.stderr || result.stdout || `ssh exited ${result.code}`).trim();
639
+ return { ok: false, message: detail };
640
+ }
641
+ async function startByocWorkerOverSsh(host, bootstrapCommand) {
642
+ if (!commandExists("ssh")) {
643
+ throw new Error("ssh is not installed or not on PATH");
644
+ }
645
+ const remoteCommand = [
646
+ "mkdir -p ~/.rudder/byoc",
647
+ `nohup sh -lc ${shellQuote(nonInteractiveDockerCommand(bootstrapCommand))} > ~/.rudder/byoc/worker.log 2>&1 < /dev/null &`,
648
+ ].join(" && ");
649
+ await runCommand("ssh", [
650
+ "-o",
651
+ "BatchMode=yes",
652
+ "-o",
653
+ "ConnectTimeout=10",
654
+ host,
655
+ remoteCommand,
656
+ ]);
657
+ }
658
+ function nonInteractiveDockerCommand(command) {
659
+ return command
660
+ .replace(/\bdocker run --rm -it\b/g, "docker run --rm")
661
+ .replace(/\bdocker run --rm -i -t\b/g, "docker run --rm")
662
+ .replace(/\bdocker run --rm -t -i\b/g, "docker run --rm");
383
663
  }
384
664
  async function cloudClient(options) {
385
665
  const baseUrl = normalizeCloudUrl(process.env.RUDDER_CLOUD_URL);
@@ -439,6 +719,39 @@ function normalizeCloudUrl(raw) {
439
719
  throw new Error("RUDDER_CLOUD_URL must be a valid http(s) URL.");
440
720
  }
441
721
  }
722
+ async function selectedCloudRuntime(explicit) {
723
+ if (explicit) {
724
+ return explicit;
725
+ }
726
+ const envRuntime = envCloudRuntime();
727
+ if (envRuntime) {
728
+ return envRuntime;
729
+ }
730
+ const baseUrl = normalizeCloudUrl(process.env.RUDDER_CLOUD_URL);
731
+ const state = await loadCloudAuth();
732
+ const savedRuntime = parseCloudRuntime(state?.defaultRuntime);
733
+ return state?.cloudUrl === baseUrl && savedRuntime ? savedRuntime : "fly";
734
+ }
735
+ function parseCloudRuntime(raw) {
736
+ const value = raw?.trim().toLowerCase();
737
+ if (!value) {
738
+ return undefined;
739
+ }
740
+ if (value === "fly" || value === "fly-machine" || value === "fly-machines") {
741
+ return "fly";
742
+ }
743
+ if (value === "byo" || value === "byoc" || value === "byo-vm" || value === "manual" || value === "self-hosted" || value === "vm") {
744
+ return "byo-vm";
745
+ }
746
+ return undefined;
747
+ }
748
+ function envCloudRuntime() {
749
+ const runtime = parseCloudRuntime(process.env.RUDDER_CLOUD_RUNTIME);
750
+ if (process.env.RUDDER_CLOUD_RUNTIME?.trim() && !runtime) {
751
+ throw new Error("RUDDER_CLOUD_RUNTIME must be `fly`, `byoc`, or `byo-vm`.");
752
+ }
753
+ return runtime;
754
+ }
442
755
  async function pollLogin(client, pollPath, deviceCode) {
443
756
  if (pollPath.startsWith("http://") || pollPath.startsWith("https://") || !deviceCode) {
444
757
  return await client.request(pollPath, { method: "GET" });
@@ -636,7 +949,7 @@ function responseErrorMessage(value) {
636
949
  ? record.message
637
950
  : undefined;
638
951
  }
639
- function printResult(result, options) {
952
+ async function printResult(result, options) {
640
953
  if (options.json) {
641
954
  printJson(result);
642
955
  return;
@@ -647,21 +960,41 @@ function printResult(result, options) {
647
960
  }
648
961
  if (result && typeof result === "object" && !Array.isArray(result)) {
649
962
  const record = result;
963
+ if (typeof record.bootstrapCommand === "string") {
964
+ const id = typeof record.id === "string" ? record.id : "BYOC sail";
965
+ const status = typeof record.status === "string" ? record.status : undefined;
966
+ const state = await loadCloudAuth();
967
+ const host = options.sshHost ?? state?.byocSshHost;
968
+ console.log(`${id}${status ? ` (${status})` : ""} is ready for BYOC.`);
969
+ if (host && process.env.RUDDER_BYOC_AUTOSTART !== "0") {
970
+ try {
971
+ await startByocWorkerOverSsh(host, record.bootstrapCommand);
972
+ console.log(`Started BYOC worker over SSH on ${host}.`);
973
+ console.log(`Remote log: ssh ${host} 'tail -f ~/.rudder/byoc/worker.log'`);
974
+ }
975
+ catch (error) {
976
+ console.log(`Could not start BYOC worker over SSH on ${host}: ${error instanceof Error ? error.message : String(error)}`);
977
+ console.log("Run this manually on your workstation/server:");
978
+ console.log(record.bootstrapCommand);
979
+ }
980
+ }
981
+ else {
982
+ console.log("Run this on your workstation/server:");
983
+ console.log(record.bootstrapCommand);
984
+ if (!host) {
985
+ console.log("\nTip: run `rudder cloud setup-byoc <ssh-host>` to have Rudder start this over SSH next time.");
986
+ }
987
+ }
988
+ if (typeof record.updatedAt === "string") {
989
+ console.log(`\nIf the command expires, run: rudder cloud bootstrap ${id}`);
990
+ }
991
+ return;
992
+ }
650
993
  const sails = record.sails ?? record.items;
651
994
  if (Array.isArray(sails)) {
652
995
  printSailList(sails);
653
996
  return;
654
997
  }
655
- if (typeof record.id === "string" && typeof record.status === "string") {
656
- const parts = [
657
- "cloud",
658
- record.id,
659
- record.status,
660
- typeof record.task === "string" && record.task ? record.task : undefined,
661
- ].filter(Boolean);
662
- console.log(parts.join(" "));
663
- return;
664
- }
665
998
  }
666
999
  console.log(JSON.stringify(result, null, 2));
667
1000
  }
@@ -679,6 +1012,7 @@ function printSailList(items) {
679
1012
  console.log([
680
1013
  sail.id,
681
1014
  sail.status,
1015
+ sail.runtime,
682
1016
  typeof sail.task === "string" && sail.task ? sail.task : undefined,
683
1017
  typeof sail.repoName === "string" && sail.repoName ? sail.repoName : undefined,
684
1018
  sail.branch,
@@ -699,11 +1033,16 @@ function printCloudHelp() {
699
1033
  Usage:
700
1034
  rudder cloud login
701
1035
  rudder cloud help
702
- rudder cloud [name]
703
- rudder cloud list
1036
+ rudder cloud [name or task]
704
1037
  rudder cloud launch [--home-path <path>] ["task"]
1038
+ rudder cloud byoc ["task"]
1039
+ rudder cloud list
705
1040
  rudder cloud onload <runId>
706
- rudder sail [name]
1041
+ rudder cloud bootstrap <id>
1042
+ rudder cloud runtime [fly|byoc]
1043
+ rudder cloud setup-byoc <ssh-host>
1044
+ rudder cloud setup-fly
1045
+ rudder sail [name or task]
707
1046
  rudder sail list
708
1047
  rudder sail pause <id>
709
1048
  rudder sail resume <id>
@@ -712,6 +1051,7 @@ Usage:
712
1051
 
713
1052
  Environment:
714
1053
  RUDDER_CLOUD_URL Cloud control plane URL (defaults to ${DEFAULT_CLOUD_URL})
1054
+ RUDDER_CLOUD_RUNTIME fly, byoc, or byo-vm (overrides saved local default)
715
1055
  RUDDER_CLOUD_HOME_PATHS Extra comma-separated HOME paths to include in snapshots
716
1056
  RUDDER_GITHUB_CLIENT_ID GitHub App OAuth client ID for setup-github
717
1057
  RUDDER_GITHUB_CLIENT_SECRET GitHub App OAuth client secret for setup-github