UNPKG

@sentry/cli

Version:

A command line utility to work with Sentry. https://docs.sentry.io/hosted/learn/cli/

350 lines (301 loc) 10.6 kB
#!/usr/bin/env node 'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const crypto = require('crypto'); const zlib = require('zlib'); const stream = require('stream'); const process = require('process'); const fetch = require('node-fetch'); const HttpsProxyAgent = require('https-proxy-agent'); const ProgressBar = require('progress'); const Proxy = require('proxy-from-env'); const which = require('which'); const helper = require('../js/helper'); const pkgInfo = require('../package.json'); const Logger = require('../js/logger'); const logger = new Logger(getLogStream('stderr')); const CDN_URL = process.env.SENTRYCLI_LOCAL_CDNURL || process.env.npm_config_sentrycli_cdnurl || process.env.SENTRYCLI_CDNURL || 'https://downloads.sentry-cdn.com/sentry-cli'; function getLogStream(defaultStream) { const logStream = process.env.SENTRYCLI_LOG_STREAM || defaultStream; if (logStream === 'stdout') { return process.stdout; } if (logStream === 'stderr') { return process.stderr; } throw new Error( `Incorrect SENTRYCLI_LOG_STREAM env variable. Possible values: 'stdout' | 'stderr'` ); } function shouldRenderProgressBar() { const silentFlag = process.argv.some((v) => v === '--silent'); const silentConfig = process.env.npm_config_loglevel === 'silent'; const silentEnv = process.env.SENTRYCLI_NO_PROGRESS_BAR; const ciEnv = process.env.CI === 'true' || process.env.CI === '1'; const notTTY = !process.stdout.isTTY; // If any of possible options is set, skip rendering of progress bar return !(silentFlag || silentConfig || silentEnv || ciEnv || notTTY); } function getDownloadUrl(platform, arch) { const releasesUrl = `${CDN_URL}/${pkgInfo.version}/sentry-cli`; let archString = ''; switch (arch) { case 'x64': archString = 'x86_64'; break; case 'x86': case 'ia32': archString = 'i686'; break; case 'arm64': archString = 'aarch64'; break; case 'arm': archString = 'armv7'; break; default: archString = arch; } switch (platform) { case 'darwin': return `${releasesUrl}-Darwin-universal`; case 'win32': // Windows arm machines can run x64 binaries if (arch === 'arm64') { archString = 'x86_64'; } return `${releasesUrl}-Windows-${archString}.exe`; case 'linux': case 'freebsd': return `${releasesUrl}-Linux-${archString}`; default: return null; } } function createProgressBar(name, total) { const incorrectTotal = typeof total !== 'number' || Number.isNaN(total); if (incorrectTotal || !shouldRenderProgressBar()) { return { tick: () => {}, }; } const logStream = getLogStream('stdout'); if (logStream.isTTY) { return new ProgressBar(`fetching ${name} :bar :percent :etas`, { complete: '█', incomplete: '░', width: 20, total, }); } let pct = null; let current = 0; return { tick: (length) => { current += length; const next = Math.round((current / total) * 100); if (next > pct) { pct = next; logStream.write(`fetching ${name} ${pct}%\n`); } }, }; } function npmCache() { const keys = ['npm_config_cache', 'npm_config_cache_folder', 'npm_config_yarn_offline_mirror']; for (let key of [...keys, ...keys.map((k) => k.toUpperCase())]) { if (process.env[key]) return process.env[key]; } if (process.env.APPDATA) { return path.join(process.env.APPDATA, 'npm-cache'); } return path.join(os.homedir(), '.npm'); } function getCachedPath(url) { const digest = crypto.createHash('md5').update(url).digest('hex').slice(0, 6); return path.join( npmCache(), 'sentry-cli', `${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, '-')}` ); } function getTempFile(cached) { return `${cached}.${process.pid}-${Math.random().toString(16).slice(2)}.tmp`; } function validateChecksum(tempPath, name) { let storedHash; try { const checksums = fs.readFileSync(path.join(__dirname, '../checksums.txt'), 'utf8'); const entries = checksums.split('\n'); for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i].split('='); if (key === name) { storedHash = value; break; } } } catch (e) { logger.log( 'Checksums are generated when the package is published to npm. They are not available directly in the source repository. Skipping validation.' ); return; } if (!storedHash) { logger.log(`Checksum for ${name} not found, skipping validation.`); return; } const currentHash = crypto.createHash('sha256').update(fs.readFileSync(tempPath)).digest('hex'); if (storedHash !== currentHash) { fs.unlinkSync(tempPath); throw new Error( `Checksum validation for ${name} failed.\nExpected: ${storedHash}\nReceived: ${currentHash}` ); } else { logger.log('Checksum validation passed.'); } } async function downloadBinary() { const arch = os.arch(); const platform = os.platform(); const outputPath = helper.getFallbackBinaryPath(); if (process.env.SENTRYCLI_USE_LOCAL === '1') { try { const binPaths = which.sync('sentry-cli', { all: true }); if (!binPaths.length) throw new Error('Binary not found'); const binPath = binPaths[binPaths.length - 1]; logger.log(`Using local binary: ${binPath}`); fs.copyFileSync(binPath, outputPath); return Promise.resolve(); } catch (e) { throw new Error( 'Configured installation of local binary, but it was not found.' + 'Make sure that `sentry-cli` executable is available in your $PATH or disable SENTRYCLI_USE_LOCAL env variable.' ); } } const downloadUrl = getDownloadUrl(platform, arch); if (!downloadUrl) { throw new Error(`Unsupported target ${platform}-${arch}`); } const cachedPath = getCachedPath(downloadUrl); if (fs.existsSync(cachedPath)) { logger.log(`Using cached binary: ${cachedPath}`); fs.copyFileSync(cachedPath, outputPath); return; } const proxyUrl = Proxy.getProxyForUrl(downloadUrl); const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : null; logger.log(`Downloading from ${downloadUrl}`); if (proxyUrl) { logger.log(`Using proxy URL: ${proxyUrl}`); } let response; try { response = await fetch(downloadUrl, { agent, compress: false, headers: { 'accept-encoding': 'gzip, deflate, br', }, redirect: 'follow', }); } catch (error) { let errorMsg = `Unable to download sentry-cli binary from ${downloadUrl}.\nError message: ${error.message}`; if (error.code) { errorMsg += `\nError code: ${error.code}`; } throw new Error(errorMsg); } if (!response.ok) { let errorMsg = `Unable to download sentry-cli binary from ${downloadUrl}.\nServer returned: ${response.status}`; if (response.statusText) { errorMsg += ` - ${response.statusText}`; } throw new Error(errorMsg); } const contentEncoding = response.headers.get('content-encoding'); let decompressor; if (/\bgzip\b/.test(contentEncoding)) { decompressor = zlib.createGunzip(); } else if (/\bdeflate\b/.test(contentEncoding)) { decompressor = zlib.createInflate(); } else if (/\bbr\b/.test(contentEncoding)) { decompressor = zlib.createBrotliDecompress(); } else { decompressor = new stream.PassThrough(); } const name = downloadUrl.match(/.*\/(.*?)$/)[1]; let downloadedBytes = 0; const totalBytes = parseInt(response.headers.get('content-length'), 10); const progressBar = createProgressBar(name, totalBytes); const tempPath = getTempFile(cachedPath); fs.mkdirSync(path.dirname(tempPath), { recursive: true }); await new Promise((resolve, reject) => { response.body .on('error', (e) => reject(e)) .on('data', (chunk) => { downloadedBytes += chunk.length; progressBar.tick(chunk.length); }) .pipe(decompressor) .pipe(fs.createWriteStream(tempPath, { mode: '0755' })) .on('error', (e) => reject(e)) .on('close', () => { if (downloadedBytes >= totalBytes) { resolve(); } else { reject(new Error('connection interrupted')); } }); }); if (process.env.SENTRYCLI_SKIP_CHECKSUM_VALIDATION !== '1') { validateChecksum(tempPath, name); } fs.copyFileSync(tempPath, cachedPath); fs.copyFileSync(tempPath, outputPath); fs.unlinkSync(tempPath); } async function checkVersion() { const output = await helper.execute(['--version']); const version = output.replace('sentry-cli ', '').trim(); const expected = pkgInfo.version; if (version !== expected) { throw new Error(`Unexpected sentry-cli version "${version}", expected "${expected}"`); } } if (process.env.SENTRYCLI_SKIP_DOWNLOAD === '1') { logger.log(`Skipping download because SENTRYCLI_SKIP_DOWNLOAD=1 detected.`); process.exit(0); } const { packageName: distributionPackageName, subpath: distributionSubpath } = helper.getDistributionForThisPlatform(); if (distributionPackageName === undefined) { helper.throwUnsupportedPlatformError(); } try { require.resolve(`${distributionPackageName}/${distributionSubpath}`); // If the `resolve` call succeeds it means a binary was installed successfully via optional dependencies so we can skip the manual postinstall download. process.exit(0); } catch (e) { // Optional dependencies likely didn't get installed - proceed with fallback downloading manually // Log message inspired by esbuild: https://github.com/evanw/esbuild/blob/914f6080c77cfe32a54888caa51ca6ea13873ce9/lib/npm/node-install.ts#L253 logger.log( `Sentry CLI failed to locate the "${distributionPackageName}" package after installation! This can happen if you use an option to disable optional dependencies during installation, like "--no-optional", "--ignore-optional", or "--omit=optional". Sentry CLI uses the "optionalDependencies" package.json feature to install the correct binary for your platform and operating system. This post-install script will now try to work around this by manually downloading the Sentry CLI binary from the Sentry CDN. If this fails, you need to remove the "--no-optional", "--ignore-optional", and "--omit=optional" flags for Sentry CLI to work.` ); downloadBinary() .then(() => checkVersion()) .then(() => { process.exit(0); }) .catch((e) => { console.error(e); process.exit(1); }); }