UNPKG

@salesforce/plugin-release-management

Version:
270 lines 11.7 kB
/* * Copyright (c) 2024, 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 */ /* eslint-disable no-await-in-loop */ import os from 'node:os'; import path from 'node:path'; import util from 'node:util'; import fs from 'node:fs/promises'; import fg from 'fast-glob'; import shelljs from 'shelljs'; import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { Messages, SfError } from '@salesforce/core'; import chalk from 'chalk'; import { entriesOf } from '@salesforce/ts-types'; import { parseJson } from '@salesforce/kit'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.versions.inspect'); const SALESFORCE_DEP_GLOBS = ['@salesforce/**/*', 'salesforce-alm', 'salesforcedx']; export var Channel; (function (Channel) { Channel["STABLE"] = "stable"; Channel["STABLE_RC"] = "stable-rc"; Channel["LATEST"] = "latest"; Channel["LATEST_RC"] = "latest-rc"; Channel["NIGHTLY"] = "nightly"; })(Channel || (Channel = {})); export var Location; (function (Location) { Location["ARCHIVE"] = "archive"; Location["NPM"] = "npm"; })(Location || (Location = {})); const defaultArchives = [ 'sf-darwin-x64.tar.gz', 'sf-darwin-x64.tar.xz', 'sf-darwin-arm64.tar.gz', 'sf-darwin-arm64.tar.xz', 'sf-linux-arm.tar.gz', 'sf-linux-arm.tar.xz', 'sf-linux-x64.tar.gz', 'sf-linux-x64.tar.xz', 'sf-win32-x64.tar.gz', 'sf-win32-x64.tar.xz', 'sf-win32-x86.tar.gz', 'sf-win32-x86.tar.xz', 'sf-win32-arm64.tar.xz', 'sf-win32-arm64.tar.xz', ]; const ARCHIVES = { [Channel.STABLE]: [...defaultArchives], [Channel.STABLE_RC]: [...defaultArchives], [Channel.NIGHTLY]: [...defaultArchives], }; const CHANNEL_MAPPING = { [Location.NPM]: { [Channel.STABLE]: Channel.LATEST, [Channel.LATEST]: Channel.LATEST, [Channel.STABLE_RC]: Channel.LATEST_RC, [Channel.LATEST_RC]: Channel.LATEST_RC, [Channel.NIGHTLY]: Channel.NIGHTLY, }, [Location.ARCHIVE]: { [Channel.STABLE]: Channel.STABLE, [Channel.LATEST]: Channel.STABLE, [Channel.STABLE_RC]: Channel.STABLE_RC, [Channel.LATEST_RC]: Channel.STABLE_RC, [Channel.NIGHTLY]: Channel.NIGHTLY, }, }; export default class Inspect extends SfCommand { static summary = messages.getMessage('description'); static description = messages.getMessage('description'); static examples = messages.getMessages('examples'); static flags = { dependencies: Flags.string({ summary: messages.getMessage('flags.dependencies.summary'), char: 'd', multiple: true, }), salesforce: Flags.boolean({ summary: messages.getMessage('flags.salesforce.summary'), char: 's', default: false, }), channels: Flags.string({ summary: messages.getMessage('flags.channels.summary'), char: 'c', options: Object.values(Channel), required: true, multiple: true, }), locations: Flags.string({ summary: messages.getMessage('flags.locations.summary'), char: 'l', options: Object.values(Location), required: true, multiple: true, }), 'ignore-missing': Flags.boolean({ summary: messages.getMessage('flags.ignoreMissing.summary'), default: false, }), }; workingDir = path.join(os.tmpdir(), 'cli_inspection'); archives; flags; async run() { const { flags } = await this.parse(Inspect); this.flags = flags; const locations = this.flags.locations; const channels = this.flags.channels; this.log(`Working Directory: ${this.workingDir}`); // ensure that we are starting with a clean directory try { await fs.rm(this.workingDir, { recursive: true, force: true }); } catch { // error means that folder doesn't exist which is okay } await fs.mkdir(this.workingDir, { recursive: true }); this.initArchives(); const results = [ ...(locations.includes(Location.ARCHIVE) ? await this.inspectArchives(channels) : []), ...(locations.includes(Location.NPM) ? await this.inspectNpm(channels) : []), ]; this.logResults(results, locations, channels); return results; } initArchives() { // Example formatted url: https://developer.salesforce.com/media/salesforce-cli/sf/channels/stable/sf-darwin-x64.tar.gz const basePath = 'https://developer.salesforce.com/media/salesforce-cli/sf/channels/%s/%s'; this.archives = {}; for (const [channel, paths] of entriesOf(ARCHIVES)) { this.archives[channel] = paths.map((p) => util.format(basePath, channel, p)); } } async inspectArchives(channels) { const tarDir = await mkdir(this.workingDir, 'tar'); const pathsByChannel = Object.fromEntries(channels // the enums are not smart enough to know that they'll definitely be archive channels .map((c) => CHANNEL_MAPPING[Location.ARCHIVE][c]) .map((c) => [c, this.archives?.[CHANNEL_MAPPING[Location.ARCHIVE][c]]])); const results = []; for (const channel of Object.keys(pathsByChannel)) { this.log(`---- ${Location.ARCHIVE} ${channel} ----`); for (const archivePath of pathsByChannel[channel] ?? []) { this.spinner.start(`Downloading: ${chalk.cyan(archivePath)}`); const curlResult = shelljs.exec(`curl ${archivePath} -Ofs`, { cwd: tarDir }); this.spinner.stop(); if (curlResult.code !== 0) { if (this.flags['ignore-missing']) { this.log(chalk.red(`Failed to download: ${archivePath}. Skipping because --ignore-missing flag is set.`)); continue; } else { throw new SfError(`Failed to download: ${archivePath}. This is a big deal. Investigate immediately.`); } } const filename = path.basename(archivePath); const unpackedDir = await mkdir(this.workingDir, 'unpacked', filename); this.spinner.start(`Unpacking: ${chalk.cyan(unpackedDir)}`); const tarResult = shelljs.exec(`tar -xf ${filename} -C ${unpackedDir} --strip-components 1`, { cwd: tarDir }); this.spinner.stop(); if (tarResult.code !== 0) { this.log(chalk.red('Failed to unpack. Skipping...')); continue; } const pkgJson = await readPackageJson(unpackedDir); results.push({ dependencies: await this.getDependencies(unpackedDir), origin: archivePath, channel, location: Location.ARCHIVE, version: pkgJson.version, }); } } return results; } async inspectNpm(channels) { const npmDir = await mkdir(this.workingDir, 'npm'); const results = []; const tags = channels.map((c) => CHANNEL_MAPPING[Location.NPM][c]); for (const tag of tags) { this.log(`---- ${Location.NPM} ${tag} ----`); const installDir = await mkdir(npmDir, tag); const name = `@salesforce/cli@${tag}`; this.spinner.start(`Installing: ${chalk.cyan(name)}`); shelljs.exec(`npm install ${name}`, { cwd: installDir, silent: true }); this.spinner.stop(); const pkgJson = await readPackageJson(path.join(installDir, 'node_modules', '@salesforce/cli')); results.push({ dependencies: await this.getDependencies(installDir), origin: `https://www.npmjs.com/package/@salesforce/cli/v/${pkgJson.version}`, channel: tag, location: Location.NPM, version: pkgJson.version, }); } return results; } async getDependencies(directory) { const depGlobs = []; if (this.flags.dependencies) { const globPatterns = this.flags.dependencies.map((d) => `${directory}/node_modules/${d}`); depGlobs.push(...globPatterns); } if (this.flags.salesforce) { const globPatterns = SALESFORCE_DEP_GLOBS.map((d) => `${directory}/node_modules/${d}`); depGlobs.push(...globPatterns); } const dependencyPaths = await fg(depGlobs, { onlyDirectories: true, deep: 1 }); const dependencies = []; for (const dep of dependencyPaths) { const pkg = await readPackageJson(dep); dependencies.push({ name: pkg.name, version: pkg.version, }); } return dependencies; } logResults(results, locations, channels) { let allMatch; let npmAndArchivesMatch; this.log(); results.forEach((result) => { this.log(chalk.bold(`${result.origin}: ${chalk.green(result.version)}`)); result.dependencies.forEach((dep) => { this.log(` ${dep.name}: ${dep.version}`); }); }); this.log(); if (locations.includes(Location.ARCHIVE)) { const archivesMatch = new Set(results.filter((r) => r.location === Location.ARCHIVE).map((r) => r.version)).size === 1; this.log(`${'All archives match?'} ${archivesMatch ? chalk.green(archivesMatch) : chalk.yellow(archivesMatch)}`); channels.forEach((channel) => { allMatch = new Set(results.filter((r) => r.channel === channel).map((r) => r.version)).size === 1; this.log(`${`All ${Location.ARCHIVE}@${channel} versions match?`} ${allMatch ? chalk.green(allMatch) : chalk.red(allMatch)}`); }); } if (locations.includes(Location.NPM) && locations.includes(Location.ARCHIVE)) { channels.forEach((channel) => { const npmChannel = CHANNEL_MAPPING[Location.NPM][channel]; const archiveChannel = CHANNEL_MAPPING[Location.ARCHIVE][channel]; npmAndArchivesMatch = new Set(results.filter((r) => r.channel === npmChannel || r.channel === archiveChannel).map((r) => r.version)) .size === 1; const match = npmAndArchivesMatch ? chalk.green(true) : chalk.red(false); this.log(`${Location.NPM}@${npmChannel} and all ${Location.ARCHIVE}@${archiveChannel} versions match? ${match}`); }); } // npmAndArchivesMatch can be undefined if ((npmAndArchivesMatch !== undefined && !npmAndArchivesMatch) || !allMatch) { throw new SfError('Version Mismatch'); } } } const readPackageJson = async (pkgDir) => { const fileData = await fs.readFile(path.join(pkgDir, 'package.json'), 'utf8'); return parseJson(fileData, path.join(pkgDir, 'package.json'), false); }; const mkdir = async (...parts) => { const dir = path.resolve(path.join(...parts)); await fs.mkdir(dir, { recursive: true }); return dir; }; //# sourceMappingURL=inspect.js.map