ubriot 0.2.1 → 1.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 +1 -1
- package/dist/api.js +39 -2
- package/dist/config.js +1 -0
- package/dist/index.js +102 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ubriot
|
|
2
2
|
|
|
3
|
-
Mobile release CLI. Ship iOS and Android native builds, submit to TestFlight and Google Play, and push OTA updates
|
|
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
|
|
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
|
@@ -42,7 +42,7 @@ const child_process_1 = require("child_process");
|
|
|
42
42
|
const fs_1 = require("fs");
|
|
43
43
|
const path_1 = require("path");
|
|
44
44
|
const crypto_1 = require("crypto");
|
|
45
|
-
commander_1.program.name("ubriot").description("Ubriot
|
|
45
|
+
commander_1.program.name("ubriot").description("Ubriot, self-hosted Expo release infrastructure").version("0.1.0");
|
|
46
46
|
const MIME = {
|
|
47
47
|
".avif": "image/avif",
|
|
48
48
|
".bundle": "application/javascript",
|
|
@@ -89,6 +89,37 @@ function readExpoConfigFromPath(configPath) {
|
|
|
89
89
|
const configJson = readJsonFile(configPath);
|
|
90
90
|
return configJson.expo ?? configJson;
|
|
91
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
|
+
}
|
|
92
123
|
function hasExpoIdentity(config) {
|
|
93
124
|
return Boolean(config?.slug ||
|
|
94
125
|
config?.name ||
|
|
@@ -564,7 +595,7 @@ commander_1.program
|
|
|
564
595
|
const token = data?.user?.token;
|
|
565
596
|
if (!token)
|
|
566
597
|
throw new Error("No token in response");
|
|
567
|
-
(0, config_1.writeConfig)({ token, email });
|
|
598
|
+
(0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? null, email });
|
|
568
599
|
console.log(`Logged in as ${email}`);
|
|
569
600
|
}
|
|
570
601
|
catch (err) {
|
|
@@ -574,8 +605,18 @@ commander_1.program
|
|
|
574
605
|
commander_1.program
|
|
575
606
|
.command("logout")
|
|
576
607
|
.description("Clear saved credentials")
|
|
577
|
-
.action(() => {
|
|
578
|
-
(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 });
|
|
579
620
|
console.log("Logged out.");
|
|
580
621
|
});
|
|
581
622
|
commander_1.program
|
|
@@ -680,7 +721,7 @@ apps
|
|
|
680
721
|
if (opts.noAutoSubmitGooglePlay)
|
|
681
722
|
body.autoSubmitGooglePlay = false;
|
|
682
723
|
if (!Object.keys(body).length) {
|
|
683
|
-
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");
|
|
684
725
|
return;
|
|
685
726
|
}
|
|
686
727
|
const { data } = await (0, api_1.makeClient)().patch(`/ubriot/apps/${opts.app}`, body);
|
|
@@ -738,12 +779,30 @@ commander_1.program
|
|
|
738
779
|
}
|
|
739
780
|
// EAS-style auto-provisioning: ensure a provisioning profile exists for iOS.
|
|
740
781
|
if (normalizedPlatform === "ios") {
|
|
741
|
-
|
|
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);
|
|
742
801
|
const provisionBody = {};
|
|
743
802
|
if (opts.reprovisionIos)
|
|
744
803
|
provisionBody.force = true;
|
|
745
|
-
if (
|
|
746
|
-
provisionBody.capabilities =
|
|
804
|
+
if (capabilities.length)
|
|
805
|
+
provisionBody.capabilities = capabilities;
|
|
747
806
|
try {
|
|
748
807
|
const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
|
|
749
808
|
if (prov?.status === "provisioned") {
|
|
@@ -754,7 +813,7 @@ commander_1.program
|
|
|
754
813
|
catch (provErr) {
|
|
755
814
|
const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
|
|
756
815
|
// When reprovisioning or capabilities were explicitly requested, a provisioning
|
|
757
|
-
// failure means the build is doomed to fail signing
|
|
816
|
+
// failure means the build is doomed to fail signing, stop here instead.
|
|
758
817
|
if (provisionExplicit)
|
|
759
818
|
throw new Error(`iOS provisioning failed: ${msg}`);
|
|
760
819
|
console.log(`⚠ Auto-provisioning skipped: ${msg}`);
|
|
@@ -766,7 +825,7 @@ commander_1.program
|
|
|
766
825
|
repoUrl,
|
|
767
826
|
branch,
|
|
768
827
|
});
|
|
769
|
-
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile})
|
|
828
|
+
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
|
|
770
829
|
const finished = await waitForBuild(client, data.id, pollIntervalMs, timeoutMs);
|
|
771
830
|
if (opts.submit === false)
|
|
772
831
|
return;
|
|
@@ -823,7 +882,7 @@ builds
|
|
|
823
882
|
repoUrl: opts.repo,
|
|
824
883
|
branch: opts.branch,
|
|
825
884
|
});
|
|
826
|
-
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile})
|
|
885
|
+
console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
|
|
827
886
|
if (!shouldWait) {
|
|
828
887
|
if (data.platform === "ios") {
|
|
829
888
|
console.log(`To submit after it succeeds: ubriot builds submit-testflight ${data.id}`);
|
|
@@ -950,8 +1009,8 @@ builds
|
|
|
950
1009
|
if (data.playSubmissionStatus === "failed") {
|
|
951
1010
|
throw new Error(`Google Play submit for build #${buildId} failed${data.playSubmissionError ? `: ${data.playSubmissionError}` : ""}`);
|
|
952
1011
|
}
|
|
953
|
-
const asc = data.ascSubmissionStatus === "succeeded" ? "
|
|
954
|
-
const play = data.playSubmissionStatus === "succeeded" ?
|
|
1012
|
+
const asc = data.ascSubmissionStatus === "succeeded" ? ", TestFlight submission succeeded" : "";
|
|
1013
|
+
const play = data.playSubmissionStatus === "succeeded" ? `, Google Play submission succeeded${data.playTrack ? ` (${data.playTrack})` : ""}` : "";
|
|
955
1014
|
console.log(`Build #${buildId} succeeded${asc}${play}.`);
|
|
956
1015
|
return;
|
|
957
1016
|
}
|
|
@@ -996,7 +1055,7 @@ builds
|
|
|
996
1055
|
(0, config_1.requireAuth)();
|
|
997
1056
|
try {
|
|
998
1057
|
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-testflight`);
|
|
999
|
-
console.log(`Build #${buildId} queued for TestFlight submit
|
|
1058
|
+
console.log(`Build #${buildId} queued for TestFlight submit, ascSubmissionStatus: ${data.ascSubmissionStatus}`);
|
|
1000
1059
|
}
|
|
1001
1060
|
catch (err) {
|
|
1002
1061
|
(0, api_1.handleError)(err);
|
|
@@ -1023,7 +1082,7 @@ builds
|
|
|
1023
1082
|
(0, config_1.requireAuth)();
|
|
1024
1083
|
try {
|
|
1025
1084
|
const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`, { track: opts.track });
|
|
1026
|
-
console.log(`Build #${buildId} queued for Google Play submit
|
|
1085
|
+
console.log(`Build #${buildId} queued for Google Play submit, playSubmissionStatus: ${data.playSubmissionStatus}`);
|
|
1027
1086
|
}
|
|
1028
1087
|
catch (err) {
|
|
1029
1088
|
(0, api_1.handleError)(err);
|
|
@@ -1109,7 +1168,7 @@ creds
|
|
|
1109
1168
|
payload: { serviceAccountJsonBase64 },
|
|
1110
1169
|
});
|
|
1111
1170
|
console.log(`Google Play service account credential created: #${data.id} "${data.name}"`);
|
|
1112
|
-
console.log(`Ready to ship
|
|
1171
|
+
console.log(`Ready to ship, run: ubriot ship android`);
|
|
1113
1172
|
}
|
|
1114
1173
|
catch (err) {
|
|
1115
1174
|
(0, api_1.handleError)(err);
|
|
@@ -1192,6 +1251,27 @@ creds
|
|
|
1192
1251
|
(0, api_1.handleError)(err);
|
|
1193
1252
|
}
|
|
1194
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
|
+
});
|
|
1195
1275
|
creds
|
|
1196
1276
|
.command("upload-ios-cert")
|
|
1197
1277
|
.description("Upload an iOS distribution certificate (.p12)")
|
|
@@ -1296,7 +1376,7 @@ creds
|
|
|
1296
1376
|
},
|
|
1297
1377
|
});
|
|
1298
1378
|
console.log(`Apple ID credential created: #${data.id} "${data.name}"`);
|
|
1299
|
-
console.log(`Ready to ship
|
|
1379
|
+
console.log(`Ready to ship, run: ubriot ship ios`);
|
|
1300
1380
|
}
|
|
1301
1381
|
catch (err) {
|
|
1302
1382
|
(0, api_1.handleError)(err);
|
|
@@ -1304,7 +1384,7 @@ creds
|
|
|
1304
1384
|
});
|
|
1305
1385
|
creds
|
|
1306
1386
|
.command("setup-testflight")
|
|
1307
|
-
.description("Interactive guided setup for TestFlight submission (like EAS
|
|
1387
|
+
.description("Interactive guided setup for TestFlight submission (like EAS, walks you through it)")
|
|
1308
1388
|
.requiredOption("--app <appId>", "App ID (run 'ubriot apps list' if unsure)")
|
|
1309
1389
|
.action(async (opts) => {
|
|
1310
1390
|
(0, config_1.requireAuth)();
|
|
@@ -1314,8 +1394,8 @@ creds
|
|
|
1314
1394
|
console.log(`\n🚀 Setting up TestFlight submission for: ${app.name}`);
|
|
1315
1395
|
console.log(` Bundle ID: ${app.iosBundleIdentifier || "(not set)"}\n`);
|
|
1316
1396
|
console.log("We need your Apple credentials to submit to TestFlight.");
|
|
1317
|
-
console.log("You only do this ONCE
|
|
1318
|
-
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");
|
|
1319
1399
|
console.log("Apple ID password won't work here (Apple blocks it for security).\n");
|
|
1320
1400
|
// 2. Get Apple ID
|
|
1321
1401
|
const appleId = await (0, readline_1.readlineSync)("Enter your Apple developer email: ");
|
|
@@ -1336,7 +1416,7 @@ Next, generate an app-specific password:
|
|
|
1336
1416
|
const appSpecificPassword = await (0, readline_1.readlineSync)("Paste the app-specific password here: ", true);
|
|
1337
1417
|
if (!appSpecificPassword.match(/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/)) {
|
|
1338
1418
|
console.log("\n⚠ That doesn't look like the expected format (xxxx-xxxx-xxxx-xxxx).");
|
|
1339
|
-
console.log(" Saving anyway
|
|
1419
|
+
console.log(" Saving anyway, if the format was wrong the submit step will tell you.\n");
|
|
1340
1420
|
}
|
|
1341
1421
|
// 5. Save credential
|
|
1342
1422
|
const { data: cred } = await (0, api_1.makeClient)().post(`/ubriot/apps/${opts.app}/credentials`, {
|
|
@@ -1346,7 +1426,7 @@ Next, generate an app-specific password:
|
|
|
1346
1426
|
payload: { appleId, appSpecificPassword },
|
|
1347
1427
|
});
|
|
1348
1428
|
console.log(`\n✅ Apple credentials saved (#${cred.id}).`);
|
|
1349
|
-
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.`);
|
|
1350
1430
|
console.log(`\nTo build and submit to TestFlight, run:`);
|
|
1351
1431
|
console.log(` ubriot ship ios\n`);
|
|
1352
1432
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ubriot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CI/CD CLI for Expo and React Native
|
|
3
|
+
"version": "1.0.0",
|
|
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": {
|