@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.
@@ -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
- return confirm(
368
- "Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable."
369
- ).then((wasConfirmed: boolean): Promise<void> => {
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
- if (isBinaryOrZip(command.package)) {
1210
- throw new Error(
1211
- "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)."
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
- throwForInvalidSemverRange(command.appStoreVersion);
1216
- const filePath: string = command.package;
1217
- let isSingleFilePackage: boolean = true;
1253
+ const expoCliPath = getExpoCliPath();
1218
1254
 
1219
- if (fs.lstatSync(filePath).isDirectory()) {
1220
- isSingleFilePackage = false;
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
- let lastTotalProgress = 0;
1224
- const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
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
- const uploadProgress = (currentProgress: number): void => {
1232
- progressBar.tick(currentProgress - lastTotalProgress);
1233
- lastTotalProgress = currentProgress;
1234
- };
1284
+ if (extraBundlerOptions.length > 0) {
1285
+ expoBundleArgs.push(...extraBundlerOptions);
1286
+ }
1235
1287
 
1236
- const updateMetadata: PackageInfo = {
1237
- description: command.description,
1238
- isDisabled: command.disabled,
1239
- isMandatory: command.mandatory,
1240
- rollout: command.rollout,
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
- .isAuthenticated(true)
1245
- .then((isAuth: boolean): Promise<void> => {
1246
- return sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress);
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((): void => {
1249
- log(
1250
- 'Successfully released an update containing the "' +
1251
- command.package +
1252
- '" ' +
1253
- (isSingleFilePackage ? "file" : "directory") +
1254
- ' to the "' +
1255
- command.deploymentName +
1256
- '" deployment of the "' +
1257
- command.appName +
1258
- '" app.'
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
- .catch((err: CodePushError) => releaseErrorHandler(err, command));
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.IReleaseCommand = <any>command;
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 release(releaseCommand);
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
- "code-push logout"
1938
+ "revopush logout"
1532
1939
  )} command at any time to delete this file and terminate your session.\r\n`
1533
1940
  );
1534
1941
  }