ubriot 1.2.0 → 2.0.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.
- package/README.md +21 -0
- package/dist/api.js +24 -10
- package/dist/app-config-signing.js +67 -0
- package/dist/binary-certificate.js +93 -0
- package/dist/build-identity.js +72 -0
- package/dist/build-preflight.js +99 -0
- package/dist/child-env.js +10 -0
- package/dist/config.js +23 -13
- package/dist/index.js +565 -54
- package/dist/ota-signing.js +271 -0
- package/dist/play-options.js +22 -0
- package/dist/publish-signing.js +74 -0
- package/dist/runtime-preflight.js +36 -0
- package/dist/signing-commands.js +609 -0
- package/dist/unreal-detect.js +69 -0
- package/dist/x509.js +117 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -38,12 +38,38 @@ const commander_1 = require("commander");
|
|
|
38
38
|
const readline_1 = require("./readline");
|
|
39
39
|
const config_1 = require("./config");
|
|
40
40
|
const api_1 = require("./api");
|
|
41
|
+
const runtime_preflight_1 = require("./runtime-preflight");
|
|
42
|
+
const build_preflight_1 = require("./build-preflight");
|
|
43
|
+
const build_identity_1 = require("./build-identity");
|
|
41
44
|
const migrate_1 = require("./migrate");
|
|
45
|
+
const signing_commands_1 = require("./signing-commands");
|
|
46
|
+
const child_env_1 = require("./child-env");
|
|
47
|
+
const unreal_detect_1 = require("./unreal-detect");
|
|
48
|
+
const publish_signing_1 = require("./publish-signing");
|
|
42
49
|
const child_process_1 = require("child_process");
|
|
43
50
|
const fs_1 = require("fs");
|
|
44
51
|
const path_1 = require("path");
|
|
45
52
|
const crypto_1 = require("crypto");
|
|
46
|
-
|
|
53
|
+
const JSON_OPTION_HELP = "Print the raw API response as JSON instead of human-readable output";
|
|
54
|
+
commander_1.program
|
|
55
|
+
.name("ubriot")
|
|
56
|
+
.description("Ubriot, mobile release infrastructure for teams that ship")
|
|
57
|
+
.version("2.0.0")
|
|
58
|
+
.enablePositionalOptions()
|
|
59
|
+
.option("--json", JSON_OPTION_HELP);
|
|
60
|
+
let jsonOutput = false;
|
|
61
|
+
commander_1.program.hook("preAction", (_program, actionCommand) => {
|
|
62
|
+
jsonOutput = commander_1.program.opts().json === true || actionCommand.opts().json === true;
|
|
63
|
+
});
|
|
64
|
+
function usingApiKey() {
|
|
65
|
+
return (0, config_1.envToken)()?.startsWith("ubk_") === true;
|
|
66
|
+
}
|
|
67
|
+
function wantsJson() {
|
|
68
|
+
return jsonOutput;
|
|
69
|
+
}
|
|
70
|
+
function printJson(data) {
|
|
71
|
+
console.log(JSON.stringify(data, null, 2));
|
|
72
|
+
}
|
|
47
73
|
const MIME = {
|
|
48
74
|
".avif": "image/avif",
|
|
49
75
|
".bundle": "application/javascript",
|
|
@@ -312,8 +338,8 @@ function optionalGitShortSha() {
|
|
|
312
338
|
return undefined;
|
|
313
339
|
return result.stdout.trim() || undefined;
|
|
314
340
|
}
|
|
315
|
-
function runLocalCommand(command, args) {
|
|
316
|
-
const result = (0, child_process_1.spawnSync)(command, args, { stdio: "inherit", env:
|
|
341
|
+
function runLocalCommand(command, args, extraEnv = {}) {
|
|
342
|
+
const result = (0, child_process_1.spawnSync)(command, args, { stdio: "inherit", env: (0, child_env_1.childProcessEnv)(extraEnv) });
|
|
317
343
|
if (result.error)
|
|
318
344
|
throw result.error;
|
|
319
345
|
if (result.status !== 0)
|
|
@@ -336,6 +362,44 @@ function parsePositiveNumber(value, fallback, label) {
|
|
|
336
362
|
throw new Error(`${label} must be a positive number`);
|
|
337
363
|
return parsed;
|
|
338
364
|
}
|
|
365
|
+
function parsePlayReleaseStatus(value) {
|
|
366
|
+
if (value === undefined)
|
|
367
|
+
return undefined;
|
|
368
|
+
const status = String(value).trim().toLowerCase();
|
|
369
|
+
if (status !== "draft" && status !== "completed") {
|
|
370
|
+
throw new Error('--release-status must be "draft" (staged in the Play Console) or "completed" (live release)');
|
|
371
|
+
}
|
|
372
|
+
return status;
|
|
373
|
+
}
|
|
374
|
+
function parseRolloutPercentage(value) {
|
|
375
|
+
if (value === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
const parsed = Number(value);
|
|
378
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= 100) {
|
|
379
|
+
throw new Error("--rollout must be greater than 0 and less than 100. Drop it and use --release-status completed for a full rollout.");
|
|
380
|
+
}
|
|
381
|
+
return parsed;
|
|
382
|
+
}
|
|
383
|
+
function playSubmitBody(options) {
|
|
384
|
+
const body = {};
|
|
385
|
+
if (options.track)
|
|
386
|
+
body.track = options.track;
|
|
387
|
+
if (options.releaseStatus)
|
|
388
|
+
body.releaseStatus = options.releaseStatus;
|
|
389
|
+
if (options.rolloutPercentage !== undefined)
|
|
390
|
+
body.rolloutPercentage = options.rolloutPercentage;
|
|
391
|
+
if (options.releaseNotes)
|
|
392
|
+
body.releaseNotes = options.releaseNotes;
|
|
393
|
+
return body;
|
|
394
|
+
}
|
|
395
|
+
function readPlaySubmitOptions(opts) {
|
|
396
|
+
return {
|
|
397
|
+
track: opts.track,
|
|
398
|
+
releaseStatus: parsePlayReleaseStatus(opts.releaseStatus),
|
|
399
|
+
rolloutPercentage: parseRolloutPercentage(opts.rollout),
|
|
400
|
+
releaseNotes: opts.releaseNotes,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
339
403
|
function buildStatusLine(build) {
|
|
340
404
|
const asc = build.ascSubmissionStatus ? `, TestFlight ${build.ascSubmissionStatus}` : "";
|
|
341
405
|
const play = build.playSubmissionStatus ? `, Google Play ${build.playSubmissionStatus}` : "";
|
|
@@ -413,7 +477,7 @@ async function waitForTestFlight(client, buildId, intervalMs, timeoutMs) {
|
|
|
413
477
|
}
|
|
414
478
|
throw new Error(`Timed out waiting for TestFlight submit for build #${buildId}`);
|
|
415
479
|
}
|
|
416
|
-
async function queueGooglePlayIfNeeded(client, build,
|
|
480
|
+
async function queueGooglePlayIfNeeded(client, build, options = {}) {
|
|
417
481
|
if (build.platform !== "android")
|
|
418
482
|
throw new Error("--submit-google-play is only valid for Android builds");
|
|
419
483
|
if (build.playSubmissionStatus === "succeeded") {
|
|
@@ -424,7 +488,7 @@ async function queueGooglePlayIfNeeded(client, build, track) {
|
|
|
424
488
|
console.log(`Build #${build.id} already has Google Play submission ${build.playSubmissionStatus}.`);
|
|
425
489
|
return build;
|
|
426
490
|
}
|
|
427
|
-
const { data } = await client.post(`/ubriot/builds/${build.id}/submit-google-play`,
|
|
491
|
+
const { data } = await client.post(`/ubriot/builds/${build.id}/submit-google-play`, playSubmitBody(options));
|
|
428
492
|
console.log(`Build #${build.id} queued for Google Play submit.`);
|
|
429
493
|
return data;
|
|
430
494
|
}
|
|
@@ -534,6 +598,7 @@ async function buildOtaPlatformPayload(input) {
|
|
|
534
598
|
},
|
|
535
599
|
extra: {
|
|
536
600
|
expoClient: input.expoConfig,
|
|
601
|
+
ubriot: { ...input.ubriotExtra, platform: input.platform },
|
|
537
602
|
},
|
|
538
603
|
};
|
|
539
604
|
return {
|
|
@@ -542,13 +607,148 @@ async function buildOtaPlatformPayload(input) {
|
|
|
542
607
|
assets: uploadedForDb,
|
|
543
608
|
};
|
|
544
609
|
}
|
|
610
|
+
function runLocalCommandIn(cwd, command, args) {
|
|
611
|
+
const result = (0, child_process_1.spawnSync)(command, args, { stdio: "inherit", env: (0, child_env_1.childProcessEnv)(), cwd });
|
|
612
|
+
if (result.error)
|
|
613
|
+
throw result.error;
|
|
614
|
+
if (result.status !== 0)
|
|
615
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`);
|
|
616
|
+
}
|
|
617
|
+
function detectBareEntryFile(projectDir) {
|
|
618
|
+
const pkgPath = (0, path_1.join)(projectDir, "package.json");
|
|
619
|
+
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
620
|
+
const pkg = readJsonFile(pkgPath);
|
|
621
|
+
if (typeof pkg.main === "string" && pkg.main.trim() && /\.[cm]?[jt]sx?$/.test(pkg.main)) {
|
|
622
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectDir, pkg.main)))
|
|
623
|
+
return pkg.main;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const candidate of ["index.js", "index.ts", "index.tsx"]) {
|
|
627
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectDir, candidate)))
|
|
628
|
+
return candidate;
|
|
629
|
+
}
|
|
630
|
+
return "index.js";
|
|
631
|
+
}
|
|
632
|
+
function isBareRnProject(projectDir) {
|
|
633
|
+
const pkgPath = (0, path_1.join)(projectDir, "package.json");
|
|
634
|
+
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
635
|
+
const pkg = readJsonFile(pkgPath);
|
|
636
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
637
|
+
if (deps.expo)
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
const appJsonPath = (0, path_1.join)(projectDir, "app.json");
|
|
641
|
+
if ((0, fs_1.existsSync)(appJsonPath)) {
|
|
642
|
+
const appJson = readJsonFile(appJsonPath);
|
|
643
|
+
if (appJson.expo)
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
function collectBundledAssets(platformDir, bundleFileName) {
|
|
649
|
+
const files = [];
|
|
650
|
+
const walk = (dir) => {
|
|
651
|
+
for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
652
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
653
|
+
if (entry.isDirectory())
|
|
654
|
+
walk(full);
|
|
655
|
+
else
|
|
656
|
+
files.push(full);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
walk(platformDir);
|
|
660
|
+
return files.filter((full) => {
|
|
661
|
+
const name = (0, path_1.basename)(full);
|
|
662
|
+
return name !== bundleFileName && name !== `${bundleFileName}.map`;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function exportBareRnBundle(input) {
|
|
666
|
+
const projectDir = (0, path_1.resolve)(input.projectDir);
|
|
667
|
+
const outDir = (0, path_1.resolve)(input.outDir);
|
|
668
|
+
const entryFile = input.entryFile ?? detectBareEntryFile(projectDir);
|
|
669
|
+
const platforms = input.platform === "all" ? ["ios", "android"] : [input.platform];
|
|
670
|
+
const bundleFileName = "index.bundle";
|
|
671
|
+
const fileMetadata = {};
|
|
672
|
+
for (const platform of platforms) {
|
|
673
|
+
if (platform !== "ios" && platform !== "android")
|
|
674
|
+
throw new Error("--platform must be ios, android, or all");
|
|
675
|
+
const platformDir = (0, path_1.join)(outDir, platform);
|
|
676
|
+
(0, fs_1.mkdirSync)(platformDir, { recursive: true });
|
|
677
|
+
const bundleOutput = (0, path_1.join)(platformDir, bundleFileName);
|
|
678
|
+
runLocalCommandIn(projectDir, "npx", [
|
|
679
|
+
"react-native",
|
|
680
|
+
"bundle",
|
|
681
|
+
"--platform",
|
|
682
|
+
platform,
|
|
683
|
+
"--dev",
|
|
684
|
+
"false",
|
|
685
|
+
"--entry-file",
|
|
686
|
+
entryFile,
|
|
687
|
+
"--bundle-output",
|
|
688
|
+
bundleOutput,
|
|
689
|
+
"--assets-dest",
|
|
690
|
+
platformDir,
|
|
691
|
+
]);
|
|
692
|
+
if (!(0, fs_1.existsSync)(bundleOutput))
|
|
693
|
+
throw new Error(`react-native bundle did not produce ${bundleOutput}`);
|
|
694
|
+
const assets = collectBundledAssets(platformDir, bundleFileName).map((full) => {
|
|
695
|
+
const ext = (0, path_1.extname)(full).replace(/^\./, "");
|
|
696
|
+
return { path: (0, path_1.relative)(outDir, full), ext: ext || null };
|
|
697
|
+
});
|
|
698
|
+
fileMetadata[platform] = { bundle: (0, path_1.relative)(outDir, bundleOutput), assets };
|
|
699
|
+
}
|
|
700
|
+
(0, fs_1.mkdirSync)(outDir, { recursive: true });
|
|
701
|
+
const metadata = { version: 0, bundler: "metro", fileMetadata };
|
|
702
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(outDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
703
|
+
return { outDir, entryFile, metadata };
|
|
704
|
+
}
|
|
705
|
+
async function confirmRuntimePreflight(client, input) {
|
|
706
|
+
const result = await (0, runtime_preflight_1.fetchRuntimePreflight)(client, input);
|
|
707
|
+
if (!result) {
|
|
708
|
+
console.log("Runtime reach check unavailable, continuing without it.");
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
for (const line of (0, runtime_preflight_1.renderPreflight)(result))
|
|
712
|
+
console.log(line);
|
|
713
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
714
|
+
if ((0, runtime_preflight_1.preflightDecision)(result, { yes: input.yes, interactive }) === "continue")
|
|
715
|
+
return true;
|
|
716
|
+
const answer = await (0, readline_1.readlineSync)("Publish anyway? [y/N] ");
|
|
717
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
718
|
+
}
|
|
719
|
+
async function confirmBuildPreflight(client, input) {
|
|
720
|
+
if (input.platform !== "ios" && input.platform !== "android")
|
|
721
|
+
return true;
|
|
722
|
+
const result = await (0, build_preflight_1.fetchBuildPreflight)(client, { app: input.app, platform: input.platform, runtime: (0, build_preflight_1.detectProjectRuntime)(input.projectDir) });
|
|
723
|
+
if (!result)
|
|
724
|
+
return true;
|
|
725
|
+
const print = wantsJson() ? console.error : console.log;
|
|
726
|
+
for (const line of (0, build_preflight_1.renderBuildPreflight)(result))
|
|
727
|
+
print(line);
|
|
728
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
729
|
+
if ((0, build_preflight_1.buildPreflightDecision)(result, { yes: input.yes, interactive }) === "continue")
|
|
730
|
+
return true;
|
|
731
|
+
const answer = await (0, readline_1.readlineSync)("Queue the build anyway? [y/N] ");
|
|
732
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
733
|
+
}
|
|
734
|
+
async function confirmLocalNode(projectDir, yes) {
|
|
735
|
+
const problem = (0, build_preflight_1.localNodeProblem)((0, build_preflight_1.detectProjectRuntime)(projectDir), process.versions.node);
|
|
736
|
+
if (!problem)
|
|
737
|
+
return true;
|
|
738
|
+
console.log(`Warning: ${problem}`);
|
|
739
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
740
|
+
if (yes || !interactive)
|
|
741
|
+
return true;
|
|
742
|
+
const answer = await (0, readline_1.readlineSync)("Export anyway? [y/N] ");
|
|
743
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
744
|
+
}
|
|
545
745
|
async function publishExportedUpdate(input) {
|
|
546
746
|
const exportDir = (0, path_1.resolve)(input.exportDir);
|
|
547
747
|
const metadataPath = (0, path_1.join)(exportDir, "metadata.json");
|
|
548
748
|
if (!(0, fs_1.existsSync)(metadataPath))
|
|
549
749
|
throw new Error(`No metadata.json found in ${exportDir}. Run: npx expo export --output-dir ${input.exportDir}`);
|
|
550
750
|
const metadataJson = readJsonFile(metadataPath);
|
|
551
|
-
const expoConfig =
|
|
751
|
+
const expoConfig = input.expoConfig;
|
|
552
752
|
const publishId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
553
753
|
const selectedPlatforms = input.platform === "all" ? ["ios", "android"] : [input.platform];
|
|
554
754
|
const platforms = [];
|
|
@@ -566,6 +766,7 @@ async function publishExportedUpdate(input) {
|
|
|
566
766
|
expoConfig,
|
|
567
767
|
metadataJson,
|
|
568
768
|
publicApiBase: input.publicApiBase,
|
|
769
|
+
ubriotExtra: input.ubriotExtra,
|
|
569
770
|
});
|
|
570
771
|
if (payload)
|
|
571
772
|
platforms.push(payload);
|
|
@@ -576,7 +777,7 @@ async function publishExportedUpdate(input) {
|
|
|
576
777
|
channel: input.channel,
|
|
577
778
|
runtimeVersion: input.runtimeVersion,
|
|
578
779
|
message: input.message,
|
|
579
|
-
platforms,
|
|
780
|
+
platforms: (0, publish_signing_1.signPlatforms)(platforms, input.signingMaterials),
|
|
580
781
|
}, { timeout: 120000 });
|
|
581
782
|
return { data, platforms };
|
|
582
783
|
}
|
|
@@ -586,13 +787,17 @@ commander_1.program
|
|
|
586
787
|
.description("Log in with your Ubriot credentials")
|
|
587
788
|
.option("--api-base <url>", "API base URL")
|
|
588
789
|
.action(async (opts) => {
|
|
790
|
+
if (!process.stdin.isTTY) {
|
|
791
|
+
console.error(`ubriot login needs an interactive terminal. For scripts, CI and agents, set UBRIOT_TOKEN to an API key from ${config_1.API_KEYS_URL}.`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
589
794
|
if (opts.apiBase)
|
|
590
795
|
(0, config_1.writeConfig)({ apiBase: opts.apiBase });
|
|
591
796
|
const email = await (0, readline_1.readlineSync)("Email: ");
|
|
592
797
|
const password = await (0, readline_1.readlineSync)("Password: ", true);
|
|
593
798
|
try {
|
|
594
799
|
const client = (0, api_1.makeClient)();
|
|
595
|
-
const { data } = await client.post("/auth/login", { email, password });
|
|
800
|
+
const { data } = await client.post("/auth/login", { email, password }, { headers: { "X-Ubriot-Client": "cli" } });
|
|
596
801
|
const token = data?.user?.token;
|
|
597
802
|
if (!token)
|
|
598
803
|
throw new Error("No token in response");
|
|
@@ -607,7 +812,7 @@ commander_1.program
|
|
|
607
812
|
.command("logout")
|
|
608
813
|
.description("Clear saved credentials")
|
|
609
814
|
.action(async () => {
|
|
610
|
-
const cfg = (0, config_1.
|
|
815
|
+
const cfg = (0, config_1.readSavedConfig)();
|
|
611
816
|
if (cfg.refreshToken) {
|
|
612
817
|
// Best-effort server-side revoke; ignore failures.
|
|
613
818
|
try {
|
|
@@ -623,9 +828,15 @@ commander_1.program
|
|
|
623
828
|
commander_1.program
|
|
624
829
|
.command("whoami")
|
|
625
830
|
.description("Show current user")
|
|
831
|
+
.option("--json", JSON_OPTION_HELP)
|
|
626
832
|
.action(() => {
|
|
627
833
|
const cfg = (0, config_1.readConfig)();
|
|
628
|
-
|
|
834
|
+
const source = (0, config_1.envToken)() ? "env" : cfg.token ? "config" : null;
|
|
835
|
+
if (wantsJson())
|
|
836
|
+
printJson({ email: cfg.email, source });
|
|
837
|
+
else if (source === "env")
|
|
838
|
+
console.log("Using UBRIOT_TOKEN from the environment.");
|
|
839
|
+
else if (cfg.email)
|
|
629
840
|
console.log(cfg.email);
|
|
630
841
|
else
|
|
631
842
|
console.log("Not logged in.");
|
|
@@ -643,10 +854,17 @@ apps
|
|
|
643
854
|
.option("--ios-bundle <id>", "iOS bundle identifier")
|
|
644
855
|
.option("--google-play-track <track>", "Default Google Play track", "internal")
|
|
645
856
|
.option("--expo-project-root <path>", "Path to the Expo app inside the repo (monorepos), e.g. products/myapp/mobile")
|
|
857
|
+
.option("--unreal", "Unreal Engine project (Android builds); reads the .uproject in the project root")
|
|
858
|
+
.option("--engine-version <x.y>", "Unreal Engine version, defaults to the .uproject EngineAssociation")
|
|
859
|
+
.option("--unreal-configuration <name>", "Unreal build configuration: Development or Shipping (default Shipping)")
|
|
860
|
+
.option("--package-format <format>", "Unreal Android package: aab or apk (default aab)")
|
|
861
|
+
.option("--accept-unreal-eula", "Confirm you hold an Unreal Engine licence for this version and accept the Unreal Engine EULA")
|
|
646
862
|
.action(async (opts) => {
|
|
647
863
|
(0, config_1.requireAuth)();
|
|
648
864
|
try {
|
|
865
|
+
const unreal = (0, unreal_detect_1.unrealAppFields)(opts, (0, path_1.resolve)(opts.expoProjectRoot ?? "."));
|
|
649
866
|
const { data } = await (0, api_1.makeClient)().post("/ubriot/apps", {
|
|
867
|
+
...(unreal ?? {}),
|
|
650
868
|
name: opts.name,
|
|
651
869
|
slug: opts.slug,
|
|
652
870
|
repoUrl: opts.repo,
|
|
@@ -665,10 +883,15 @@ apps
|
|
|
665
883
|
apps
|
|
666
884
|
.command("list")
|
|
667
885
|
.description("List your release apps")
|
|
886
|
+
.option("--json", JSON_OPTION_HELP)
|
|
668
887
|
.action(async () => {
|
|
669
888
|
(0, config_1.requireAuth)();
|
|
670
889
|
try {
|
|
671
890
|
const { data } = await (0, api_1.makeClient)().get("/ubriot/apps");
|
|
891
|
+
if (wantsJson()) {
|
|
892
|
+
printJson(data);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
672
895
|
if (!data.length) {
|
|
673
896
|
console.log("No apps yet.");
|
|
674
897
|
return;
|
|
@@ -692,15 +915,20 @@ apps
|
|
|
692
915
|
.option("--android-id <id>", "Android application ID / package name")
|
|
693
916
|
.option("--asc-app-id <id>", "App Store Connect app id (from ASC app URL)")
|
|
694
917
|
.option("--clear-asc-app-id", "Remove stored ASC app id (use bundle id lookup)", false)
|
|
695
|
-
.option("--auto-submit-testflight", "Enable auto-submit after each successful iOS build"
|
|
696
|
-
.option("--no-auto-submit-testflight", "Disable auto-submit"
|
|
918
|
+
.option("--auto-submit-testflight", "Enable auto-submit after each successful iOS build")
|
|
919
|
+
.option("--no-auto-submit-testflight", "Disable auto-submit")
|
|
697
920
|
.option("--google-play-track <track>", "Default Google Play track, e.g. internal, alpha, beta, production")
|
|
698
|
-
.option("--auto-submit-google-play", "Enable auto-submit to Google Play after each successful Android build"
|
|
699
|
-
.option("--no-auto-submit-google-play", "Disable Google Play auto-submit"
|
|
921
|
+
.option("--auto-submit-google-play", "Enable auto-submit to Google Play after each successful Android build")
|
|
922
|
+
.option("--no-auto-submit-google-play", "Disable Google Play auto-submit")
|
|
923
|
+
.option("--unreal", "Switch to (or update) an Unreal Engine project; reads the .uproject in the project root")
|
|
924
|
+
.option("--engine-version <x.y>", "Unreal Engine version, defaults to the .uproject EngineAssociation")
|
|
925
|
+
.option("--unreal-configuration <name>", "Unreal build configuration: Development or Shipping")
|
|
926
|
+
.option("--package-format <format>", "Unreal Android package: aab or apk")
|
|
927
|
+
.option("--accept-unreal-eula", "Confirm you hold an Unreal Engine licence for this version and accept the Unreal Engine EULA")
|
|
700
928
|
.action(async (opts) => {
|
|
701
929
|
(0, config_1.requireAuth)();
|
|
702
930
|
try {
|
|
703
|
-
const body = {};
|
|
931
|
+
const body = { ...((0, unreal_detect_1.unrealAppFields)(opts, (0, path_1.resolve)(opts.expoProjectRoot ?? ".")) ?? {}) };
|
|
704
932
|
if (opts.iosBundle !== undefined)
|
|
705
933
|
body.iosBundleIdentifier = opts.iosBundle || null;
|
|
706
934
|
if (opts.expoProjectRoot !== undefined)
|
|
@@ -711,18 +939,14 @@ apps
|
|
|
711
939
|
body.appStoreConnectAppId = null;
|
|
712
940
|
else if (opts.ascAppId !== undefined)
|
|
713
941
|
body.appStoreConnectAppId = opts.ascAppId;
|
|
714
|
-
if (opts.autoSubmitTestflight)
|
|
715
|
-
body.autoSubmitTestflight =
|
|
716
|
-
if (opts.noAutoSubmitTestflight)
|
|
717
|
-
body.autoSubmitTestflight = false;
|
|
942
|
+
if (opts.autoSubmitTestflight !== undefined)
|
|
943
|
+
body.autoSubmitTestflight = opts.autoSubmitTestflight;
|
|
718
944
|
if (opts.googlePlayTrack !== undefined)
|
|
719
945
|
body.googlePlayTrack = opts.googlePlayTrack || null;
|
|
720
|
-
if (opts.autoSubmitGooglePlay)
|
|
721
|
-
body.autoSubmitGooglePlay =
|
|
722
|
-
if (opts.noAutoSubmitGooglePlay)
|
|
723
|
-
body.autoSubmitGooglePlay = false;
|
|
946
|
+
if (opts.autoSubmitGooglePlay !== undefined)
|
|
947
|
+
body.autoSubmitGooglePlay = opts.autoSubmitGooglePlay;
|
|
724
948
|
if (!Object.keys(body).length) {
|
|
725
|
-
console.log("Nothing to update, pass --ios-bundle, --android-id, --asc-app-id, --google-play-track, and/or auto-submit flags");
|
|
949
|
+
console.log("Nothing to update, pass --ios-bundle, --android-id, --asc-app-id, --google-play-track, --unreal, and/or auto-submit flags");
|
|
726
950
|
return;
|
|
727
951
|
}
|
|
728
952
|
const { data } = await (0, api_1.makeClient)().patch(`/ubriot/apps/${opts.app}`, body);
|
|
@@ -732,6 +956,37 @@ apps
|
|
|
732
956
|
(0, api_1.handleError)(err);
|
|
733
957
|
}
|
|
734
958
|
});
|
|
959
|
+
apps
|
|
960
|
+
.command("activity <app>")
|
|
961
|
+
.description("Show who changed what on a release app, newest first")
|
|
962
|
+
.option("--limit <n>", "Number of entries (max 200)", "50")
|
|
963
|
+
.option("--offset <n>", "Skip this many newer entries", "0")
|
|
964
|
+
.option("--json", JSON_OPTION_HELP)
|
|
965
|
+
.action(async (app, opts) => {
|
|
966
|
+
(0, config_1.requireAuth)();
|
|
967
|
+
try {
|
|
968
|
+
const { data } = await (0, api_1.makeClient)().get(`/ubriot/apps/${encodeURIComponent(app)}/activity`, {
|
|
969
|
+
params: { limit: opts.limit, offset: opts.offset },
|
|
970
|
+
});
|
|
971
|
+
if (wantsJson()) {
|
|
972
|
+
printJson(data);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (!data.items.length) {
|
|
976
|
+
console.log("No activity yet.");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
for (const entry of data.items) {
|
|
980
|
+
console.log(` ${new Date(entry.createdAt).toISOString()} ${entry.actor.padEnd(24)} ${entry.summary}`);
|
|
981
|
+
}
|
|
982
|
+
const shown = Number(opts.offset) + data.items.length;
|
|
983
|
+
if (shown < data.total)
|
|
984
|
+
console.log(` … ${data.total - shown} older, use --offset ${shown}`);
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
(0, api_1.handleError)(err);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
735
990
|
// ─── Releases ────────────────────────────────────────────────────────────────
|
|
736
991
|
commander_1.program
|
|
737
992
|
.command("ship <platform>")
|
|
@@ -742,11 +997,15 @@ commander_1.program
|
|
|
742
997
|
.option("--branch <branch>", "Branch to build")
|
|
743
998
|
.option("--local [path]", "Build from a local directory instead of cloning from remote (path defaults to CWD)")
|
|
744
999
|
.option("--track <track>", "Google Play track for Android", "internal")
|
|
1000
|
+
.option("--release-status <status>", "Google Play release status: draft (default on production) or completed for a live release")
|
|
1001
|
+
.option("--rollout <percent>", "Staged rollout percentage for a live release, greater than 0 and less than 100")
|
|
1002
|
+
.option("--release-notes <text>", "Release notes to attach to the Google Play release")
|
|
745
1003
|
.option("--no-submit", "Build only; do not submit to TestFlight / Google Play")
|
|
746
1004
|
.option("--reprovision-ios", "Force-regenerate the iOS provisioning profile before building", false)
|
|
747
1005
|
.option("--ios-capability <type...>", "Apple capability to ensure on the bundle ID (e.g. HEALTHKIT); regenerates the profile if newly added")
|
|
748
1006
|
.option("--poll-interval <seconds>", "Polling interval while waiting", "15")
|
|
749
1007
|
.option("--timeout <minutes>", "Maximum wait time for the build and store submit", "180")
|
|
1008
|
+
.option("--yes", "Queue without confirming toolchain warnings", false)
|
|
750
1009
|
.action(async (platform, opts) => {
|
|
751
1010
|
(0, config_1.requireAuth)();
|
|
752
1011
|
try {
|
|
@@ -762,6 +1021,7 @@ commander_1.program
|
|
|
762
1021
|
const client = (0, api_1.makeClient)();
|
|
763
1022
|
const pollIntervalMs = parsePositiveNumber(opts.pollInterval, 15, "--poll-interval") * 1000;
|
|
764
1023
|
const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
|
|
1024
|
+
const playSubmitOptions = readPlaySubmitOptions(opts);
|
|
765
1025
|
// --local builds from a file:// path so the worker clones locally without needing a remote push
|
|
766
1026
|
let repoUrl = opts.repo;
|
|
767
1027
|
let branch = opts.branch;
|
|
@@ -778,6 +1038,12 @@ commander_1.program
|
|
|
778
1038
|
}
|
|
779
1039
|
console.log(`Building from local path: ${localPath} (branch: ${branch ?? "default"})`);
|
|
780
1040
|
}
|
|
1041
|
+
const projectDir = opts.local !== undefined && opts.local !== true ? (0, path_1.resolve)(opts.local) : process.cwd();
|
|
1042
|
+
if (!(await confirmBuildPreflight(client, { app, platform: normalizedPlatform, projectDir, yes: opts.yes === true }))) {
|
|
1043
|
+
console.log("Aborted. No build was queued.");
|
|
1044
|
+
process.exitCode = 1;
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
781
1047
|
// EAS-style auto-provisioning: ensure a provisioning profile exists for iOS.
|
|
782
1048
|
if (normalizedPlatform === "ios") {
|
|
783
1049
|
// Auto-detect capabilities from the local Expo config so users don't have
|
|
@@ -804,21 +1070,27 @@ commander_1.program
|
|
|
804
1070
|
provisionBody.force = true;
|
|
805
1071
|
if (capabilities.length)
|
|
806
1072
|
provisionBody.capabilities = capabilities;
|
|
807
|
-
|
|
808
|
-
const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
|
|
809
|
-
if (prov?.status === "provisioned") {
|
|
810
|
-
const caps = prov.capabilitiesAdded?.length ? ` [capabilities added: ${prov.capabilitiesAdded.join(", ")}]` : "";
|
|
811
|
-
console.log(`Auto-provisioned iOS signing: ${prov.profileName} (${prov.bundleId})${caps}`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
catch (provErr) {
|
|
815
|
-
const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
|
|
816
|
-
// When reprovisioning or capabilities were explicitly requested, a provisioning
|
|
817
|
-
// failure means the build is doomed to fail signing, stop here instead.
|
|
1073
|
+
if (usingApiKey()) {
|
|
818
1074
|
if (provisionExplicit)
|
|
819
|
-
throw new Error(
|
|
820
|
-
console.log(
|
|
1075
|
+
throw new Error("iOS provisioning needs a signed-in session and does not accept API keys. Run ubriot login on a machine with a terminal, or drop --reprovision-ios and --ios-capability.");
|
|
1076
|
+
console.log("Auto-provisioning skipped: it needs a signed-in session, and UBRIOT_TOKEN is an API key. The build uses the provisioning profile already stored for this app.");
|
|
821
1077
|
}
|
|
1078
|
+
else
|
|
1079
|
+
try {
|
|
1080
|
+
const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
|
|
1081
|
+
if (prov?.status === "provisioned") {
|
|
1082
|
+
const caps = prov.capabilitiesAdded?.length ? ` [capabilities added: ${prov.capabilitiesAdded.join(", ")}]` : "";
|
|
1083
|
+
console.log(`Auto-provisioned iOS signing: ${prov.profileName} (${prov.bundleId})${caps}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
catch (provErr) {
|
|
1087
|
+
const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
|
|
1088
|
+
// When reprovisioning or capabilities were explicitly requested, a provisioning
|
|
1089
|
+
// failure means the build is doomed to fail signing, stop here instead.
|
|
1090
|
+
if (provisionExplicit)
|
|
1091
|
+
throw new Error(`iOS provisioning failed: ${msg}`);
|
|
1092
|
+
console.log(`⚠ Auto-provisioning skipped: ${msg}`);
|
|
1093
|
+
}
|
|
822
1094
|
}
|
|
823
1095
|
const { data } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/builds`, {
|
|
824
1096
|
platform: normalizedPlatform,
|
|
@@ -836,7 +1108,7 @@ commander_1.program
|
|
|
836
1108
|
console.log(`Build #${submitted.id} is valid in TestFlight.`);
|
|
837
1109
|
}
|
|
838
1110
|
else {
|
|
839
|
-
const submitted = await queueGooglePlayIfNeeded(client, finished,
|
|
1111
|
+
const submitted = await queueGooglePlayIfNeeded(client, finished, playSubmitOptions);
|
|
840
1112
|
await waitForGooglePlay(client, submitted.id, pollIntervalMs, timeoutMs);
|
|
841
1113
|
console.log(`Build #${submitted.id} was submitted to Google Play track "${submitted.playTrack ?? opts.track}".`);
|
|
842
1114
|
}
|
|
@@ -859,14 +1131,20 @@ builds
|
|
|
859
1131
|
.option("--submit-testflight", "For iOS: wait for the build, submit to TestFlight, and wait for App Store Connect", false)
|
|
860
1132
|
.option("--submit-google-play", "For Android: wait for the build, submit to Google Play, and wait for commit", false)
|
|
861
1133
|
.option("--track <track>", "Google Play track for --submit-google-play", "internal")
|
|
1134
|
+
.option("--release-status <status>", "Google Play release status: draft (default on production) or completed for a live release")
|
|
1135
|
+
.option("--rollout <percent>", "Staged rollout percentage for a live release, greater than 0 and less than 100")
|
|
1136
|
+
.option("--release-notes <text>", "Release notes to attach to the Google Play release")
|
|
862
1137
|
.option("--poll-interval <seconds>", "Polling interval while waiting", "15")
|
|
863
1138
|
.option("--timeout <minutes>", "Maximum wait time for the build and store submit", "180")
|
|
1139
|
+
.option("--yes", "Queue without confirming toolchain warnings", false)
|
|
1140
|
+
.option("--json", JSON_OPTION_HELP)
|
|
864
1141
|
.action(async (opts) => {
|
|
865
1142
|
(0, config_1.requireAuth)();
|
|
866
1143
|
try {
|
|
867
1144
|
const client = (0, api_1.makeClient)();
|
|
868
1145
|
const pollIntervalMs = parsePositiveNumber(opts.pollInterval, 15, "--poll-interval") * 1000;
|
|
869
1146
|
const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
|
|
1147
|
+
const playSubmitOptions = readPlaySubmitOptions(opts);
|
|
870
1148
|
const shouldWait = Boolean(opts.wait || opts.submitTestflight || opts.submitGooglePlay);
|
|
871
1149
|
if (opts.submitTestflight && opts.platform !== "ios") {
|
|
872
1150
|
throw new Error("--submit-testflight can only be used with --platform ios");
|
|
@@ -877,12 +1155,21 @@ builds
|
|
|
877
1155
|
if (opts.submitGooglePlay && opts.profile === "preview") {
|
|
878
1156
|
throw new Error("--submit-google-play requires a production AAB. Use --wait without --submit-google-play for preview Android builds.");
|
|
879
1157
|
}
|
|
1158
|
+
if (!(await confirmBuildPreflight(client, { app: opts.app, platform: opts.platform, projectDir: process.cwd(), yes: opts.yes === true }))) {
|
|
1159
|
+
console.log("Aborted. No build was queued.");
|
|
1160
|
+
process.exitCode = 1;
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
880
1163
|
const { data } = await client.post(`/ubriot/apps/${opts.app}/builds`, {
|
|
881
1164
|
platform: opts.platform,
|
|
882
1165
|
profile: opts.profile,
|
|
883
1166
|
repoUrl: opts.repo,
|
|
884
1167
|
branch: opts.branch,
|
|
885
1168
|
});
|
|
1169
|
+
if (!shouldWait && wantsJson()) {
|
|
1170
|
+
printJson(data);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
886
1173
|
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
|
|
887
1174
|
if (!shouldWait) {
|
|
888
1175
|
if (data.platform === "ios") {
|
|
@@ -895,7 +1182,7 @@ builds
|
|
|
895
1182
|
}
|
|
896
1183
|
const finished = await waitForBuild(client, data.id, pollIntervalMs, timeoutMs);
|
|
897
1184
|
if (opts.submitGooglePlay) {
|
|
898
|
-
const submitted = await queueGooglePlayIfNeeded(client, finished,
|
|
1185
|
+
const submitted = await queueGooglePlayIfNeeded(client, finished, playSubmitOptions);
|
|
899
1186
|
await waitForGooglePlay(client, submitted.id, pollIntervalMs, timeoutMs);
|
|
900
1187
|
console.log(`Build #${submitted.id} was submitted to Google Play track "${submitted.playTrack ?? opts.track}".`);
|
|
901
1188
|
return;
|
|
@@ -914,11 +1201,16 @@ builds
|
|
|
914
1201
|
.command("list")
|
|
915
1202
|
.description("List recent builds")
|
|
916
1203
|
.option("--app <appId>", "Filter by app ID")
|
|
1204
|
+
.option("--json", JSON_OPTION_HELP)
|
|
917
1205
|
.action(async (opts) => {
|
|
918
1206
|
(0, config_1.requireAuth)();
|
|
919
1207
|
try {
|
|
920
1208
|
const url = opts.app ? `/ubriot/apps/${opts.app}/builds` : "/ubriot/builds";
|
|
921
1209
|
const { data } = await (0, api_1.makeClient)().get(url);
|
|
1210
|
+
if (wantsJson()) {
|
|
1211
|
+
printJson(data);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
922
1214
|
if (!data.length) {
|
|
923
1215
|
console.log("No builds found.");
|
|
924
1216
|
return;
|
|
@@ -932,6 +1224,67 @@ builds
|
|
|
932
1224
|
(0, api_1.handleError)(err);
|
|
933
1225
|
}
|
|
934
1226
|
});
|
|
1227
|
+
builds
|
|
1228
|
+
.command("queue")
|
|
1229
|
+
.description("Explain why your queued builds have not started: queue position, worker availability, rough start time")
|
|
1230
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1231
|
+
.action(async () => {
|
|
1232
|
+
(0, config_1.requireAuth)();
|
|
1233
|
+
try {
|
|
1234
|
+
const { data } = await (0, api_1.makeClient)().get("/ubriot/builds/queue");
|
|
1235
|
+
if (wantsJson()) {
|
|
1236
|
+
printJson(data);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const rows = data?.builds ?? [];
|
|
1240
|
+
if (!rows.length) {
|
|
1241
|
+
console.log("No queued builds.");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
for (const q of rows) {
|
|
1245
|
+
console.log(` #${q.buildId} ${q.platform} position ${q.position}, waiting ${q.waitingMinutes} min`);
|
|
1246
|
+
console.log(` ${q.headline}`);
|
|
1247
|
+
console.log(` ${q.detail}`);
|
|
1248
|
+
for (const c of q.commands ?? [])
|
|
1249
|
+
console.log(` $ ${c}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
catch (err) {
|
|
1253
|
+
(0, api_1.handleError)(err);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
builds
|
|
1257
|
+
.command("last-good")
|
|
1258
|
+
.description("Show the last successful build per app, platform and profile, and how many builds have failed since")
|
|
1259
|
+
.option("--app <appId>", "Only this app id")
|
|
1260
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1261
|
+
.action(async (opts) => {
|
|
1262
|
+
(0, config_1.requireAuth)();
|
|
1263
|
+
try {
|
|
1264
|
+
const params = opts.app ? { appId: opts.app } : undefined;
|
|
1265
|
+
const { data } = await (0, api_1.makeClient)().get("/ubriot/builds/last-good", { params });
|
|
1266
|
+
if (wantsJson()) {
|
|
1267
|
+
printJson(data);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const lanes = data?.lanes ?? [];
|
|
1271
|
+
if (!lanes.length) {
|
|
1272
|
+
console.log("No builds yet.");
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
for (const lane of lanes) {
|
|
1276
|
+
console.log(` app ${lane.releaseAppId} ${lane.platform}/${lane.profile} ${lane.state.replace(/_/g, " ")}, latest #${lane.latestBuildId} ${lane.latestStatus}`);
|
|
1277
|
+
console.log(` ${lane.headline}`);
|
|
1278
|
+
if (lane.detail)
|
|
1279
|
+
console.log(` ${lane.detail}`);
|
|
1280
|
+
for (const c of lane.commands ?? [])
|
|
1281
|
+
console.log(` $ ${c}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
catch (err) {
|
|
1285
|
+
(0, api_1.handleError)(err);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
935
1288
|
builds
|
|
936
1289
|
.command("show <buildId>")
|
|
937
1290
|
.description("Show build details")
|
|
@@ -1079,10 +1432,13 @@ builds
|
|
|
1079
1432
|
.command("submit-google-play <buildId>")
|
|
1080
1433
|
.description("Queue upload of the Android .aab to Google Play; requires a Google service account on the app")
|
|
1081
1434
|
.option("--track <track>", "Google Play track", "internal")
|
|
1435
|
+
.option("--release-status <status>", "Google Play release status: draft (default on production) or completed for a live release")
|
|
1436
|
+
.option("--rollout <percent>", "Staged rollout percentage for a live release, greater than 0 and less than 100")
|
|
1437
|
+
.option("--release-notes <text>", "Release notes to attach to the Google Play release")
|
|
1082
1438
|
.action(async (buildId, opts) => {
|
|
1083
1439
|
(0, config_1.requireAuth)();
|
|
1084
1440
|
try {
|
|
1085
|
-
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`,
|
|
1441
|
+
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`, playSubmitBody(readPlaySubmitOptions(opts)));
|
|
1086
1442
|
console.log(`Build #${buildId} queued for Google Play submit, playSubmissionStatus: ${data.playSubmissionStatus}`);
|
|
1087
1443
|
}
|
|
1088
1444
|
catch (err) {
|
|
@@ -1105,10 +1461,15 @@ builds
|
|
|
1105
1461
|
builds
|
|
1106
1462
|
.command("artifacts <buildId>")
|
|
1107
1463
|
.description("List artifacts for a build")
|
|
1464
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1108
1465
|
.action(async (buildId) => {
|
|
1109
1466
|
(0, config_1.requireAuth)();
|
|
1110
1467
|
try {
|
|
1111
1468
|
const { data } = await (0, api_1.makeClient)().get(`/ubriot/builds/${buildId}/artifacts`);
|
|
1469
|
+
if (wantsJson()) {
|
|
1470
|
+
printJson(data);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1112
1473
|
if (!data.length) {
|
|
1113
1474
|
console.log("No artifacts yet.");
|
|
1114
1475
|
return;
|
|
@@ -1237,10 +1598,15 @@ creds
|
|
|
1237
1598
|
.command("list")
|
|
1238
1599
|
.description("List credentials for an app")
|
|
1239
1600
|
.requiredOption("--app <appId>")
|
|
1601
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1240
1602
|
.action(async (opts) => {
|
|
1241
1603
|
(0, config_1.requireAuth)();
|
|
1242
1604
|
try {
|
|
1243
1605
|
const { data } = await (0, api_1.makeClient)().get(`/ubriot/apps/${opts.app}/credentials`);
|
|
1606
|
+
if (wantsJson()) {
|
|
1607
|
+
printJson(data);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1244
1610
|
if (!data.length) {
|
|
1245
1611
|
console.log("No credentials.");
|
|
1246
1612
|
return;
|
|
@@ -1383,11 +1749,18 @@ creds
|
|
|
1383
1749
|
(0, api_1.handleError)(err);
|
|
1384
1750
|
}
|
|
1385
1751
|
});
|
|
1752
|
+
function uploadAppleIdCommand(app) {
|
|
1753
|
+
return `ubriot credentials upload-apple-id --app ${app} --name <name> --apple-id <email> --app-specific-password <password>`;
|
|
1754
|
+
}
|
|
1386
1755
|
creds
|
|
1387
1756
|
.command("setup-testflight")
|
|
1388
1757
|
.description("Interactive guided setup for TestFlight submission (like EAS, walks you through it)")
|
|
1389
1758
|
.requiredOption("--app <appId>", "App ID (run 'ubriot apps list' if unsure)")
|
|
1390
1759
|
.action(async (opts) => {
|
|
1760
|
+
if (!process.stdin.isTTY) {
|
|
1761
|
+
console.error(`ubriot credentials setup-testflight needs an interactive terminal. Use instead: ${uploadAppleIdCommand(opts.app)}`);
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
}
|
|
1391
1764
|
(0, config_1.requireAuth)();
|
|
1392
1765
|
try {
|
|
1393
1766
|
// 1. Confirm the app exists
|
|
@@ -1401,7 +1774,7 @@ creds
|
|
|
1401
1774
|
// 2. Get Apple ID
|
|
1402
1775
|
const appleId = await (0, readline_1.readlineSync)("Enter your Apple developer email: ");
|
|
1403
1776
|
if (!appleId.includes("@")) {
|
|
1404
|
-
console.error(
|
|
1777
|
+
console.error(`That doesn't look like a valid email. Run the command again, or use: ${uploadAppleIdCommand(opts.app)}`);
|
|
1405
1778
|
process.exit(1);
|
|
1406
1779
|
}
|
|
1407
1780
|
// 3. Guide them to generate an app-specific password
|
|
@@ -1656,10 +2029,15 @@ push
|
|
|
1656
2029
|
.command("list-server-keys")
|
|
1657
2030
|
.description("List app-scoped Ubriot Push server keys")
|
|
1658
2031
|
.requiredOption("--app <appId>")
|
|
2032
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1659
2033
|
.action(async (opts) => {
|
|
1660
2034
|
(0, config_1.requireAuth)();
|
|
1661
2035
|
try {
|
|
1662
2036
|
const { data } = await (0, api_1.makeClient)().get(`/ubriot/apps/${opts.app}/push/server-keys`);
|
|
2037
|
+
if (wantsJson()) {
|
|
2038
|
+
printJson(data);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1663
2041
|
if (!data.length) {
|
|
1664
2042
|
console.log("No push server keys.");
|
|
1665
2043
|
return;
|
|
@@ -1691,10 +2069,15 @@ const channels = commander_1.program.command("channels").description("Manage OTA
|
|
|
1691
2069
|
channels
|
|
1692
2070
|
.command("list")
|
|
1693
2071
|
.requiredOption("--app <appId>")
|
|
2072
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1694
2073
|
.action(async (opts) => {
|
|
1695
2074
|
(0, config_1.requireAuth)();
|
|
1696
2075
|
try {
|
|
1697
2076
|
const { data } = await (0, api_1.makeClient)().get(`/ubriot/apps/${opts.app}/channels`);
|
|
2077
|
+
if (wantsJson()) {
|
|
2078
|
+
printJson(data);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
1698
2081
|
if (!data.length) {
|
|
1699
2082
|
console.log("No channels.");
|
|
1700
2083
|
return;
|
|
@@ -1740,27 +2123,83 @@ channels
|
|
|
1740
2123
|
});
|
|
1741
2124
|
commander_1.program
|
|
1742
2125
|
.command("update")
|
|
1743
|
-
.description("Export and publish an OTA update from the current Expo app")
|
|
2126
|
+
.description("Export and publish an OTA update from the current Expo or bare React Native app")
|
|
1744
2127
|
.option("--app <appIdOrSlug>", "App ID or slug; defaults to expo.slug from app.json")
|
|
1745
2128
|
.option("--channel <channel>", "OTA channel", "production")
|
|
1746
2129
|
.option("--runtime-version <version>", "Runtime version; defaults to runtimeVersion from app.json")
|
|
1747
2130
|
.option("--message <msg>", "Release note; defaults to the current git short SHA")
|
|
1748
2131
|
.option("--platform <platform>", "ios, android, or all", "all")
|
|
1749
|
-
.option("--export-dir <dir>", "
|
|
1750
|
-
.option("--expo-config <path>", "Path to app.json or exported Expo config JSON
|
|
2132
|
+
.option("--export-dir <dir>", "Export output directory", "dist")
|
|
2133
|
+
.option("--expo-config <path>", "Path to app.json or exported Expo config JSON; defaults to the resolved app.config.js or app.json")
|
|
1751
2134
|
.option("--public-url <url>", "Public API base URL used in generated asset URLs")
|
|
1752
|
-
.option("--
|
|
2135
|
+
.option("--bare", "Bundle a bare (non-Expo) React Native app with the RN bundler instead of expo export")
|
|
2136
|
+
.option("--entry-file <file>", "Bare RN entry file (defaults to package.json main or index.js)")
|
|
2137
|
+
.option("--no-export", "Publish an existing export directory without exporting again")
|
|
2138
|
+
.option("--yes", "Publish without confirming the runtime reach warning or the local Node.js version warning", false)
|
|
2139
|
+
.option("--private-key-path <path>", "OTA signing private key PEM (or set UBRIOT_OTA_PRIVATE_KEY)")
|
|
2140
|
+
.option("--certificate-path <path>", "OTA signing certificate PEM matching the private key")
|
|
2141
|
+
.option("--sign-with <keyid>", "Sign with this local key; repeat during a rotation", (value, previous) => [...previous, value], [])
|
|
1753
2142
|
.action(async (opts) => {
|
|
1754
2143
|
(0, config_1.requireAuth)();
|
|
1755
2144
|
try {
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
2145
|
+
const projectDir = process.cwd();
|
|
2146
|
+
const bare = opts.bare === true || (opts.bare === undefined && isBareRnProject(projectDir));
|
|
2147
|
+
const staticConfig = readExpoConfig(opts.expoConfig);
|
|
2148
|
+
const app = resolveAppSlug(opts.app, staticConfig);
|
|
2149
|
+
const client = (0, api_1.makeClient)();
|
|
2150
|
+
const releaseApp = bare ? null : (await client.get(`/ubriot/apps/${encodeURIComponent(app)}`)).data;
|
|
2151
|
+
const identityEnv = releaseApp ? (0, build_identity_1.buildIdentityEnv)(releaseApp) : {};
|
|
2152
|
+
const dynamicConfig = !bare && !opts.expoConfig && (0, build_identity_1.hasDynamicExpoConfig)(projectDir);
|
|
2153
|
+
const expoConfig = dynamicConfig ? (0, build_identity_1.resolveExpoConfig)(projectDir, identityEnv) : staticConfig;
|
|
1758
2154
|
const runtimeVersion = resolveRuntimeVersion(opts.runtimeVersion, expoConfig);
|
|
2155
|
+
if (releaseApp)
|
|
2156
|
+
(0, build_identity_1.assertUpdateMatchesApp)(expoConfig, releaseApp, runtimeVersion);
|
|
1759
2157
|
const message = opts.message ?? optionalGitShortSha();
|
|
1760
|
-
|
|
2158
|
+
console.log(`Build identity: ${(0, build_identity_1.describeBuildIdentity)(identityEnv)}. ${dynamicConfig ? "Resolved app config" : "App config"}: ${expoConfig.name ?? "unnamed"}, runtime ${runtimeVersion}.`);
|
|
1761
2159
|
const publicApiBase = (opts.publicUrl || (0, config_1.readConfig)().apiBase).replace(/\/$/, "");
|
|
2160
|
+
const signing = await (0, publish_signing_1.prepareSigning)({
|
|
2161
|
+
client,
|
|
2162
|
+
app,
|
|
2163
|
+
projectDir,
|
|
2164
|
+
env: process.env,
|
|
2165
|
+
privateKeyPath: opts.privateKeyPath,
|
|
2166
|
+
certificatePath: opts.certificatePath,
|
|
2167
|
+
signWith: opts.signWith,
|
|
2168
|
+
runtimeVersion,
|
|
2169
|
+
});
|
|
2170
|
+
for (const warning of signing.warnings)
|
|
2171
|
+
console.log(`Warning: ${warning}`);
|
|
2172
|
+
const identity = await (0, publish_signing_1.fetchAppIdentity)(client, app);
|
|
2173
|
+
const proceed = await confirmRuntimePreflight(client, {
|
|
2174
|
+
app,
|
|
2175
|
+
runtimeVersion,
|
|
2176
|
+
channel: opts.channel,
|
|
2177
|
+
platform: opts.platform,
|
|
2178
|
+
yes: opts.yes === true,
|
|
2179
|
+
});
|
|
2180
|
+
if (!proceed) {
|
|
2181
|
+
console.log("Aborted. Nothing was published.");
|
|
2182
|
+
process.exitCode = 1;
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (opts.export !== false && !(await confirmLocalNode(projectDir, opts.yes === true))) {
|
|
2186
|
+
console.log("Aborted. Nothing was published.");
|
|
2187
|
+
process.exitCode = 1;
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
1762
2190
|
if (opts.export !== false) {
|
|
1763
|
-
|
|
2191
|
+
if (bare) {
|
|
2192
|
+
const { entryFile } = exportBareRnBundle({
|
|
2193
|
+
projectDir,
|
|
2194
|
+
platform: opts.platform,
|
|
2195
|
+
outDir: (0, path_1.resolve)(opts.exportDir),
|
|
2196
|
+
entryFile: opts.entryFile,
|
|
2197
|
+
});
|
|
2198
|
+
console.log(`Bundled bare React Native app (entry: ${entryFile}) into ${opts.exportDir}`);
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
runLocalCommand("npx", ["expo", "export", "--platform", opts.platform, "--output-dir", opts.exportDir], identityEnv);
|
|
2202
|
+
}
|
|
1764
2203
|
}
|
|
1765
2204
|
const { data, platforms } = await publishExportedUpdate({
|
|
1766
2205
|
client,
|
|
@@ -1769,11 +2208,13 @@ commander_1.program
|
|
|
1769
2208
|
runtimeVersion,
|
|
1770
2209
|
message,
|
|
1771
2210
|
exportDir: opts.exportDir,
|
|
1772
|
-
|
|
2211
|
+
expoConfig,
|
|
1773
2212
|
platform: opts.platform,
|
|
1774
2213
|
publicApiBase,
|
|
2214
|
+
ubriotExtra: (0, publish_signing_1.ubriotManifestExtra)(identity, app, opts.channel),
|
|
2215
|
+
signingMaterials: signing.materials,
|
|
1775
2216
|
});
|
|
1776
|
-
console.log(`Published update group #${data.updateGroupId} on channel "${data.channel}" (${platforms.map((p) => p.platform).join(", ")})`);
|
|
2217
|
+
console.log(`Published update group #${data.updateGroupId} on channel "${data.channel}" (${platforms.map((p) => p.platform).join(", ")})${signing.materials.length ? `, signed with ${signing.materials.map((m) => m.keyid).join(", ")}` : ""}`);
|
|
1777
2218
|
}
|
|
1778
2219
|
catch (err) {
|
|
1779
2220
|
(0, api_1.handleError)(err);
|
|
@@ -1784,10 +2225,15 @@ const updates = commander_1.program.command("updates").description("Manage OTA u
|
|
|
1784
2225
|
updates
|
|
1785
2226
|
.command("list")
|
|
1786
2227
|
.requiredOption("--app <appId>")
|
|
2228
|
+
.option("--json", JSON_OPTION_HELP)
|
|
1787
2229
|
.action(async (opts) => {
|
|
1788
2230
|
(0, config_1.requireAuth)();
|
|
1789
2231
|
try {
|
|
1790
2232
|
const { data } = await (0, api_1.makeClient)().get(`/ubriot/apps/${opts.app}/updates`);
|
|
2233
|
+
if (wantsJson()) {
|
|
2234
|
+
printJson(data);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
1791
2237
|
if (!data.length) {
|
|
1792
2238
|
console.log("No updates.");
|
|
1793
2239
|
return;
|
|
@@ -1812,22 +2258,62 @@ updates
|
|
|
1812
2258
|
.option("--public-url <url>", "Public API base URL used in generated asset URLs")
|
|
1813
2259
|
.option("--manifest-android <path>", "Path to android manifest JSON")
|
|
1814
2260
|
.option("--manifest-ios <path>", "Path to ios manifest JSON")
|
|
2261
|
+
.option("--yes", "Publish without confirming runtime reach warnings", false)
|
|
2262
|
+
.option("--private-key-path <path>", "OTA signing private key PEM (or set UBRIOT_OTA_PRIVATE_KEY)")
|
|
2263
|
+
.option("--certificate-path <path>", "OTA signing certificate PEM matching the private key")
|
|
2264
|
+
.option("--sign-with <keyid>", "Sign with this local key; repeat during a rotation", (value, previous) => [...previous, value], [])
|
|
1815
2265
|
.action(async (opts) => {
|
|
1816
2266
|
(0, config_1.requireAuth)();
|
|
1817
2267
|
try {
|
|
1818
2268
|
const platforms = [];
|
|
1819
2269
|
const client = (0, api_1.makeClient)();
|
|
2270
|
+
const releaseApp = (await client.get(`/ubriot/apps/${encodeURIComponent(opts.app)}`)).data;
|
|
1820
2271
|
const publicApiBase = (opts.publicUrl || (0, config_1.readConfig)().apiBase).replace(/\/$/, "");
|
|
2272
|
+
const signing = await (0, publish_signing_1.prepareSigning)({
|
|
2273
|
+
client,
|
|
2274
|
+
app: opts.app,
|
|
2275
|
+
projectDir: process.cwd(),
|
|
2276
|
+
env: process.env,
|
|
2277
|
+
privateKeyPath: opts.privateKeyPath,
|
|
2278
|
+
certificatePath: opts.certificatePath,
|
|
2279
|
+
signWith: opts.signWith,
|
|
2280
|
+
runtimeVersion: opts.runtimeVersion,
|
|
2281
|
+
});
|
|
2282
|
+
for (const warning of signing.warnings)
|
|
2283
|
+
console.log(`Warning: ${warning}`);
|
|
2284
|
+
const preflightPlatform = opts.exportDir
|
|
2285
|
+
? opts.platform
|
|
2286
|
+
: opts.manifestAndroid && !opts.manifestIos
|
|
2287
|
+
? "android"
|
|
2288
|
+
: opts.manifestIos && !opts.manifestAndroid
|
|
2289
|
+
? "ios"
|
|
2290
|
+
: "all";
|
|
2291
|
+
const proceed = await confirmRuntimePreflight(client, {
|
|
2292
|
+
app: opts.app,
|
|
2293
|
+
runtimeVersion: opts.runtimeVersion,
|
|
2294
|
+
channel: opts.channel,
|
|
2295
|
+
platform: preflightPlatform,
|
|
2296
|
+
yes: opts.yes === true,
|
|
2297
|
+
});
|
|
2298
|
+
if (!proceed) {
|
|
2299
|
+
console.log("Aborted. Nothing was published.");
|
|
2300
|
+
process.exitCode = 1;
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
1821
2303
|
if (opts.exportDir) {
|
|
1822
2304
|
const exportDir = (0, path_1.resolve)(opts.exportDir);
|
|
1823
2305
|
const metadataPath = (0, path_1.join)(exportDir, "metadata.json");
|
|
1824
2306
|
if (!(0, fs_1.existsSync)(metadataPath))
|
|
1825
2307
|
throw new Error(`No metadata.json found in ${exportDir}. Run: npx expo export --output-dir ${opts.exportDir}`);
|
|
1826
2308
|
const metadataJson = readJsonFile(metadataPath);
|
|
1827
|
-
|
|
2309
|
+
if (!(0, fs_1.existsSync)((0, path_1.resolve)(opts.expoConfig)))
|
|
2310
|
+
throw new Error(`Expo config not found: ${(0, path_1.resolve)(opts.expoConfig)}`);
|
|
2311
|
+
const configJson = readJsonFile((0, path_1.resolve)(opts.expoConfig));
|
|
1828
2312
|
const expoConfig = configJson.expo ?? configJson;
|
|
2313
|
+
(0, build_identity_1.assertUpdateMatchesApp)(expoConfig, releaseApp, opts.runtimeVersion);
|
|
1829
2314
|
const publishId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1830
2315
|
const selectedPlatforms = opts.platform === "all" ? ["ios", "android"] : [opts.platform];
|
|
2316
|
+
const ubriotExtra = (0, publish_signing_1.ubriotManifestExtra)(await (0, publish_signing_1.fetchAppIdentity)(client, opts.app), opts.app, opts.channel);
|
|
1831
2317
|
for (const platform of selectedPlatforms) {
|
|
1832
2318
|
if (platform !== "ios" && platform !== "android")
|
|
1833
2319
|
throw new Error("--platform must be ios, android, or all");
|
|
@@ -1842,6 +2328,7 @@ updates
|
|
|
1842
2328
|
expoConfig,
|
|
1843
2329
|
metadataJson,
|
|
1844
2330
|
publicApiBase,
|
|
2331
|
+
ubriotExtra,
|
|
1845
2332
|
});
|
|
1846
2333
|
if (payload)
|
|
1847
2334
|
platforms.push(payload);
|
|
@@ -1850,10 +2337,16 @@ updates
|
|
|
1850
2337
|
else {
|
|
1851
2338
|
if (opts.manifestAndroid) {
|
|
1852
2339
|
const manifestJson = (0, fs_1.readFileSync)(opts.manifestAndroid, "utf8");
|
|
2340
|
+
const manifest = JSON.parse(manifestJson);
|
|
2341
|
+
if (manifest?.extra?.expoClient)
|
|
2342
|
+
(0, build_identity_1.assertUpdateMatchesApp)(manifest.extra.expoClient, releaseApp, opts.runtimeVersion);
|
|
1853
2343
|
platforms.push({ platform: "android", manifestJson, assets: [] });
|
|
1854
2344
|
}
|
|
1855
2345
|
if (opts.manifestIos) {
|
|
1856
2346
|
const manifestJson = (0, fs_1.readFileSync)(opts.manifestIos, "utf8");
|
|
2347
|
+
const manifest = JSON.parse(manifestJson);
|
|
2348
|
+
if (manifest?.extra?.expoClient)
|
|
2349
|
+
(0, build_identity_1.assertUpdateMatchesApp)(manifest.extra.expoClient, releaseApp, opts.runtimeVersion);
|
|
1857
2350
|
platforms.push({ platform: "ios", manifestJson, assets: [] });
|
|
1858
2351
|
}
|
|
1859
2352
|
}
|
|
@@ -1865,14 +2358,15 @@ updates
|
|
|
1865
2358
|
channel: opts.channel,
|
|
1866
2359
|
runtimeVersion: opts.runtimeVersion,
|
|
1867
2360
|
message: opts.message,
|
|
1868
|
-
platforms,
|
|
2361
|
+
platforms: (0, publish_signing_1.signPlatforms)(platforms, signing.materials),
|
|
1869
2362
|
}, { timeout: 120000 });
|
|
1870
|
-
console.log(`Published update group #${data.updateGroupId} on channel "${data.channel}" (${platforms.map((p) => p.platform).join(", ")})`);
|
|
2363
|
+
console.log(`Published update group #${data.updateGroupId} on channel "${data.channel}" (${platforms.map((p) => p.platform).join(", ")})${signing.materials.length ? `, signed with ${signing.materials.map((m) => m.keyid).join(", ")}` : ""}`);
|
|
1871
2364
|
}
|
|
1872
2365
|
catch (err) {
|
|
1873
2366
|
(0, api_1.handleError)(err);
|
|
1874
2367
|
}
|
|
1875
2368
|
});
|
|
2369
|
+
(0, signing_commands_1.registerSigningCommands)(commander_1.program, updates);
|
|
1876
2370
|
// ─── Migrate ─────────────────────────────────────────────────────────────────
|
|
1877
2371
|
const migrate = commander_1.program.command("migrate").description("Migrate onto Ubriot from other services");
|
|
1878
2372
|
migrate
|
|
@@ -1947,4 +2441,21 @@ migrate
|
|
|
1947
2441
|
(0, api_1.handleError)(err);
|
|
1948
2442
|
}
|
|
1949
2443
|
});
|
|
2444
|
+
commander_1.program
|
|
2445
|
+
.command("ask <question>")
|
|
2446
|
+
.description("Ask the Release Copilot about your builds, failures, release health and readiness")
|
|
2447
|
+
.action(async (question) => {
|
|
2448
|
+
(0, config_1.requireAuth)();
|
|
2449
|
+
try {
|
|
2450
|
+
const { data } = await (0, api_1.makeClient)().post("/ubriot/copilot/chat", { message: question, history: [] });
|
|
2451
|
+
process.stdout.write(`\n${data.reply}\n`);
|
|
2452
|
+
if (Array.isArray(data.toolsUsed) && data.toolsUsed.length > 0) {
|
|
2453
|
+
const seen = Array.from(new Set(data.toolsUsed));
|
|
2454
|
+
process.stdout.write(`\nLooked at: ${seen.join(", ")}\n`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
catch (err) {
|
|
2458
|
+
(0, api_1.handleError)(err);
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
1950
2461
|
commander_1.program.parse(process.argv);
|