appcenter-cli
Version:
Command line tool for Visual Studio App Center
184 lines (147 loc) • 7.66 kB
text/typescript
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 {
public specifiedDeploymentName: string;
public description: string;
public disabled: boolean;
public mandatory: boolean;
public privateKeyPath: string;
public disableDuplicateReleaseError: boolean;
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();
}
}