UNPKG

release-it

Version:

Interactive release tool for Git repositories. Increment version, commit, tag, push, build, publish to npm. Supports to build and release to a distribution/component repository.

294 lines (263 loc) 8.86 kB
const util = require('./util'), path = require('path'), run = require('./shell').run, config = require('./config'), GitHubApi = require('github'), repoPathParse = require('parse-repo'), retry = require('p-retry'), glob = require('glob'), log = require('./log'); const noop = Promise.resolve(); const commitRefRe = /#.+$/; var _githubClient = null; function isGitRepo() { return run('!git rev-parse --git-dir', {isReadOnly: true}); } function tagExists(tag) { return run(`!git show-ref --tags --quiet --verify -- "refs/tags/${tag}"`, {isReadOnly: true}).then(() => true, () => false); } function getRemoteUrl() { return run('!git config --get remote.origin.url', {isReadOnly: true}).then(stdout => { if(stdout) { return stdout.trim(); } throw new Error('Could not get remote Git url.'); }); } function isWorkingDirClean(requireCleanWorkingDir) { return requireCleanWorkingDir ? run('!git diff-index --name-only HEAD --exit-code', {isReadOnly: true}).catch(() => { throw new Error('Working dir must be clean.'); }) : noop; } function hasChanges(repo) { // Inverted: reject if run promise is resolved (i.e. `git diff-index` returns exit code 0) return new Promise(resolve => { run('!git diff-index --name-only HEAD --exit-code', {isReadOnly: true}).then(() => { config.setRuntimeOption(`${repo}_has_changes`, false); log.warn(`No changes in ${repo} repo.`); resolve(); }).catch(resolve); }); } function clone(repo, dir) { const commitRef = repo.match(commitRefRe); const branch = commitRef && commitRef[0] ? commitRef[0].replace(/^\#/, '') : 'master'; const cleanRepo = repo.replace(commitRef, ''); return run(`rm -rf ${dir}`).then(() => { return run(`git clone ${cleanRepo} -b ${branch} --single-branch ${dir}`).catch(err => { log.error(`Unable to clone ${repo}`); throw new Error(err); }) }); } function stage(file) { if(file) { const files = typeof file === 'string' ? file : file.join(' '); return run(`git add ${files}`).catch(err => { log.debug(err); log.warn(`Could not stage ${file}`); }); } else { return noop; } } function stageDir(baseDir) { baseDir = baseDir || '.'; return run(`git add ${baseDir} --all`); } function status() { return run('!git status --short --untracked-files=no', {isReadOnly: true}).then(stdout => { // Output also when not verbose !config.isVerbose && log.log(stdout); }); } function commit(path, message, version) { return run(`git commit ${config.isForce ? '--allow-empty ' : ''}--message="${util.format(message, version)}"`, path ).catch(err => { log.debug(err); log.warn('No changes to commit. The latest commit will be tagged.'); }); } function tag(version, tag, annotation) { const force = config.isForce ? '--force ' : ''; const message = util.format(annotation, version); const formattedVersion = util.format(tag, version); return run(`git tag ${force}--annotate --message="${message}" ${formattedVersion}`).then(() => { config.setRuntimeOption('tag_set', true); }).catch(() => { log.warn(`Could not tag. Does tag "${version}" already exist? Use --force to move a tag.`); }); } function getLatestTag() { return run('!git describe --tags --abbrev=0', {isReadOnly: true}).then(stdout => { const latestTag = stdout ? stdout.trim() : null; return latestTag; }); } function push(remoteUrl, pushUrl, version) { const repository = pushUrl || ''; return run(`git push --tags ${config.isForce ? '--force ' : ''}${repository}`).catch(err => { log.error('Please make sure an upstream remote repository is configured for the current branch. Example commands:\n' + `git remote add origin ${remoteUrl}\n` + 'git push --set-upstream origin master'); throw new Error(err); }); } function getGithubClient(repo) { if(!_githubClient) { _githubClient = new GitHubApi({ version: '3.0.0', debug: config.isDebug, protocol: 'https', host: repo.host === 'github.com' ? '' : repo.host, pathPrefix: repo.host === 'github.com' ? '' : '/api/v3', timeout: 10000, headers: { 'user-agent': 'webpro/release-it' } }); _githubClient.authenticate({ type: 'oauth', token: config.getRuntimeOption('github_token') }); } return _githubClient; } function getGithubToken(tokenRef) { const token = process.env[tokenRef]; config.setRuntimeOption('github_token', token); return token; } function getChangelog(options) { function runChangelogCommand(command) { return run(command, {isReadOnly: true}).then(stdout => { if(config.isVerbose) { process.stdout.write('\n'); } config.setRuntimeOption('changelog', stdout); return options; }); } if(options.changelogCommand) { if(options.changelogCommand.match(/\[REV_RANGE\]/)) { const previousVersion = config.getRuntimeOption('previousVersion'); const previousTag = util.format(options.src.tagName, previousVersion); return tagExists(previousTag).then(hasTag => { const command = options.changelogCommand.replace(/\[REV_RANGE\]/, hasTag ? `${previousTag}...HEAD` : ''); return runChangelogCommand(command); }).catch(err => { log.warn('Probably the current version in package.json is not a known tag in the repository.'); log.debug(err); throw new Error(`Could not create changelog from latest tag (${previousTag}) to HEAD.`); }); } else { return runChangelogCommand(options.changelogCommand); } } else { return noop; } } function release(options, remoteUrl, tagName) { const repo = repoPathParse(remoteUrl); const version = config.getRuntimeOption('version'); log.execution('node-github releases#createRelease (start)', repo.repository); const githubClient = getGithubClient(repo); const retries = 3; let attempt = 0; if(!config.isDryRun) { const run = () => new Promise((resolve, reject) => { const tag_name = util.format(tagName, version); const name = util.format(options.github.releaseName, version); githubClient.repos.createRelease({ owner: repo.owner, repo: repo.project, tag_name, name, body: config.getRuntimeOption('changelog'), prerelease: options.github.preRelease, draft: options.github.draft }, (err, response) => { if(err) { let message = err.message; try { message = JSON.parse(err.message).message; } catch(err) {} if(err.code === 401) { log.error(`${message} (Attempt 1 of 1)`); return reject(new retry.AbortError('Unauthorized')); } log.error(`${message} (Attempt ${attempt++ + 1} of ${retries + 1})`); return reject(err); } else { config.setRuntimeOption('githubReleaseId', response.data.id); log.execution(`node-github releases#createRelease (success) ${response.meta.location} ${response.data.tag_name} "${response.data.name}"`); log.debug(response); resolve(); } }); }); return retry(run, {retries}).catch(err => { log.debug(err); }) } else { return noop; } } function uploadAsset(repo, filePath) { const githubClient = getGithubClient(repo); const id = config.getRuntimeOption('githubReleaseId'); const name = path.basename(filePath); return new Promise((resolve, reject) => { githubClient.repos.uploadAsset({ owner: repo.owner, repo: repo.project, id, filePath, name }, (err, response) => { if(err) return reject(err); log.execution('node-github releases#uploadAsset (success)', response.data.browser_download_url); log.debug(response); resolve(); }); }); } function uploadAssets(options, remoteUrl, assets) { const repo = repoPathParse(remoteUrl); log.execution('node-github releases#uploadAsset (start)', repo.repository); if(!config.isDryRun) { return new Promise((resolve, reject) => { if (Object.prototype.toString.call(assets) === '[object Array]') { assets = `{${assets.join(',')}}`; } glob(assets, function (err, files) { if(err) return reject(err); if(!files.length) { log.execution('node-github releases#uploadAsset', 'No assets found', assets, process.cwd()); } Promise.all(files.map(filePath => uploadAsset(repo, filePath))).then(resolve, reject); }) }) } else { return noop; } } module.exports = { isGitRepo, getRemoteUrl, status, clone, stage, stageDir, commit, tag, getLatestTag, push, getChangelog, getGithubToken, release, uploadAssets, isWorkingDirClean, hasChanges };