From 4f32fd058c125fab3ada45a2670c758810a8688a Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Wed, 21 Dec 2016 16:58:02 +0300 Subject: [PATCH 001/198] debug: fix android device detection (#357) Replace 'isDeviceAvailable' method with 'getNumberOfAvailableDevices' method, add new throw statement in case more than 1 attached android device was detected, because for now there is no ability to specify device for debug like 'code-push debug android "192.168.121.102:5555"'. Fix issue https://github.com/Microsoft/code-push/issues/352 --- cli/script/commands/debug.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cli/script/commands/debug.ts b/cli/script/commands/debug.ts index 3d50d9bf..1df35ef5 100644 --- a/cli/script/commands/debug.ts +++ b/cli/script/commands/debug.ts @@ -20,10 +20,19 @@ class AndroidDebugPlatform implements IDebugPlatform { throw new Error("ADB command not found. Please ensure it is installed and available on your path."); } - if (!this.isDeviceAvailable()) { + const numberOfAvailableDevices = this.getNumberOfAvailableDevices(); + if (numberOfAvailableDevices === 0) { throw new Error("No Android devices found. Re-run this command after starting one."); } + // For now there is no ability to specify device for debug like: + // code-push debug android "192.168.121.102:5555" + // So we have to throw an error in case more than 1 android device was attached + // otherwise we will very likely run into an exception while trying to read ‘adb logcat’ from device which codepushified app is not running on. + if (numberOfAvailableDevices > 1) { + throw new Error(`Found "${numberOfAvailableDevices}" android devices. Please leave only one device you need to debug.`); + } + return childProcess.spawn("adb", ["logcat"]); } @@ -31,10 +40,15 @@ class AndroidDebugPlatform implements IDebugPlatform { // like when running the "adb devices" command. // // List of devices attached - // emulator-5554 device - private isDeviceAvailable(): boolean { + // emulator-5554 device + // 192.168.121.102:5555 device + private getNumberOfAvailableDevices(): number { const output = childProcess.execSync("adb devices").toString(); - return output.search(/^[\w-]+\s+device$/mi) > -1; + const matches = output.match(/\b(device)\b/mig); + if (matches != null) { + return matches.length; + } + return 0; } public normalizeLogMessage(message: string): string { From bf06e33035dc5318b8309584ac9eb0b86f73b4e4 Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Wed, 21 Dec 2016 17:35:03 +0300 Subject: [PATCH 002/198] command-executor: fix versionName parsing issue (#358) Implement workaround for the case when 'build.gradle' file contains several 'android' nodes. In this case 'buildGradle.android' prop (parsed by 'gradle-to-js' library) represents array instead of object due to parsing issue in 'g2js.parseFile' method. --- cli/script/command-executor.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 53c029f5..cfeec362 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -902,15 +902,31 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); }) .then((buildGradle: any) => { - if (!buildGradle.android || !buildGradle.android.defaultConfig || !buildGradle.android.defaultConfig.versionName) { + let versionName: string = null; + + // First 'if' statement was implemented as workaround for case + // when 'build.gradle' file contains several 'android' nodes. + // In this case 'buildGradle.android' prop represents array instead of object + // due to parsing issue in 'g2js.parseFile' method. + if (buildGradle.android instanceof Array) { + for (var i = 0; i < buildGradle.android.length; i++) { + var gradlePart = buildGradle.android[i]; + if (gradlePart.defaultConfig && gradlePart.defaultConfig.versionName) { + versionName = gradlePart.defaultConfig.versionName; + break; + } + } + } else if (buildGradle.android && buildGradle.android.defaultConfig && buildGradle.android.defaultConfig.versionName) { + versionName = buildGradle.android.defaultConfig.versionName; + } else { throw new Error(`The "${buildGradlePath}" file doesn't specify a value for the "android.defaultConfig.versionName" property.`); } - if (typeof buildGradle.android.defaultConfig.versionName !== "string") { + if (typeof versionName !== "string") { throw new Error(`The "android.defaultConfig.versionName" property value in "${buildGradlePath}" is not a valid string. If this is expected, consider using the --targetBinaryVersion option to specify the value manually.`); } - let appVersion: string = buildGradle.android.defaultConfig.versionName.replace(/"/g, "").trim(); + let appVersion: string = versionName.replace(/"/g, "").trim(); if (isValidVersion(appVersion)) { // The versionName property is a valid semver string, From dbd9f24964ad0b319362a1d485288c0a2a04416c Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Tue, 17 Jan 2017 23:54:40 +0300 Subject: [PATCH 003/198] command-executor: search for gradle properties across all `gradle.properties` files (#365) In case if there are both `android/app/gradle.properties` and `android/gradle.properties` exist, only the first one is considered. Improvement for https://github.com/Microsoft/react-native-code-push/issues/660 --- cli/script/command-executor.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index cfeec362..109dadda 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -950,14 +950,22 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj path.join("android", propertiesFileName) ]; - const propertiesFile: string = (knownLocations).find(fileExists); - const propertiesContent: string = fs.readFileSync(propertiesFile).toString(); - - try { - const parsedProperties: any = properties.parse(propertiesContent); - appVersion = parsedProperties[propertyName]; - } catch (e) { - throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`); + // Search for gradle properties across all `gradle.properties` files + var propertiesFile: string = null; + for (var i = 0; i < knownLocations.length; i++) { + propertiesFile = knownLocations[i]; + if (fileExists(propertiesFile)) { + const propertiesContent: string = fs.readFileSync(propertiesFile).toString(); + try { + const parsedProperties: any = properties.parse(propertiesContent); + appVersion = parsedProperties[propertyName]; + if (appVersion) { + break; + } + } catch (e) { + throw new Error(`Unable to parse "${propertiesFile}". Please ensure it is a well-formed properties file.`); + } + } } if (!appVersion) { From 5f2f07e2e8060280a1eb93d255b4c6bd0ff7ee93 Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Fri, 27 Jan 2017 11:01:43 +0300 Subject: [PATCH 004/198] =?UTF-8?q?cli=5Freadme:=20improved=20=E2=80=9CApp?= =?UTF-8?q?=20Management=E2=80=9D=20chapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To avoid issues like this one https://github.com/Microsoft/cordova-plugin-code-push/issues/189 “App Management” chapter probably should pay attention on the fact, that “using the same app for iOS and Android may cause installation exceptions because the CodePush update package produced for iOS will have different content from the update produced for Android.”. (#373) --- cli/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index 6b4181c3..3d264a1f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -143,13 +143,15 @@ Before you can deploy any updates, you need to register an app with the CodePush code-push app add ``` -If your app targets both iOS and Android, we recommend creating separate apps with CodePush. One for each platform. This way, you can manage and release updates to them separately, which in the long run, tends to make things simpler. The naming convention that most folks use is to suffix the app name with `-iOS` and `-Android`. For example: +If your app targets both iOS and Android, we highly recommend creating separate apps with CodePush. One for each platform. This way, you can manage and release updates to them separately, which in the long run, tends to make things simpler. The naming convention that most folks use is to suffix the app name with `-iOS` and `-Android`. For example: ``` code-push app add MyApp-Android code-push app add MyApp-iOS ``` +*NOTE: Using the same app for iOS and Android may cause installation exceptions because the CodePush update package produced for iOS will have different content from the update produced for Android.* + All new apps automatically come with two deployments (`Staging` and `Production`) so that you can begin distributing updates to multiple channels without needing to do anything extra (see deployment instructions below). After you create an app, the CLI will output the deployment keys for the `Staging` and `Production` deployments, which you can begin using to configure your mobile clients via their respective SDKs (details for [Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)). If you decide that you don't like the name you gave to an app, you can rename it at any time using the following command: From 81716550ebbc485c0137db48b25fadea5faa698b Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Wed, 8 Feb 2017 12:43:45 +0300 Subject: [PATCH 005/198] =?UTF-8?q?code-push-cli:=20implement=20=E2=80=9Co?= =?UTF-8?q?utputDir=E2=80=9D=20parameter=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit “outputDir” parameter is used to specify the path where the assets, JS bundle and sourcemap should be written. --- cli/README.md | 7 +++++++ cli/definitions/cli.ts | 1 + cli/script/command-executor.ts | 12 ++++++++++-- cli/script/command-parser.ts | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 3d264a1f..adae791d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -431,6 +431,7 @@ code-push release-react [--sourcemapOutput ] [--targetBinaryVersion ] [--rollout ] +[--outputDir ] ``` The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: @@ -556,6 +557,12 @@ This specifies the relative path to where the generated JS bundle's sourcemap fi *NOTE: This parameter can be set using either --sourcemapOutput or -s* +#### Output directory parameter + +This specifies the relative path to where the assets, JS bundle and sourcemap files should be written. If left unspecified, the assets, JS bundle and sourcemap will be copied to the `/tmp/CodePush` folder. + +*NOTE: This parameter can be set using either --outputDir or -o* + ### Releasing Updates (Cordova) ```shell diff --git a/cli/definitions/cli.ts b/cli/definitions/cli.ts index d7ddf954..dca8fd9a 100644 --- a/cli/definitions/cli.ts +++ b/cli/definitions/cli.ts @@ -197,6 +197,7 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand { plistFile?: string; plistFilePrefix?: string; sourcemapOutput?: string; + outputDir?: string; } export interface IRollbackCommand extends ICommand { diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 109dadda..cec2cf19 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -1223,7 +1223,7 @@ export var releaseCordova = (command: cli.IReleaseCordovaCommand): Promise export var releaseReact = (command: cli.IReleaseReactCommand): Promise => { var bundleName: string = command.bundleName; var entryFile: string = command.entryFile; - var outputFolder: string = path.join(os.tmpdir(), "CodePush"); + var outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush"); var platform: string = command.platform = command.platform.toLowerCase(); var releaseCommand: cli.IReleaseCommand = command; releaseCommand.package = outputFolder; @@ -1280,6 +1280,10 @@ export var releaseReact = (command: cli.IReleaseReactCommand): Promise => ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); + if (command.outputDir) { + command.sourcemapOutput = path.join(command.outputDir, bundleName + ".map"); + } + return appVersionPromise .then((appVersion: string) => { releaseCommand.appStoreVersion = appVersion; @@ -1293,7 +1297,11 @@ export var releaseReact = (command: cli.IReleaseReactCommand): Promise => log(chalk.cyan("\nReleasing update contents to CodePush:\n")); return release(releaseCommand); }) - .then(() => deleteFolder(outputFolder)) + .then(() => { + if (!command.outputDir) { + deleteFolder(outputFolder); + } + }) .catch((err: Error) => { deleteFolder(outputFolder); throw err; diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index bbe2d505..74b4991b 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -431,6 +431,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this release should be immediately available to", type: "string" }) .option("sourcemapOutput", { alias: "s", default: null, demand: false, description: "Path to where the sourcemap for the resulting bundle should be written. If omitted, a sourcemap will not be generated.", type: "string" }) .option("targetBinaryVersion", { alias: "t", default: null, demand: false, description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the release will target the exact version specified in the \"Info.plist\" (iOS), \"build.gradle\" (Android) or \"Package.appxmanifest\" (Windows) files.", type: "string" }) + .option("outputDir", { alias: "o", default: null, demand: false, description: "Path to where the bundle and sourcemap should be written. If omitted, a bundle and sourcemap will not be written.", type: "string" }) .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); addCommonConfiguration(yargs); @@ -829,6 +830,7 @@ function createCommand(): cli.ICommand { releaseReactCommand.plistFilePrefix = argv["plistFilePrefix"]; releaseReactCommand.rollout = getRolloutValue(argv["rollout"]); releaseReactCommand.sourcemapOutput = argv["sourcemapOutput"]; + releaseReactCommand.outputDir = argv["outputDir"]; } break; From 6d13e693eeebf0bb967a25f2fe2b1cbbaec4a9ea Mon Sep 17 00:00:00 2001 From: max-mironov Date: Wed, 8 Feb 2017 15:52:34 +0300 Subject: [PATCH 006/198] Readme Docs For CLI Description Flag Incorrect (#387) Fixed mistype in readme docs, see https://github.com/Microsoft/code-push/issues/351 for details. --- cli/README-cn.md | 2 +- cli/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/README-cn.md b/cli/README-cn.md index dc3c009b..61395c48 100644 --- a/cli/README-cn.md +++ b/cli/README-cn.md @@ -333,7 +333,7 @@ code-push release 给部署提供一个可选的"更新日志"。当被检测到有更新时这个值就会完整的传到客户端,所以你的应用可以选择显示给终端用户(如:通过一个`哪些新东西?`的对话框)。这个字符串可以接受控制字符如`\n` 和 `\t`,以便你可以包含空白格式在你的描述里来提高可读性。 -*注意:这个参数可以用"--description" 或 "-desc"来设置。* +*注意:这个参数可以用"--description" 或 "-des"来设置。* #### Mandatory (强制性)参数 diff --git a/cli/README.md b/cli/README.md index adae791d..e7396860 100644 --- a/cli/README.md +++ b/cli/README.md @@ -363,7 +363,7 @@ This specifies which deployment you want to release the update to. This defaults This provides an optional "change log" for the deployment. The value is simply round tripped to the client so that when the update is detected, your app can choose to display it to the end-user (e.g. via a "What's new?" dialog). This string accepts control characters such as `\n` and `\t` so that you can include whitespace formatting within your descriptions for improved readability. -*NOTE: This parameter can be set using either "--description" or "-desc"* +*NOTE: This parameter can be set using either "--description" or "-des"* #### Disabled parameter From 08a8274ba3e2e6b9cf75de3dd1d61632f81f1ba7 Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Thu, 9 Feb 2017 11:45:36 +0300 Subject: [PATCH 007/198] package: update `gradle-to-js` package version to `0.2.5` (#359) `0.2.5` package version contains fix of following issue: > parsing issue occurs in case build.gradle file contains several `android` closures > https://github.com/ninetwozero/gradle-to-js/issues/4 Fix https://github.com/Microsoft/code-push/issues/349 --- cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/package.json b/cli/package.json index ec977ce7..9ee09145 100644 --- a/cli/package.json +++ b/cli/package.json @@ -30,7 +30,7 @@ "cli-table": "^0.3.1", "code-push": "1.11.1-beta", "email-validator": "^1.0.3", - "gradle-to-js": "0.1.1", + "gradle-to-js": "0.2.5", "moment": "^2.10.6", "opener": "^1.4.1", "parse-duration": "0.1.1", From b697f7135981179e4e8660e1d2293da0af249f8e Mon Sep 17 00:00:00 2001 From: Max P Date: Thu, 9 Feb 2017 14:12:58 -0800 Subject: [PATCH 008/198] Add anchor for automatic documentation replacement. --- cli/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/README.md b/cli/README.md index e7396860..a36cc8d5 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,6 +4,8 @@ CodePush is a cloud service that enables Cordova and React Native developers to ![CodePush CLI](https://cloud.githubusercontent.com/assets/116461/16246693/2e7df77c-37bb-11e6-9456-e392af7f7b84.png) + + * [Installation](#installation) * [Getting Started](#getting-started) * [Account Management](#account-management) @@ -26,6 +28,8 @@ CodePush is a cloud service that enables Cordova and React Native developers to [[Chinese version 中文版]](./README-cn.md) + + ## Installation * Install [Node.js](https://nodejs.org/) From 25f4fb8b54ac0ee453a912df293b7516fe511adc Mon Sep 17 00:00:00 2001 From: max-mironov Date: Mon, 13 Feb 2017 11:27:58 +0300 Subject: [PATCH 009/198] Update yargs to 6.5.0 (#360) * Update yargs to 6.5.0 This shpuld fix issue https://github.com/Microsoft/code-push/issues/339 "Can not perform rollback if deployment name is same as CodePush CLI command #339" * Update yargs to 6.5.0 Fixed issue when it was not allowed to use additional undocument parameter for some commands --- cli/package.json | 2 +- cli/script/command-parser.ts | 64 ++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/cli/package.json b/cli/package.json index 9ee09145..4c0562ff 100644 --- a/cli/package.json +++ b/cli/package.json @@ -47,6 +47,6 @@ "which": "^1.2.7", "wordwrap": "1.0.0", "xml2js": "^0.4.16", - "yargs": "^3.15.0" + "yargs": "^6.5.0" } } diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 74b4991b..2d90a266 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -43,7 +43,7 @@ function updateCheck(): void { function accessKeyAdd(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " access-key " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("access-key " + commandName + " \"VSTS Integration\"", "Creates a new access key with the name \"VSTS Integration\", which expires in 60 days") .example("access-key " + commandName + " \"One time key\" --ttl 5m", "Creates a new access key with the name \"One time key\", which expires in 5 minutes") .option("ttl", { default: "60d", demand: false, description: "Duration string which specifies the amount of time that the access key should remain valid for (e.g 5m, 60d, 1y)", type: "string" }); @@ -54,7 +54,7 @@ function accessKeyAdd(commandName: string, yargs: yargs.Argv): void { function accessKeyPatch(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " access-key " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("access-key " + commandName + " \"Key for build server\" --name \"Key for CI machine\"", "Renames the access key named \"Key for build server\" to \"Key for CI machine\"") .example("access-key " + commandName + " \"Key for build server\" --ttl 7d", "Updates the access key named \"Key for build server\" to expire in 7 days") .option("name", { default: null, demand: false, description: "Display name for the access key", type: "string" }) @@ -65,7 +65,7 @@ function accessKeyPatch(commandName: string, yargs: yargs.Argv): void { function accessKeyList(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " access-key " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. + .demand(/*count*/ 0, /*max*/ 0) .example("access-key " + commandName, "Lists your access keys in tabular format") .example("access-key " + commandName + " --format json", "Lists your access keys in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display your access keys with (\"json\" or \"table\")", type: "string" }); @@ -76,7 +76,7 @@ function accessKeyList(commandName: string, yargs: yargs.Argv): void { function accessKeyRemove(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " access-key " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("access-key " + commandName + " \"VSTS Integration\"", "Removes the \"VSTS Integration\" access key"); addCommonConfiguration(yargs); @@ -92,7 +92,7 @@ function addCommonConfiguration(yargs: yargs.Argv): void { function appList(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " app " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. + .demand(/*count*/ 0, /*max*/ 0) .example("app " + commandName, "List your apps in tabular format") .example("app " + commandName + " --format json", "List your apps in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display your apps with (\"json\" or \"table\")", type: "string" }); @@ -103,7 +103,7 @@ function appList(commandName: string, yargs: yargs.Argv): void { function appRemove(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " app " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("app " + commandName + " MyApp", "Removes app \"MyApp\""); addCommonConfiguration(yargs); @@ -112,7 +112,7 @@ function appRemove(commandName: string, yargs: yargs.Argv): void { function listCollaborators(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " collaborator " + commandName + " [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("collaborator " + commandName + " MyApp", "Lists the collaborators for app \"MyApp\" in tabular format") .example("collaborator " + commandName + " MyApp --format json", "Lists the collaborators for app \"MyApp\" in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display collaborators with (\"json\" or \"table\")", type: "string" }); @@ -123,7 +123,7 @@ function listCollaborators(commandName: string, yargs: yargs.Argv): void { function removeCollaborator(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " collaborator " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("collaborator " + commandName + " MyApp foo@bar.com", "Removes foo@bar.com as a collaborator from app \"MyApp\""); addCommonConfiguration(yargs); @@ -132,7 +132,7 @@ function removeCollaborator(commandName: string, yargs: yargs.Argv): void { function sessionList(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " session " + commandName + " [options]") - .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments. + .demand(/*count*/ 0, /*max*/ 0) .example("session " + commandName, "Lists your sessions in tabular format") .example("session " + commandName + " --format json", "Lists your login sessions in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display your login sessions with (\"json\" or \"table\")", type: "string" }); @@ -143,7 +143,7 @@ function sessionList(commandName: string, yargs: yargs.Argv): void { function sessionRemove(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " session " + commandName + " ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("session " + commandName + " \"John's PC\"", "Removes the existing login session from \"John's PC\""); addCommonConfiguration(yargs); @@ -152,7 +152,7 @@ function sessionRemove(commandName: string, yargs: yargs.Argv): void { function deploymentHistoryClear(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("deployment " + commandName + " MyApp MyDeployment", "Clears the release history associated with deployment \"MyDeployment\" from app \"MyApp\""); addCommonConfiguration(yargs); @@ -161,7 +161,7 @@ function deploymentHistoryClear(commandName: string, yargs: yargs.Argv): void { function deploymentList(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment " + commandName + " [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("deployment " + commandName + " MyApp", "Lists the deployments for app \"MyApp\" in tabular format") .example("deployment " + commandName + " MyApp --format json", "Lists the deployments for app \"MyApp\" in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display your deployments with (\"json\" or \"table\")", type: "string" }) @@ -172,7 +172,7 @@ function deploymentList(commandName: string, yargs: yargs.Argv): void { function deploymentRemove(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment " + commandName + " ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("deployment " + commandName + " MyApp MyDeployment", "Removes deployment \"MyDeployment\" from app \"MyApp\""); addCommonConfiguration(yargs); @@ -181,7 +181,7 @@ function deploymentRemove(commandName: string, yargs: yargs.Argv): void { function deploymentHistory(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment " + commandName + " [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("deployment " + commandName + " MyApp MyDeployment", "Displays the release history for deployment \"MyDeployment\" from app \"MyApp\" in tabular format") .example("deployment " + commandName + " MyApp MyDeployment --format json", "Displays the release history for deployment \"MyDeployment\" from app \"MyApp\" in JSON format") .option("format", { default: "table", demand: false, description: "Output format to display the release history with (\"json\" or \"table\")", type: "string" }) @@ -213,7 +213,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("add", "Add a new app to your account", (yargs: yargs.Argv): void => { isValidCommand = true; yargs.usage(USAGE_PREFIX + " app add ") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("app add MyApp", "Adds app \"MyApp\""); addCommonConfiguration(yargs); @@ -223,7 +223,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("rename", "Rename an existing app", (yargs: yargs.Argv) => { isValidCommand = true; yargs.usage(USAGE_PREFIX + " app rename ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("app rename CurrentName NewName", "Renames app \"CurrentName\" to \"NewName\""); addCommonConfiguration(yargs); @@ -232,7 +232,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("ls", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("ls", yargs)) .command("transfer", "Transfer the ownership of an app to another account", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " app transfer ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("app transfer MyApp foo@bar.com", "Transfers the ownership of app \"MyApp\" to an account with email \"foo@bar.com\""); addCommonConfiguration(yargs); @@ -248,7 +248,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("add", "Add a new collaborator to an app", (yargs: yargs.Argv): void => { isValidCommand = true; yargs.usage(USAGE_PREFIX + " collaborator add ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("collaborator add MyApp foo@bar.com", "Adds foo@bar.com as a collaborator to app \"MyApp\""); addCommonConfiguration(yargs); @@ -265,7 +265,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " debug ") - .demand(/*count*/ 2, /*max*/ 2) + .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option arguments .example("debug android", "View the CodePush debug logs for an Android emulator or device") .example("debug ios", "View the CodePush debug logs for the iOS simulator"); @@ -278,7 +278,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("add", "Add a new deployment to an app", (yargs: yargs.Argv): void => { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment add ") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("deployment add MyApp MyDeployment", "Adds deployment \"MyDeployment\" to app \"MyApp\""); addCommonConfiguration(yargs); @@ -289,7 +289,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .command("rename", "Rename an existing deployment", (yargs: yargs.Argv) => { isValidCommand = true; yargs.usage(USAGE_PREFIX + " deployment rename ") - .demand(/*count*/ 5, /*max*/ 5) // Require exactly five non-option arguments. + .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments .example("deployment rename MyApp CurrentDeploymentName NewDeploymentName", "Renames deployment \"CurrentDeploymentName\" to \"NewDeploymentName\""); addCommonConfiguration(yargs); @@ -306,7 +306,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " link") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. + .demand(/*count*/ 0, /*max*/ 1) //set 'max' to one to allow usage of serverUrl undocument parameter for testing .example("link", "Links an account on the CodePush server") .check((argv: any, aliases: { [aliases: string]: string }): any => isValidCommand); // Report unrecognized, non-hyphenated command category. @@ -316,7 +316,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " login [options]") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. + .demand(/*count*/ 0, /*max*/ 1) //set 'max' to one to allow usage of serverUrl undocument parameter for testing .example("login", "Logs in to the CodePush server") .example("login --accessKey mykey", "Logs in on behalf of the user who owns and created the access key \"mykey\"") .example("login --proxy http://someproxy.com:455", "Logs in with the specified proxy url") @@ -331,13 +331,13 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " logout") - .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option argument. + .demand(/*count*/ 0, /*max*/ 0) .example("logout", "Logs out and ends your current session"); addCommonConfiguration(yargs); }) .command("patch", "Update the metadata for an existing release", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " patch [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("patch MyApp Production --des \"Updated description\" -r 50%", "Updates the description of the latest release for \"MyApp\" app's \"Production\" deployment and updates the rollout value to 50%") .example("patch MyApp Production -l v3 --des \"Updated description for v3\"", "Updates the description of the release with label v3 for \"MyApp\" app's \"Production\" deployment") .option("label", { alias: "l", default: null, demand: false, description: "Label of the release to update. Defaults to the latest release within the specified deployment", type: "string" }) @@ -352,7 +352,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") }) .command("promote", "Promote the latest release from one app deployment to another", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " promote [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments .example("promote MyApp Staging Production", "Promotes the latest release within the \"Staging\" deployment of \"MyApp\" to \"Production\"") .example("promote MyApp Staging Production --des \"Production rollout\" -r 25", "Promotes the latest release within the \"Staging\" deployment of \"MyApp\" to \"Production\", with an updated description, and targeting only 25% of the users") .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app with this release. If omitted, the description from the release being promoted will be used.", type: "string" }) @@ -369,7 +369,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " register") - .demand(/*count*/ 1, /*max*/ 2) // Require one non-optional and one optional argument. + .demand(/*count*/ 0, /*max*/ 1) //set 'max' to one to allow usage of serverUrl undocument parameter for testing .example("register", "Registers a new CodePush account") .example("register --proxy http://someproxy.com:455", "Registers with the specified proxy url") .option("proxy", { default: null, demand: false, description: "URL of the proxy server to use", type: "string" }) @@ -380,7 +380,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") }) .command("release", "Release an update to an app deployment", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " release [options]") - .demand(/*count*/ 4, /*max*/ 4) // Require exactly four non-option arguments. + .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. .example("release MyApp app.js \"*\"", "Releases the \"app.js\" file to the \"MyApp\" app's \"Staging\" deployment, targeting any binary version using the \"*\" wildcard range syntax.") .example("release MyApp ./platforms/ios/www 1.0.3 -d Production", "Releases the \"./platforms/ios/www\" folder and all its contents to the \"MyApp\" app's \"Production\" deployment, targeting only the 1.0.3 binary version") .example("release MyApp ./platforms/ios/www 1.0.3 -d Production -r 20", "Releases the \"./platforms/ios/www\" folder and all its contents to the \"MyApp\" app's \"Production\" deployment, targeting the 1.0.3 binary version and rolling out to about 20% of the users") @@ -396,7 +396,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") }) .command("release-cordova", "Release a Cordova update to an app deployment", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " release-cordova [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("release-cordova MyApp ios", "Releases the Cordova iOS project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") .example("release-cordova MyApp android -d Production", "Releases the Cordova Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") .option("build", { alias: "b", default: false, demand: false, description: "Invoke \"cordova build\" instead of \"cordova prepare\"", type: "boolean" }) @@ -413,7 +413,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") }) .command("release-react", "Release a React Native update to an app deployment", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " release-react [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("release-react MyApp ios", "Releases the React Native iOS project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") .example("release-react MyApp android -d Production", "Releases the React Native Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") .example("release-react MyApp windows --dev", "Releases the development bundle of the React Native Windows project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") @@ -438,7 +438,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") }) .command("rollback", "Rollback the latest release for an app deployment", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " rollback [options]") - .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments. + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments .example("rollback MyApp Production", "Performs a rollback on the \"Production\" deployment of \"MyApp\"") .example("rollback MyApp Production --targetRelease v4", "Performs a rollback on the \"Production\" deployment of \"MyApp\" to the v4 release") .option("targetRelease", { alias: "r", default: null, demand: false, description: "Label of the release to roll the specified deployment back to (e.g. v4). If omitted, the deployment will roll back to the previous release.", type: "string" }); @@ -461,7 +461,7 @@ var argv = yargs.usage(USAGE_PREFIX + " ") isValidCommandCategory = true; isValidCommand = true; yargs.usage(USAGE_PREFIX + " whoami") - .demand(/*count*/ 1, /*max*/ 1) // Require exactly one non-option argument. + .demand(/*count*/ 0, /*max*/ 0) .example("whoami", "Display the account info for the current login session"); addCommonConfiguration(yargs); }) From f23617b90299c59be7dd831ae2fecc0920eceb96 Mon Sep 17 00:00:00 2001 From: Zachary Kim Date: Wed, 15 Feb 2017 14:16:19 -1000 Subject: [PATCH 010/198] Allow setting of node binary arguments via NODE_ARGS env var (#395) Example usage: `env NODE_ARGS="--max-old-space-size=4096" code-push release-react APP_NAME ios` I couldn't find anywhere else secondary node processes are being spawned, but happy to add this functionality to those places as well. Issue #215 --- cli/script/command-executor.ts | 23 +++++++++++------- cli/test/cli.ts | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index cec2cf19..0cbc650d 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -1347,14 +1347,21 @@ function requestAccessKey(): Promise { } export var runReactNativeBundleCommand = (bundleName: string, development: boolean, entryFile: string, outputFolder: string, platform: string, sourcemapOutput: string): Promise => { - var reactNativeBundleArgs = [ - path.join("node_modules", "react-native", "local-cli", "cli.js"), "bundle", - "--assets-dest", outputFolder, - "--bundle-output", path.join(outputFolder, bundleName), - "--dev", development, - "--entry-file", entryFile, - "--platform", platform, - ]; + let reactNativeBundleArgs: string[] = []; + let envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS; + + if (typeof envNodeArgs !== "undefined") { + Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/)); + } + + Array.prototype.push.apply(reactNativeBundleArgs, [ + path.join("node_modules", "react-native", "local-cli", "cli.js"), "bundle", + "--assets-dest", outputFolder, + "--bundle-output", path.join(outputFolder, bundleName), + "--dev", development, + "--entry-file", entryFile, + "--platform", platform, + ]); if (sourcemapOutput) { reactNativeBundleArgs.push("--sourcemap-output", sourcemapOutput); diff --git a/cli/test/cli.ts b/cli/test/cli.ts index 7b7b7bca..5bbe2301 100644 --- a/cli/test/cli.ts +++ b/cli/test/cli.ts @@ -1681,6 +1681,50 @@ describe("CLI", () => { .done(); }); + it("release-react applies arguments to node binary provided via the CODE_PUSH_NODE_ARGS env var", (done: MochaDone): void => { + var bundleName = "bundle.js"; + var command: cli.IReleaseReactCommand = { + type: cli.CommandType.releaseReact, + appName: "a", + appStoreVersion: null, + bundleName: bundleName, + deploymentName: "Staging", + description: "Test default entry file", + mandatory: false, + rollout: null, + platform: "ios" + }; + + ensureInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); + + var _CODE_PUSH_NODE_ARGS: string = process.env.CODE_PUSH_NODE_ARGS; + process.env.CODE_PUSH_NODE_ARGS = " --foo=bar --baz "; + + cmdexec.execute(command) + .then(() => { + var releaseCommand: cli.IReleaseCommand = command; + releaseCommand.package = path.join(os.tmpdir(), "CodePush"); + releaseCommand.appStoreVersion = "1.2.3"; + + sinon.assert.calledOnce(spawn); + var spawnCommand: string = spawn.args[0][0]; + var spawnCommandArgs: string = spawn.args[0][1].join(" "); + assert.equal(spawnCommand, "node"); + assert.equal( + spawnCommandArgs, + `--foo=bar --baz ${path.join("node_modules", "react-native", "local-cli", "cli.js")} bundle --assets-dest ${path.join(os.tmpdir(), "CodePush")} --bundle-output ${path.join(os.tmpdir(), "CodePush", bundleName)} --dev false --entry-file index.ios.js --platform ios` + ); + assertJsonDescribesObject(JSON.stringify(release.args[0][0], /*replacer=*/ null, /*spacing=*/ 2), releaseCommand); + + process.env.CODE_PUSH_NODE_ARGS = _CODE_PUSH_NODE_ARGS; + + done(); + }) + .done(); + }); + it("sessionList lists session name and expires fields", (done: MochaDone): void => { var command: cli.IAccessKeyListCommand = { type: cli.CommandType.sessionList, From 1bbf9a159e2e11bb6d4d4ca60368cc1f63424a55 Mon Sep 17 00:00:00 2001 From: max-mironov Date: Fri, 24 Feb 2017 04:29:28 +0300 Subject: [PATCH 011/198] Make confirmation prompt more severe on app/deployment rm (#402) * Make the prompt defaults to 'no' if no input is entered Make the message more severe than it currently for app rm and deployment rm * Fix upper/lower-case issues * Make prompt messages consistent --- cli/script/command-executor.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 0cbc650d..cf9cd672 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -72,7 +72,8 @@ export var execSync = childProcess.execSync; var connectionInfo: ILoginConnectionInfo; -export var confirm = (): Promise => { +export var confirm = (message: string = "Are you sure?"): Promise => { + message += " (y/N):"; return Promise((resolve, reject, notify): void => { prompt.message = ""; prompt.delimiter = ""; @@ -82,14 +83,19 @@ export var confirm = (): Promise => { prompt.get({ properties: { response: { - description: chalk.cyan("Are you sure? (Y/n):") + description: chalk.cyan(message) } } }, (err: any, result: any): void => { - if (!result.response || result.response === "" || result.response === "Y") { + var accepted = result.response && result.response.toLowerCase() === "y"; + var rejected = !result.response || result.response.toLowerCase() === "n"; + + if (accepted){ resolve(true); } else { - if (result.response !== "n") console.log("Invalid response: \"" + result.response + "\""); + if (!rejected){ + console.log("Invalid response: \"" + result.response + "\""); + } resolve(false); } }); @@ -179,7 +185,7 @@ function appList(command: cli.IAppListCommand): Promise { } function appRemove(command: cli.IAppRemoveCommand): Promise { - return confirm() + return confirm("Are you sure you want to remove this app? Note that its deployment keys will be PERMANENTLY unrecoverable.") .then((wasConfirmed: boolean): Promise => { if (wasConfirmed) { return sdk.removeApp(command.appName) @@ -337,7 +343,7 @@ export var deploymentList = (command: cli.IDeploymentListCommand, showPackage: b } function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise { - return confirm() + return confirm("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.") .then((wasConfirmed: boolean): Promise => { if (wasConfirmed) { return sdk.removeDeployment(command.appName, command.deploymentName) From 5dfefc118ca800b75be0975a484e52cbb6228b4a Mon Sep 17 00:00:00 2001 From: Sergey Akhalkov Date: Sat, 25 Feb 2017 03:01:12 +0300 Subject: [PATCH 012/198] Check that app and deployment exist before releasing an update (#399) * command-executor: check for app and deployment exist before releasing an update * Remove catch block * Add getDeployment() mock so that unit tests work * Release commands now return rejected promises instead of throwing - do not assert exceptions are thrown during unit tests --- cli/script/command-executor.ts | 200 +++++++++++++++++---------------- cli/test/cli.ts | 78 +++++++------ 2 files changed, 150 insertions(+), 128 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index cf9cd672..e443076f 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -1155,58 +1155,63 @@ export var release = (command: cli.IReleaseCommand): Promise => { } export var releaseCordova = (command: cli.IReleaseCordovaCommand): Promise => { - var platform: string = command.platform.toLowerCase(); - var projectRoot: string = process.cwd(); - var platformFolder: string = path.join(projectRoot, "platforms", platform); - var platformCordova: string = path.join(platformFolder, "cordova"); - var outputFolder: string; - - if (platform === "ios") { - outputFolder = path.join(platformFolder, "www"); - } else if (platform === "android") { - outputFolder = path.join(platformFolder, "assets", "www"); - } else { - throw new Error("Platform must be either \"ios\" or \"android\"."); - } + var releaseCommand: cli.IReleaseCommand = command; + // Check for app and deployment exist before releasing an update. + // This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name. + return sdk.getDeployment(command.appName, command.deploymentName) + .then((): any => { + var platform: string = command.platform.toLowerCase(); + var projectRoot: string = process.cwd(); + var platformFolder: string = path.join(projectRoot, "platforms", platform); + var platformCordova: string = path.join(platformFolder, "cordova"); + var outputFolder: string; + + if (platform === "ios") { + outputFolder = path.join(platformFolder, "www"); + } else if (platform === "android") { + outputFolder = path.join(platformFolder, "assets", "www"); + } else { + throw new Error("Platform must be either \"ios\" or \"android\"."); + } - var cordovaCommand: string = command.build ? "build" : "prepare"; - var cordovaCLI: string = "cordova"; + var cordovaCommand: string = command.build ? "build" : "prepare"; + var cordovaCLI: string = "cordova"; - // Check whether the Cordova or PhoneGap CLIs are - // installed, and if not, fail early - try { - which.sync(cordovaCLI); - } catch (e) { - try { - cordovaCLI = "phonegap"; - which.sync(cordovaCLI); - } catch (e) { - throw new Error(`Unable to ${cordovaCommand} project. Please ensure that either the Cordova or PhoneGap CLI is installed.`); - } - } + // Check whether the Cordova or PhoneGap CLIs are + // installed, and if not, fail early + try { + which.sync(cordovaCLI); + } catch (e) { + try { + cordovaCLI = "phonegap"; + which.sync(cordovaCLI); + } catch (e) { + throw new Error(`Unable to ${cordovaCommand} project. Please ensure that either the Cordova or PhoneGap CLI is installed.`); + } + } - log(chalk.cyan(`Running "${cordovaCLI} ${cordovaCommand}" command:\n`)); - try { - execSync([cordovaCLI, cordovaCommand, platform, "--verbose"].join(" "), { stdio: "inherit" }); - } catch (error) { - throw new Error(`Unable to ${cordovaCommand} project. Please ensure that the CWD represents a Cordova project and that the "${platform}" platform was added by running "${cordovaCLI} platform add ${platform}".`); - } + log(chalk.cyan(`Running "${cordovaCLI} ${cordovaCommand}" command:\n`)); + try { + execSync([cordovaCLI, cordovaCommand, platform, "--verbose"].join(" "), { stdio: "inherit" }); + } catch (error) { + throw new Error(`Unable to ${cordovaCommand} project. Please ensure that the CWD represents a Cordova project and that the "${platform}" platform was added by running "${cordovaCLI} platform add ${platform}".`); + } - try { - var configString: string = fs.readFileSync(path.join(projectRoot, "config.xml"), { encoding: "utf8" }); - } catch (error) { - throw new Error(`Unable to find or read "config.xml" in the CWD. The "release-cordova" command must be executed in a Cordova project folder.`); - } + try { + var configString: string = fs.readFileSync(path.join(projectRoot, "config.xml"), { encoding: "utf8" }); + } catch (error) { + throw new Error(`Unable to find or read "config.xml" in the CWD. The "release-cordova" command must be executed in a Cordova project folder.`); + } - var configPromise: Promise = parseXml(configString); - var releaseCommand: cli.IReleaseCommand = command; + var configPromise: Promise = parseXml(configString); - releaseCommand.package = outputFolder; - releaseCommand.type = cli.CommandType.release; + releaseCommand.package = outputFolder; + releaseCommand.type = cli.CommandType.release; - return configPromise - .catch((err: any) => { - throw new Error(`Unable to parse "config.xml" in the CWD. Ensure that the contents of "config.xml" is valid XML.`); + return configPromise + .catch((err: any) => { + throw new Error(`Unable to parse "config.xml" in the CWD. Ensure that the contents of "config.xml" is valid XML.`); + }); }) .then((parsedConfig: any) => { var config: any = parsedConfig.widget; @@ -1232,65 +1237,70 @@ export var releaseReact = (command: cli.IReleaseReactCommand): Promise => var outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush"); var platform: string = command.platform = command.platform.toLowerCase(); var releaseCommand: cli.IReleaseCommand = command; - releaseCommand.package = outputFolder; - - switch (platform) { - case "android": - case "ios": - case "windows": - if (!bundleName) { - bundleName = platform === "ios" - ? "main.jsbundle" - : `index.${platform}.bundle`; - } + // Check for app and deployment exist before releasing an update. + // This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name. + return sdk.getDeployment(command.appName, command.deploymentName) + .then((): any => { + releaseCommand.package = outputFolder; + + switch (platform) { + case "android": + case "ios": + case "windows": + if (!bundleName) { + bundleName = platform === "ios" + ? "main.jsbundle" + : `index.${platform}.bundle`; + } - break; - default: - throw new Error("Platform must be either \"android\", \"ios\" or \"windows\"."); - } + break; + default: + throw new Error("Platform must be either \"android\", \"ios\" or \"windows\"."); + } - try { - var projectPackageJson: any = require(path.join(process.cwd(), "package.json")); - var projectName: string = projectPackageJson.name; - if (!projectName) { - throw new Error("The \"package.json\" file in the CWD does not have the \"name\" field set."); - } + try { + var projectPackageJson: any = require(path.join(process.cwd(), "package.json")); + var projectName: string = projectPackageJson.name; + if (!projectName) { + throw new Error("The \"package.json\" file in the CWD does not have the \"name\" field set."); + } - if (!projectPackageJson.dependencies["react-native"]) { - throw new Error("The project in the CWD is not a React Native project."); - } - } catch (error) { - throw new Error("Unable to find or read \"package.json\" in the CWD. The \"release-react\" command must be executed in a React Native project folder."); - } + if (!projectPackageJson.dependencies["react-native"]) { + throw new Error("The project in the CWD is not a React Native project."); + } + } catch (error) { + throw new Error("Unable to find or read \"package.json\" in the CWD. The \"release-react\" command must be executed in a React Native project folder."); + } - if (!entryFile) { - entryFile = `index.${platform}.js`; - if (fileDoesNotExistOrIsDirectory(entryFile)) { - entryFile = "index.js"; - } + if (!entryFile) { + entryFile = `index.${platform}.js`; + if (fileDoesNotExistOrIsDirectory(entryFile)) { + entryFile = "index.js"; + } - if (fileDoesNotExistOrIsDirectory(entryFile)) { - throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`); - } - } else { - if (fileDoesNotExistOrIsDirectory(entryFile)) { - throw new Error(`Entry file "${entryFile}" does not exist.`); - } - } + if (fileDoesNotExistOrIsDirectory(entryFile)) { + throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`); + } + } else { + if (fileDoesNotExistOrIsDirectory(entryFile)) { + throw new Error(`Entry file "${entryFile}" does not exist.`); + } + } - if (command.appStoreVersion) { - throwForInvalidSemverRange(command.appStoreVersion); - } + if (command.appStoreVersion) { + throwForInvalidSemverRange(command.appStoreVersion); + } - var appVersionPromise: Promise = command.appStoreVersion - ? Q(command.appStoreVersion) - : getReactNativeProjectAppVersion(command, projectName); + var appVersionPromise: Promise = command.appStoreVersion + ? Q(command.appStoreVersion) + : getReactNativeProjectAppVersion(command, projectName); - if (command.outputDir) { - command.sourcemapOutput = path.join(command.outputDir, bundleName + ".map"); - } + if (command.outputDir) { + command.sourcemapOutput = path.join(command.outputDir, bundleName + ".map"); + } - return appVersionPromise + return appVersionPromise; + }) .then((appVersion: string) => { releaseCommand.appStoreVersion = appVersion; return createEmptyTempReleaseFolder(outputFolder); diff --git a/cli/test/cli.ts b/cli/test/cli.ts index 5bbe2301..976b4a6e 100644 --- a/cli/test/cli.ts +++ b/cli/test/cli.ts @@ -32,6 +32,25 @@ const DEFAULT_ACCESS_KEY_MAX_AGE = 1000 * 60 * 60 * 24 * 60; // 60 days const TEST_MACHINE_NAME = "Test machine"; export class SdkStub { + private productionDeployment: codePush.Deployment = { + name: "Production", + key: "6" + }; + private stagingDeployment: codePush.Deployment = { + name: "Staging", + key: "6", + package: { + appVersion: "1.0.0", + description: "fgh", + label: "v2", + packageHash: "jkl", + isMandatory: true, + size: 10, + blobUrl: "http://mno.pqr", + uploadTime: 1000 + } + }; + public getAccountInfo(): Promise { return Q({ email: "a@a.com" @@ -65,14 +84,14 @@ export class SdkStub { return Q(null); } - public addDeployment(appId: string, name: string): Promise { + public addDeployment(appName: string, deploymentName: string): Promise { return Q({ - name: name, + name: deploymentName, key: "6" }); } - public clearDeploymentHistory(appId: string, deployment: string): Promise { + public clearDeploymentHistory(appName: string, deploymentName: string): Promise { return Q(null); } @@ -103,27 +122,27 @@ export class SdkStub { }]); } - public getDeployments(appId: string): Promise { - return Q([{ - name: "Production", - key: "6" - }, { - name: "Staging", - key: "6", - package: { - appVersion: "1.0.0", - description: "fgh", - label: "v2", - packageHash: "jkl", - isMandatory: true, - size: 10, - blobUrl: "http://mno.pqr", - uploadTime: 1000 + public getDeployments(appName: string): Promise { + if (appName === "a") { + return Q([this.productionDeployment, this.stagingDeployment]); + } + + return Q.reject(); + } + + public getDeployment(appName: string, deploymentName: string): Promise { + if (appName === "a") { + if (deploymentName === "Production") { + return Q(this.productionDeployment); + } else if (deploymentName === "Staging") { + return Q(this.stagingDeployment); } - }]); + } + + return Q.reject(); } - public getDeploymentHistory(appId: string, deploymentId: string): Promise { + public getDeploymentHistory(appName: string, deploymentName: string): Promise { return Q([ { description: null, @@ -148,7 +167,7 @@ export class SdkStub { ]); } - public getDeploymentMetrics(appId: string, deploymentId: string): Promise { + public getDeploymentMetrics(appName: string, deploymentName: string): Promise { return Q({ "1.0.0": { active: 123 @@ -189,7 +208,7 @@ export class SdkStub { return Q(null); } - public release(appId: string, deploymentId: string): Promise { + public release(appName: string, deploymentName: string): Promise { return Q("Successfully released"); } @@ -197,7 +216,7 @@ export class SdkStub { return Q(null); } - public removeApp(appId: string): Promise { + public removeApp(appName: string): Promise { return Q(null); } @@ -205,7 +224,7 @@ export class SdkStub { return Q(null); } - public removeDeployment(appId: string, deployment: string): Promise { + public removeDeployment(appName: string, deploymentName: string): Promise { return Q(null); } @@ -225,7 +244,7 @@ export class SdkStub { return Q(null); } - public renameDeployment(appId: string, deployment: codePush.Deployment): Promise { + public renameDeployment(appName: string, deploymentName: codePush.Deployment): Promise { return Q(null); } } @@ -1128,7 +1147,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, `Unable to ${cordovaCommand} project. Please ensure that the CWD represents a Cordova project and that the "${command.platform}" platform was added by running "cordova platform add ${command.platform}".`); sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); done(); }) .done(); @@ -1158,7 +1176,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, `Unable to find or read "config.xml" in the CWD. The "release-cordova" command must be executed in a Cordova project folder.`); sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); sinon.assert.calledOnce(execSync); done(); }) @@ -1188,7 +1205,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, "Platform must be either \"ios\" or \"android\"."); sinon.assert.notCalled(release); - sinon.assert.threw(releaseCordova, "Error"); sinon.assert.notCalled(spawn); done(); }) @@ -1311,7 +1327,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, "Unable to find or read \"package.json\" in the CWD. The \"release-react\" command must be executed in a React Native project folder."); sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); sinon.assert.notCalled(spawn); done(); }) @@ -1343,7 +1358,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, "Entry file \"doesntexist.js\" does not exist."); sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); sinon.assert.notCalled(spawn); done(); }) @@ -1374,7 +1388,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, "Platform must be either \"android\", \"ios\" or \"windows\"."); sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); sinon.assert.notCalled(spawn); done(); }) @@ -1408,7 +1421,6 @@ describe("CLI", () => { .catch((err) => { assert.equal(err.message, "Please use a semver-compliant target binary version range, for example \"1.0.0\", \"*\" or \"^1.2.3\"."); sinon.assert.notCalled(release); - sinon.assert.threw(releaseReact, "Error"); sinon.assert.notCalled(spawn); done(); }) From 840a2a5fda9e1492cad6a3bef3424639a07968a7 Mon Sep 17 00:00:00 2001 From: max-mironov Date: Sat, 25 Feb 2017 03:02:10 +0300 Subject: [PATCH 013/198] Add ability to promote a specific label (#394) * Add ability to promote a specific label * Updated Documentation and log message from CLI * Modified README --- cli/README.md | 5 +++++ cli/definitions/cli.ts | 1 + cli/script/command-executor.ts | 3 ++- cli/script/command-parser.ts | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index a36cc8d5..171b52c1 100644 --- a/cli/README.md +++ b/cli/README.md @@ -732,6 +732,7 @@ Once you've tested an update against a specific deployment (e.g. `Staging`), and ``` code-push promote [--description ] +[--label