ubriot 0.2.0 → 0.2.2
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 +2 -1
- package/dist/api.js +39 -2
- package/dist/config.js +1 -0
- package/dist/index.js +243 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ubriot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Mobile release CLI. Ship iOS and Android native builds, submit to TestFlight and Google Play, and push OTA updates, all from one command.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -29,6 +29,7 @@ ubriot update # publish an OTA update
|
|
|
29
29
|
| `ubriot push setup --app <name>` | Set up native push notifications |
|
|
30
30
|
| `ubriot apps list` | List your release apps |
|
|
31
31
|
| `ubriot builds list --app <name>` | List recent builds |
|
|
32
|
+
| `ubriot builds watch <buildId>` | Watch a build live: streaming logs + status until it finishes |
|
|
32
33
|
|
|
33
34
|
## Documentation
|
|
34
35
|
|
package/dist/api.js
CHANGED
|
@@ -7,13 +7,50 @@ exports.makeClient = makeClient;
|
|
|
7
7
|
exports.handleError = handleError;
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const config_1 = require("./config");
|
|
10
|
+
async function refreshAccessToken() {
|
|
11
|
+
const cfg = (0, config_1.readConfig)();
|
|
12
|
+
if (!cfg.refreshToken)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const { data } = await axios_1.default.post(`${cfg.apiBase}/auth/refresh`, {
|
|
16
|
+
refreshToken: cfg.refreshToken,
|
|
17
|
+
});
|
|
18
|
+
const token = data?.user?.token;
|
|
19
|
+
if (!token)
|
|
20
|
+
return null;
|
|
21
|
+
(0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? cfg.refreshToken });
|
|
22
|
+
return token;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
10
28
|
function makeClient() {
|
|
11
29
|
const cfg = (0, config_1.readConfig)();
|
|
12
|
-
|
|
30
|
+
const client = axios_1.default.create({
|
|
13
31
|
baseURL: cfg.apiBase,
|
|
14
32
|
headers: cfg.token ? { Authorization: `Bearer ${cfg.token}` } : {},
|
|
15
|
-
timeout:
|
|
33
|
+
timeout: 120000,
|
|
34
|
+
});
|
|
35
|
+
// Expired access token: refresh once and retry the original request.
|
|
36
|
+
client.interceptors.response.use((res) => res, async (error) => {
|
|
37
|
+
const original = error.config;
|
|
38
|
+
const status = error.response?.status;
|
|
39
|
+
const isAuthCall = typeof original?.url === "string" && original.url.includes("/auth/");
|
|
40
|
+
if (status !== 401 || !original || original._retry || isAuthCall) {
|
|
41
|
+
return Promise.reject(error);
|
|
42
|
+
}
|
|
43
|
+
original._retry = true;
|
|
44
|
+
const newToken = await refreshAccessToken();
|
|
45
|
+
if (!newToken) {
|
|
46
|
+
console.error("Session expired. Run: ubriot login");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
original.headers = original.headers ?? {};
|
|
50
|
+
original.headers.Authorization = `Bearer ${newToken}`;
|
|
51
|
+
return client(original);
|
|
16
52
|
});
|
|
53
|
+
return client;
|
|
17
54
|
}
|
|
18
55
|
function handleError(err) {
|
|
19
56
|
if (axios_1.default.isAxiosError(err)) {
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
37
|
const commander_1 = require("commander");
|
|
5
38
|
const readline_1 = require("./readline");
|
|
@@ -9,7 +42,7 @@ const child_process_1 = require("child_process");
|
|
|
9
42
|
const fs_1 = require("fs");
|
|
10
43
|
const path_1 = require("path");
|
|
11
44
|
const crypto_1 = require("crypto");
|
|
12
|
-
commander_1.program.name("ubriot").description("Ubriot
|
|
45
|
+
commander_1.program.name("ubriot").description("Ubriot, self-hosted Expo release infrastructure").version("0.1.0");
|
|
13
46
|
const MIME = {
|
|
14
47
|
".avif": "image/avif",
|
|
15
48
|
".bundle": "application/javascript",
|
|
@@ -56,6 +89,37 @@ function readExpoConfigFromPath(configPath) {
|
|
|
56
89
|
const configJson = readJsonFile(configPath);
|
|
57
90
|
return configJson.expo ?? configJson;
|
|
58
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Derive the Apple capabilities an app needs straight from its Expo config, so
|
|
94
|
+
* users don't have to remember flags like `--ios-capability PUSH_NOTIFICATIONS`.
|
|
95
|
+
* Reads plugins + ios.entitlements + ios.infoPlist for clear, unambiguous signals.
|
|
96
|
+
*/
|
|
97
|
+
function detectIosCapabilities(config) {
|
|
98
|
+
const caps = new Set();
|
|
99
|
+
const plugins = Array.isArray(config?.plugins) ? config.plugins : [];
|
|
100
|
+
const pluginNames = plugins.map((p) => (Array.isArray(p) ? p[0] : p)).filter((p) => typeof p === "string");
|
|
101
|
+
const ent = config?.ios?.entitlements ?? {};
|
|
102
|
+
// Push notifications: expo-notifications plugin or an aps-environment entitlement.
|
|
103
|
+
if (pluginNames.some((p) => p.includes("expo-notifications")) || ent["aps-environment"]) {
|
|
104
|
+
caps.add("PUSH_NOTIFICATIONS");
|
|
105
|
+
}
|
|
106
|
+
// Sign in with Apple.
|
|
107
|
+
if (ent["com.apple.developer.applesignin"])
|
|
108
|
+
caps.add("APPLE_ID_AUTH");
|
|
109
|
+
// Associated Domains (universal links).
|
|
110
|
+
if (config?.ios?.associatedDomains?.length || ent["com.apple.developer.associated-domains"]) {
|
|
111
|
+
caps.add("ASSOCIATED_DOMAINS");
|
|
112
|
+
}
|
|
113
|
+
// HealthKit.
|
|
114
|
+
if (pluginNames.some((p) => p.includes("health")) || ent["com.apple.developer.healthkit"]) {
|
|
115
|
+
caps.add("HEALTHKIT");
|
|
116
|
+
}
|
|
117
|
+
// iCloud.
|
|
118
|
+
if (ent["com.apple.developer.icloud-services"] || ent["com.apple.developer.icloud-container-identifiers"]) {
|
|
119
|
+
caps.add("ICLOUD");
|
|
120
|
+
}
|
|
121
|
+
return [...caps];
|
|
122
|
+
}
|
|
59
123
|
function hasExpoIdentity(config) {
|
|
60
124
|
return Boolean(config?.slug ||
|
|
61
125
|
config?.name ||
|
|
@@ -531,7 +595,7 @@ commander_1.program
|
|
|
531
595
|
const token = data?.user?.token;
|
|
532
596
|
if (!token)
|
|
533
597
|
throw new Error("No token in response");
|
|
534
|
-
(0, config_1.writeConfig)({ token, email });
|
|
598
|
+
(0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? null, email });
|
|
535
599
|
console.log(`Logged in as ${email}`);
|
|
536
600
|
}
|
|
537
601
|
catch (err) {
|
|
@@ -541,8 +605,18 @@ commander_1.program
|
|
|
541
605
|
commander_1.program
|
|
542
606
|
.command("logout")
|
|
543
607
|
.description("Clear saved credentials")
|
|
544
|
-
.action(() => {
|
|
545
|
-
(0, config_1.
|
|
608
|
+
.action(async () => {
|
|
609
|
+
const cfg = (0, config_1.readConfig)();
|
|
610
|
+
if (cfg.refreshToken) {
|
|
611
|
+
// Best-effort server-side revoke; ignore failures.
|
|
612
|
+
try {
|
|
613
|
+
await (0, api_1.makeClient)().post("/auth/logout", { refreshToken: cfg.refreshToken });
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// ignore
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
(0, config_1.writeConfig)({ token: null, refreshToken: null, email: null });
|
|
546
620
|
console.log("Logged out.");
|
|
547
621
|
});
|
|
548
622
|
commander_1.program
|
|
@@ -567,6 +641,7 @@ apps
|
|
|
567
641
|
.option("--android-id <id>", "Android application ID")
|
|
568
642
|
.option("--ios-bundle <id>", "iOS bundle identifier")
|
|
569
643
|
.option("--google-play-track <track>", "Default Google Play track", "internal")
|
|
644
|
+
.option("--expo-project-root <path>", "Path to the Expo app inside the repo (monorepos), e.g. products/myapp/mobile")
|
|
570
645
|
.action(async (opts) => {
|
|
571
646
|
(0, config_1.requireAuth)();
|
|
572
647
|
try {
|
|
@@ -578,6 +653,7 @@ apps
|
|
|
578
653
|
androidApplicationId: opts.androidId,
|
|
579
654
|
iosBundleIdentifier: opts.iosBundle,
|
|
580
655
|
googlePlayTrack: opts.googlePlayTrack,
|
|
656
|
+
expoProjectRoot: opts.expoProjectRoot,
|
|
581
657
|
});
|
|
582
658
|
console.log(`Created app: ${data.name} (id: ${data.id}, slug: ${data.slug})`);
|
|
583
659
|
}
|
|
@@ -611,6 +687,7 @@ apps
|
|
|
611
687
|
.description("Update release app settings (TestFlight mapping, auto-submit)")
|
|
612
688
|
.requiredOption("--app <appId>", "Release app id")
|
|
613
689
|
.option("--ios-bundle <id>", "iOS bundle identifier")
|
|
690
|
+
.option("--expo-project-root <path>", "Path to the Expo app inside the repo (monorepos)")
|
|
614
691
|
.option("--android-id <id>", "Android application ID / package name")
|
|
615
692
|
.option("--asc-app-id <id>", "App Store Connect app id (from ASC app URL)")
|
|
616
693
|
.option("--clear-asc-app-id", "Remove stored ASC app id (use bundle id lookup)", false)
|
|
@@ -625,6 +702,8 @@ apps
|
|
|
625
702
|
const body = {};
|
|
626
703
|
if (opts.iosBundle !== undefined)
|
|
627
704
|
body.iosBundleIdentifier = opts.iosBundle || null;
|
|
705
|
+
if (opts.expoProjectRoot !== undefined)
|
|
706
|
+
body.expoProjectRoot = opts.expoProjectRoot;
|
|
628
707
|
if (opts.androidId !== undefined)
|
|
629
708
|
body.androidApplicationId = opts.androidId || null;
|
|
630
709
|
if (opts.clearAscAppId)
|
|
@@ -642,7 +721,7 @@ apps
|
|
|
642
721
|
if (opts.noAutoSubmitGooglePlay)
|
|
643
722
|
body.autoSubmitGooglePlay = false;
|
|
644
723
|
if (!Object.keys(body).length) {
|
|
645
|
-
console.log("Nothing to update
|
|
724
|
+
console.log("Nothing to update, pass --ios-bundle, --android-id, --asc-app-id, --google-play-track, and/or auto-submit flags");
|
|
646
725
|
return;
|
|
647
726
|
}
|
|
648
727
|
const { data } = await (0, api_1.makeClient)().patch(`/ubriot/apps/${opts.app}`, body);
|
|
@@ -660,8 +739,11 @@ commander_1.program
|
|
|
660
739
|
.option("--profile <profile>", "Build profile", "production")
|
|
661
740
|
.option("--repo <url>", "Override repo URL")
|
|
662
741
|
.option("--branch <branch>", "Branch to build")
|
|
742
|
+
.option("--local [path]", "Build from a local directory instead of cloning from remote (path defaults to CWD)")
|
|
663
743
|
.option("--track <track>", "Google Play track for Android", "internal")
|
|
664
744
|
.option("--no-submit", "Build only; do not submit to TestFlight / Google Play")
|
|
745
|
+
.option("--reprovision-ios", "Force-regenerate the iOS provisioning profile before building", false)
|
|
746
|
+
.option("--ios-capability <type...>", "Apple capability to ensure on the bundle ID (e.g. HEALTHKIT); regenerates the profile if newly added")
|
|
665
747
|
.option("--poll-interval <seconds>", "Polling interval while waiting", "15")
|
|
666
748
|
.option("--timeout <minutes>", "Maximum wait time for the build and store submit", "180")
|
|
667
749
|
.action(async (platform, opts) => {
|
|
@@ -679,13 +761,71 @@ commander_1.program
|
|
|
679
761
|
const client = (0, api_1.makeClient)();
|
|
680
762
|
const pollIntervalMs = parsePositiveNumber(opts.pollInterval, 15, "--poll-interval") * 1000;
|
|
681
763
|
const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
|
|
764
|
+
// --local builds from a file:// path so the worker clones locally without needing a remote push
|
|
765
|
+
let repoUrl = opts.repo;
|
|
766
|
+
let branch = opts.branch;
|
|
767
|
+
if (opts.local !== undefined) {
|
|
768
|
+
const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
769
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
770
|
+
const localPath = opts.local === true ? process.cwd() : resolve(opts.local);
|
|
771
|
+
repoUrl = `file://${localPath}`;
|
|
772
|
+
if (!branch) {
|
|
773
|
+
try {
|
|
774
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: localPath, stdio: "pipe" }).toString().trim();
|
|
775
|
+
}
|
|
776
|
+
catch { }
|
|
777
|
+
}
|
|
778
|
+
console.log(`Building from local path: ${localPath} (branch: ${branch ?? "default"})`);
|
|
779
|
+
}
|
|
780
|
+
// EAS-style auto-provisioning: ensure a provisioning profile exists for iOS.
|
|
781
|
+
if (normalizedPlatform === "ios") {
|
|
782
|
+
// Auto-detect capabilities from the local Expo config so users don't have
|
|
783
|
+
// to pass --ios-capability by hand. Explicit flags still win / extend.
|
|
784
|
+
let capabilities = opts.iosCapability ?? [];
|
|
785
|
+
try {
|
|
786
|
+
const cfgPath = opts.expoConfig
|
|
787
|
+
? (0, path_1.resolve)(opts.expoConfig)
|
|
788
|
+
: opts.expoProjectRoot
|
|
789
|
+
? (0, path_1.join)((0, path_1.resolve)(opts.expoProjectRoot), "app.json")
|
|
790
|
+
: "app.json";
|
|
791
|
+
const localCfg = (0, fs_1.existsSync)((0, path_1.resolve)(cfgPath)) ? readExpoConfigFromPath((0, path_1.resolve)(cfgPath)) : readExpoConfig();
|
|
792
|
+
const detected = detectIosCapabilities(localCfg);
|
|
793
|
+
const added = detected.filter((c) => !capabilities.includes(c));
|
|
794
|
+
if (added.length) {
|
|
795
|
+
capabilities = [...capabilities, ...added];
|
|
796
|
+
console.log(`Auto-detected iOS capabilities from app config: ${added.join(", ")}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch { /* detection is best-effort; fall back to explicit flags */ }
|
|
800
|
+
const provisionExplicit = Boolean(opts.reprovisionIos || capabilities.length);
|
|
801
|
+
const provisionBody = {};
|
|
802
|
+
if (opts.reprovisionIos)
|
|
803
|
+
provisionBody.force = true;
|
|
804
|
+
if (capabilities.length)
|
|
805
|
+
provisionBody.capabilities = capabilities;
|
|
806
|
+
try {
|
|
807
|
+
const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
|
|
808
|
+
if (prov?.status === "provisioned") {
|
|
809
|
+
const caps = prov.capabilitiesAdded?.length ? ` [capabilities added: ${prov.capabilitiesAdded.join(", ")}]` : "";
|
|
810
|
+
console.log(`Auto-provisioned iOS signing: ${prov.profileName} (${prov.bundleId})${caps}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (provErr) {
|
|
814
|
+
const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
|
|
815
|
+
// When reprovisioning or capabilities were explicitly requested, a provisioning
|
|
816
|
+
// failure means the build is doomed to fail signing, stop here instead.
|
|
817
|
+
if (provisionExplicit)
|
|
818
|
+
throw new Error(`iOS provisioning failed: ${msg}`);
|
|
819
|
+
console.log(`⚠ Auto-provisioning skipped: ${msg}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
682
822
|
const { data } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/builds`, {
|
|
683
823
|
platform: normalizedPlatform,
|
|
684
824
|
profile: opts.profile,
|
|
685
|
-
repoUrl
|
|
686
|
-
branch
|
|
825
|
+
repoUrl,
|
|
826
|
+
branch,
|
|
687
827
|
});
|
|
688
|
-
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile})
|
|
828
|
+
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
|
|
689
829
|
const finished = await waitForBuild(client, data.id, pollIntervalMs, timeoutMs);
|
|
690
830
|
if (opts.submit === false)
|
|
691
831
|
return;
|
|
@@ -742,7 +882,7 @@ builds
|
|
|
742
882
|
repoUrl: opts.repo,
|
|
743
883
|
branch: opts.branch,
|
|
744
884
|
});
|
|
745
|
-
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile})
|
|
885
|
+
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
|
|
746
886
|
if (!shouldWait) {
|
|
747
887
|
if (data.platform === "ios") {
|
|
748
888
|
console.log(`To submit after it succeeds: ubriot builds submit-testflight ${data.id}`);
|
|
@@ -818,6 +958,70 @@ builds
|
|
|
818
958
|
(0, api_1.handleError)(err);
|
|
819
959
|
}
|
|
820
960
|
});
|
|
961
|
+
builds
|
|
962
|
+
.command("watch <buildId>")
|
|
963
|
+
.description("Watch a build live: streams logs and status changes until the build and any store submission finish")
|
|
964
|
+
.option("--poll-interval <seconds>", "Polling interval while watching", "5")
|
|
965
|
+
.option("--timeout <minutes>", "Maximum watch time", "180")
|
|
966
|
+
.option("--no-logs", "Only print status changes; skip streaming build logs")
|
|
967
|
+
.action(async (buildId, opts) => {
|
|
968
|
+
(0, config_1.requireAuth)();
|
|
969
|
+
try {
|
|
970
|
+
const client = (0, api_1.makeClient)();
|
|
971
|
+
const intervalMs = parsePositiveNumber(opts.pollInterval, 5, "--poll-interval") * 1000;
|
|
972
|
+
const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
|
|
973
|
+
const deadline = Date.now() + timeoutMs;
|
|
974
|
+
let lastStatusLine = "";
|
|
975
|
+
let lastLogId = 0;
|
|
976
|
+
while (Date.now() <= deadline) {
|
|
977
|
+
const { data } = await client.get(`/ubriot/builds/${buildId}`);
|
|
978
|
+
if (opts.logs !== false) {
|
|
979
|
+
// The logs endpoint has no incremental cursor, so fetch all and print only ids we haven't seen.
|
|
980
|
+
try {
|
|
981
|
+
const { data: logs } = await client.get(`/ubriot/builds/${buildId}/logs`);
|
|
982
|
+
for (const l of logs) {
|
|
983
|
+
if (l.id <= lastLogId)
|
|
984
|
+
continue;
|
|
985
|
+
lastLogId = l.id;
|
|
986
|
+
const message = l.message.endsWith("\n") ? l.message : `${l.message}\n`;
|
|
987
|
+
process.stdout.write(`[${l.phase ?? "-"}] ${message}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
// Logs are best-effort; a transient fetch error shouldn't kill the watch.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const statusLine = buildStatusLine(data);
|
|
995
|
+
if (statusLine !== lastStatusLine) {
|
|
996
|
+
console.log(`── ${statusLine}`);
|
|
997
|
+
lastStatusLine = statusLine;
|
|
998
|
+
}
|
|
999
|
+
const buildDone = BUILD_TERMINAL_STATUSES.has(data.status);
|
|
1000
|
+
const ascPending = data.ascSubmissionStatus != null && !ASC_TERMINAL_STATUSES.has(data.ascSubmissionStatus);
|
|
1001
|
+
const playPending = data.playSubmissionStatus != null && !PLAY_TERMINAL_STATUSES.has(data.playSubmissionStatus);
|
|
1002
|
+
if (buildDone && !ascPending && !playPending) {
|
|
1003
|
+
if (data.status !== "succeeded") {
|
|
1004
|
+
throw new Error(`Build #${buildId} ${data.status}${data.errorMessage ? `: ${data.errorMessage}` : ""}`);
|
|
1005
|
+
}
|
|
1006
|
+
if (data.ascSubmissionStatus === "failed") {
|
|
1007
|
+
throw new Error(`TestFlight submit for build #${buildId} failed${data.ascSubmissionError ? `: ${data.ascSubmissionError}` : ""}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (data.playSubmissionStatus === "failed") {
|
|
1010
|
+
throw new Error(`Google Play submit for build #${buildId} failed${data.playSubmissionError ? `: ${data.playSubmissionError}` : ""}`);
|
|
1011
|
+
}
|
|
1012
|
+
const asc = data.ascSubmissionStatus === "succeeded" ? ", TestFlight submission succeeded" : "";
|
|
1013
|
+
const play = data.playSubmissionStatus === "succeeded" ? `, Google Play submission succeeded${data.playTrack ? ` (${data.playTrack})` : ""}` : "";
|
|
1014
|
+
console.log(`Build #${buildId} succeeded${asc}${play}.`);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
await sleep(intervalMs);
|
|
1018
|
+
}
|
|
1019
|
+
throw new Error(`Timed out watching build #${buildId} after ${opts.timeout} minutes`);
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
(0, api_1.handleError)(err);
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
821
1025
|
builds
|
|
822
1026
|
.command("cancel <buildId>")
|
|
823
1027
|
.description("Cancel a queued/running build")
|
|
@@ -851,7 +1055,7 @@ builds
|
|
|
851
1055
|
(0, config_1.requireAuth)();
|
|
852
1056
|
try {
|
|
853
1057
|
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-testflight`);
|
|
854
|
-
console.log(`Build #${buildId} queued for TestFlight submit
|
|
1058
|
+
console.log(`Build #${buildId} queued for TestFlight submit, ascSubmissionStatus: ${data.ascSubmissionStatus}`);
|
|
855
1059
|
}
|
|
856
1060
|
catch (err) {
|
|
857
1061
|
(0, api_1.handleError)(err);
|
|
@@ -878,7 +1082,7 @@ builds
|
|
|
878
1082
|
(0, config_1.requireAuth)();
|
|
879
1083
|
try {
|
|
880
1084
|
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`, { track: opts.track });
|
|
881
|
-
console.log(`Build #${buildId} queued for Google Play submit
|
|
1085
|
+
console.log(`Build #${buildId} queued for Google Play submit, playSubmissionStatus: ${data.playSubmissionStatus}`);
|
|
882
1086
|
}
|
|
883
1087
|
catch (err) {
|
|
884
1088
|
(0, api_1.handleError)(err);
|
|
@@ -964,7 +1168,7 @@ creds
|
|
|
964
1168
|
payload: { serviceAccountJsonBase64 },
|
|
965
1169
|
});
|
|
966
1170
|
console.log(`Google Play service account credential created: #${data.id} "${data.name}"`);
|
|
967
|
-
console.log(`Ready to ship
|
|
1171
|
+
console.log(`Ready to ship, run: ubriot ship android`);
|
|
968
1172
|
}
|
|
969
1173
|
catch (err) {
|
|
970
1174
|
(0, api_1.handleError)(err);
|
|
@@ -1047,6 +1251,27 @@ creds
|
|
|
1047
1251
|
(0, api_1.handleError)(err);
|
|
1048
1252
|
}
|
|
1049
1253
|
});
|
|
1254
|
+
creds
|
|
1255
|
+
.command("copy")
|
|
1256
|
+
.description("Copy a credential to another app in the same team (reuses team-wide keys like APNs/ASC without re-uploading the file)")
|
|
1257
|
+
.requiredOption("--from <credId>", "Source credential id (see `credentials list`)")
|
|
1258
|
+
.requiredOption("--to-app <appIdOrSlug>", "Destination app id or slug")
|
|
1259
|
+
.option("--name <name>", "Name for the copied credential")
|
|
1260
|
+
.option("--bundle-id <id>", "Override bundle id (APNs topic); defaults to the destination app's bundle id")
|
|
1261
|
+
.action(async (opts) => {
|
|
1262
|
+
(0, config_1.requireAuth)();
|
|
1263
|
+
try {
|
|
1264
|
+
const { data } = await (0, api_1.makeClient)().post(`/ubriot/credentials/${opts.from}/copy`, {
|
|
1265
|
+
toApp: opts.toApp,
|
|
1266
|
+
name: opts.name,
|
|
1267
|
+
bundleId: opts.bundleId,
|
|
1268
|
+
});
|
|
1269
|
+
console.log(`Copied credential → #${data.id} "${data.name}" (${data.kind}) on app ${data.toAppId}`);
|
|
1270
|
+
}
|
|
1271
|
+
catch (err) {
|
|
1272
|
+
(0, api_1.handleError)(err);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1050
1275
|
creds
|
|
1051
1276
|
.command("upload-ios-cert")
|
|
1052
1277
|
.description("Upload an iOS distribution certificate (.p12)")
|
|
@@ -1151,7 +1376,7 @@ creds
|
|
|
1151
1376
|
},
|
|
1152
1377
|
});
|
|
1153
1378
|
console.log(`Apple ID credential created: #${data.id} "${data.name}"`);
|
|
1154
|
-
console.log(`Ready to ship
|
|
1379
|
+
console.log(`Ready to ship, run: ubriot ship ios`);
|
|
1155
1380
|
}
|
|
1156
1381
|
catch (err) {
|
|
1157
1382
|
(0, api_1.handleError)(err);
|
|
@@ -1159,7 +1384,7 @@ creds
|
|
|
1159
1384
|
});
|
|
1160
1385
|
creds
|
|
1161
1386
|
.command("setup-testflight")
|
|
1162
|
-
.description("Interactive guided setup for TestFlight submission (like EAS
|
|
1387
|
+
.description("Interactive guided setup for TestFlight submission (like EAS, walks you through it)")
|
|
1163
1388
|
.requiredOption("--app <appId>", "App ID (run 'ubriot apps list' if unsure)")
|
|
1164
1389
|
.action(async (opts) => {
|
|
1165
1390
|
(0, config_1.requireAuth)();
|
|
@@ -1169,8 +1394,8 @@ creds
|
|
|
1169
1394
|
console.log(`\n🚀 Setting up TestFlight submission for: ${app.name}`);
|
|
1170
1395
|
console.log(` Bundle ID: ${app.iosBundleIdentifier || "(not set)"}\n`);
|
|
1171
1396
|
console.log("We need your Apple credentials to submit to TestFlight.");
|
|
1172
|
-
console.log("You only do this ONCE
|
|
1173
|
-
console.log("Apple requires a special one-time password for scripts
|
|
1397
|
+
console.log("You only do this ONCE, the same credentials work for all your apps.");
|
|
1398
|
+
console.log("Apple requires a special one-time password for scripts, your regular");
|
|
1174
1399
|
console.log("Apple ID password won't work here (Apple blocks it for security).\n");
|
|
1175
1400
|
// 2. Get Apple ID
|
|
1176
1401
|
const appleId = await (0, readline_1.readlineSync)("Enter your Apple developer email: ");
|
|
@@ -1191,7 +1416,7 @@ Next, generate an app-specific password:
|
|
|
1191
1416
|
const appSpecificPassword = await (0, readline_1.readlineSync)("Paste the app-specific password here: ", true);
|
|
1192
1417
|
if (!appSpecificPassword.match(/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/)) {
|
|
1193
1418
|
console.log("\n⚠ That doesn't look like the expected format (xxxx-xxxx-xxxx-xxxx).");
|
|
1194
|
-
console.log(" Saving anyway
|
|
1419
|
+
console.log(" Saving anyway, if the format was wrong the submit step will tell you.\n");
|
|
1195
1420
|
}
|
|
1196
1421
|
// 5. Save credential
|
|
1197
1422
|
const { data: cred } = await (0, api_1.makeClient)().post(`/ubriot/apps/${opts.app}/credentials`, {
|
|
@@ -1201,7 +1426,7 @@ Next, generate an app-specific password:
|
|
|
1201
1426
|
payload: { appleId, appSpecificPassword },
|
|
1202
1427
|
});
|
|
1203
1428
|
console.log(`\n✅ Apple credentials saved (#${cred.id}).`);
|
|
1204
|
-
console.log(` These will be used automatically for all your apps
|
|
1429
|
+
console.log(` These will be used automatically for all your apps, no setup needed for future apps.`);
|
|
1205
1430
|
console.log(`\nTo build and submit to TestFlight, run:`);
|
|
1206
1431
|
console.log(` ubriot ship ios\n`);
|
|
1207
1432
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ubriot",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "CI/CD CLI for Expo and React Native
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "CI/CD CLI for Expo and React Native, ship iOS, Android, and OTA updates from your terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://ubriot.dev",
|
|
7
7
|
"repository": {
|