@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
JavaScript
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);
});
}
;