UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

376 lines (346 loc) 11.7 kB
const cp = require('child_process') const os = require('os') const path = require('path') const fs = require('fs') const log = require('../../log') const { GIT_COMMIT_SHA, GIT_BRANCH, GIT_REPOSITORY_URL, GIT_TAG, GIT_COMMIT_MESSAGE, GIT_COMMIT_COMMITTER_DATE, GIT_COMMIT_COMMITTER_EMAIL, GIT_COMMIT_COMMITTER_NAME, GIT_COMMIT_AUTHOR_DATE, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, CI_WORKSPACE_PATH } = require('./tags') const { incrementCountMetric, distributionMetric, TELEMETRY_GIT_COMMAND, TELEMETRY_GIT_COMMAND_MS, TELEMETRY_GIT_COMMAND_ERRORS } = require('../../ci-visibility/telemetry') const { filterSensitiveInfoFromRepository } = require('./url') const { storage } = require('../../../../datadog-core') const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB function sanitizedExec ( cmd, flags, operationMetric, durationMetric, errorMetric, shouldTrim = true ) { const store = storage('legacy').getStore() storage('legacy').enterWith({ noop: true }) let startTime if (operationMetric) { incrementCountMetric(operationMetric.name, operationMetric.tags) } if (durationMetric) { startTime = Date.now() } try { let result = cp.execFileSync(cmd, flags, { stdio: 'pipe' }).toString() if (shouldTrim) { result = result.replace(/(\r\n|\n|\r)/gm, '') } if (durationMetric) { distributionMetric(durationMetric.name, durationMetric.tags, Date.now() - startTime) } return result } catch (err) { if (errorMetric) { incrementCountMetric(errorMetric.name, { ...errorMetric.tags, errorType: err.code, exitCode: err.status || err.errno }) } log.error('Git plugin error executing command', err) return '' } finally { storage('legacy').enterWith(store) } } function isDirectory (path) { try { const stats = fs.statSync(path) return stats.isDirectory() } catch (e) { return false } } function isGitAvailable () { const isWindows = os.platform() === 'win32' const command = isWindows ? 'where' : 'which' try { cp.execFileSync(command, ['git'], { stdio: 'pipe' }) return true } catch (e) { incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'check_git', exitCode: 'missing' }) return false } } function isShallowRepository () { return sanitizedExec( 'git', ['rev-parse', '--is-shallow-repository'], { name: TELEMETRY_GIT_COMMAND, tags: { command: 'check_shallow' } }, { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'check_shallow' } }, { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } } ) === 'true' } function getGitVersion () { const gitVersionString = sanitizedExec('git', ['version']) const gitVersionMatches = gitVersionString.match(/git version (\d+)\.(\d+)\.(\d+)/) try { return { major: parseInt(gitVersionMatches[1]), minor: parseInt(gitVersionMatches[2]), patch: parseInt(gitVersionMatches[3]) } } catch (e) { return null } } function unshallowRepository () { const gitVersion = getGitVersion() if (!gitVersion) { log.warn('Git version could not be extracted, so git unshallow will not proceed') return } if (gitVersion.major < 2 || (gitVersion.major === 2 && gitVersion.minor < 27)) { log.warn('Git version is <2.27, so git unshallow will not proceed') return } const defaultRemoteName = sanitizedExec('git', ['config', '--default', 'origin', '--get', 'clone.defaultRemoteName']) const revParseHead = sanitizedExec('git', ['rev-parse', 'HEAD']) const baseGitOptions = [ 'fetch', '--shallow-since="1 month ago"', '--update-shallow', '--filter=blob:none', '--recurse-submodules=no', defaultRemoteName ] incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'unshallow' }) const start = Date.now() try { cp.execFileSync('git', [ ...baseGitOptions, revParseHead ], { stdio: 'pipe' }) } catch (err) { // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail. log.error('Git plugin error executing git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } ) const upstreamRemote = sanitizedExec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']) try { cp.execFileSync('git', [ ...baseGitOptions, upstreamRemote ], { stdio: 'pipe' }) } catch (err) { // If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail. log.error('Git plugin error executing fallback git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } ) // We use sanitizedExec here because if this last option fails, we'll give up. sanitizedExec( 'git', baseGitOptions, null, null, { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'unshallow' } } // we log the error in sanitizedExec ) } } distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'unshallow' }, Date.now() - start) } function getRepositoryUrl () { return sanitizedExec( 'git', ['config', '--get', 'remote.origin.url'], { name: TELEMETRY_GIT_COMMAND, tags: { command: 'get_repository' } }, { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'get_repository' } }, { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'get_repository' } } ) } function getLatestCommits () { incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_local_commits' }) const startTime = Date.now() try { const result = cp.execFileSync('git', ['log', '--format=%H', '-n 1000', '--since="1 month ago"'], { stdio: 'pipe' }) .toString() .split('\n') .filter(commit => commit) distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_local_commits' }, Date.now() - startTime) return result } catch (err) { log.error('Get latest commits failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_local_commits', errorType: err.status } ) return [] } } function getCommitsRevList (commitsToExclude, commitsToInclude) { let result = null const commitsToExcludeString = commitsToExclude.map(commit => `^${commit}`) incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_objects' }) const startTime = Date.now() try { result = cp.execFileSync( 'git', [ 'rev-list', '--objects', '--no-object-names', '--filter=blob:none', '--since="1 month ago"', ...commitsToExcludeString, ...commitsToInclude ], { stdio: 'pipe', maxBuffer: GIT_REV_LIST_MAX_BUFFER }) .toString() .split('\n') .filter(commit => commit) } catch (err) { log.error('Get commits to upload failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_objects', errorType: err.code, exitCode: err.status || err.errno } // err.status might be null ) } distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_objects' }, Date.now() - startTime) return result } function generatePackFilesForCommits (commitsToUpload) { let result = [] const tmpFolder = os.tmpdir() if (!isDirectory(tmpFolder)) { log.error(new Error('Provided path to generate packfiles is not a directory')) return [] } const randomPrefix = String(Math.floor(Math.random() * 10000)) const temporaryPath = path.join(tmpFolder, randomPrefix) const cwdPath = path.join(process.cwd(), randomPrefix) incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'pack_objects' }) const startTime = Date.now() // Generates pack files to upload and // returns the ordered list of packfiles' paths function execGitPackObjects (targetPath) { return cp.execFileSync( 'git', [ 'pack-objects', '--compression=9', '--max-pack-size=3m', targetPath ], { stdio: 'pipe', input: commitsToUpload.join('\n') } ).toString().split('\n').filter(commit => commit).map(commit => `${targetPath}-${commit}.pack`) } try { result = execGitPackObjects(temporaryPath) } catch (err) { log.error('Git plugin error executing git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } ) /** * The generation of pack files in the temporary folder (from `os.tmpdir()`) * sometimes fails in certain CI setups with the error message * `unable to rename temporary pack file: Invalid cross-device link`. * The reason why is unclear. * * A workaround is to attempt to generate the pack files in `process.cwd()`. * While this works most of the times, it's not ideal since it affects the git status. * This workaround is intended to be temporary. * * TODO: fix issue and remove workaround. */ try { result = execGitPackObjects(cwdPath) } catch (err) { log.error('Git plugin error executing fallback git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } ) } } distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'pack_objects' }, Date.now() - startTime) return result } // If there is ciMetadata, it takes precedence. function getGitMetadata (ciMetadata) { const { commitSHA, branch, repositoryUrl, tag, commitMessage, authorName: ciAuthorName, authorEmail: ciAuthorEmail, ciWorkspacePath } = ciMetadata // With stdio: 'pipe', errors in this command will not be output to the parent process, // so if `git` is not present in the env, we won't show a warning to the user. const [ authorName, authorEmail, authorDate, committerName, committerEmail, committerDate ] = sanitizedExec('git', ['show', '-s', '--format=%an,%ae,%aI,%cn,%ce,%cI']).split(',') const tags = { [GIT_COMMIT_MESSAGE]: commitMessage || sanitizedExec('git', ['show', '-s', '--format=%B'], null, null, null, false), [GIT_BRANCH]: branch || sanitizedExec('git', ['rev-parse', '--abbrev-ref', 'HEAD']), [GIT_COMMIT_SHA]: commitSHA || sanitizedExec('git', ['rev-parse', 'HEAD']), [CI_WORKSPACE_PATH]: ciWorkspacePath || sanitizedExec('git', ['rev-parse', '--show-toplevel']) } const entries = [ GIT_REPOSITORY_URL, filterSensitiveInfoFromRepository(repositoryUrl || sanitizedExec('git', ['ls-remote', '--get-url'])), GIT_COMMIT_AUTHOR_DATE, authorDate, GIT_COMMIT_AUTHOR_NAME, ciAuthorName || authorName, GIT_COMMIT_AUTHOR_EMAIL, ciAuthorEmail || authorEmail, GIT_COMMIT_COMMITTER_DATE, committerDate, GIT_COMMIT_COMMITTER_NAME, committerName, GIT_COMMIT_COMMITTER_EMAIL, committerEmail, GIT_TAG, tag ] for (let i = 0; i < entries.length; i += 2) { const value = entries[i + 1] if (value) { tags[entries[i]] = value } } return tags } module.exports = { getGitMetadata, getLatestCommits, getRepositoryUrl, generatePackFilesForCommits, getCommitsRevList, GIT_REV_LIST_MAX_BUFFER, isShallowRepository, unshallowRepository, isGitAvailable }