UNPKG

@salesforce/plugin-release-management

Version:
271 lines 11.1 kB
/* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { text } from 'node:stream/consumers'; import chalk from 'chalk'; import { valid as validSemVer } from 'semver'; import shelljs from 'shelljs'; import { Logger, Messages, SfError } from '@salesforce/core'; import { ensureString, isString } from '@salesforce/ts-types'; import { SfCommand, Flags, arrayWithDeprecation } from '@salesforce/sf-plugins-core'; import { AmazonS3 } from '../../amazonS3.js'; import { verifyDependencies } from '../../dependencies.js'; import { CLI, Channel } from '../../types.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'channel.promote'); const TARGETS = ['linux-x64', 'linux-arm', 'win32-x64', 'win32-x86', 'darwin-x64']; export default class Promote extends SfCommand { static description = messages.getMessage('description'); static summary = messages.getMessage('summary'); static examples = messages.getMessages('examples'); static flags = { dryrun: Flags.boolean({ char: 'd', default: false, summary: messages.getMessage('flags.dryrun.summary'), }), 'promote-to-channel': Flags.string({ char: 't', default: Channel.STABLE, summary: messages.getMessage('flags.promote-to-channel.summary'), // options: Object.values(Channel), required: true, aliases: ['target'], }), 'promote-from-channel': Flags.string({ char: 'C', summary: messages.getMessage('flags.promote-from-channel.summary'), // options: Object.values(Channel), exactlyOne: ['sha', 'version', 'promote-from-channel'], aliases: ['candidate'], }), platform: arrayWithDeprecation({ char: 'p', summary: messages.getMessage('flags.platform.summary'), options: ['win', 'macos', 'deb'], }), cli: Flags.custom({ options: Object.values(CLI), })({ char: 'c', summary: messages.getMessage('flags.cli.summary'), required: true, }), sha: Flags.string({ char: 's', summary: messages.getMessage('flags.sha.summary'), exactlyOne: ['sha', 'version', 'promote-from-channel'], parse: (input) => Promise.resolve(input.slice(0, 7)), validate: (input) => { if (input.length < 7) { return false; } return true; }, }), 'max-age': Flags.integer({ char: 'm', summary: messages.getMessage('flags.max-age.summary'), default: 300, aliases: ['maxage'], }), indexes: Flags.boolean({ char: 'i', summary: messages.getMessage('flags.indexes.summary'), default: true, allowNo: true, }), xz: Flags.boolean({ char: 'x', summary: messages.getMessage('flags.xz.summary'), default: true, allowNo: true, }), 'architecture-target': arrayWithDeprecation({ char: 'T', summary: messages.getMessage('targets'), options: TARGETS, aliases: ['targets'], }), version: Flags.string({ char: 'v', summary: messages.getMessage('flags.version.summary'), exactlyOne: ['sha', 'version', 'promote-from-channel'], parse: (input) => Promise.resolve(input.trim()), validate: (input) => validSemVer(input) !== null, }), }; flags; async run() { const { flags } = await this.parse(Promote); this.flags = flags; this.validateFlags(); // preparing parameters for call to oclif promote commands const cli = this.flags.cli; const target = ensureString(this.flags['promote-to-channel']); const indexes = this.flags.indexes ? '--indexes' : ''; const xz = this.flags.xz ? '--xz' : '--no-xz'; const { sha, version } = await determineShaAndVersion(cli, this.flags['promote-from-channel'], this.flags.version, this.flags.sha); const platforms = this.flags.platform?.map((p) => `--${p}`); if (!this.flags.dryrun) { const params = [ '--version', version, '--sha', sha, '--channel', target, '--max-age', this.flags['max-age'].toString(), ...(platforms ?? []), ...(this.flags['architecture-target'] ?? []), indexes, xz, ]; const results = shelljs.exec(`yarn oclif promote ${params.join(' ')}`); this.log(results.stdout); } else if (!this.flags.json) { this.log(messages.getMessage('DryRunMessage', [cli, version, sha, target, this.flags.platform?.join(', ') ?? 'all'].map((s) => chalk.bold(s)))); } return { dryRun: !!this.flags.dryrun, cli, target, sha, version, platforms: this.flags.platform ?? [], }; } /** * validate flag combinations * * @private */ validateFlags() { // cannot promote when channel names are the same if (this.flags['promote-from-channel'] && this.flags['promote-from-channel'] === this.flags['promote-to-channel']) { throw new SfError(messages.getMessage('CannotPromoteToSameChannel')); } // make sure necessary runtime dependencies are present const deps = verifyDependencies(this.flags, (dep) => dep.name.startsWith('AWS'), (args) => !args.dryrun); if (deps.failures > 0) { const errType = 'MissingDependencies'; const missing = deps.results .filter((d) => d.passed === false) .map((d) => d.message) .filter(isString); throw new SfError(messages.getMessage(errType), errType, missing); } } } /** * find a manifest file in the channel * * @param cli * @param channel * @private */ const findManifestForCandidate = async (cli, channel) => { const amazonS3 = new AmazonS3({ cli, channel }); return amazonS3.getManifestFromChannel(channel); }; /** * find the version that owns the named sha * * @param cli * @param sha * @private */ const findVersionForSha = async (cli, sha) => { const amazonS3 = new AmazonS3({ cli }); const foundVersionPrefix = (await Promise.all((await amazonS3.listCommonPrefixes('versions')).map(async (versionPrefix) => amazonS3.listCommonPrefixes(versionPrefix)))) .flat() .find((s) => s.replace(/\/$/, '').endsWith(sha)); if (foundVersionPrefix) { // Prefix looks like this "media/salesforce-cli/sf/versions/0.0.10/1d4b10d/", // when reversed after split version number should occupy entry 1 of the array return foundVersionPrefix?.replace(/\/$/, '').split('/').reverse()[1]; } const error = new SfError(messages.getMessage('CouldNotLocateVersionForSha', [sha])); const logger = Logger.childFromRoot('Promote.findVersionForSha'); logger.debug(error); throw error; }; /** * Based on which flag was provided, locate the sha and version in S3 that will be used in the promote * * when candidate channel flag present, find sha a version via the channel for candidate * when version flag present, find the sha from version subfolders with the most recent modified date * when sha flag is present, find the version that owns the subfolder named as sha value * * @param cli * @private */ const determineShaAndVersion = async (cli, candidate, version, sha) => { if (candidate) { const manifest = await findManifestForCandidate(cli, candidate); return { sha: manifest.sha, version: manifest.version }; } else if (version) { const shaFromVersion = await findShaForVersion(cli, ensureString(version)); return { sha: shaFromVersion, version: ensureString(version) }; } else if (sha) { ensureString(sha); const versionFromSha = await findVersionForSha(cli, sha); return { sha, version: versionFromSha }; } throw new SfError(messages.getMessage('CouldNotDetermineShaAndVersion')); }; /** * find the sha that was uploaded most recently for the named version * * @param cli * @param version * @private */ const findShaForVersion = async (cli, version) => { const logger = Logger.childFromRoot('Promote.findShaForVersion'); const amazonS3 = new AmazonS3({ cli }); const versions = await amazonS3.listCommonPrefixes('versions'); const foundVersion = versions.find((v) => v.endsWith(`${version}/`)); if (foundVersion) { logger.debug(`Looking for version ${version} for cli ${cli}. Found ${foundVersion}`); const versionShas = await amazonS3.listCommonPrefixes(foundVersion); logger.debug(`Looking for version ${version} for cli ${cli} shas. Found ${versionShas.length} entries`); const manifestForMostRecentSha = (await Promise.all(versionShas.map(async (versionSha) => { if (!versionSha) { throw new SfError(`Could not find prefix for ${versionSha}`); } const versionShaContents = (await amazonS3.listKeyContents(versionSha)); return versionShaContents.map((content) => ({ ...content, ...{ LastModifiedDate: new Date(content.LastModified) }, })); }))).flat() .filter((content) => content.Key.includes('manifest')) .sort((left, right) => right.LastModifiedDate.getMilliseconds() - left.LastModifiedDate.getMilliseconds()) .find((content) => content); if (manifestForMostRecentSha) { const manifest = await amazonS3.getObject({ Key: manifestForMostRecentSha.Key, ResponseContentType: 'application/json', }); if (!manifest.Body) { throw new SfError(`Could not load manifest body from S3 getObject response for ${manifestForMostRecentSha.Key}`); } const bodyString = await text(manifest.Body); logger.debug(`Loaded manifest ${manifestForMostRecentSha.Key} contents: ${bodyString}`); const json = JSON.parse(bodyString); return json.sha; } } const error = new SfError(messages.getMessage('CouldNotLocateShaForVersion', [version])); logger.debug(error); throw error; }; //# sourceMappingURL=promote.js.map