From d6f9c151207b28258f6516a1bbee813691ac4433 Mon Sep 17 00:00:00 2001 From: EddyVerbruggen Date: Mon, 8 May 2017 10:56:45 +0200 Subject: [PATCH 1/3] Added NativeScript --- .github/ISSUE_TEMPLATE.md | 1 + README.md | 2 +- cli/README.md | 32 +++-- cli/definitions/cli.ts | 11 ++ cli/package.json | 3 +- cli/script/command-executor.ts | 166 ++++++++++++++++++++++++ cli/script/command-parser.ts | 43 ++++++ cli/test/cli.ts | 164 +++++++++++++++++++++++ cli/test/resources/TestApp/package.json | 8 ++ 9 files changed, 417 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9c7fe28e..f3dfa239 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,4 +3,5 @@ Thanks so much for filing an issue or feature request! We will address it as soo 1. This repository is for the CodePush CLI and management SDK. For issues relating to the CodePush client SDK's, please see: * react-native-code-push: https://github.com/Microsoft/react-native-code-push * cordova-plugin-code-push: https://github.com/Microsoft/cordova-plugin-code-push + * nativescript-code-push: https://github.com/EddyVerbruggen/nativescript-code-push 2. In your description, please include the version of `code-push-cli` or `code-push` that you are using. diff --git a/README.md b/README.md index e3167241..05ba014f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CodePush -[CodePush](https://microsoft.github.io/code-push) is a cloud service that enables Cordova and React Native developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using provided client SDKs for [Cordova](https://github.com/Microsoft/cordova-plugin-code-push) and [React Native](https://github.com/Microsoft/react-native-code-push)). This allows you to have a more deterministic and direct engagement model with your userbase, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. +[CodePush](https://microsoft.github.io/code-push) is a cloud service that enables Cordova, React Native and NativeScript developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using provided client SDKs for [Cordova](https://github.com/Microsoft/cordova-plugin-code-push), [React Native](https://github.com/Microsoft/react-native-code-push) and [NativeScript](https://github.com/EddyVerbruggen/nativescript-code-push)). This allows you to have a more deterministic and direct engagement model with your userbase, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. This repo includes the [management CLI](https://github.com/Microsoft/code-push/tree/master/cli) and [Node.js management SDK](https://github.com/Microsoft/code-push/tree/master/sdk), which allows you to manage and automate the needs of your Cordova and React Native apps. To get started using CodePush, refer to our [documentation](http://microsoft.github.io/code-push/index.html#getting_started), otherwise, read the following steps if you'd like to build/contribute to the project from source. diff --git a/cli/README.md b/cli/README.md index 56ca1b3b..b8243617 100644 --- a/cli/README.md +++ b/cli/README.md @@ -19,6 +19,7 @@ CodePush is a cloud service that enables Cordova and React Native developers to * [Releasing Updates (General)](#releasing-updates-general) * [Releasing Updates (React Native)](#releasing-updates-react-native) * [Releasing Updates (Cordova)](#releasing-updates-cordova) + * [Releasing Updates (NativeScript)](#releasing-updates-nativescript) * [Debugging CodePush Integration](#debugging-codepush-integration) * [Patching Update Metadata](#patching-update-metadata) * [Promoting Updates](#promoting-updates) @@ -39,7 +40,7 @@ CodePush is a cloud service that enables Cordova and React Native developers to 1. Create a [CodePush account](#account-creation) push using the CodePush CLI 2. Register your [app](#app-management) with CodePush, and optionally [share it](#app-collaboration) with other developers on your team -3. CodePush-ify your app and point it at the deployment you wish to use ([Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)) +3. CodePush-ify your app and point it at the deployment you wish to use ([Cordova](http://github.com/Microsoft/cordova-plugin-code-push), [React Native](http://github.com/Microsoft/react-native-code-push) and [NativeScript](http://github.com/EddyVerbruggen/nativescript-code-push)) 4. [Release](#releasing-updates) an update for your app 5. Check out the [debug logs](#debugging-codepush-integration) to ensure everything is working as expected 6. Live long and prosper! ([details](https://en.wikipedia.org/wiki/Vulcan_salute)) @@ -156,7 +157,7 @@ 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)). +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), [React Native](http://github.com/Microsoft/react-native-code-push) and [NativeScript](http://github.com/EddyVerbruggen/nativescript-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: @@ -290,6 +291,8 @@ Once your app has been configured to query for updates against the CodePush serv 3. [Cordova](#releasing-updates-cordova) - Performs the same functionality as the general release command, but also handles the task of preparing the app update for you, instead of requiring you to run both `cordova prepare` (or `phonegap prepare`) and then `code-push release`. +4. [NativeScript](#releasing-updates-nativescript) - Performs the same functionality as the general release command, but also handles the task of going into the platform build folder, instead of requiring you to figure out what that folder is and then running `code-push release` with the correct switches. + Which of these commands you should use is mostly a matter of requirements and/or preference. However, we generally recommend using the relevant platform-specific command to start (since it greatly simplifies the experience), and then leverage the general-purpose `release` command if/when greater control is needed. ### Releasing Updates (General) @@ -321,7 +324,9 @@ It's important that the path you specify refers to the platform-specific, prepar | React Native wo/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output --dev false` | Value of the `--bundle-output` option | | React Native w/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | | React Native wo/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output --dev false` | Value of the `--bundle-output` option | -| React Native w/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | +| React Native w/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | +| NativeScript (iOS) | `tns build ios [--release]` | `./platforms/ios/` directory | +| NativeScript (Android) | `tns build android [--release]` | `./platforms/android/src/main/assets` directory | #### Target binary version parameter @@ -350,12 +355,14 @@ If you ever want an update to target multiple versions of the app store binary, The following table outlines the version value that CodePush expects your update's semver range to satisfy for each respective app type: -| Platform | Source of app store version | -|------------------------|------------------------------------------------------------------------------| -| Cordova | The `` attribute in the `config.xml` file | -| React Native (Android) | The `android.defaultConfig.versionName` property in your `build.gradle` file | -| React Native (iOS) | The `CFBundleShortVersionString` key in the `Info.plist` file | -| React Native (Windows) | The `` key in the `Package.appxmanifest` file | +| Platform | Source of app store version | +|------------------------|---------------------------------------------------------------------------------------| +| Cordova | The `` attribute in the `config.xml` file | +| React Native (Android) | The `android.defaultConfig.versionName` property in your `build.gradle` file | +| React Native (iOS) | The `CFBundleShortVersionString` key in the `Info.plist` file | +| React Native (Windows) | The `` key in the `Package.appxmanifest` file | +| NativeScript (iOS) | The `CFBundleShortVersionString` key in the `App_Resources/iOS/Info.plist` file | +| NativeScript (Android) | The `android:versionName` key in the `App_Resources/Android/AndroidManifest.xml` file | *NOTE: If the app store version in the metadata files are missing a patch version, e.g. `2.0`, it will be treated as having a patch version of `0`, i.e. `2.0 -> 2.0.0`.* @@ -650,9 +657,12 @@ This is the same parameter as the one described in the [above section](#rollout- This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, the command defaults to targeting only the specified version in the project's metadata (`Info.plist` if this update is for iOS clients, and `build.gradle` for Android clients). +### Releasing Updates (NativeScript) +TODO + ## Debugging CodePush Integration -Once you've released an update, and the Cordova or React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: +Once you've released an update, and the Cordova, React Native or NativeScript plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: ```shell code-push debug @@ -668,7 +678,7 @@ code-push debug android -Under the covers, this command simply automates the usage of the iOS system logs and ADB logcat, but provides a platform-agnostic, filtered view of all logs coming from the CodePush plugin, for both Cordova or React Native. This way, you don't need to learn and/or use another tool simply to be able to answer basic questions about how CodePush is behaving. +Under the covers, this command simply automates the usage of the iOS system logs and ADB logcat, but provides a platform-agnostic, filtered view of all logs coming from the CodePush plugin, for Cordova, React Native or NativeScript. This way, you don't need to learn and/or use another tool simply to be able to answer basic questions about how CodePush is behaving. *NOTE: The debug command supports both emulators and devices for Android, but currently only supports listening to logs from the iOS simulator. We hope to add device support soon.* diff --git a/cli/definitions/cli.ts b/cli/definitions/cli.ts index 9ca421fa..dcc4c34a 100644 --- a/cli/definitions/cli.ts +++ b/cli/definitions/cli.ts @@ -28,6 +28,7 @@ release, releaseCordova, releaseReact, + releaseNativeScript, rollback, sessionList, sessionRemove, @@ -202,6 +203,16 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand { outputDir?: string; } +export interface IReleaseNativeScriptCommand extends IReleaseBaseCommand { + build?: boolean; + platform: string; + isReleaseBuildType?: boolean; + keystorePath?: string; + keystorePassword?: string; + keystoreAlias?: string; + keystoreAliasPassword?: string; +} + export interface IRollbackCommand extends ICommand { appName: string; deploymentName: string; diff --git a/cli/package.json b/cli/package.json index 61bbd353..d6fc6f8b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -18,7 +18,8 @@ "push", "cordova", "react-native", - "react" + "react", + "nativescript" ], "homepage": "https://microsoft.github.io/code-push", "author": "Microsoft Corporation", diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index d2f49c5b..f90040e7 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -524,6 +524,9 @@ export function execute(command: cli.ICommand): Promise { case cli.CommandType.releaseReact: return releaseReact(command); + case cli.CommandType.releaseNativeScript: + return releaseNativeScript(command); + case cli.CommandType.rollback: return rollback(command); @@ -1007,6 +1010,76 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj } } +function getNativeScriptProjectAppVersion(command: cli.IReleaseReactCommand): Promise { + const fileExists = (file: string): boolean => { + try { return fs.statSync(file).isFile() } + catch (e) { return false } + }; + + const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version); + + log(chalk.cyan(`Detecting ${command.platform} app version:\n`)); + + var projectRoot: string = process.cwd(); + var appResourcesFolder: string = path.join(projectRoot, "app", "App_Resources"); + + if (command.platform === "ios") { + + var iOSResourcesFolder: string = path.join(appResourcesFolder, "iOS"); + var plistFile: string = path.join(iOSResourcesFolder, "Info.plist"); + + if (!fileExists(plistFile)) { + throw new Error(`There's no Info.plist file at ${plistFile}. Please check that the iOS project is valid.`); + } + + const plistContents = fs.readFileSync(plistFile).toString(); + + try { + var parsedPlist = plist.parse(plistContents); + } catch (e) { + throw new Error(`Unable to parse "${plistFile}". Please ensure it is a well-formed plist file.`); + } + + if (parsedPlist && parsedPlist.CFBundleShortVersionString) { + if (isValidVersion(parsedPlist.CFBundleShortVersionString)) { + log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${plistFile}".\n`); + return Q(parsedPlist.CFBundleShortVersionString); + } else { + throw new Error(`The "CFBundleShortVersionString" key in the "${plistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`); + } + } else { + throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${plistFile}" file.`); + } + } else if (command.platform === "android") { + var androidResourcesFolder: string = path.join(appResourcesFolder, "Android"); + var androidManifest: string = path.join(androidResourcesFolder, "AndroidManifest.xml"); + + try { + var androidManifestContents: string = fs.readFileSync(androidManifest).toString(); + } catch (err) { + throw new Error(`Unable to find or read "${androidManifest}".`); + } + + return parseXml(androidManifestContents) + .catch((err: any) => { + throw new Error(`Unable to parse the "${androidManifest}" file, it could be malformed.`); + }) + .then((parsedAndroidManifest: any) => { + try { + console.log("----- parsedAndroidManifest: " + parsedAndroidManifest); + console.log("----- parsedAndroidManifest.manifest[0]: " + parsedAndroidManifest.manifest[0]); + console.log('----- parsedAndroidManifest.manifest[0]["$"]: ' + parsedAndroidManifest.manifest[0]["$"]); + console.log('----- parsedAndroidManifest.manifest[0]["$"]["android:versionName"]: ' + parsedAndroidManifest.manifest[0]["$"]["android:versionName"]); + return parsedAndroidManifest.manifest[0]["$"]["android:versionName"].match(/^\d+\.\d+\.\d+/)[0]; + } catch (e) { + throw new Error(`Unable to parse the package version from the "${androidManifest}" file.`); + } + }); + } else { + throw new Error(`Unknown platform '${command.platform}' (expected 'ios' or 'android'), can't extract version information.`); + } +} + function printJson(object: any): void { log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); } @@ -1326,6 +1399,99 @@ export var releaseReact = (command: cli.IReleaseReactCommand): Promise => }); } +export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Promise => { + 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 projectPackageJson: any; + try { + projectPackageJson = require(path.join(process.cwd(), "package.json")); + } catch (error) { + throw new Error("Unable to find or read \"package.json\" in the CWD. The \"release-nativescript\" command must be executed in a NativeScript project folder."); + } + + if (!projectPackageJson.nativescript) { + throw new Error("The project in the CWD is not a NativeScript project."); + } + + var platform: string = command.platform.toLowerCase(); + var projectRoot: string = process.cwd(); + var platformFolder: string = path.join(projectRoot, "platforms", platform); + var iOSFolder = path.basename(projectRoot); + var outputFolder: string; + console.log("##### using iOSFolder: " + iOSFolder); + + if (platform === "ios") { + outputFolder = path.join(platformFolder, iOSFolder, "app"); + } else if (platform === "android") { + outputFolder = path.join(platformFolder, "src", "main", "assets", "app"); + } else { + throw new Error("Platform must be either \"android\" or \"ios\"."); + } + + console.log("##### using outputFolder: " + outputFolder); + + if (command.build) { + var nativeScriptCLI: string = "tns"; + // Check whether the NativeScript CLIs is installed, and if not, fail early + try { + which.sync(nativeScriptCLI); + } catch (e) { + throw new Error(`Unable to run "${nativeScriptCLI} ${nativeScriptCommand}". Please ensure that the NativeScript CLI is installed.`); + } + + var nativeScriptCommand: string = "build " + platform; + + if (command.isReleaseBuildType) { + nativeScriptCommand += " --release"; + if (platform === "android") { + if (!command.keystorePath || !command.keystorePassword || !command.keystoreAlias || !command.keystoreAliasPassword) { + throw new Error(`When requesting a release build for Android, these parameters are required: keystorePassword, keystoreAlias and keystoreAliasPassword.`); + } + nativeScriptCommand += ` --key-store-path ${command.keystorePath} --key-store-password ${command.keystorePassword} --key-store-alias ${command.keystoreAlias} --key-store-alias-password ${command.keystoreAliasPassword}`; + } + } + + log(chalk.cyan(`Running "${nativeScriptCLI} ${nativeScriptCommand}" command:\n`)); + try { + execSync([nativeScriptCLI, nativeScriptCommand].join(" "), { stdio: "inherit" }); + } catch (error) { + throw new Error(`Unable to ${nativeScriptCommand} project. Please ensure that the CWD represents a NativeScript project and that the "${platform}" platform was added by running "${nativeScriptCLI} platform add ${platform}".`); + } + + } else { + // if a build was not requested we expect a 'ready to go' ${outputFolder} folder + if (fileDoesNotExistOrIsDirectory(`${outputFolder}`)) { + throw new Error(`No "build" folder found - perform a "tns build" first, or add the "--build" flag to the "codepush" command.`); + } + } + + // TODO remove this + if (fileDoesNotExistOrIsDirectory("blaaaaaa")) { + throw new Error(`xxxx No "build" folder found - perform a "tns build" first, or add the "--build" flag to the "codepush" command.`); + } + + releaseCommand.package = outputFolder; + releaseCommand.type = cli.CommandType.release; + + if (command.appStoreVersion) { + throwForInvalidSemverRange(command.appStoreVersion); + } + + return command.appStoreVersion + ? Q(command.appStoreVersion) + : getNativeScriptProjectAppVersion(command); + }) + .then((appVersion: string) => { + releaseCommand.appStoreVersion = appVersion; + log(chalk.cyan("\nReleasing update contents to CodePush:\n")); + return release(releaseCommand); + }); +} + function rollback(command: cli.IRollbackCommand): Promise { return confirm() .then((wasConfirmed: boolean) => { diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 830ce666..19da3d03 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -438,6 +438,28 @@ var argv = yargs.usage(USAGE_PREFIX + " ") addCommonConfiguration(yargs); }) + .command("release-nativescript", "Release a NativeScript update to an app deployment", (yargs: yargs.Argv) => { + yargs.usage(USAGE_PREFIX + " release-nativescript [options]") + .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments + .example("release-nativescript MyApp ios", "Releases the NativeScript iOS project in the current working directory to the \"MyApp\" app's \"Staging\" deployment") + .example("release-nativescript MyApp android -d Production", "Releases the NativeScript Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") + .option("build", { alias: "b", default: false, demand: false, description: "Invoke \"tns build\" instead of assuming there's aleady a build waiting to be pushed", type: "boolean" }) + .option("isReleaseBuildType", { alias: "rb", default: false, demand: false, description: "If \"build\" option is true specifies whether to perform a release build", type: "boolean" }) + .option("keystorePath", { alias: "ksp", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the path to the .keystore file", type: "string" }) + .option("keystorePassword", { alias: "kspw", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the .keystore file", type: "string" }) + .option("keystoreAlias", { alias: "ksa", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the alias in the .keystore file", type: "string" }) + .option("keystoreAliasPassword", { alias: "ksapwd", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the alias in the .keystore file", type: "string" }) + .option("deploymentName", { alias: "d", default: "Staging", demand: false, description: "Deployment to release the update to", type: "string" }) + .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app in this release", type: "string" }) + .option("disabled", { alias: "x", default: false, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) + .option("mandatory", { alias: "m", default: false, demand: false, description: "Specifies whether this release should be considered mandatory", type: "boolean" }) + .option("noDuplicateReleaseError", { default: false, demand: false, description: "When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error", type: "boolean" }) + .option("rollout", { alias: "r", default: "100%", demand: false, description: "Percentage of users this release should be immediately available to", 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 config.xml file.", type: "string" }) + .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); + + addCommonConfiguration(yargs); + }) .command("rollback", "Rollback the latest release for an app deployment", (yargs: yargs.Argv) => { yargs.usage(USAGE_PREFIX + " rollback [options]") .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments @@ -838,6 +860,27 @@ function createCommand(): cli.ICommand { } break; + case "release-nativescript": + if (arg1 && arg2) { + cmd = { type: cli.CommandType.releaseNativeScript }; + + var releaseNativeScriptCommand = cmd; + + releaseNativeScriptCommand.appName = arg1; + releaseNativeScriptCommand.platform = arg2; + + releaseNativeScriptCommand.build = argv["build"]; + releaseNativeScriptCommand.deploymentName = argv["deploymentName"]; + releaseNativeScriptCommand.description = argv["description"] ? backslash(argv["description"]) : ""; + releaseNativeScriptCommand.disabled = argv["disabled"]; + releaseNativeScriptCommand.mandatory = argv["mandatory"]; + releaseNativeScriptCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"]; + releaseNativeScriptCommand.rollout = getRolloutValue(argv["rollout"]); + releaseNativeScriptCommand.appStoreVersion = argv["targetBinaryVersion"]; + releaseNativeScriptCommand.isReleaseBuildType = argv["isReleaseBuildType"]; + } + break; + case "rollback": if (arg1 && arg2) { cmd = { type: cli.CommandType.rollback }; diff --git a/cli/test/cli.ts b/cli/test/cli.ts index 976b4a6e..60e63b36 100644 --- a/cli/test/cli.ts +++ b/cli/test/cli.ts @@ -23,6 +23,12 @@ function ensureInTestAppDirectory(): void { } } +function ensureNotInTestAppDirectory(): void { + if (!~__dirname.indexOf("/resources")) { + process.chdir(__dirname + "/resources"); + } +} + function isDefined(object: any): boolean { return object !== undefined && object !== null; } @@ -1737,6 +1743,164 @@ describe("CLI", () => { .done(); }); + it("release-nativescript fails if CWD does not contain a package.json", (done: MochaDone): void => { + var command: cli.IReleaseNativeScriptCommand = { + type: cli.CommandType.releaseNativeScript, + appName: "a", + appStoreVersion: null, + deploymentName: "Staging", + description: "Test invalid folder", + mandatory: false, + rollout: null, + platform: "ios" + }; + + ensureNotInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); + var releaseNativeScript: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseNativeScript"); + + cmdexec.execute(command) + .then(() => { + done(new Error("Did not throw error.")); + }) + .catch((err) => { + assert.equal(err.message, "Unable to find or read \"package.json\" in the CWD. The \"release-nativescript\" command must be executed in a NativeScript project folder."); + sinon.assert.notCalled(release); + sinon.assert.notCalled(spawn); + done(); + }) + .done(); + }); + + it("release-nativescript fails if platform is invalid", (done: MochaDone): void => { + var command: cli.IReleaseNativeScriptCommand = { + type: cli.CommandType.releaseNativeScript, + appName: "a", + appStoreVersion: null, + deploymentName: "Staging", + description: "Test invalid platform", + mandatory: false, + rollout: null, + platform: "blackberry", + }; + + ensureInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.spy(cmdexec, "release"); + var releaseNativeScript: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseNativeScript"); + + cmdexec.execute(command) + .then(() => { + done(new Error("Did not throw error.")); + }) + .catch((err) => { + assert.equal(err.message, "Platform must be either \"android\" or \"ios\"."); + sinon.assert.notCalled(release); + sinon.assert.notCalled(spawn); + done(); + }) + .done(); + }); + + it("release-nativescript fails if the platforms/app build folder can't be found and the --build switch was false", (done: MochaDone): void => { + var bundleName = "bundle.js"; + var command: cli.IReleaseNativeScriptCommand = { + type: cli.CommandType.releaseNativeScript, + appName: "a", + appStoreVersion: null, + deploymentName: "Staging", + description: "Test no build folder", + mandatory: false, + rollout: null, + build: false, + platform: "android" + }; + + ensureInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); + var releaseNativeScript: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseNativeScript"); + + cmdexec.execute(command) + .then(() => { + done(new Error("Did not throw error.")); + }) + .catch((err) => { + assert.equal(err.message, "No \"build\" folder found - perform a \"tns build\" first, or add the \"--build\" flag to the \"codepush\" command."); + sinon.assert.notCalled(release); + sinon.assert.notCalled(spawn); + done(); + }) + .done(); + }); + + it("release-nativescript fails if a release build was requested for Android without the keystore switches", (done: MochaDone): void => { + var bundleName = "bundle.js"; + var command: cli.IReleaseNativeScriptCommand = { + type: cli.CommandType.releaseNativeScript, + appName: "a", + appStoreVersion: null, + deploymentName: "Staging", + description: "Test no build folder", + mandatory: false, + rollout: null, + build: true, + isReleaseBuildType: true, + platform: "android" + }; + + ensureInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); + var releaseNativeScript: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseNativeScript"); + + cmdexec.execute(command) + .then(() => { + done(new Error("Did not throw error.")); + }) + .catch((err) => { + assert.equal(err.message, "When requesting a release build for Android, these parameters are required: keystorePassword, keystoreAlias and keystoreAliasPassword."); + sinon.assert.notCalled(release); + sinon.assert.notCalled(spawn); + done(); + }) + .done(); + }); + + it("release-nativescript fails if targetBinaryRange is not a valid semver range expression", (done: MochaDone): void => { + var bundleName = "bundle.js"; + var command: cli.IReleaseNativeScriptCommand = { + type: cli.CommandType.releaseNativeScript, + appName: "a", + appStoreVersion: "notsemver", + deploymentName: "Staging", + description: "Test uses targetBinaryRange", + mandatory: false, + rollout: null, + build: true, + isReleaseBuildType: false, + platform: "android" + }; + + ensureInTestAppDirectory(); + + var release: Sinon.SinonSpy = sandbox.stub(cmdexec, "release", () => { return Q(null) }); + var releaseNativeScript: Sinon.SinonSpy = sandbox.spy(cmdexec, "releaseNativeScript"); + + cmdexec.execute(command) + .then(() => { + done(new Error("Did not throw error.")); + }) + .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.notCalled(spawn); + done(); + }) + .done(); + }); + it("sessionList lists session name and expires fields", (done: MochaDone): void => { var command: cli.IAccessKeyListCommand = { type: cli.CommandType.sessionList, diff --git a/cli/test/resources/TestApp/package.json b/cli/test/resources/TestApp/package.json index 06c4003c..0de07255 100644 --- a/cli/test/resources/TestApp/package.json +++ b/cli/test/resources/TestApp/package.json @@ -2,5 +2,13 @@ "name": "TestApp", "dependencies": { "react-native": "0.19.0" + }, + "nativescript": { + "tns-android": { + "version": "3.0.0" + }, + "tns-ios": { + "version": "3.0.0" + } } } From 1e76ecd11c1708c22e50b88909b97acf22cc7ffe Mon Sep 17 00:00:00 2001 From: EddyVerbruggen Date: Mon, 8 May 2017 17:07:18 +0200 Subject: [PATCH 2/3] Added NativeScript --- cli/README.md | 111 +++++++++++++++++++++++++++++++-- cli/script/command-executor.ts | 29 +++------ cli/script/command-parser.ts | 12 ++-- cli/test/cli.ts | 2 +- 4 files changed, 126 insertions(+), 28 deletions(-) diff --git a/cli/README.md b/cli/README.md index b8243617..ff89f08a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,6 @@ # CodePush Management CLI -CodePush is a cloud service that enables Cordova and React Native developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using the provided client SDKs for [Cordova](http://github.com/Microsoft/cordova-plugin-code-push) and [React Native](http://github.com/Microsoft/react-native-code-push)). This allows you to have a more deterministic and direct engagement model with your user base, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. +CodePush is a cloud service that enables Cordova and React Native developers to deploy mobile app updates directly to their users' devices. It works by acting as a central repository that developers can publish updates to (JS, HTML, CSS and images), and that apps can query for updates from (using the provided client SDKs for [Cordova](http://github.com/Microsoft/cordova-plugin-code-push), [React Native](http://github.com/Microsoft/react-native-code-push) and [NativeScript](http://github.com/EddyVerbruggen/nativescript-code-push)). This allows you to have a more deterministic and direct engagement model with your user base, when addressing bugs and/or adding small features that don't require you to re-build a binary and re-distribute it through the respective app stores. ![CodePush CLI](https://cloud.githubusercontent.com/assets/116461/16246693/2e7df77c-37bb-11e6-9456-e392af7f7b84.png) @@ -325,8 +325,8 @@ It's important that the path you specify refers to the platform-specific, prepar | React Native w/assets (Android) | `react-native bundle --platform android --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | | React Native wo/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output --dev false` | Value of the `--bundle-output` option | | React Native w/assets (iOS) | `react-native bundle --platform ios --entry-file --bundle-output / --assets-dest --dev false` | Value of the `--assets-dest` option, which should represent a newly created directory that includes your assets and JS bundle | -| NativeScript (iOS) | `tns build ios [--release]` | `./platforms/ios/` directory | -| NativeScript (Android) | `tns build android [--release]` | `./platforms/android/src/main/assets` directory | +| NativeScript (iOS) | `tns build ios [--release]` | `./platforms/ios//app` directory | +| NativeScript (Android) | `tns build android [--release]` | `./platforms/android/src/main/assets/app` directory | #### Target binary version parameter @@ -658,7 +658,110 @@ This is the same parameter as the one described in the [above section](#rollout- This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, the command defaults to targeting only the specified version in the project's metadata (`Info.plist` if this update is for iOS clients, and `build.gradle` for Android clients). ### Releasing Updates (NativeScript) -TODO +TODO (last task before a PR can be sent) + + +```shell +code-push release-nativescript +[--build] +[--deploymentName ] +[--description ] +[--isReleaseBuildType] +[--keystorePath] +[--keystorePassword] +[--keystoreAlias] +[--keystoreAliasPassword] +[--mandatory] +[--noDuplicateReleaseError] +[--rollout ] +[--targetBinaryVersion ] +``` + +The `release-nativescript` command is a NativeScript-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: + +1. Running the `tns build` command in order to generate the [update contents](#update-contents-parameter) (`/platform`'s app folder) that will be released to the CodePush server. + +2. Inferring the [`targetBinaryVersion`](#target-binary-version-parameter) of this release by using the version name that is specified in your project's `app/App_Resources/iOS/Info.plist` (iOS) or `app/App_Resources/Android/AndroidManifest.xml` (Android) file. + +To illustrate the difference that the `release-nativescript` command can make, the following is an example of how you might generate and release an update for a NativeScript app using the "vanilla" `release` command: + +```shell +tns build ios --release +code-push release MyApp-iOS platforms/ios/myapp/app 1.0.0 +``` + +Achieving the equivalent behavior with the `release-nativescript` command would simply require the following command, which is generally less error-prone: + +```shell +code-push release-nativescript MyApp-iOS ios +``` + +#### App name parameter + +This is the same parameter as the one described in the [above section](#app-name-parameter). + +#### Platform parameter + +This specifies which platform the current update is targeting, and can be either `ios` or `android` (case-insensitive). + +#### Build parameter + +Specifies whether you want to run `tns build` instead of publishing anything already in the platform's `app` folder (which is the default behavior). + +*NOTE: If you build your app differently (Webpack for instance) do your specialized build as usual and omit this parameter.* + +*NOTE: This parameter can be set using either --build or -b* + +#### Deployment name parameter + +This is the same parameter as the one described in the [above section](#deployment-name-parameter). + +#### Description parameter + +This is the same parameter as the one described in the [above section](#description-parameter). + +#### Disabled parameter + +This is the same parameter as the one described in the [above section](#disabled-parameter). + +#### IsReleaseBuildType parameter + +If `build` option is true specifies whether perform a release build. If left unspecified, this defaults to `debug`. + +*NOTE: If you use TypeScript this flag will also remove any `.ts` files from your distributed package, which is probably what you want.* + +#### keystorePath parameter + +If `isReleaseBuildType` option is true and `platform` is `android` specifies the path to the .keystore file. + +#### keystorePassword parameter + +If `isReleaseBuildType` option is true and `platform` is `android` specifies the password for the .keystore file. + +#### keystoreAlias parameter + +If `isReleaseBuildType` option is true and `platform` is `android` specifies the alias in the .keystore file. + +#### keystoreAliasPassword parameter + +If `isReleaseBuildType` option is true and `platform` is `android` specifies the password for the alias in the .keystore file. + +#### Mandatory parameter + +This is the same parameter as the one described in the [above section](#mandatory-parameter). + +#### No duplicate release error parameter + +This is the same parameter as the one described in the [above section](#no-duplicate-release-error-parameter). + +#### Rollout parameter + +This is the same parameter as the one described in the [above section](#rollout-parameter). If left unspecified, the release will be made available to all users. + +#### Target binary version parameter + +This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, the command defaults to targeting only the specified version in the project's metadata (`Info.plist` if this update is for iOS clients, and `build.gradle` for Android clients). + ## Debugging CodePush Integration diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index f90040e7..74e62847 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -1066,11 +1066,8 @@ function getNativeScriptProjectAppVersion(command: cli.IReleaseReactCommand): Pr }) .then((parsedAndroidManifest: any) => { try { - console.log("----- parsedAndroidManifest: " + parsedAndroidManifest); - console.log("----- parsedAndroidManifest.manifest[0]: " + parsedAndroidManifest.manifest[0]); - console.log('----- parsedAndroidManifest.manifest[0]["$"]: ' + parsedAndroidManifest.manifest[0]["$"]); - console.log('----- parsedAndroidManifest.manifest[0]["$"]["android:versionName"]: ' + parsedAndroidManifest.manifest[0]["$"]["android:versionName"]); - return parsedAndroidManifest.manifest[0]["$"]["android:versionName"].match(/^\d+\.\d+\.\d+/)[0]; + var version = parsedAndroidManifest.manifest["$"]["android:versionName"]; + return version.match(/^[0-9.]+/)[0]; } catch (e) { throw new Error(`Unable to parse the package version from the "${androidManifest}" file.`); } @@ -1422,7 +1419,6 @@ export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Pro var platformFolder: string = path.join(projectRoot, "platforms", platform); var iOSFolder = path.basename(projectRoot); var outputFolder: string; - console.log("##### using iOSFolder: " + iOSFolder); if (platform === "ios") { outputFolder = path.join(platformFolder, iOSFolder, "app"); @@ -1432,7 +1428,9 @@ export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Pro throw new Error("Platform must be either \"android\" or \"ios\"."); } - console.log("##### using outputFolder: " + outputFolder); + if (command.appStoreVersion) { + throwForInvalidSemverRange(command.appStoreVersion); + } if (command.build) { var nativeScriptCLI: string = "tns"; @@ -1449,9 +1447,9 @@ export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Pro nativeScriptCommand += " --release"; if (platform === "android") { if (!command.keystorePath || !command.keystorePassword || !command.keystoreAlias || !command.keystoreAliasPassword) { - throw new Error(`When requesting a release build for Android, these parameters are required: keystorePassword, keystoreAlias and keystoreAliasPassword.`); + throw new Error(`When requesting a release build for Android, these parameters are required: keystorePath, keystorePassword, keystoreAlias and keystoreAliasPassword.`); } - nativeScriptCommand += ` --key-store-path ${command.keystorePath} --key-store-password ${command.keystorePassword} --key-store-alias ${command.keystoreAlias} --key-store-alias-password ${command.keystoreAliasPassword}`; + nativeScriptCommand += ` --key-store-path "${command.keystorePath}" --key-store-password ${command.keystorePassword} --key-store-alias ${command.keystoreAlias} --key-store-alias-password ${command.keystoreAliasPassword}`; } } @@ -1464,23 +1462,16 @@ export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Pro } else { // if a build was not requested we expect a 'ready to go' ${outputFolder} folder - if (fileDoesNotExistOrIsDirectory(`${outputFolder}`)) { + try { + fs.lstatSync(outputFolder).isDirectory(); + } catch (error) { throw new Error(`No "build" folder found - perform a "tns build" first, or add the "--build" flag to the "codepush" command.`); } } - // TODO remove this - if (fileDoesNotExistOrIsDirectory("blaaaaaa")) { - throw new Error(`xxxx No "build" folder found - perform a "tns build" first, or add the "--build" flag to the "codepush" command.`); - } - releaseCommand.package = outputFolder; releaseCommand.type = cli.CommandType.release; - if (command.appStoreVersion) { - throwForInvalidSemverRange(command.appStoreVersion); - } - return command.appStoreVersion ? Q(command.appStoreVersion) : getNativeScriptProjectAppVersion(command); diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 19da3d03..bd47b07b 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -445,10 +445,10 @@ var argv = yargs.usage(USAGE_PREFIX + " ") .example("release-nativescript MyApp android -d Production", "Releases the NativeScript Android project in the current working directory to the \"MyApp\" app's \"Production\" deployment") .option("build", { alias: "b", default: false, demand: false, description: "Invoke \"tns build\" instead of assuming there's aleady a build waiting to be pushed", type: "boolean" }) .option("isReleaseBuildType", { alias: "rb", default: false, demand: false, description: "If \"build\" option is true specifies whether to perform a release build", type: "boolean" }) - .option("keystorePath", { alias: "ksp", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the path to the .keystore file", type: "string" }) - .option("keystorePassword", { alias: "kspw", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the .keystore file", type: "string" }) - .option("keystoreAlias", { alias: "ksa", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the alias in the .keystore file", type: "string" }) - .option("keystoreAliasPassword", { alias: "ksapwd", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the alias in the .keystore file", type: "string" }) + .option("keystorePath", { alias: "kp", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the path to the .keystore file", type: "string" }) + .option("keystorePassword", { alias: "kpw", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the .keystore file", type: "string" }) + .option("keystoreAlias", { alias: "ka", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the alias in the .keystore file", type: "string" }) + .option("keystoreAliasPassword", { alias: "kapw", default: null, demand: false, description: "If \"isReleaseBuildType\" option is true and \"platform\" is \"android\" specifies the password for the alias in the .keystore file", type: "string" }) .option("deploymentName", { alias: "d", default: "Staging", demand: false, description: "Deployment to release the update to", type: "string" }) .option("description", { alias: "des", default: null, demand: false, description: "Description of the changes made to the app in this release", type: "string" }) .option("disabled", { alias: "x", default: false, demand: false, description: "Specifies whether this release should be immediately downloadable", type: "boolean" }) @@ -878,6 +878,10 @@ function createCommand(): cli.ICommand { releaseNativeScriptCommand.rollout = getRolloutValue(argv["rollout"]); releaseNativeScriptCommand.appStoreVersion = argv["targetBinaryVersion"]; releaseNativeScriptCommand.isReleaseBuildType = argv["isReleaseBuildType"]; + releaseNativeScriptCommand.keystorePath = argv["keystorePath"]; + releaseNativeScriptCommand.keystorePassword = argv["keystorePassword"]; + releaseNativeScriptCommand.keystoreAlias = argv["keystoreAlias"]; + releaseNativeScriptCommand.keystoreAliasPassword = argv["keystoreAliasPassword"]; } break; diff --git a/cli/test/cli.ts b/cli/test/cli.ts index 60e63b36..15dd2397 100644 --- a/cli/test/cli.ts +++ b/cli/test/cli.ts @@ -1860,7 +1860,7 @@ describe("CLI", () => { done(new Error("Did not throw error.")); }) .catch((err) => { - assert.equal(err.message, "When requesting a release build for Android, these parameters are required: keystorePassword, keystoreAlias and keystoreAliasPassword."); + assert.equal(err.message, "When requesting a release build for Android, these parameters are required: keystorePath, keystorePassword, keystoreAlias and keystoreAliasPassword."); sinon.assert.notCalled(release); sinon.assert.notCalled(spawn); done(); From 9adf7e89d80f8d99c287faec8f955353fd1b2af7 Mon Sep 17 00:00:00 2001 From: EddyVerbruggen Date: Tue, 9 May 2017 21:37:40 +0200 Subject: [PATCH 3/3] Forgot to remove a TODO --- cli/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index ff89f08a..af0c8ced 100644 --- a/cli/README.md +++ b/cli/README.md @@ -658,8 +658,6 @@ This is the same parameter as the one described in the [above section](#rollout- This is the same parameter as the one described in the [above section](#target-binary-version-parameter). If left unspecified, the command defaults to targeting only the specified version in the project's metadata (`Info.plist` if this update is for iOS clients, and `build.gradle` for Android clients). ### Releasing Updates (NativeScript) -TODO (last task before a PR can be sent) - ```shell code-push release-nativescript