@revopush/code-push-cli 0.0.7 → 0.0.8-rc.1
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/bin/script/binary-utils.js +176 -0
- package/bin/script/command-executor.js +377 -44
- package/bin/script/command-parser.js +265 -1
- package/bin/script/expo-utils.js +13 -0
- package/bin/script/management-sdk.js +50 -3
- package/bin/script/react-native-utils.js +23 -2
- package/bin/script/types/cli.js +2 -0
- package/bin/script/utils/file-utils.js +33 -2
- package/bin/test/management-sdk.js +7 -0
- package/package.json +8 -3
- package/script/binary-utils.ts +209 -0
- package/script/command-executor.ts +458 -51
- package/script/command-parser.ts +295 -2
- package/script/expo-utils.ts +14 -0
- package/script/management-sdk.ts +65 -3
- package/script/react-native-utils.ts +34 -3
- package/script/types/cli.ts +13 -0
- package/script/types/rest-definitions.ts +12 -0
- package/script/types.ts +1 -0
- package/script/utils/file-utils.ts +36 -1
- package/test/management-sdk.ts +9 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
|
|
4
|
+
import { extractMetadataFromAndroid, extractMetadataFromIOS, getIosVersion } from "./binary-utils";
|
|
5
|
+
|
|
4
6
|
const childProcess = require("child_process");
|
|
5
7
|
import debugCommand from "./commands/debug";
|
|
6
8
|
import * as fs from "fs";
|
|
@@ -12,6 +14,7 @@ import * as Q from "q";
|
|
|
12
14
|
import * as semver from "semver";
|
|
13
15
|
import * as cli from "../script/types/cli";
|
|
14
16
|
import sign from "./sign";
|
|
17
|
+
const ApkReader = require("@devicefarmer/adbkit-apkreader");
|
|
15
18
|
import {
|
|
16
19
|
AccessKey,
|
|
17
20
|
Account,
|
|
@@ -34,12 +37,15 @@ import {
|
|
|
34
37
|
isHermesEnabled,
|
|
35
38
|
isValidVersion,
|
|
36
39
|
runHermesEmitBinaryCommand,
|
|
40
|
+
takeHermesBaseBytecode,
|
|
37
41
|
} from "./react-native-utils";
|
|
38
|
-
import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip } from "./utils/file-utils";
|
|
42
|
+
import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip, extractIPA, extractAPK } from "./utils/file-utils";
|
|
39
43
|
|
|
40
44
|
import AccountManager = require("./management-sdk");
|
|
41
45
|
import wordwrap = require("wordwrap");
|
|
42
46
|
import Promise = Q.Promise;
|
|
47
|
+
import { ReactNativePackageInfo } from "./types/rest-definitions";
|
|
48
|
+
import { getExpoCliPath } from "./expo-utils";
|
|
43
49
|
|
|
44
50
|
const g2js = require("gradle-to-js/lib/parser");
|
|
45
51
|
|
|
@@ -364,9 +370,11 @@ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage:
|
|
|
364
370
|
};
|
|
365
371
|
|
|
366
372
|
function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise<void> {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
373
|
+
const confirmation = command.isForce
|
|
374
|
+
? Q.resolve(true)
|
|
375
|
+
: confirm("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.");
|
|
376
|
+
|
|
377
|
+
return confirmation.then((wasConfirmed: boolean): Promise<void> => {
|
|
370
378
|
if (wasConfirmed) {
|
|
371
379
|
return sdk.removeDeployment(command.appName, command.deploymentName).then((): void => {
|
|
372
380
|
log('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
|
|
@@ -550,6 +558,12 @@ export function execute(command: cli.ICommand) {
|
|
|
550
558
|
case cli.CommandType.releaseReact:
|
|
551
559
|
return releaseReact(<cli.IReleaseReactCommand>command);
|
|
552
560
|
|
|
561
|
+
case cli.CommandType.releaseExpo:
|
|
562
|
+
return releaseExpo(<cli.IReleaseReactCommand>command);
|
|
563
|
+
|
|
564
|
+
case cli.CommandType.releaseNative:
|
|
565
|
+
return releaseNative(<cli.IReleaseNativeCommand>command);
|
|
566
|
+
|
|
553
567
|
case cli.CommandType.rollback:
|
|
554
568
|
return rollback(<cli.IRollbackCommand>command);
|
|
555
569
|
|
|
@@ -1206,59 +1220,237 @@ function patch(command: cli.IPatchCommand): Promise<void> {
|
|
|
1206
1220
|
}
|
|
1207
1221
|
|
|
1208
1222
|
export const release = (command: cli.IReleaseCommand): Promise<void> => {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1223
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
1224
|
+
const updateMetadata: PackageInfo = {
|
|
1225
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
1226
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
1227
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
1228
|
+
isInitial: command.initial,
|
|
1229
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
1230
|
+
appVersion: command.appStoreVersion,
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
return doRelease(command, updateMetadata);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
export const runExpoExportEmbedCommand = async (
|
|
1237
|
+
command: cli.IReleaseReactCommand,
|
|
1238
|
+
bundleName: string,
|
|
1239
|
+
development: boolean,
|
|
1240
|
+
// entryFile: string,
|
|
1241
|
+
outputFolder: string,
|
|
1242
|
+
sourcemapOutputFolder: string,
|
|
1243
|
+
platform: string,
|
|
1244
|
+
extraBundlerOptions: string[]
|
|
1245
|
+
) => {
|
|
1246
|
+
const expoBundleArgs: string[] = [];
|
|
1247
|
+
const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
|
|
1248
|
+
|
|
1249
|
+
if (typeof envNodeArgs !== "undefined") {
|
|
1250
|
+
Array.prototype.push.apply(expoBundleArgs, envNodeArgs.trim().split(/\s+/));
|
|
1213
1251
|
}
|
|
1214
1252
|
|
|
1215
|
-
|
|
1216
|
-
const filePath: string = command.package;
|
|
1217
|
-
let isSingleFilePackage: boolean = true;
|
|
1253
|
+
const expoCliPath = getExpoCliPath();
|
|
1218
1254
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1255
|
+
Array.prototype.push.apply(expoBundleArgs, [
|
|
1256
|
+
expoCliPath,
|
|
1257
|
+
"export:embed",
|
|
1258
|
+
"--assets-dest",
|
|
1259
|
+
outputFolder,
|
|
1260
|
+
"--bundle-output",
|
|
1261
|
+
path.join(outputFolder, bundleName),
|
|
1262
|
+
"--dev",
|
|
1263
|
+
development,
|
|
1264
|
+
"--platform",
|
|
1265
|
+
platform,
|
|
1266
|
+
"--minify",
|
|
1267
|
+
false,
|
|
1268
|
+
"--reset-cache",
|
|
1269
|
+
]);
|
|
1270
|
+
|
|
1271
|
+
if (sourcemapOutputFolder) {
|
|
1272
|
+
let bundleSourceMapOutput = sourcemapOutputFolder;
|
|
1273
|
+
if (!sourcemapOutputFolder.endsWith(".map")) {
|
|
1274
|
+
// user defined directory, нужно вычислить полный путь
|
|
1275
|
+
bundleSourceMapOutput = await getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
expoBundleArgs.push("--sourcemap-output", bundleSourceMapOutput);
|
|
1221
1279
|
}
|
|
1222
1280
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
complete: "=",
|
|
1226
|
-
incomplete: " ",
|
|
1227
|
-
width: 50,
|
|
1228
|
-
total: 100,
|
|
1229
|
-
});
|
|
1281
|
+
// const minifyValue = await getMinifyParams(command);
|
|
1282
|
+
// Array.prototype.push.apply(expoBundleArgs, minifyValue);
|
|
1230
1283
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
};
|
|
1284
|
+
if (extraBundlerOptions.length > 0) {
|
|
1285
|
+
expoBundleArgs.push(...extraBundlerOptions);
|
|
1286
|
+
}
|
|
1235
1287
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1288
|
+
log(chalk.cyan('Running "expo export:embed" command:\n'));
|
|
1289
|
+
|
|
1290
|
+
const projectRoot = process.cwd();
|
|
1291
|
+
expoBundleArgs.push(projectRoot);
|
|
1292
|
+
|
|
1293
|
+
log("expoBundleArgs raw:" + JSON.stringify(expoBundleArgs, null, 2));
|
|
1294
|
+
|
|
1295
|
+
const expoBundleProcess = spawn("node", expoBundleArgs);
|
|
1296
|
+
log(`node ${expoBundleArgs.join(" ")}`);
|
|
1297
|
+
|
|
1298
|
+
return Promise<void>((resolve, reject, notify) => {
|
|
1299
|
+
expoBundleProcess.stdout.on("data", (data: Buffer) => {
|
|
1300
|
+
log(data.toString().trim());
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
expoBundleProcess.stderr.on("data", (data: Buffer) => {
|
|
1304
|
+
console.error(data.toString().trim());
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
expoBundleProcess.on("close", (exitCode: number) => {
|
|
1308
|
+
if (exitCode) {
|
|
1309
|
+
reject(new Error(`"expo export:embed" command exited with code ${exitCode}.`));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
resolve(<void>null);
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
export const releaseExpo = (command: cli.IReleaseReactCommand): Promise<void> => {
|
|
1318
|
+
let bundleName: string = command.bundleName;
|
|
1319
|
+
// let entryFile: string = command.entryFile;
|
|
1320
|
+
const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
|
|
1321
|
+
const sourcemapOutputFolder: string = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
|
|
1322
|
+
const baseReleaseTmpFolder: string = path.join(os.tmpdir(), "CodePushBaseRelease");
|
|
1323
|
+
const platform: string = (command.platform = command.platform.toLowerCase());
|
|
1324
|
+
const releaseCommand: cli.IReleaseReactCommand = <any>command;
|
|
1242
1325
|
|
|
1243
1326
|
return sdk
|
|
1244
|
-
.
|
|
1245
|
-
.then((
|
|
1246
|
-
|
|
1327
|
+
.getDeployment(command.appName, command.deploymentName)
|
|
1328
|
+
.then(async () => {
|
|
1329
|
+
switch (platform) {
|
|
1330
|
+
case "android":
|
|
1331
|
+
case "ios":
|
|
1332
|
+
if (!bundleName) {
|
|
1333
|
+
bundleName = platform === "ios" ? "main.jsbundle" : `index.${platform}.bundle`;
|
|
1334
|
+
}
|
|
1335
|
+
break;
|
|
1336
|
+
|
|
1337
|
+
default:
|
|
1338
|
+
throw new Error('Platform must be either "android" or "ios" for the "release-expo" command.');
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
releaseCommand.package = outputFolder;
|
|
1342
|
+
releaseCommand.outputDir = outputFolder;
|
|
1343
|
+
releaseCommand.bundleName = bundleName;
|
|
1344
|
+
|
|
1345
|
+
let projectName: string;
|
|
1346
|
+
|
|
1347
|
+
try {
|
|
1348
|
+
const projectPackageJson: any = require(path.join(process.cwd(), "package.json"));
|
|
1349
|
+
projectName = projectPackageJson.name;
|
|
1350
|
+
if (!projectName) {
|
|
1351
|
+
throw new Error('The "package.json" file in the CWD does not have the "name" field set.');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (!projectPackageJson.dependencies["react-native"]) {
|
|
1355
|
+
throw new Error("The project in the CWD is not a React Native project.");
|
|
1356
|
+
}
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
'Unable to find or read "package.json" in the CWD. The "release-expo" command must be executed in a React Native project folder.'
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// TODO: do we really need entryFile for expo?
|
|
1364
|
+
|
|
1365
|
+
// if (!entryFile) {
|
|
1366
|
+
// entryFile = `index.${platform}.js`;
|
|
1367
|
+
// if (fileDoesNotExistOrIsDirectory(entryFile)) {
|
|
1368
|
+
// entryFile = "index.js";
|
|
1369
|
+
// }
|
|
1370
|
+
//
|
|
1371
|
+
// if (fileDoesNotExistOrIsDirectory(entryFile)) {
|
|
1372
|
+
// throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`);
|
|
1373
|
+
// }
|
|
1374
|
+
// } else {
|
|
1375
|
+
// if (fileDoesNotExistOrIsDirectory(entryFile)) {
|
|
1376
|
+
// throw new Error(`Entry file "${entryFile}" does not exist.`);
|
|
1377
|
+
// }
|
|
1378
|
+
// }
|
|
1379
|
+
|
|
1380
|
+
const appVersionPromise: Promise<string> = command.appStoreVersion
|
|
1381
|
+
? Q(command.appStoreVersion)
|
|
1382
|
+
: getReactNativeProjectAppVersion(command, projectName);
|
|
1383
|
+
|
|
1384
|
+
if (!sourcemapOutputFolder.endsWith(".map") && !command.sourcemapOutput) {
|
|
1385
|
+
await createEmptyTempReleaseFolder(sourcemapOutputFolder);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return appVersionPromise;
|
|
1247
1389
|
})
|
|
1248
|
-
.then((
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1390
|
+
.then((appVersion: string) => {
|
|
1391
|
+
throwForInvalidSemverRange(appVersion);
|
|
1392
|
+
releaseCommand.appStoreVersion = appVersion;
|
|
1393
|
+
|
|
1394
|
+
return createEmptyTempReleaseFolder(outputFolder);
|
|
1395
|
+
})
|
|
1396
|
+
.then(() => deleteFolder(`${os.tmpdir()}/react-*`))
|
|
1397
|
+
.then(async () => {
|
|
1398
|
+
await runExpoExportEmbedCommand(
|
|
1399
|
+
command,
|
|
1400
|
+
bundleName,
|
|
1401
|
+
command.development || false,
|
|
1402
|
+
// entryFile,
|
|
1403
|
+
outputFolder,
|
|
1404
|
+
sourcemapOutputFolder,
|
|
1405
|
+
platform,
|
|
1406
|
+
command.extraBundlerOptions
|
|
1259
1407
|
);
|
|
1260
1408
|
})
|
|
1261
|
-
.
|
|
1409
|
+
.then(async () => {
|
|
1410
|
+
const isHermes = await isHermesEnabled(command, platform);
|
|
1411
|
+
|
|
1412
|
+
if (isHermes) {
|
|
1413
|
+
await createEmptyTempReleaseFolder(baseReleaseTmpFolder);
|
|
1414
|
+
const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName);
|
|
1415
|
+
|
|
1416
|
+
log(chalk.cyan("\nRunning hermes compiler.\n"));
|
|
1417
|
+
await runHermesEmitBinaryCommand(
|
|
1418
|
+
command,
|
|
1419
|
+
bundleName,
|
|
1420
|
+
outputFolder,
|
|
1421
|
+
sourcemapOutputFolder,
|
|
1422
|
+
command.extraHermesFlags,
|
|
1423
|
+
command.gradleFile,
|
|
1424
|
+
baseBytecode
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
})
|
|
1428
|
+
.then(async () => {
|
|
1429
|
+
if (command.privateKeyPath) {
|
|
1430
|
+
log(chalk.cyan("\nSigning the bundle:\n"));
|
|
1431
|
+
await sign(command.privateKeyPath, outputFolder);
|
|
1432
|
+
} else {
|
|
1433
|
+
console.log("private key was not provided");
|
|
1434
|
+
}
|
|
1435
|
+
})
|
|
1436
|
+
.then(() => {
|
|
1437
|
+
log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
|
|
1438
|
+
return releaseReactNative(releaseCommand);
|
|
1439
|
+
})
|
|
1440
|
+
.then(async () => {
|
|
1441
|
+
if (!command.outputDir) {
|
|
1442
|
+
await deleteFolder(outputFolder);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (!command.sourcemapOutput) {
|
|
1446
|
+
await deleteFolder(sourcemapOutputFolder);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
await deleteFolder(baseReleaseTmpFolder);
|
|
1450
|
+
})
|
|
1451
|
+
.catch(async (err: Error) => {
|
|
1452
|
+
throw err;
|
|
1453
|
+
});
|
|
1262
1454
|
};
|
|
1263
1455
|
|
|
1264
1456
|
export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> => {
|
|
@@ -1266,16 +1458,15 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1266
1458
|
let entryFile: string = command.entryFile;
|
|
1267
1459
|
const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
|
|
1268
1460
|
const sourcemapOutputFolder: string = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
|
|
1461
|
+
const baseReleaseTmpFolder: string = path.join(os.tmpdir(), "CodePushBaseRelease");
|
|
1269
1462
|
const platform: string = (command.platform = command.platform.toLowerCase());
|
|
1270
|
-
const releaseCommand: cli.
|
|
1463
|
+
const releaseCommand: cli.IReleaseReactCommand = <any>command;
|
|
1271
1464
|
// Check for app and deployment exist before releasing an update.
|
|
1272
1465
|
// This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name.
|
|
1273
1466
|
return (
|
|
1274
1467
|
sdk
|
|
1275
1468
|
.getDeployment(command.appName, command.deploymentName)
|
|
1276
1469
|
.then(async () => {
|
|
1277
|
-
releaseCommand.package = outputFolder;
|
|
1278
|
-
|
|
1279
1470
|
switch (platform) {
|
|
1280
1471
|
case "android":
|
|
1281
1472
|
case "ios":
|
|
@@ -1289,6 +1480,10 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1289
1480
|
throw new Error('Platform must be either "android", "ios" or "windows".');
|
|
1290
1481
|
}
|
|
1291
1482
|
|
|
1483
|
+
releaseCommand.package = outputFolder;
|
|
1484
|
+
releaseCommand.outputDir = outputFolder;
|
|
1485
|
+
releaseCommand.bundleName = bundleName;
|
|
1486
|
+
|
|
1292
1487
|
let projectName: string;
|
|
1293
1488
|
|
|
1294
1489
|
try {
|
|
@@ -1307,6 +1502,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1307
1502
|
);
|
|
1308
1503
|
}
|
|
1309
1504
|
|
|
1505
|
+
// TODO: check entry file detection
|
|
1310
1506
|
if (!entryFile) {
|
|
1311
1507
|
entryFile = `index.${platform}.js`;
|
|
1312
1508
|
if (fileDoesNotExistOrIsDirectory(entryFile)) {
|
|
@@ -1358,6 +1554,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1358
1554
|
const isHermes = await isHermesEnabled(command, platform);
|
|
1359
1555
|
|
|
1360
1556
|
if (isHermes) {
|
|
1557
|
+
await createEmptyTempReleaseFolder(baseReleaseTmpFolder);
|
|
1558
|
+
const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName);
|
|
1559
|
+
|
|
1361
1560
|
log(chalk.cyan("\nRunning hermes compiler...\n"));
|
|
1362
1561
|
await runHermesEmitBinaryCommand(
|
|
1363
1562
|
command,
|
|
@@ -1365,7 +1564,8 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1365
1564
|
outputFolder,
|
|
1366
1565
|
sourcemapOutputFolder,
|
|
1367
1566
|
command.extraHermesFlags,
|
|
1368
|
-
command.gradleFile
|
|
1567
|
+
command.gradleFile,
|
|
1568
|
+
baseBytecode
|
|
1369
1569
|
);
|
|
1370
1570
|
}
|
|
1371
1571
|
})
|
|
@@ -1379,7 +1579,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1379
1579
|
})
|
|
1380
1580
|
.then(() => {
|
|
1381
1581
|
log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
|
|
1382
|
-
return
|
|
1582
|
+
return releaseReactNative(releaseCommand);
|
|
1383
1583
|
})
|
|
1384
1584
|
.then(async () => {
|
|
1385
1585
|
if (!command.outputDir) {
|
|
@@ -1389,6 +1589,8 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1389
1589
|
if (!command.sourcemapOutput) {
|
|
1390
1590
|
await deleteFolder(sourcemapOutputFolder);
|
|
1391
1591
|
}
|
|
1592
|
+
|
|
1593
|
+
await deleteFolder(baseReleaseTmpFolder);
|
|
1392
1594
|
})
|
|
1393
1595
|
.catch(async (err: Error) => {
|
|
1394
1596
|
throw err;
|
|
@@ -1396,6 +1598,211 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1396
1598
|
);
|
|
1397
1599
|
};
|
|
1398
1600
|
|
|
1601
|
+
export const releaseNative = (command: cli.IReleaseNativeCommand): Promise<void> => {
|
|
1602
|
+
const platform: string = command.platform.toLowerCase();
|
|
1603
|
+
let bundleName: string = command.bundleName;
|
|
1604
|
+
const targetBinaryPath: string = command.targetBinary;
|
|
1605
|
+
const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
|
|
1606
|
+
const extractFolder: string = path.join(os.tmpdir(), "CodePushBinaryExtract");
|
|
1607
|
+
// Validate platform
|
|
1608
|
+
if (platform !== "ios" && platform !== "android") {
|
|
1609
|
+
throw new Error('Platform must be either "ios" or "android" for the "release-native" command.');
|
|
1610
|
+
}
|
|
1611
|
+
// Validate target binary file exists
|
|
1612
|
+
if (!fileExists(targetBinaryPath)) {
|
|
1613
|
+
throw new Error(`Target binary file "${targetBinaryPath}" does not exist.`);
|
|
1614
|
+
}
|
|
1615
|
+
// Validate file extension matches platform
|
|
1616
|
+
if (platform === "ios" && !targetBinaryPath.toLowerCase().endsWith(".ipa")) {
|
|
1617
|
+
throw new Error("For iOS platform, target binary must be an .ipa file.");
|
|
1618
|
+
}
|
|
1619
|
+
if (platform === "android" && !targetBinaryPath.toLowerCase().endsWith(".apk")) {
|
|
1620
|
+
throw new Error("For Android platform, target binary must be an .apk file.");
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return sdk
|
|
1624
|
+
.getDeployment(command.appName, command.deploymentName)
|
|
1625
|
+
.then(async () => {
|
|
1626
|
+
try {
|
|
1627
|
+
await createEmptyTempReleaseFolder(outputFolder);
|
|
1628
|
+
await createEmptyTempReleaseFolder(extractFolder);
|
|
1629
|
+
|
|
1630
|
+
if (!bundleName) {
|
|
1631
|
+
bundleName = platform === "ios" ? "main.jsbundle" : `index.android.bundle`;
|
|
1632
|
+
}
|
|
1633
|
+
let releaseCommandPartial: Partial<cli.IReleaseReactCommand>;
|
|
1634
|
+
|
|
1635
|
+
if (platform === "ios") {
|
|
1636
|
+
log(chalk.cyan(`\nExtracting IPA file:\n`));
|
|
1637
|
+
await extractIPA(targetBinaryPath, extractFolder);
|
|
1638
|
+
const metadataZip = await extractMetadataFromIOS(extractFolder, outputFolder);
|
|
1639
|
+
const buildVersion = await getIosVersion(extractFolder)
|
|
1640
|
+
releaseCommandPartial = { package: metadataZip, appStoreVersion: buildVersion?.version };
|
|
1641
|
+
} else {
|
|
1642
|
+
log(chalk.cyan(`\nExtracting APK/ARR file:\n`));
|
|
1643
|
+
await extractAPK(targetBinaryPath, extractFolder);
|
|
1644
|
+
|
|
1645
|
+
const reader = await ApkReader.open(targetBinaryPath);
|
|
1646
|
+
const { versionName: appStoreVersion } = await reader.readManifest();
|
|
1647
|
+
const metadataZip = await extractMetadataFromAndroid(extractFolder, outputFolder);
|
|
1648
|
+
releaseCommandPartial = { package: metadataZip, appStoreVersion };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const { package: metadataZip, appStoreVersion } = releaseCommandPartial;
|
|
1652
|
+
// Use the zip file as package for release
|
|
1653
|
+
const releaseCommand: cli.IReleaseReactCommand = {
|
|
1654
|
+
type: cli.CommandType.release,
|
|
1655
|
+
appName: command.appName,
|
|
1656
|
+
deploymentName: command.deploymentName,
|
|
1657
|
+
appStoreVersion: command.appStoreVersion || appStoreVersion,
|
|
1658
|
+
description: command.description,
|
|
1659
|
+
disabled: command.disabled,
|
|
1660
|
+
mandatory: command.mandatory,
|
|
1661
|
+
rollout: command.rollout,
|
|
1662
|
+
initial: command.initial,
|
|
1663
|
+
noDuplicateReleaseError: command.noDuplicateReleaseError,
|
|
1664
|
+
platform: platform,
|
|
1665
|
+
outputDir: outputFolder,
|
|
1666
|
+
bundleName: bundleName,
|
|
1667
|
+
package: metadataZip,
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
return doNativeRelease(releaseCommand).then(async () => {
|
|
1671
|
+
// Clean up zip file
|
|
1672
|
+
if (fs.existsSync(releaseCommandPartial.package)) {
|
|
1673
|
+
fs.unlinkSync(releaseCommandPartial.package);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
} finally {
|
|
1677
|
+
try {
|
|
1678
|
+
await deleteFolder(extractFolder);
|
|
1679
|
+
await deleteFolder(outputFolder);
|
|
1680
|
+
} catch (ignored) {}
|
|
1681
|
+
}
|
|
1682
|
+
})
|
|
1683
|
+
.catch(async (err: Error) => {
|
|
1684
|
+
throw err;
|
|
1685
|
+
});
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
const releaseReactNative = (command: cli.IReleaseReactCommand): Promise<void> => {
|
|
1689
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
1690
|
+
const updateMetadata: ReactNativePackageInfo = {
|
|
1691
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
1692
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
1693
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
1694
|
+
isInitial: command.initial,
|
|
1695
|
+
bundleName: command.bundleName,
|
|
1696
|
+
outputDir: command.outputDir,
|
|
1697
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
1698
|
+
appVersion: command.appStoreVersion,
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
return doRelease(command, updateMetadata);
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
const doRelease = (command: cli.IReleaseCommand | cli.IReleaseReactCommand, updateMetadata: PackageInfo): Promise<void> => {
|
|
1705
|
+
if (isBinaryOrZip(command.package)) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
"It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
throwForInvalidSemverRange(command.appStoreVersion);
|
|
1712
|
+
const filePath: string = command.package;
|
|
1713
|
+
let isSingleFilePackage: boolean = true;
|
|
1714
|
+
|
|
1715
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
1716
|
+
isSingleFilePackage = false;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
let lastTotalProgress = 0;
|
|
1720
|
+
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
1721
|
+
complete: "=",
|
|
1722
|
+
incomplete: " ",
|
|
1723
|
+
width: 50,
|
|
1724
|
+
total: 100,
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
const uploadProgress = (currentProgress: number): void => {
|
|
1728
|
+
progressBar.tick(currentProgress - lastTotalProgress);
|
|
1729
|
+
lastTotalProgress = currentProgress;
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
return sdk
|
|
1733
|
+
.isAuthenticated(true)
|
|
1734
|
+
.then((isAuth: boolean): Promise<void> => {
|
|
1735
|
+
log("Release file path: " + filePath);
|
|
1736
|
+
log("Metadata: " + JSON.stringify(updateMetadata));
|
|
1737
|
+
return sdk.release(command.appName, command.deploymentName, filePath, updateMetadata, uploadProgress);
|
|
1738
|
+
})
|
|
1739
|
+
.then((): void => {
|
|
1740
|
+
log(
|
|
1741
|
+
'Successfully released an update containing the "' +
|
|
1742
|
+
command.package +
|
|
1743
|
+
'" ' +
|
|
1744
|
+
(isSingleFilePackage ? "file" : "directory") +
|
|
1745
|
+
' to the "' +
|
|
1746
|
+
command.deploymentName +
|
|
1747
|
+
'" deployment of the "' +
|
|
1748
|
+
command.appName +
|
|
1749
|
+
'" app.'
|
|
1750
|
+
);
|
|
1751
|
+
})
|
|
1752
|
+
.catch((err: CodePushError) => releaseErrorHandler(err, command));
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
const doNativeRelease = (releaseCommand: cli.IReleaseReactCommand): Promise<void> => {
|
|
1756
|
+
throwForInvalidSemverRange(releaseCommand.appStoreVersion);
|
|
1757
|
+
|
|
1758
|
+
const filePath: string = releaseCommand.package;
|
|
1759
|
+
|
|
1760
|
+
const updateMetadata: ReactNativePackageInfo = {
|
|
1761
|
+
description: releaseCommand.initial ? `Zero release for v${releaseCommand.appStoreVersion}` : releaseCommand.description,
|
|
1762
|
+
isDisabled: releaseCommand.initial ? true : releaseCommand.disabled,
|
|
1763
|
+
isMandatory: releaseCommand.initial ? false : releaseCommand.mandatory,
|
|
1764
|
+
isInitial: releaseCommand.initial,
|
|
1765
|
+
bundleName: releaseCommand.bundleName,
|
|
1766
|
+
outputDir: releaseCommand.outputDir,
|
|
1767
|
+
rollout: releaseCommand.initial ? undefined : releaseCommand.rollout,
|
|
1768
|
+
appVersion: releaseCommand.appStoreVersion,
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
let lastTotalProgress = 0;
|
|
1772
|
+
|
|
1773
|
+
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
1774
|
+
complete: "=",
|
|
1775
|
+
incomplete: " ",
|
|
1776
|
+
width: 50,
|
|
1777
|
+
total: 100,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
const uploadProgress = (currentProgress: number): void => {
|
|
1781
|
+
progressBar.tick(currentProgress - lastTotalProgress);
|
|
1782
|
+
lastTotalProgress = currentProgress;
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
return sdk
|
|
1786
|
+
.isAuthenticated(true)
|
|
1787
|
+
.then((): Promise<void> => {
|
|
1788
|
+
return sdk.releaseNative(releaseCommand.appName, releaseCommand.deploymentName, filePath, updateMetadata, uploadProgress);
|
|
1789
|
+
})
|
|
1790
|
+
.then((): void => {
|
|
1791
|
+
log(
|
|
1792
|
+
'Successfully released an update containing the "' +
|
|
1793
|
+
releaseCommand.package +
|
|
1794
|
+
'" ' +
|
|
1795
|
+
"directory" +
|
|
1796
|
+
' to the "' +
|
|
1797
|
+
releaseCommand.deploymentName +
|
|
1798
|
+
'" deployment of the "' +
|
|
1799
|
+
releaseCommand.appName +
|
|
1800
|
+
'" app.'
|
|
1801
|
+
);
|
|
1802
|
+
})
|
|
1803
|
+
.catch((err: CodePushError) => releaseErrorHandler(err, releaseCommand));
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1399
1806
|
function rollback(command: cli.IRollbackCommand): Promise<void> {
|
|
1400
1807
|
return confirm().then((wasConfirmed: boolean) => {
|
|
1401
1808
|
if (!wasConfirmed) {
|
|
@@ -1528,7 +1935,7 @@ function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: b
|
|
|
1528
1935
|
|
|
1529
1936
|
log(
|
|
1530
1937
|
`\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan(
|
|
1531
|
-
"
|
|
1938
|
+
"revopush logout"
|
|
1532
1939
|
)} command at any time to delete this file and terminate your session.\r\n`
|
|
1533
1940
|
);
|
|
1534
1941
|
}
|