UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

184 lines (147 loc) 7.66 kB
import { AppCommand, CommandResult, ErrorCodes, failure, hasArg, help, longName, shortName, success, defaultValue } from "../../../util/commandline"; import { CommandArgs } from "../../../util/commandline/command"; import { AppCenterClient, models, clientRequest } from "../../../util/apis"; import { out } from "../../../util/interaction"; import { getUser, DefaultApp } from "../../../util/profile/index"; import { inspect } from "util"; import * as fs from "fs"; import * as pfs from "../../../util/misc/promisfied-fs"; import * as chalk from "chalk"; import { sign, zip } from "../lib/update-contents-tasks"; import { isBinaryOrZip, getLastFolderInPath, moveReleaseFilesInTmpFolder, isDirectory } from "../lib/file-utils"; import { environments } from "../lib/environment"; import { isValidRange, isValidRollout, isValidDeployment } from "../lib/validation-utils"; import { LegacyCodePushRelease } from "../lib/release-strategy/index"; import { getTokenFromEnvironmentVar } from "../../../util/profile/environment-vars"; const debug = require("debug")("appcenter-cli:commands:codepush:release-skeleton"); export interface ReleaseStrategy { release(client: AppCenterClient, app: DefaultApp, deploymentName: string, updateContentsZipPath: string, updateMetadata: { appVersion?: string; description?: string; isDisabled?: boolean; isMandatory?: boolean; rollout?: number; }, token?: string, serverUrl?: string): Promise<void>; } export default class CodePushReleaseCommandSkeleton extends AppCommand { @help("Deployment to release the update to") @shortName("d") @longName("deployment-name") @defaultValue("Staging") @hasArg public specifiedDeploymentName: string; @help("Description of the changes made to the app in this release") @longName("description") @hasArg public description: string; @help("Specifies whether this release should be immediately downloadable") @shortName("x") @longName("disabled") public disabled: boolean; @help("Specifies whether this release should be considered mandatory") @shortName("m") @longName("mandatory") public mandatory: boolean; @help("Specifies the location of a RSA private key to sign the release with." + chalk.yellow("NOTICE:") + " use it for react native applications only, client SDK on other platforms will be ignoring signature verification for now!") @shortName("k") @longName("private-key-path") @hasArg public privateKeyPath: string; @help("When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error") @longName("disable-duplicate-release-error") public disableDuplicateReleaseError: boolean; @help("Percentage of users this release should be available to") @shortName("r") @longName("rollout") @defaultValue("100") @hasArg public specifiedRollout: string; protected rollout: number; // We assume that if this field is assigned than it is already validated (help us not to validate twice). protected deploymentName: string; protected updateContentsPath: string; protected targetBinaryVersion: string; private readonly releaseStrategy: ReleaseStrategy; constructor(args: CommandArgs) { super(args); // Сurrently use old service due to we have limitation of 1MB payload limit through bifrost service this.releaseStrategy = new LegacyCodePushRelease(); } public async run(client: AppCenterClient): Promise<CommandResult> { throw new Error("For dev purposes only!"); } protected async release(client: AppCenterClient): Promise<CommandResult> { this.rollout = Number(this.specifiedRollout); const validationResult: CommandResult = await this.validate(client); if (!validationResult.succeeded) { return validationResult; } this.deploymentName = this.specifiedDeploymentName; if (this.privateKeyPath) { const appInfo = (await out.progress("Getting app info...", clientRequest<models.AppResponse>( (cb) => client.apps.get(this.app.ownerName, this.app.appName, cb)))).result; const platform = appInfo.platform.toLowerCase(); // In React-Native case we should add "CodePush" name folder as root for relase files for keeping sync with React Native client SDK. // Also single file also should be in "CodePush" folder. if (platform === "react-native" && (getLastFolderInPath(this.updateContentsPath) !== "CodePush" || !isDirectory(this.updateContentsPath))) { await moveReleaseFilesInTmpFolder(this.updateContentsPath).then((tmpPath: string) => { this.updateContentsPath = tmpPath; }); } await sign(this.privateKeyPath, this.updateContentsPath); } const updateContentsZipPath = await zip(this.updateContentsPath); try { const app = this.app; const serverUrl = this.getServerUrl(); const token = this.token || getTokenFromEnvironmentVar() || await getUser().accessToken; await out.progress("Creating CodePush release...", this.releaseStrategy.release(client, app, this.deploymentName, updateContentsZipPath, { appVersion: this.targetBinaryVersion, description: this.description, isDisabled: this.disabled, isMandatory: this.mandatory, rollout: this.rollout }, token, serverUrl)); out.text(`Successfully released an update containing the "${this.updateContentsPath}" ` + `${fs.lstatSync(this.updateContentsPath).isDirectory() ? "directory" : "file"}` + ` to the "${this.deploymentName}" deployment of the "${this.app.appName}" app.`); return success(); } catch (error) { if (error.response && error.response.statusCode === 409 && this.disableDuplicateReleaseError) { // 409 (Conflict) status code means that uploaded package is identical // to the contents of the specified deployment's current release console.warn(chalk.yellow("[Warning] " + error.response.body)); return success(); } else { debug(`Failed to release a CodePush update - ${inspect(error)}`); return failure(ErrorCodes.Exception, error.response ? error.response.body : error); } } finally { await pfs.rmDir(updateContentsZipPath); } } private getServerUrl(): string | undefined { const environment = environments(this.getEnvironmentName()); return environment && environment.managementEndpoint; } private getEnvironmentName(): string | undefined { if (this.environmentName) { return this.environmentName; } const user = getUser(); if (user) { return user.environment; } } private async validate(client: AppCenterClient): Promise<CommandResult> { if (isBinaryOrZip(this.updateContentsPath)) { return failure(ErrorCodes.InvalidParameter, "It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."); } if (!isValidRange(this.targetBinaryVersion)) { return failure(ErrorCodes.InvalidParameter, "Invalid binary version(s) for a release."); } if (!Number.isSafeInteger(this.rollout) || !isValidRollout(this.rollout)) { return failure(ErrorCodes.InvalidParameter, `Rollout value should be integer value between ${chalk.bold("0")} or ${chalk.bold("100")}.`); } if (!this.deploymentName && !(await isValidDeployment(client, this.app, this.specifiedDeploymentName))) { return failure(ErrorCodes.InvalidParameter, `Deployment "${this.specifiedDeploymentName}" does not exist.`); } return success(); } }