Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
135 changes: 123 additions & 12 deletions cli/README.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions cli/definitions/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
release,
releaseCordova,
releaseReact,
releaseNativeScript,
rollback,
sessionList,
sessionRemove,
Expand Down Expand Up @@ -205,6 +206,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;
Expand Down
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"push",
"cordova",
"react-native",
"react"
"react",
"nativescript"
],
"homepage": "https://microsoft.github.io/code-push",
"author": "Microsoft Corporation",
Expand Down
157 changes: 157 additions & 0 deletions cli/script/command-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,9 @@ export function execute(command: cli.ICommand): Promise<void> {
case cli.CommandType.releaseReact:
return releaseReact(<cli.IReleaseReactCommand>command);

case cli.CommandType.releaseNativeScript:
return releaseNativeScript(<cli.IReleaseNativeScriptCommand>command);

case cli.CommandType.rollback:
return rollback(<cli.IRollbackCommand>command);

Expand Down Expand Up @@ -1061,6 +1064,73 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj
}
}

function getNativeScriptProjectAppVersion(command: cli.IReleaseReactCommand): Promise<string> {
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 {
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.`);
}
});
} 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));
}
Expand Down Expand Up @@ -1381,6 +1451,93 @@ export var releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =>
});
}

export var releaseNativeScript = (command: cli.IReleaseNativeScriptCommand): Promise<void> => {
var releaseCommand: cli.IReleaseCommand = <any>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;

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\".");
}

if (command.appStoreVersion) {
throwForInvalidSemverRange(command.appStoreVersion);
}

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: 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}`;
}
}

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
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.`);
}
}

releaseCommand.package = outputFolder;
releaseCommand.type = cli.CommandType.release;

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 validateDeployment(appName: string, deploymentName: string): Promise<void> {
return sdk.getDeployment(appName, deploymentName)
.catch((err: any) => {
Expand Down
47 changes: 47 additions & 0 deletions cli/script/command-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,28 @@ var argv = yargs.usage(USAGE_PREFIX + " <command>")

addCommonConfiguration(yargs);
})
.command("release-nativescript", "Release a NativeScript update to an app deployment", (yargs: yargs.Argv) => {
yargs.usage(USAGE_PREFIX + " release-nativescript <appName> <platform> [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: "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" })
.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 <appName> <deploymentName> [options]")
.demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments
Expand Down Expand Up @@ -858,6 +880,31 @@ function createCommand(): cli.ICommand {
}
break;

case "release-nativescript":
if (arg1 && arg2) {
cmd = { type: cli.CommandType.releaseNativeScript };

var releaseNativeScriptCommand = <cli.IReleaseNativeScriptCommand>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"];
releaseNativeScriptCommand.keystorePath = argv["keystorePath"];
releaseNativeScriptCommand.keystorePassword = argv["keystorePassword"];
releaseNativeScriptCommand.keystoreAlias = argv["keystoreAlias"];
releaseNativeScriptCommand.keystoreAliasPassword = argv["keystoreAliasPassword"];
}
break;

case "rollback":
if (arg1 && arg2) {
cmd = { type: cli.CommandType.rollback };
Expand Down
Loading