UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

533 lines (532 loc) 27.3 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const commandline_1 = require("../../util/commandline"); const interaction_1 = require("../../util/interaction"); const util_1 = require("util"); const _ = require("lodash"); const Path = require("path"); const Pfs = require("../../util/misc/promisfied-fs"); const profile_1 = require("../../util/profile"); const ac_fus_api_1 = require("./lib/ac-fus-api"); const distribute_util_1 = require("./lib/distribute-util"); const environment_vars_1 = require("../../util/profile/environment-vars"); const appcenter_file_upload_client_node_1 = require("appcenter-file-upload-client-node"); const environments_1 = require("../../util/profile/environments"); const debug = require("debug")("appcenter-cli:commands:distribute:release"); let ReleaseBinaryCommand = class ReleaseBinaryCommand extends commandline_1.AppCommand { run(client) { return __awaiter(this, void 0, void 0, function* () { const app = this.app; this.validateParameters(); debug("Loading prerequisites"); const [distributionGroupUsersCount, storeInformation, releaseNotesString] = yield interaction_1.out.progress("Loading prerequisites...", this.getPrerequisites(client)); this.validateParametersWithPrerequisites(storeInformation); const createdReleaseUpload = yield this.createReleaseUpload(client, app); const releaseId = yield this.uploadFile(createdReleaseUpload, app); yield this.uploadReleaseNotes(releaseNotesString, client, app, releaseId); yield this.distributeToGroups(client, app, releaseId); yield this.distributeToStore(storeInformation, client, app, releaseId); yield this.checkReleaseOnThePortal(distributionGroupUsersCount, client, app, releaseId); return commandline_1.success(); }); } uploadFile(releaseUploadParams, app) { return __awaiter(this, void 0, void 0, function* () { const uploadId = releaseUploadParams.id; const assetId = releaseUploadParams.package_asset_id; const urlEncodedToken = releaseUploadParams.url_encoded_token; const uploadDomain = releaseUploadParams.upload_domain; try { yield interaction_1.out.progress("Uploading release binary...", this.uploadFileToUri(assetId, urlEncodedToken, uploadDomain)); yield interaction_1.out.progress("Finishing the upload...", this.patchUpload(app, uploadId)); return yield interaction_1.out.progress("Checking the uploaded file...", this.loadReleaseIdUntilSuccess(app, uploadId)); } catch (error) { interaction_1.out.text("Release upload failed"); throw commandline_1.failure(commandline_1.ErrorCodes.Exception, error.message); } }); } uploadReleaseNotes(releaseNotesString, client, app, releaseId) { return __awaiter(this, void 0, void 0, function* () { if (releaseNotesString && releaseNotesString.length > 0) { debug("Setting release notes"); yield this.putReleaseDetails(client, app, releaseId, releaseNotesString); } else { debug("Skipping empty release notes"); } }); } distributeToGroups(client, app, releaseId) { return __awaiter(this, void 0, void 0, function* () { if (!_.isNil(this.distributionGroup)) { debug("Distributing the release to group(s)"); const groups = distribute_util_1.parseDistributionGroups(this.distributionGroup); for (const group of groups) { const distributionGroupResponse = yield distribute_util_1.getDistributionGroup({ client, releaseId, app: this.app, destination: group, destinationType: "group", }); yield distribute_util_1.addGroupToRelease({ client, releaseId, distributionGroup: distributionGroupResponse, app: this.app, destination: group, destinationType: "group", mandatory: this.mandatory, silent: this.silent, }); } } }); } distributeToStore(storeInformation, client, app, releaseId) { return __awaiter(this, void 0, void 0, function* () { if (!_.isNil(storeInformation)) { debug("Distributing the release to a store"); try { yield this.publishToStore(client, app, storeInformation, releaseId); } catch (error) { if (!_.isNil(this.distributionGroup)) { interaction_1.out.text(`Release was successfully distributed to group(s) '${distribute_util_1.printGroups(this.distributionGroup)}' but could not be published to store '${this.storeName}'.`); } throw error; } } }); } checkReleaseOnThePortal(distributionGroupUsersCount, client, app, releaseId) { return __awaiter(this, void 0, void 0, function* () { debug("Retrieving the release"); const releaseDetails = yield this.getDistributeRelease(client, app, releaseId); if (releaseDetails) { if (!_.isNil(this.distributionGroup)) { const storeComment = !_.isNil(this.storeName) ? ` and to store '${this.storeName}'` : ""; if (_.isNull(distributionGroupUsersCount)) { interaction_1.out.text((rd) => `Release ${rd.shortVersion} (${rd.version}) was successfully released to ${distribute_util_1.printGroups(this.distributionGroup)}${storeComment}`, releaseDetails); } else { interaction_1.out.text((rd) => `Release ${rd.shortVersion} (${rd.version}) was successfully released to ${distributionGroupUsersCount} testers in ${distribute_util_1.printGroups(this.distributionGroup)}${storeComment}`, releaseDetails); } } else { interaction_1.out.text((rd) => `Release ${rd.shortVersion} (${rd.version}) was successfully released to store '${this.storeName}'`, releaseDetails); } } else { interaction_1.out.text(`Release was successfully released.`); } }); } validateParameters() { debug("Checking for invalid parameter combinations"); if (!_.isNil(this.releaseNotes) && !_.isNil(this.releaseNotesFile)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, "'--release-notes' and '--release-notes-file' switches are mutually exclusive"); } if (_.isNil(this.distributionGroup) && _.isNil(this.storeName)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, "At least one of '--group' or '--store' must be specified"); } if (!_.isNil(this.storeName)) { if (![".aab", ".apk", ".ipa"].includes(this.fileExtension)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `Files of type '${this.fileExtension}' can not be distributed to stores`); } } if (_.isNil(this.buildVersion)) { if ([".zip", ".msi"].includes(this.fileExtension)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `--build-version parameter must be specified when uploading ${this.fileExtension} files`); } } if (_.isNil(this.buildNumber) || _.isNil(this.buildVersion)) { if ([".pkg", ".dmg"].includes(this.fileExtension)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `--build-version and --build-number must both be specified when uploading ${this.fileExtension} files`); } } if (!_.isNil(this.filePath)) { const binary = new appcenter_file_upload_client_node_1.ACFile(this.filePath); if (!binary || binary.size <= 0) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `File '${this.filePath}' does not exist.`); } } if (!_.isNil(this.timeout)) { if (!(Number.parseInt(this.timeout, 10) >= 0)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `--timeout must be an unsigned int value`); } } } validateParametersWithPrerequisites(storeInformation) { debug("Checking for invalid parameter combinations with prerequisites"); if (storeInformation && storeInformation.type === "apple" && _.isNil(this.releaseNotes) && _.isNil(this.releaseNotesFile)) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, "At least one of '--release-notes' or '--release-notes-file' must be specified when publishing to an Apple store."); } } getPrerequisites(client) { return __awaiter(this, void 0, void 0, function* () { // load release notes file or use provided release notes if none was specified const releaseNotesString = this.getReleaseNotesString(); let distributionGroupUsersNumber; let storeInformation; if (!_.isNil(this.distributionGroup)) { // get number of users in distribution group(s) (and check each distribution group existence) // return null if request has failed because of any reason except non-existing group name. distributionGroupUsersNumber = this.getDistributionGroupUsersNumber(client); } if (!_.isNil(this.storeName)) { // get distribution store type to check existence and further filtering storeInformation = this.getStoreDetails(client); } return Promise.all([distributionGroupUsersNumber, storeInformation, releaseNotesString]); }); } getReleaseNotesString() { return __awaiter(this, void 0, void 0, function* () { if (!_.isNil(this.releaseNotesFile)) { try { return yield Pfs.readFile(this.releaseNotesFile, "utf8"); } catch (error) { if (error.code === "ENOENT") { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `release notes file '${this.releaseNotesFile}' doesn't exist`); } else { throw error; } } } else { return this.releaseNotes; } }); } getDistributionGroupUsersNumber(client) { var _a; return __awaiter(this, void 0, void 0, function* () { let userCount = 0; const groups = distribute_util_1.parseDistributionGroups(this.distributionGroup); for (const group of groups) { let distributionGroupUsersRequestResponse; try { distributionGroupUsersRequestResponse = yield client.distributionGroups.listUsers(this.app.ownerName, this.app.appName, group); } catch (error) { if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.code) === 404) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `distribution group ${group} was not found`); } else { debug(`Failed to get users of distribution group ${group}, returning null - ${util_1.inspect(error)}`); return null; } } userCount += distributionGroupUsersRequestResponse.length; } return userCount; }); } getStoreDetails(client) { return __awaiter(this, void 0, void 0, function* () { try { const storeDetailsResponse = yield client.stores.get(this.storeName, this.app.ownerName, this.app.appName); return storeDetailsResponse; } catch (error) { if (error.statusCode === 404) { throw commandline_1.failure(commandline_1.ErrorCodes.InvalidParameter, `store '${this.storeName}' was not found`); } else { debug(`Failed to get store details for '${this.storeName}', returning null - ${util_1.inspect(error)}`); return null; } } }); } createReleaseUpload(client, app) { return __awaiter(this, void 0, void 0, function* () { debug("Creating release upload"); const profile = profile_1.getUser(); const endpoint = yield this.getEndpoint(profile); const accessToken = yield this.getToken(profile); const url = ac_fus_api_1.getFileUploadLink(endpoint, app.ownerName, app.appName); const body = JSON.stringify({ build_version: this.buildVersion, build_number: this.buildNumber }); const response = yield appcenter_file_upload_client_node_1.fetchWithOptions(url, { method: "POST", headers: { "Content-Type": "application/json", "x-api-token": accessToken, }, body: body, }); const json = yield response.json(); if (!json.package_asset_id || (json.statusCode && json.statusCode !== 200)) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `Failed to create release upload for ${this.filePath}. Backend response: ${json.message}`); } return json; }); } uploadFileToUri(assetId, urlEncodedToken, uploadDomain) { return new Promise((resolve, reject) => { debug("Uploading the release binary"); const uploadSettings = { assetId: assetId, urlEncodedToken: urlEncodedToken, uploadDomain: uploadDomain, tenant: "distribution", onProgressChanged: (progress) => { debug("onProgressChanged: " + progress.percentCompleted); }, onMessage: (message, properties, level) => { debug(`onMessage: ${message} \nMessage properties: ${JSON.stringify(properties)}`); if (level === appcenter_file_upload_client_node_1.ACFusMessageLevel.Error) { this.acFusUploader.cancel(); reject(new Error(`Uploading file error: ${message}`)); } }, onStateChanged: (status) => { debug(`onStateChanged: ${status.toString()}`); }, onCompleted: (uploadStats) => { debug("Upload completed, total time: " + uploadStats.totalTimeInSeconds); resolve(); }, }; this.acFusUploader = new appcenter_file_upload_client_node_1.ACFusNodeUploader(uploadSettings); const appFile = new appcenter_file_upload_client_node_1.ACFile(this.filePath); this.acFusUploader.start(appFile); }); } patchUpload(app, uploadId) { return __awaiter(this, void 0, void 0, function* () { debug("Patching the upload"); const profile = profile_1.getUser(); const endpoint = yield this.getEndpoint(profile); const accessToken = yield this.getToken(profile); const url = ac_fus_api_1.getPatchUploadLink(endpoint, app.ownerName, app.appName, uploadId); const response = yield appcenter_file_upload_client_node_1.fetchWithOptions(url, { method: "PATCH", headers: { "Content-Type": "application/json", "x-api-token": accessToken, }, body: '{"upload_status":"uploadFinished"}', }); if (!response.ok) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `Failed to patch release upload. HTTP Status:${response.status} - ${response.statusText}`); } const json = yield response.json(); const { upload_status, message } = json; if (upload_status !== "uploadFinished") { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `Failed to patch release upload: ${message}`); } }); } loadReleaseIdUntilSuccess(app, uploadId) { return __awaiter(this, void 0, void 0, function* () { const t0 = Date.now(); const t1 = t0 + (_.isNil(this.timeout) ? 0 : Number.parseInt(this.timeout, 10) * 1000); return new Promise((resolve, reject) => { const check = () => __awaiter(this, void 0, void 0, function* () { let response; try { response = yield this.loadReleaseId(app, uploadId); } catch (error) { reject(new Error(`Loading release id failed with error: ${error.errorMessage}`)); } const releaseId = response.release_distinct_id; debug(`Received release id is ${releaseId}`); if (response.upload_status === "readyToBePublished" && releaseId) { debug(`Loading release id completed, total time: ${(Date.now() - t0) / 1000}`); resolve(Number(releaseId)); } else if (response.upload_status === "error") { debug(`Loading release id completed, total time: ${(Date.now() - t0) / 1000}`); reject(new Error(`Loading release id failed: ${response.error_details}`)); } else if (t1 > t0 && Date.now() >= t1) { reject(new Error(`Loading release id failed by timeout: ${this.timeout}`)); } else { setTimeout(check, 2000); } }); check(); }); }); } loadReleaseId(app, uploadId) { return __awaiter(this, void 0, void 0, function* () { try { debug("Loading release id..."); const profile = profile_1.getUser(); const endpoint = yield this.getEndpoint(profile); const accessToken = yield this.getToken(profile); const url = ac_fus_api_1.getPatchUploadLink(endpoint, app.ownerName, app.appName, uploadId); const response = yield appcenter_file_upload_client_node_1.fetchWithOptions(url, { method: "GET", headers: { "Content-Type": "application/json", "x-api-token": accessToken, }, }); if (response.status < 200 || response.status >= 300) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `failed to get release id with HTTP status: ${response.status} - ${response.statusText}`); } return yield response.json(); } catch (error) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `failed to get release id for upload id: ${uploadId}, error: ${JSON.stringify(error)}`); } }); } getToken(profile) { var _a; return __awaiter(this, void 0, void 0, function* () { if (((_a = this.token) === null || _a === void 0 ? void 0 : _a.length) > 0) { return this.token; } else if (profile) { const accessToken = yield profile.accessToken; if ((accessToken === null || accessToken === void 0 ? void 0 : accessToken.length) > 0) { return accessToken; } } return environment_vars_1.getTokenFromEnvironmentVar(); }); } getEndpoint(profile) { return __awaiter(this, void 0, void 0, function* () { if (this.environmentName || !profile) { return environments_1.environments(this.environmentName).endpoint; } else { return profile.endpoint; } }); } getDistributeRelease(client, app, releaseId) { return __awaiter(this, void 0, void 0, function* () { try { return yield interaction_1.out.progress(`Retrieving the release...`, client.releases.getLatestByUser(releaseId.toString(), app.ownerName, app.appName)); } catch (error) { if (error === 400) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, "release_id is not an integer or the string latest"); } else if (error === 404) { throw commandline_1.failure(commandline_1.ErrorCodes.Exception, `The release ${releaseId} can't be found`); } else { return null; } } }); } putReleaseDetails(client, app, releaseId, releaseNotesString) { return __awaiter(this, void 0, void 0, function* () { try { const result = yield interaction_1.out.progress(`Updating release details...`, client.releases.updateDetails(releaseId, app.ownerName, app.appName, { releaseNotes: releaseNotesString, })); return result; } catch (error) { debug(`Failed to set the release notes - ${util_1.inspect(error)}`); throw commandline_1.failure(commandline_1.ErrorCodes.Exception, error.message); } }); } publishToStore(client, app, storeInformation, releaseId) { return __awaiter(this, void 0, void 0, function* () { try { yield interaction_1.out.progress(`Publishing to store '${storeInformation.name}'...`, client.releases.addStore(releaseId, app.ownerName, app.appName, storeInformation.id)); } catch (error) { debug(`Failed to distribute the release to store - ${util_1.inspect(error)}`); throw commandline_1.failure(commandline_1.ErrorCodes.Exception, error.message); } }); } get fileExtension() { return Path.parse(this.filePath).ext.toLowerCase(); } }; __decorate([ commandline_1.help("Path to binary file"), commandline_1.shortName("f"), commandline_1.longName("file"), commandline_1.required, commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "filePath", void 0); __decorate([ commandline_1.help("Build version parameter required for .zip, .msi, .pkg and .dmg files"), commandline_1.shortName("b"), commandline_1.longName("build-version"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "buildVersion", void 0); __decorate([ commandline_1.help("Build number parameter required for macOS .pkg and .dmg files"), commandline_1.shortName("n"), commandline_1.longName("build-number"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "buildNumber", void 0); __decorate([ commandline_1.help("Comma-separated distribution group names"), commandline_1.shortName("g"), commandline_1.longName("group"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "distributionGroup", void 0); __decorate([ commandline_1.help("Store name"), commandline_1.shortName("s"), commandline_1.longName("store"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "storeName", void 0); __decorate([ commandline_1.help("Release notes text (5000 characters max)"), commandline_1.shortName("r"), commandline_1.longName("release-notes"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "releaseNotes", void 0); __decorate([ commandline_1.help("Path to release notes file (markdown supported, 5000 characters max)"), commandline_1.shortName("R"), commandline_1.longName("release-notes-file"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "releaseNotesFile", void 0); __decorate([ commandline_1.help("Do not notify testers of this release"), commandline_1.longName("silent") ], ReleaseBinaryCommand.prototype, "silent", void 0); __decorate([ commandline_1.help("Make the release mandatory for the testers (default is false)"), commandline_1.longName("mandatory") ], ReleaseBinaryCommand.prototype, "mandatory", void 0); __decorate([ commandline_1.help("Timeout for waiting release id (in seconds)"), commandline_1.shortName("t"), commandline_1.longName("timeout"), commandline_1.hasArg ], ReleaseBinaryCommand.prototype, "timeout", void 0); ReleaseBinaryCommand = __decorate([ commandline_1.help("Upload release binary and trigger distribution, at least one of --store or --group must be specified") ], ReleaseBinaryCommand); exports.default = ReleaseBinaryCommand;