From c9335d5bf6074830cd088374acfea26a696151f0 Mon Sep 17 00:00:00 2001 From: JYC <jycouet@gmail.com> Date: Sun, 12 Nov 2017 10:26:53 +0100 Subject: [PATCH] Add VSTS support (#1049) This PR adds support for Microsoft's [Visual Studio Team Services](https://www.visualstudio.com/team-services/) platform (in addition to existing GitHub and GitLab support). Closes #571 --- lib/config/index.js | 11 + lib/platform/index.js | 3 + lib/platform/vsts/index.js | 486 ++++++++++++++ lib/platform/vsts/vsts-got-wrapper.js | 19 + lib/platform/vsts/vsts-helper.js | 256 +++++++ lib/workers/global/index.js | 1 + lib/workers/pr/index.js | 7 + package.json | 6 +- readme.md | 6 +- test/config/index.spec.js | 39 ++ .../platform/__snapshots__/index.spec.js.snap | 36 +- test/platform/index.spec.js | 24 +- .../vsts/__snapshots__/index.spec.js.snap | 185 +++++ .../vsts-got-wrapper.spec.js.snap | 57 ++ .../__snapshots__/vsts-helper.spec.js.snap | 128 ++++ test/platform/vsts/index.spec.js | 631 ++++++++++++++++++ test/platform/vsts/vsts-got-wrapper.spec.js | 46 ++ test/platform/vsts/vsts-helper.spec.js | 295 ++++++++ .../pr/__snapshots__/index.spec.js.snap | 26 + test/workers/pr/index.spec.js | 9 + test/workers/repository/init/apis.spec.js | 12 + yarn.lock | 304 +++++---- 22 files changed, 2461 insertions(+), 126 deletions(-) create mode 100644 lib/platform/vsts/index.js create mode 100644 lib/platform/vsts/vsts-got-wrapper.js create mode 100644 lib/platform/vsts/vsts-helper.js create mode 100644 test/platform/vsts/__snapshots__/index.spec.js.snap create mode 100644 test/platform/vsts/__snapshots__/vsts-got-wrapper.spec.js.snap create mode 100644 test/platform/vsts/__snapshots__/vsts-helper.spec.js.snap create mode 100644 test/platform/vsts/index.spec.js create mode 100644 test/platform/vsts/vsts-got-wrapper.spec.js create mode 100644 test/platform/vsts/vsts-helper.spec.js diff --git a/lib/config/index.js b/lib/config/index.js index a6a1bc0f6c..97c850a381 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,5 +1,6 @@ const githubApi = require('../platform/github'); const gitlabApi = require('../platform/gitlab'); +const vstsApi = require('../platform/vsts'); const definitions = require('./definitions'); @@ -62,6 +63,10 @@ async function parseConfigs(env, argv) { if (!config.token && !env.GITLAB_TOKEN) { throw new Error('You need to supply a GitLab token.'); } + } else if (config.platform === 'vsts') { + if (!config.token && !env.VSTS_TOKEN) { + throw new Error('You need to supply a VSTS token.'); + } } else { throw new Error(`Unsupported platform: ${config.platform}.`); } @@ -80,6 +85,12 @@ async function parseConfigs(env, argv) { config.token, config.endpoint ); + } else if (config.platform === 'vsts') { + logger.info('Autodiscovering vsts repositories'); + config.repositories = await vstsApi.getRepos( + config.token, + config.endpoint + ); } if (!config.repositories || config.repositories.length === 0) { // Soft fail (no error thrown) if no accessible repositories diff --git a/lib/platform/index.js b/lib/platform/index.js index 1af5e29404..711d984ccc 100644 --- a/lib/platform/index.js +++ b/lib/platform/index.js @@ -1,11 +1,14 @@ const github = require('./github'); const gitlab = require('./gitlab'); +const vsts = require('./vsts'); function initPlatform(val) { if (val === 'github') { global.platform = github; } else if (val === 'gitlab') { global.platform = gitlab; + } else if (val === 'vsts') { + global.platform = vsts; } } diff --git a/lib/platform/vsts/index.js b/lib/platform/vsts/index.js new file mode 100644 index 0000000000..3d1d5d3847 --- /dev/null +++ b/lib/platform/vsts/index.js @@ -0,0 +1,486 @@ +// @ts-nocheck //because of logger, we can't ts-check +const vstsHelper = require('./vsts-helper'); +const gitApi = require('./vsts-got-wrapper'); + +const config = {}; + +module.exports = { + // Initialization + getRepos, + initRepo, + setBaseBranch, + // Search + getFileList, + // Branch + branchExists, + getAllRenovateBranches, + isBranchStale, + getBranchPr, + getBranchStatus, + getBranchStatusCheck, + setBranchStatus, + deleteBranch, + mergeBranch, + getBranchLastCommitTime, + // issue + addAssignees, + addReviewers, + // Comments + ensureComment, + ensureCommentRemoval, + // PR + findPr, + createPr, + getPr, + getPrFiles, + updatePr, + mergePr, + // file + commitFilesToBranch, + getFile, + // Commits + getCommitMessages, +}; + +async function getRepos(token, endpoint) { + logger.debug('getRepos(token, endpoint)'); + vstsHelper.setTokenAndEndpoint(token, endpoint); + const repos = await gitApi().getRepositories(); + return repos.map(repo => repo.name); +} + +async function initRepo(repoName, token, endpoint) { + logger.debug(`initRepo("${repoName}")`); + vstsHelper.setTokenAndEndpoint(token, endpoint); + config.repoName = repoName; + config.fileList = null; + config.prList = null; + const repos = await gitApi().getRepositories(); + const repo = repos.filter(c => c.name === repoName)[0]; + logger.debug({ repositoryDetails: repo }, 'Repository details'); + config.repoId = repo.id; + config.privateRepo = true; + config.isFork = false; + config.owner = '?owner?'; + logger.debug(`${repoName} owner = ${config.owner}`); + // Use default branch as PR target unless later overridden + config.defaultBranch = repo.defaultBranch; + config.baseBranch = config.defaultBranch; + logger.debug(`${repoName} default branch = ${config.defaultBranch}`); + config.baseCommitSHA = await getBranchCommit(config.baseBranch); + + // Todo VSTS: Get Merge method + config.mergeMethod = 'merge'; + // if (res.body.allow_rebase_merge) { + // config.mergeMethod = 'rebase'; + // } else if (res.body.allow_squash_merge) { + // config.mergeMethod = 'squash'; + // } else if (res.body.allow_merge_commit) { + // config.mergeMethod = 'merge'; + // } else { + // logger.debug('Could not find allowed merge methods for repo'); + // } + + // Todo VSTS: Get getBranchProtection + config.repoForceRebase = false; + // try { + // const branchProtection = await getBranchProtection(config.baseBranch); + // if (branchProtection.strict) { + // logger.debug('Repo has branch protection and needs PRs up-to-date'); + // config.repoForceRebase = true; + // } else { + // logger.debug( + // 'Repo has branch protection but does not require up-to-date' + // ); + // } + // } catch (err) { + // if (err.statusCode === 404) { + // logger.debug('Repo has no branch protection'); + // } else if (err.statusCode === 403) { + // logger.debug('Do not have permissions to detect branch protection'); + // } else { + // throw err; + // } + // } + return config; +} + +async function setBaseBranch(branchName) { + if (branchName) { + logger.debug(`Setting baseBranch to ${branchName}`); + config.baseBranch = branchName; + config.baseCommitSHA = await getBranchCommit(config.baseBranch); + } +} + +async function getBranchCommit(fullBranchName) { + const commit = await gitApi().getBranch( + config.repoId, + vstsHelper.getBranchNameWithoutRefsheadsPrefix(fullBranchName) + ); + return commit.commit.commitId; +} + +async function getCommitMessages() { + logger.debug('getCommitMessages'); + try { + // @ts-ignore + const res = await gitApi().getCommits(config.repoId); + const msg = res.map(commit => commit.comment); + return msg; + } catch (err) { + logger.error({ err }, `getCommitMessages error`); + return []; + } +} + +async function getFile(filePath, branchName = config.baseBranch) { + logger.trace(`getFile(filePath=${filePath}, branchName=${branchName})`); + const f = await vstsHelper.getFile( + config.repoId, + config.name, + filePath, + branchName + ); + return f; +} + +async function findPr(branchName, prTitle, state = 'all') { + logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); + let prsFiltered = []; + try { + const prs = await gitApi().getPullRequests(config.repoId, null); + + prsFiltered = prs.filter( + item => item.sourceRefName === vstsHelper.getNewBranchName(branchName) + ); + + if (prTitle) { + prsFiltered = prsFiltered.filter(item => item.title === prTitle); + } + + // update format + prsFiltered = prsFiltered.map(item => vstsHelper.getRenovatePRFormat(item)); + + switch (state) { + case 'all': + // no more filter needed, we can go further... + break; + case 'open': + prsFiltered = prsFiltered.filter(item => item.isClosed === false); + break; + case 'closed': + prsFiltered = prsFiltered.filter(item => item.isClosed === true); + break; + default: + logger.error(`VSTS unmanaged state of a PR (${state})`); + break; + } + } catch (error) { + logger.error('findPr ' + error); + } + if (prsFiltered.length === 0) { + return null; + } + return prsFiltered[0]; +} + +async function getFileList(branchName = config.baseBranch) { + logger.trace(`getFileList('${branchName})'`); + try { + if (config.fileList) { + return config.fileList; + } + const items = await gitApi().getItems( + config.repoId, + null, + null, + 120, // full + null, + null, + null, + false + ); + config.fileList = items + .filter(c => !c.isFolder) + .map(c => c.path.substring(1, c.path.length)) + .sort(); + return config.fileList; + } catch (error) { + logger.error(`getFileList('${branchName})'`); + return []; + } +} + +async function commitFilesToBranch( + branchName, + files, + message, + parentBranch = config.baseBranch +) { + logger.debug( + `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'` + ); + + // Create the new Branch + let branchRef = await vstsHelper.getVSTSBranchObj( + config.repoId, + branchName, + parentBranch + ); + + // create commits + for (const file of files) { + const isBranchExisting = await branchExists(`refs/heads/${branchName}`); + if (isBranchExisting) { + branchRef = await vstsHelper.getVSTSBranchObj( + config.repoId, + branchName, + branchName + ); + } + + const commit = await vstsHelper.getVSTSCommitObj( + message, + file.name, + file.contents, + config.repoId, + config.name, + parentBranch + ); + await gitApi().createPush( + // @ts-ignore + { + commits: [commit], + refUpdates: [branchRef], + }, + config.repoId + ); + } +} + +async function branchExists(branchName) { + logger.debug(`Checking if branch exists: ${branchName}`); + + const branchNameToUse = !branchName.startsWith('refs/heads/') + ? `refs/heads/${branchName}` + : branchName; + + const branchs = await vstsHelper.getRefs(config.repoId, branchNameToUse); + if (branchs.length === 0) { + return false; + } + return true; +} + +async function getBranchPr(branchName) { + logger.debug(`getBranchPr(${branchName})`); + const existingPr = await findPr(branchName, null, 'open'); + return existingPr ? getPr(existingPr.pullRequestId) : null; +} + +async function getBranchStatus(branchName, requiredStatusChecks) { + logger.debug(`getBranchStatus(${branchName})`); + if (!requiredStatusChecks) { + // null means disable status checks, so it always succeeds + return 'success'; + } + if (requiredStatusChecks.length) { + // This is Unsupported + logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`); + return 'failed'; + } + const branchStatusCheck = await getBranchStatusCheck(branchName); + return branchStatusCheck; +} + +async function getBranchStatusCheck(branchName, context) { + logger.trace(`getBranchStatusCheck(${branchName}, ${context})`); + const branch = await gitApi().getBranch( + config.repoId, + vstsHelper.getBranchNameWithoutRefsheadsPrefix(branchName) + ); + if (branch.aheadCount === 0) { + return 'success'; + } + return 'pending'; +} + +async function getPr(pullRequestId) { + logger.debug(`getPr(${pullRequestId})`); + if (!pullRequestId) { + return null; + } + const prs = await gitApi().getPullRequests(config.repoId, null); + const vstsPr = prs.filter(item => item.pullRequestId === pullRequestId); + if (vstsPr.length === 0) { + return null; + } + logger.debug(`pr: (${vstsPr[0]})`); + const pr = vstsHelper.getRenovatePRFormat(vstsPr[0]); + return pr; +} + +async function createPr(branchName, title, body, labels, useDefaultBranch) { + const sourceRefName = vstsHelper.getNewBranchName(branchName); + const targetRefName = useDefaultBranch + ? config.defaultBranch + : config.baseBranch; + const description = vstsHelper.max4000Chars(body); + const pr = await gitApi().createPullRequest( + { + sourceRefName, + targetRefName, + title, + description, + }, + config.repoId + ); + return vstsHelper.getRenovatePRFormat(pr); +} + +async function updatePr(prNo, title, body) { + logger.debug(`updatePr(${prNo}, ${title}, body)`); + await gitApi().updatePullRequest( + // @ts-ignore + { title, description: vstsHelper.max4000Chars(body) }, + config.repoId, + prNo + ); +} + +async function isBranchStale(branchName) { + logger.info(`isBranchStale(${branchName})`); + // Check if branch's parent SHA = master SHA + const branchCommit = await getBranchCommit(branchName); + logger.debug(`branchCommit=${branchCommit}`); + const commitDetails = await vstsHelper.getCommitDetails( + branchCommit, + config.repoId + ); + logger.debug({ commitDetails }, `commitDetails`); + const parentSha = commitDetails.parents[0]; + logger.debug(`parentSha=${parentSha}`); + logger.debug(`config.baseCommitSHA=${config.baseCommitSHA}`); + // Return true if the SHAs don't match + return parentSha !== config.baseCommitSHA; +} + +async function ensureComment(issueNo, topic, content) { + logger.debug(`ensureComment(${issueNo}, ${topic}, content)`); + const body = `### ${topic}\n\n${content}`; + await gitApi().createThread( + { + comments: [{ content: body, commentType: 1, parentCommentId: 0 }], + status: 1, + }, + config.repoId, + issueNo + ); +} + +async function ensureCommentRemoval(issueNo, topic) { + logger.debug(`ensureCommentRemoval(issueNo, topic)(${issueNo}, ${topic})`); + if (issueNo) { + const threads = await gitApi().getThreads(config.repoId, issueNo); + let threadIdFound = null; + + threads.forEach(thread => { + if (thread.comments[0].content.startsWith(`### ${topic}\n\n`)) { + threadIdFound = thread.id; + } + }); + + if (threadIdFound) { + await gitApi().updateThread( + { + status: 4, // close + }, + config.repoId, + issueNo, + threadIdFound + ); + } + } +} + +async function getAllRenovateBranches(branchPrefix) { + logger.debug(`getAllRenovateBranches(branchPrefix)(${branchPrefix})`); + const branches = await gitApi().getBranches(config.repoId); + return branches.filter(c => c.name.startsWith(branchPrefix)).map(c => c.name); +} + +async function deleteBranch(branchName) { + logger.debug(`deleteBranch(branchName)(${branchName})`); + const ref = await vstsHelper.getRefs( + config.repoId, + vstsHelper.getNewBranchName(branchName) + ); + return gitApi().updateRefs( + [ + { + name: ref[0].name, + oldObjectId: ref[0].objectId, + newObjectId: '0000000000000000000000000000000000000000', + }, + ], + config.repoId + ); + + // TODO: Delete PR too? or put it to abandon? +} + +async function getBranchLastCommitTime(branchName) { + logger.debug(`getBranchLastCommitTime(branchName)(${branchName})`); + const branch = await gitApi().getBranch( + config.repoId, + vstsHelper.getBranchNameWithoutRefsheadsPrefix(branchName) + ); + return branch.commit.committer.date; +} + +function setBranchStatus(branchName, context, description, state, targetUrl) { + logger.debug( + `setBranchStatus(${branchName}, ${context}, ${description}, ${state}, ${ + targetUrl + }) - Not supported by VSTS (yet!)` + ); +} + +async function mergeBranch(branchName, mergeType) { + logger.info( + `mergeBranch(branchName, mergeType)(${branchName}, ${ + mergeType + }) - Not supported by VSTS (yet!)` + ); + await null; +} + +async function mergePr(pr) { + logger.info(`mergePr(pr)(${pr}) - Not supported by VSTS (yet!)`); + await null; +} + +async function addAssignees(issueNo, assignees) { + logger.info( + `addAssignees(issueNo, assignees)(${issueNo}, ${ + assignees + }) - Not supported by VSTS (yet!)` + ); + await null; +} + +async function addReviewers(issueNo, reviewers) { + logger.info( + `addReviewers(issueNo, reviewers)(${issueNo}, ${ + reviewers + }) - Not supported by VSTS (yet!)` + ); + await null; +} + +// to become async? +function getPrFiles(prNo) { + logger.info(`getPrFiles(prNo)(${prNo}) - Not supported by VSTS (yet!)`); + return []; +} diff --git a/lib/platform/vsts/vsts-got-wrapper.js b/lib/platform/vsts/vsts-got-wrapper.js new file mode 100644 index 0000000000..6b47efe59f --- /dev/null +++ b/lib/platform/vsts/vsts-got-wrapper.js @@ -0,0 +1,19 @@ +const vsts = require('vso-node-api'); + +function gitApi() { + if (!process.env.VSTS_TOKEN) { + throw new Error(`No token found for vsts`); + } + if (!process.env.VSTS_ENDPOINT) { + throw new Error( + `You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)` + ); + } + const authHandler = vsts.getPersonalAccessTokenHandler( + process.env.VSTS_TOKEN + ); + const connect = new vsts.WebApi(process.env.VSTS_ENDPOINT, authHandler); + return connect.getGitApi(); +} + +module.exports = gitApi; diff --git a/lib/platform/vsts/vsts-helper.js b/lib/platform/vsts/vsts-helper.js new file mode 100644 index 0000000000..65c5e18561 --- /dev/null +++ b/lib/platform/vsts/vsts-helper.js @@ -0,0 +1,256 @@ +// @ts-nocheck + +const gitApi = require('./vsts-got-wrapper'); + +module.exports = { + setTokenAndEndpoint, + getBranchNameWithoutRefsheadsPrefix, + getRefs, + getVSTSBranchObj, + getVSTSCommitObj, + getNewBranchName, + getFile, + max4000Chars, + getRenovatePRFormat, + getCommitDetails, +}; + +/** + * + * @param {string} token + * @param {string} endpoint + */ +function setTokenAndEndpoint(token, endpoint) { + if (token) { + process.env.VSTS_TOKEN = token; + } else if (!process.env.VSTS_TOKEN) { + throw new Error(`No token found for vsts`); + } + if (endpoint) { + process.env.VSTS_ENDPOINT = endpoint; + } else { + throw new Error( + `You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)` + ); + } +} + +/** + * + * @param {string} branchName + */ +function getNewBranchName(branchName) { + if (branchName && !branchName.startsWith('refs/heads/')) { + return `refs/heads/${branchName}`; + } + return branchName; +} + +/** + * + * @param {string} branchPath + */ +function getBranchNameWithoutRefsheadsPrefix(branchPath) { + if (!branchPath) { + logger.error(`getBranchNameWithoutRefsheadsPrefix(${branchPath})`); + return null; + } + if (!branchPath.startsWith('refs/heads/')) { + logger.trace( + `The refs/heads/ name should have started with 'refs/heads/' but it didn't. (${ + branchPath + })` + ); + return branchPath; + } + return branchPath.substring(11, branchPath.length); +} + +/** + * + * @param {string} branchPath + */ +function getBranchNameWithoutRefsPrefix(branchPath) { + if (!branchPath) { + logger.error(`getBranchNameWithoutRefsPrefix(${branchPath})`); + return null; + } + if (!branchPath.startsWith('refs/')) { + logger.trace( + `The ref name should have started with 'refs/' but it didn't. (${ + branchPath + })` + ); + return branchPath; + } + return branchPath.substring(5, branchPath.length); +} + +/** + * + * @param {string} repoId + * @param {string} branchName + */ +async function getRefs(repoId, branchName) { + logger.debug(`getRefs(${repoId}, ${branchName})`); + const refs = await gitApi().getRefs( + repoId, + null, + getBranchNameWithoutRefsPrefix(branchName) + ); + return refs; +} + +/** + * + * @param {string} branchName + * @param {string} from + */ +async function getVSTSBranchObj(repoId, branchName, from) { + const fromBranchName = getNewBranchName(from); + const refs = await getRefs(repoId, fromBranchName); + if (refs.length === 0) { + logger.debug(`getVSTSBranchObj without a valid from, so initial commit.`); + return { + name: getNewBranchName(branchName), + oldObjectId: '0000000000000000000000000000000000000000', + }; + } + return { + name: getNewBranchName(branchName), + oldObjectId: refs[0].objectId, + }; +} +/** + * + * @param {string} msg + * @param {string} filePath + * @param {string} fileContent + * @param {string} repoId + * @param {string} repoName + * @param {string} branchName + */ +async function getVSTSCommitObj( + msg, + filePath, + fileContent, + repoId, + repoName, + branchName +) { + // Add or update + let changeType = 1; + const fileAlreadyThere = await getFile( + repoId, + repoName, + filePath, + branchName + ); + if (fileAlreadyThere) { + changeType = 2; + } + + return { + comment: msg, + author: { + name: 'VSTS Renovate', // Todo... this is not working + }, + committer: { + name: 'VSTS Renovate', // Todo... this is not working + }, + changes: [ + { + changeType, + item: { + path: filePath, + }, + newContent: { + Content: fileContent, + ContentType: 0, // RawText + }, + }, + ], + }; +} + +/** + * if no branchName, look globaly + * @param {string} repoId + * @param {string} repoName + * @param {string} filePath + * @param {string} branchName + */ +async function getFile(repoId, repoName, filePath, branchName) { + logger.trace(`getFile(filePath=${filePath}, branchName=${branchName})`); + const item = await gitApi().getItemText( + repoId, + filePath, + null, + null, + 0, // because we look for 1 file + false, + false, + true, + { + versionType: 0, // branch + versionOptions: 0, + version: getBranchNameWithoutRefsheadsPrefix(branchName), + } + ); + + if (item && item.readable) { + const buffer = item.read(); + // @ts-ignore + const fileContent = Buffer.from(buffer, 'base64').toString(); + try { + const jTmp = JSON.parse(fileContent); + if (jTmp.typeKey === 'GitItemNotFoundException') { + // file not found + return null; + } else if (jTmp.typeKey === 'GitUnresolvableToCommitException') { + // branch not found + return null; + } + } catch (error) { + // it 's not a JSON, so I send the content directly with the line under + } + return fileContent; + } + return null; // no file found +} + +/** + * + * @param {string} str + */ +function max4000Chars(str) { + if (str.length >= 4000) { + return str.substring(0, 3999); + } + return str; +} + +function getRenovatePRFormat(vstsPr) { + const pr = vstsPr; + + pr.displayNumber = `Pull Request #${vstsPr.pullRequestId}`; + pr.number = vstsPr.pullRequestId; + + if (vstsPr.status === 2 || vstsPr.status === 3) { + pr.isClosed = true; + } else { + pr.isClosed = false; + } + + if (vstsPr.mergeStatus === 2) { + pr.isUnmergeable = true; + } + + return pr; +} + +async function getCommitDetails(commit, repoId) { + logger.debug(`getCommitDetails(${commit}, ${repoId})`); + const results = await gitApi().getCommit(commit, repoId); + return results; +} diff --git a/lib/workers/global/index.js b/lib/workers/global/index.js index c9411d2cd6..458cec4c39 100644 --- a/lib/workers/global/index.js +++ b/lib/workers/global/index.js @@ -46,5 +46,6 @@ function getRepositoryConfig(globalConfig, index) { const repoConfig = configParser.mergeChildConfig(globalConfig, repository); repoConfig.isGitHub = repoConfig.platform === 'github'; repoConfig.isGitLab = repoConfig.platform === 'gitlab'; + repoConfig.isVsts = repoConfig.platform === 'vsts'; return configParser.filterConfig(repoConfig, 'repository'); } diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js index 3889723de7..a29adc7a06 100644 --- a/lib/workers/pr/index.js +++ b/lib/workers/pr/index.js @@ -144,6 +144,13 @@ async function ensurePr(prConfig) { .replace('</h4>', ' </h4>') // See #954 .replace(/Pull Request/g, 'Merge Request') .replace(/PR/g, 'MR'); + } else if (config.isVsts) { + // Remove any HTML we use + prBody = prBody + .replace('<summary>', '**') + .replace('</summary>', '**') + .replace('<details>', '') + .replace('</details>', ''); } try { diff --git a/package.json b/package.json index 00dd72b413..403521b3f6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "update" ], "author": "Rhys Arkins <rhys@arkins.net>", + "contributors": [ + "Jean-Yves Couët <jycouet@gmail.com>" + ], "license": "MIT", "bugs": { "url": "https://github.com/singapore/renovate/issues" @@ -72,7 +75,8 @@ "showdown": "1.8.2", "tmp-promise": "1.0.4", "traverse": "0.6.6", - "yarn": "1.3.2" + "yarn": "1.3.2", + "vso-node-api": "6.2.8-preview" }, "devDependencies": { "babel-plugin-transform-object-rest-spread": "6.26.0", diff --git a/readme.md b/readme.md index 70b03498e6..32a6950f0d 100644 --- a/readme.md +++ b/readme.md @@ -18,7 +18,7 @@ Keep dependencies up-to-date. - Configurable via file, environment, CLI, and `package.json` - Supports eslint-like preset configs for ease of use - Updates `yarn.lock` and `package-lock.json` files natively -- Supports GitHub and GitLab +- Supports GitHub, GitLab and VSTS (in beta) - Open source and can be self-hosted or used via GitHub App ## Configuration Help @@ -43,8 +43,10 @@ You can find instructions for GitHub [here](https://help.github.com/articles/cre You can find instructions for GitLab [here](https://docs.gitlab.com/ee/api/README.html#personal-access-tokens). +You can find instructions for VSTS [vsts](https://www.visualstudio.com/en-us/docs/integrate/get-started/authentication/pats). + This token needs to be configured via file, environment variable, or CLI. See [docs/configuration.md](docs/configuration.md) for details. -The simplest way is to expose it as `GITHUB_TOKEN` or `GITLAB_TOKEN`. +The simplest way is to expose it as `GITHUB_TOKEN` or `GITLAB_TOKEN` or `VSTS_TOKEN`. ## Usage diff --git a/test/config/index.spec.js b/test/config/index.spec.js index 4abc970c28..4e306243c0 100644 --- a/test/config/index.spec.js +++ b/test/config/index.spec.js @@ -7,6 +7,8 @@ describe('config/index', () => { let defaultArgv; let ghGot; let get; + let gitApi; + let vstsHelper; beforeEach(() => { jest.resetModules(); configParser = require('../../lib/config/index.js'); @@ -15,6 +17,10 @@ describe('config/index', () => { ghGot = require('gh-got'); jest.mock('gl-got'); get = require('gl-got'); + jest.mock('../../lib/platform/vsts/vsts-got-wrapper'); + gitApi = require('../../lib/platform/vsts/vsts-got-wrapper'); + jest.mock('../../lib/platform/vsts/vsts-helper'); + vstsHelper = require('../../lib/platform/vsts/vsts-helper'); }); it('throws for invalid platform', async () => { const env = {}; @@ -47,6 +53,16 @@ describe('config/index', () => { } expect(err.message).toBe('You need to supply a GitLab token.'); }); + it('throws for no vsts token', async () => { + const env = { RENOVATE_PLATFORM: 'vsts' }; + let err; + try { + await configParser.parseConfigs(env, defaultArgv); + } catch (e) { + err = e; + } + expect(err.message).toBe('You need to supply a VSTS token.'); + }); it('supports token in env', async () => { const env = { GITHUB_TOKEN: 'abc' }; await configParser.parseConfigs(env, defaultArgv); @@ -92,6 +108,29 @@ describe('config/index', () => { expect(ghGot.mock.calls.length).toBe(0); expect(get.mock.calls.length).toBe(1); }); + it('autodiscovers vsts platform', async () => { + const env = {}; + defaultArgv = defaultArgv.concat([ + '--autodiscover', + '--platform=vsts', + '--token=abc', + ]); + vstsHelper.getFile.mockImplementationOnce(() => `Hello Renovate!`); + gitApi.mockImplementationOnce(() => ({ + getRepositories: jest.fn(() => [ + { + name: 'a/b', + }, + { + name: 'c/d', + }, + ]), + })); + await configParser.parseConfigs(env, defaultArgv); + expect(ghGot.mock.calls.length).toBe(0); + expect(get.mock.calls.length).toBe(0); + expect(gitApi.mock.calls.length).toBe(1); + }); it('logs if no autodiscovered repositories', async () => { const env = { GITHUB_TOKEN: 'abc' }; defaultArgv = defaultArgv.concat(['--autodiscover']); diff --git a/test/platform/__snapshots__/index.spec.js.snap b/test/platform/__snapshots__/index.spec.js.snap index 985ec7ba90..82f61393e7 100644 --- a/test/platform/__snapshots__/index.spec.js.snap +++ b/test/platform/__snapshots__/index.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`platform has same API for github and gitlab 1`] = ` +exports[`platform has a list of supported methods for github 1`] = ` Array [ "getRepos", "initRepo", @@ -32,7 +32,39 @@ Array [ ] `; -exports[`platform has same API for github and gitlab 2`] = ` +exports[`platform has a list of supported methods for gitlab 1`] = ` +Array [ + "getRepos", + "initRepo", + "setBaseBranch", + "getFileList", + "branchExists", + "getAllRenovateBranches", + "isBranchStale", + "getBranchPr", + "getBranchStatus", + "getBranchStatusCheck", + "setBranchStatus", + "deleteBranch", + "mergeBranch", + "getBranchLastCommitTime", + "addAssignees", + "addReviewers", + "ensureComment", + "ensureCommentRemoval", + "findPr", + "createPr", + "getPr", + "getPrFiles", + "updatePr", + "mergePr", + "commitFilesToBranch", + "getFile", + "getCommitMessages", +] +`; + +exports[`platform has a list of supported methods for vsts 1`] = ` Array [ "getRepos", "initRepo", diff --git a/test/platform/index.spec.js b/test/platform/index.spec.js index 46658e3996..dac18f8aaf 100644 --- a/test/platform/index.spec.js +++ b/test/platform/index.spec.js @@ -1,12 +1,32 @@ const github = require('../../lib/platform/github'); const gitlab = require('../../lib/platform/gitlab'); +const vsts = require('../../lib/platform/vsts'); describe('platform', () => { - it('has same API for github and gitlab', () => { + it('has a list of supported methods for github', () => { const githubMethods = Object.keys(github); - const gitlabMethods = Object.keys(gitlab); expect(githubMethods).toMatchSnapshot(); + }); + + it('has a list of supported methods for gitlab', () => { + const gitlabMethods = Object.keys(gitlab); expect(gitlabMethods).toMatchSnapshot(); + }); + + it('has a list of supported methods for vsts', () => { + const vstsMethods = Object.keys(vsts); + expect(vstsMethods).toMatchSnapshot(); + }); + + it('has same API for github and gitlab', () => { + const githubMethods = Object.keys(github); + const gitlabMethods = Object.keys(gitlab); expect(githubMethods).toMatchObject(gitlabMethods); }); + + it('has same API for github and vsts', () => { + const githubMethods = Object.keys(github); + const vstsMethods = Object.keys(vsts); + expect(githubMethods).toMatchObject(vstsMethods); + }); }); diff --git a/test/platform/vsts/__snapshots__/index.spec.js.snap b/test/platform/vsts/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000000..1b1c18bd19 --- /dev/null +++ b/test/platform/vsts/__snapshots__/index.spec.js.snap @@ -0,0 +1,185 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`platform/vsts createPr() should create and return a PR object 1`] = ` +Object { + "displayNumber": "Pull Request #456", + "number": 456, + "pullRequestId": 456, +} +`; + +exports[`platform/vsts createPr() should create and return a PR object from base branch 1`] = ` +Object { + "displayNumber": "Pull Request #456", + "number": 456, + "pullRequestId": 456, +} +`; + +exports[`platform/vsts deleteBranch should delete the branch 1`] = ` +Array [ + Object { + "name": "refs/head/testBranch", + "newObjectId": "0000000000000000000000000000000000000000", + "oldObjectId": "123456", + }, +] +`; + +exports[`platform/vsts ensureComment add comment 1`] = ` +Array [ + Array [], + Array [], + Array [], +] +`; + +exports[`platform/vsts findPr(branchName, prTitle, state) returns pr if found it all state 1`] = ` +Object { + "head": Object { + "ref": "branch-a", + }, + "isClosed": true, + "number": 1, + "title": "branch a pr", +} +`; + +exports[`platform/vsts findPr(branchName, prTitle, state) returns pr if found it but add an error 1`] = ` +Object { + "head": Object { + "ref": "branch-a", + }, + "isClosed": true, + "number": 1, + "title": "branch a pr", +} +`; + +exports[`platform/vsts findPr(branchName, prTitle, state) returns pr if found it close 1`] = ` +Object { + "head": Object { + "ref": "branch-a", + }, + "isClosed": true, + "number": 1, + "title": "branch a pr", +} +`; + +exports[`platform/vsts findPr(branchName, prTitle, state) returns pr if found it open 1`] = ` +Object { + "head": Object { + "ref": "branch-a", + }, + "isClosed": false, + "number": 1, + "title": "branch a pr", +} +`; + +exports[`platform/vsts getAllRenovateBranches() should return all renovate branches 1`] = ` +Array [ + "renovate/a", + "renovate/b", +] +`; + +exports[`platform/vsts getBranchLastCommitTime should return a Date 1`] = `"1986-11-07T00:00:00Z"`; + +exports[`platform/vsts getBranchPr(branchName) should return the pr 1`] = ` +Object { + "head": Object { + "ref": "branch-a", + }, + "isClosed": false, + "number": 1, + "pullRequestId": 1, + "title": "branch a pr", +} +`; + +exports[`platform/vsts getCommitMessages() returns commits messages 1`] = ` +Array [ + "com1", + "com2", + "com3", +] +`; + +exports[`platform/vsts getFile(filePatch, branchName) should return the encoded file content 1`] = `"Hello Renovate!"`; + +exports[`platform/vsts getFileList should return the files matching the fileName 1`] = ` +Array [ + "package.json", + "src/app/package.json", + "src/otherapp/package.json", + "symlinks/package.json", +] +`; + +exports[`platform/vsts getPr(prNo) should return a pr in the right format 1`] = ` +Object { + "pullRequestId": 1234, +} +`; + +exports[`platform/vsts getRepos should return an array of repos 1`] = ` +Array [ + Array [], +] +`; + +exports[`platform/vsts getRepos should return an array of repos 2`] = ` +Array [ + "a/b", + "c/d", +] +`; + +exports[`platform/vsts initRepo should initialise the config for a repo 1`] = ` +Array [ + Array [], + Array [], +] +`; + +exports[`platform/vsts initRepo should initialise the config for a repo 2`] = ` +Object { + "baseBranch": "defBr", + "baseCommitSHA": "1234", + "defaultBranch": "defBr", + "fileList": null, + "isFork": false, + "mergeMethod": "merge", + "owner": "?owner?", + "prList": null, + "privateRepo": true, + "repoForceRebase": false, + "repoId": "1", + "repoName": "some/repo", +} +`; + +exports[`platform/vsts setBaseBranch(branchName) sets the base branch 1`] = ` +Array [ + Array [], + Array [], + Array [], +] +`; + +exports[`platform/vsts setBaseBranch(branchName) sets the base branch 2`] = ` +Array [ + Array [], + Array [], +] +`; + +exports[`platform/vsts updatePr(prNo, title, body) should update the PR 1`] = ` +Array [ + Array [], + Array [], + Array [], +] +`; diff --git a/test/platform/vsts/__snapshots__/vsts-got-wrapper.spec.js.snap b/test/platform/vsts/__snapshots__/vsts-got-wrapper.spec.js.snap new file mode 100644 index 0000000000..ce50951676 --- /dev/null +++ b/test/platform/vsts/__snapshots__/vsts-got-wrapper.spec.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`platform/vsts/vsts-got-wrapper gitApi should set token and endpoint 1`] = ` +GitApi { + "baseUrl": "myEndpoint", + "http": HttpClient { + "_certConfig": undefined, + "_httpProxy": undefined, + "_ignoreSslError": false, + "_socketTimeout": undefined, + "handlers": Array [ + PersonalAccessTokenCredentialHandler { + "token": "myToken", + }, + ], + "requestOptions": Object {}, + "userAgent": "node-Git-api", + }, + "rest": RestClient { + "client": HttpClient { + "_certConfig": undefined, + "_httpProxy": undefined, + "_ignoreSslError": false, + "_socketTimeout": undefined, + "handlers": Array [ + PersonalAccessTokenCredentialHandler { + "token": "myToken", + }, + ], + "requestOptions": Object {}, + "userAgent": "node-Git-api", + }, + }, + "userAgent": "node-Git-api", + "vsoClient": VsoClient { + "_initializationPromise": Promise {}, + "_locationsByAreaPromises": Object {}, + "basePath": "myEndpoint", + "baseUrl": "myEndpoint", + "restClient": RestClient { + "client": HttpClient { + "_certConfig": undefined, + "_httpProxy": undefined, + "_ignoreSslError": false, + "_socketTimeout": undefined, + "handlers": Array [ + PersonalAccessTokenCredentialHandler { + "token": "myToken", + }, + ], + "requestOptions": Object {}, + "userAgent": "node-Git-api", + }, + }, + }, +} +`; diff --git a/test/platform/vsts/__snapshots__/vsts-helper.spec.js.snap b/test/platform/vsts/__snapshots__/vsts-helper.spec.js.snap new file mode 100644 index 0000000000..c977866b9c --- /dev/null +++ b/test/platform/vsts/__snapshots__/vsts-helper.spec.js.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`platform/vsts/helpers getCommitDetails should get commit details 1`] = ` +Object { + "parents": Array [ + "123456", + ], +} +`; + +exports[`platform/vsts/helpers getFile should return the file content because it is not a json 1`] = `"{\\"hello\\"= \\"test\\"}"`; + +exports[`platform/vsts/helpers getRef should get the ref 1`] = ` +Array [ + Object { + "objectId": 132, + }, +] +`; + +exports[`platform/vsts/helpers getRef should get the ref 2`] = ` +Array [ + Object { + "objectId": "132", + }, +] +`; + +exports[`platform/vsts/helpers getRenovatePRFormat should be formated (closed v2) 1`] = ` +Object { + "displayNumber": "Pull Request #undefined", + "isClosed": true, + "number": undefined, + "status": 3, +} +`; + +exports[`platform/vsts/helpers getRenovatePRFormat should be formated (closed) 1`] = ` +Object { + "displayNumber": "Pull Request #undefined", + "isClosed": true, + "number": undefined, + "status": 2, +} +`; + +exports[`platform/vsts/helpers getRenovatePRFormat should be formated (isUnmergeable) 1`] = ` +Object { + "displayNumber": "Pull Request #undefined", + "isClosed": false, + "isUnmergeable": true, + "mergeStatus": 2, + "number": undefined, +} +`; + +exports[`platform/vsts/helpers getRenovatePRFormat should be formated (not closed) 1`] = ` +Object { + "displayNumber": "Pull Request #undefined", + "isClosed": false, + "number": undefined, + "status": 1, +} +`; + +exports[`platform/vsts/helpers getVSTSBranchObj should be the branch object formated 1`] = ` +Object { + "name": "refs/heads/branchName", + "oldObjectId": "132", +} +`; + +exports[`platform/vsts/helpers getVSTSBranchObj should be the branch object formated 2`] = ` +Object { + "name": "refs/heads/branchName", + "oldObjectId": "0000000000000000000000000000000000000000", +} +`; + +exports[`platform/vsts/helpers getVSTSCommitObj should be get the commit obj formated (file to create) 1`] = ` +Object { + "author": Object { + "name": "VSTS Renovate", + }, + "changes": Array [ + Object { + "changeType": 1, + "item": Object { + "path": "./myFilePath/test", + }, + "newContent": Object { + "Content": "Hello world!", + "ContentType": 0, + }, + }, + ], + "comment": "Commit msg", + "committer": Object { + "name": "VSTS Renovate", + }, +} +`; + +exports[`platform/vsts/helpers getVSTSCommitObj should be get the commit obj formated (file to update) 1`] = ` +Object { + "author": Object { + "name": "VSTS Renovate", + }, + "changes": Array [ + Object { + "changeType": 2, + "item": Object { + "path": "./myFilePath/test", + }, + "newContent": Object { + "Content": "Hello world!", + "ContentType": 0, + }, + }, + ], + "comment": "Commit msg", + "committer": Object { + "name": "VSTS Renovate", + }, +} +`; + +exports[`platform/vsts/helpers max4000Chars should be the same 1`] = `"Hello"`; diff --git a/test/platform/vsts/index.spec.js b/test/platform/vsts/index.spec.js new file mode 100644 index 0000000000..584c73e7a1 --- /dev/null +++ b/test/platform/vsts/index.spec.js @@ -0,0 +1,631 @@ +describe('platform/vsts', () => { + let vsts; + let gitApi; + let vstsHelper; + beforeEach(() => { + // clean up env + delete process.env.VSTS_TOKEN; + delete process.env.VSTS_ENDPOINT; + + // reset module + jest.resetModules(); + jest.mock('../../../lib/platform/vsts/vsts-got-wrapper'); + jest.mock('../../../lib/platform/vsts/vsts-helper'); + vsts = require('../../../lib/platform/vsts'); + gitApi = require('../../../lib/platform/vsts/vsts-got-wrapper'); + vstsHelper = require('../../../lib/platform/vsts/vsts-helper'); + }); + + function getRepos(token, endpoint) { + gitApi.mockImplementationOnce(() => ({ + getRepositories: jest.fn(() => [ + { + name: 'a/b', + }, + { + name: 'c/d', + }, + ]), + })); + return vsts.getRepos(token, endpoint); + } + + describe('getRepos', () => { + it('should return an array of repos', async () => { + const repos = await getRepos( + 'sometoken', + 'https://fabrikam.VisualStudio.com/DefaultCollection' + ); + expect(gitApi.mock.calls).toMatchSnapshot(); + expect(repos).toMatchSnapshot(); + }); + }); + + function initRepo(...args) { + gitApi.mockImplementationOnce(() => ({ + getRepositories: jest.fn(() => [ + { + name: 'some/repo', + id: '1', + privateRepo: true, + isFork: false, + defaultBranch: 'defBr', + }, + { + name: 'c/d', + }, + ]), + })); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ commit: { commitId: '1234' } })), + })); + + return vsts.initRepo(...args); + } + + describe('initRepo', () => { + it(`should initialise the config for a repo`, async () => { + const config = await initRepo( + 'some/repo', + 'token', + 'https://my.custom.endpoint/' + ); + expect(gitApi.mock.calls).toMatchSnapshot(); + expect(config).toMatchSnapshot(); + }); + }); + + describe('setBaseBranch(branchName)', () => { + it('sets the base branch', async () => { + await initRepo('some/repo', 'token'); + // getBranchCommit + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ + commit: { commitId: '1234' }, + })), + })); + await vsts.setBaseBranch('some-branch'); + expect(gitApi.mock.calls).toMatchSnapshot(); + }); + it('sets the base branch', async () => { + await initRepo('some/repo', 'token'); + // getBranchCommit + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ + commit: { commitId: '1234' }, + })), + })); + await vsts.setBaseBranch(); + expect(gitApi.mock.calls).toMatchSnapshot(); + }); + }); + + describe('getCommitMessages()', () => { + it('returns commits messages', async () => { + const config = await initRepo( + 'some/repo', + 'token', + 'https://my.custom.endpoint/' + ); + expect(config.repoId).toBe('1'); + gitApi.mockImplementationOnce(() => ({ + getCommits: jest.fn(() => [ + { comment: 'com1' }, + { comment: 'com2' }, + { comment: 'com3' }, + ]), + })); + const msg = await vsts.getCommitMessages(); + expect(msg).toMatchSnapshot(); + }); + it('returns empty array if error', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => { + throw new Error('some error'); + }); + const msgs = await vsts.getCommitMessages(); + expect(msgs).toEqual([]); + }); + }); + + describe('getFile(filePatch, branchName)', () => { + it('should return the encoded file content', async () => { + await initRepo('some/repo', 'token'); + vstsHelper.getFile.mockImplementationOnce(() => `Hello Renovate!`); + const content = await vsts.getFile('package.json'); + expect(content).toMatchSnapshot(); + }); + }); + + describe('findPr(branchName, prTitle, state)', () => { + it('returns pr if found it open', async () => { + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => [ + { + pullRequestId: 1, + sourceRefName: 'refs/heads/branch-a', + title: 'branch a pr', + status: 2, + }, + ]), + })); + vstsHelper.getNewBranchName.mockImplementationOnce( + () => 'refs/heads/branch-a' + ); + vstsHelper.getRenovatePRFormat.mockImplementationOnce(() => ({ + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + isClosed: false, + })); + const res = await vsts.findPr('branch-a', 'branch a pr', 'open'); + expect(res).toMatchSnapshot(); + }); + it('returns pr if found it close', async () => { + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => [ + { + pullRequestId: 1, + sourceRefName: 'refs/heads/branch-a', + title: 'branch a pr', + status: 2, + }, + ]), + })); + vstsHelper.getNewBranchName.mockImplementationOnce( + () => 'refs/heads/branch-a' + ); + vstsHelper.getRenovatePRFormat.mockImplementationOnce(() => ({ + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + isClosed: true, + })); + const res = await vsts.findPr('branch-a', 'branch a pr', 'closed'); + expect(res).toMatchSnapshot(); + }); + it('returns pr if found it all state', async () => { + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => [ + { + pullRequestId: 1, + sourceRefName: 'refs/heads/branch-a', + title: 'branch a pr', + status: 2, + }, + ]), + })); + vstsHelper.getNewBranchName.mockImplementationOnce( + () => 'refs/heads/branch-a' + ); + vstsHelper.getRenovatePRFormat.mockImplementationOnce(() => ({ + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + isClosed: true, + })); + const res = await vsts.findPr('branch-a', 'branch a pr'); + expect(res).toMatchSnapshot(); + }); + it('returns pr if found it but add an error', async () => { + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => [ + { + pullRequestId: 1, + sourceRefName: 'refs/heads/branch-a', + title: 'branch a pr', + status: 2, + }, + ]), + })); + vstsHelper.getNewBranchName.mockImplementationOnce( + () => 'refs/heads/branch-a' + ); + vstsHelper.getRenovatePRFormat.mockImplementationOnce(() => ({ + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + isClosed: true, + })); + const res = await vsts.findPr('branch-a', 'branch a pr', 'blabla'); + expect(res).toMatchSnapshot(); + }); + it('returns null if error', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => { + throw new Error('some error'); + }); + const pr = await vsts.findPr('branch-a', 'branch a pr'); + expect(pr).toBeNull(); + }); + }); + + describe('getFileList', () => { + it('returns empty array if error', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => { + throw new Error('some error'); + }); + const files = await vsts.getFileList(); + expect(files).toEqual([]); + }); + it('caches the result', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getItems: jest.fn(() => [ + { path: '/symlinks/package.json' }, + { isFolder: false, path: '/package.json' }, + { isFolder: true, path: '/some-dir' }, + { type: 'blob', path: '/src/app/package.json' }, + { type: 'blob', path: '/src/otherapp/package.json' }, + ]), + })); + let files = await vsts.getFileList(); + expect(files.length).toBe(4); + files = await vsts.getFileList(); + expect(files.length).toBe(4); + }); + it('should return the files matching the fileName', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getItems: jest.fn(() => [ + { path: '/symlinks/package.json' }, + { isFolder: false, path: '/package.json' }, + { isFolder: true, path: '/some-dir' }, + { type: 'blob', path: '/src/app/package.json' }, + { type: 'blob', path: '/src/otherapp/package.json' }, + ]), + })); + const files = await vsts.getFileList(); + expect(files).toMatchSnapshot(); + }); + }); + + describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => { + it('should add a new commit to the branch', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + createPush: jest.fn(() => true), + })); + vstsHelper.getVSTSBranchObj.mockImplementationOnce(() => 'newBranch'); + vstsHelper.getRefs.mockImplementation(() => [{ objectId: '123' }]); + + const files = [ + { + name: 'package.json', + contents: 'hello world', + }, + ]; + await vsts.commitFilesToBranch( + 'package.json', + files, + 'my commit message' + ); + expect(gitApi.mock.calls.length).toBe(3); + }); + it('should add a new commit to an existing branch', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + createPush: jest.fn(() => true), + })); + vstsHelper.getVSTSBranchObj.mockImplementationOnce(() => 'newBranch'); + vstsHelper.getRefs.mockImplementation(() => []); + + const files = [ + { + name: 'package.json', + contents: 'hello world', + }, + ]; + await vsts.commitFilesToBranch( + 'package.json', + files, + 'my commit message' + ); + expect(gitApi.mock.calls.length).toBe(3); + }); + }); + + describe('branchExists(branchName)', () => { + it('should return false if the branch does not exist', async () => { + await initRepo('some/repo', 'token'); + vstsHelper.getRefs.mockImplementation(() => []); + const exists = await vsts.branchExists('thebranchname'); + expect(exists).toBe(false); + }); + }); + + describe('getBranchPr(branchName)', () => { + it('should return null if no PR exists', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + findPr: jest.fn(() => false), + getPr: jest.fn(() => { + 'myPRName'; + }), + })); + const pr = await vsts.getBranchPr('somebranch'); + expect(pr).toBe(null); + }); + it('should return the pr', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementation(() => ({ + getPullRequests: jest.fn(() => [ + { + pullRequestId: 1, + sourceRefName: 'refs/heads/branch-a', + title: 'branch a pr', + status: 2, + }, + ]), + })); + vstsHelper.getNewBranchName.mockImplementation( + () => 'refs/heads/branch-a' + ); + vstsHelper.getRenovatePRFormat.mockImplementation(() => ({ + pullRequestId: 1, + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + isClosed: false, + })); + const pr = await vsts.getBranchPr('somebranch'); + expect(pr).toMatchSnapshot(); + }); + }); + + describe('getBranchStatus(branchName, requiredStatusChecks)', () => { + it('return success if requiredStatusChecks null', async () => { + await initRepo('some/repo', 'token'); + const res = await vsts.getBranchStatus('somebranch', null); + expect(res).toEqual('success'); + }); + it('return failed if unsupported requiredStatusChecks', async () => { + await initRepo('some/repo', 'token'); + const res = await vsts.getBranchStatus('somebranch', ['foo']); + expect(res).toEqual('failed'); + }); + it('should pass through success', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ aheadCount: 0 })), + })); + const res = await vsts.getBranchStatus('somebranch', []); + expect(res).toEqual('success'); + }); + it('should pass through failed', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ aheadCount: 123 })), + })); + const res = await vsts.getBranchStatus('somebranch', []); + expect(res).toEqual('pending'); + }); + }); + + describe('getPr(prNo)', () => { + it('should return null if no prNo is passed', async () => { + const pr = await vsts.getPr(null); + expect(pr).toBe(null); + }); + it('should return null if no PR is returned from vsts', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => []), + })); + const pr = await vsts.getPr(1234); + expect(pr).toBe(null); + }); + it('should return a pr in the right format', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getPullRequests: jest.fn(() => [{ pullRequestId: 1234 }]), + })); + vstsHelper.getRenovatePRFormat.mockImplementation(() => ({ + pullRequestId: 1234, + })); + const pr = await vsts.getPr(1234); + expect(pr).toMatchSnapshot(); + }); + }); + + describe('createPr()', () => { + it('should create and return a PR object', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + createPullRequest: jest.fn(() => ({ + pullRequestId: 456, + displayNumber: `Pull Request #456`, + })), + })); + vstsHelper.getRenovatePRFormat.mockImplementation(() => ({ + displayNumber: 'Pull Request #456', + number: 456, + pullRequestId: 456, + })); + const pr = await vsts.createPr( + 'some-branch', + 'The Title', + 'Hello world', + ['deps', 'renovate'] + ); + expect(pr).toMatchSnapshot(); + }); + it('should create and return a PR object from base branch', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + createPullRequest: jest.fn(() => ({ + pullRequestId: 456, + displayNumber: `Pull Request #456`, + })), + })); + vstsHelper.getRenovatePRFormat.mockImplementation(() => ({ + displayNumber: 'Pull Request #456', + number: 456, + pullRequestId: 456, + })); + const pr = await vsts.createPr( + 'some-branch', + 'The Title', + 'Hello world', + ['deps', 'renovate'], + true + ); + expect(pr).toMatchSnapshot(); + }); + }); + + describe('updatePr(prNo, title, body)', () => { + it('should update the PR', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + updatePullRequest: jest.fn(), + })); + await vsts.updatePr(1234, 'The New Title', 'Hello world again'); + expect(gitApi.mock.calls).toMatchSnapshot(); + }); + }); + + describe('ensureComment', () => { + it('add comment', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementation(() => ({ + createThread: jest.fn(() => [{ id: 123 }]), + })); + await vsts.ensureComment(42, 'some-subject', 'some\ncontent'); + expect(gitApi.mock.calls).toMatchSnapshot(); + }); + }); + + describe('isBranchStale', () => { + it('should return true', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ commit: { commitId: '123456' } })), + })); + vstsHelper.getCommitDetails.mockImplementation(() => ({ + parents: ['789654'], + })); + const res = await vsts.isBranchStale(); + expect(res).toBe(true); + }); + it('should return false', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ commit: { commitId: '123457' } })), + })); + vstsHelper.getCommitDetails.mockImplementation(() => ({ + parents: ['1234'], + })); + const res = await vsts.isBranchStale('branch'); + expect(res).toBe(false); + }); + }); + + describe('getAllRenovateBranches()', () => { + it('should return all renovate branches', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranches: jest.fn(() => [ + { name: 'master' }, + { name: 'renovate/a' }, + { name: 'renovate/b' }, + ]), + })); + const res = await vsts.getAllRenovateBranches('renovate/'); + expect(res).toMatchSnapshot(); + }); + }); + + describe('ensureCommentRemoval', () => { + it('deletes comment if found', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementation(() => ({ + getThreads: jest.fn(() => [ + { comments: [{ content: '### some-subject\n\nblabla' }], id: 123 }, + ]), + updateThread: jest.fn(), + })); + await vsts.ensureCommentRemoval(42, 'some-subject'); + expect(gitApi.mock.calls.length).toBe(4); + }); + it('nothing should happen, no number', async () => { + await vsts.ensureCommentRemoval(); + expect(gitApi.mock.calls.length).toBe(0); + }); + it('comment not found', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementation(() => ({ + getThreads: jest.fn(() => [ + { comments: [{ content: 'stupid comment' }], id: 123 }, + ]), + updateThread: jest.fn(), + })); + await vsts.ensureCommentRemoval(42, 'some-subject'); + expect(gitApi.mock.calls.length).toBe(3); + }); + }); + + describe('getBranchLastCommitTime', () => { + it('should return a Date', async () => { + await initRepo('some/repo', 'token'); + gitApi.mockImplementationOnce(() => ({ + getBranch: jest.fn(() => ({ + commit: { committer: { date: '1986-11-07T00:00:00Z' } }, + })), + })); + const res = await vsts.getBranchLastCommitTime('some-branch'); + expect(res).toMatchSnapshot(); + }); + }); + + describe('deleteBranch', () => { + it('should delete the branch', async () => { + vstsHelper.getRefs.mockImplementation(() => [{ objectId: '123' }]); + gitApi.mockImplementationOnce(() => ({ + updateRefs: jest.fn(() => [ + { + name: 'refs/head/testBranch', + oldObjectId: '123456', + newObjectId: '0000000000000000000000000000000000000000', + }, + ]), + })); + const res = await vsts.deleteBranch(); + expect(res).toMatchSnapshot(); + }); + }); + + describe('Not supported by VSTS (yet!)', () => { + it('setBranchStatus', () => { + const res = vsts.setBranchStatus(); + expect(res).toBeUndefined(); + }); + + it('mergeBranch', async () => { + const res = await vsts.mergeBranch(); + expect(res).toBeUndefined(); + }); + + it('mergePr', async () => { + const res = await vsts.mergePr(); + expect(res).toBeUndefined(); + }); + + it('addAssignees', async () => { + const res = await vsts.addAssignees(); + expect(res).toBeUndefined(); + }); + + it('addReviewers', async () => { + const res = await vsts.addReviewers(); + expect(res).toBeUndefined(); + }); + + // to become async? + it('getPrFiles', () => { + const res = vsts.getPrFiles(46); + expect(res.length).toBe(0); + }); + }); +}); diff --git a/test/platform/vsts/vsts-got-wrapper.spec.js b/test/platform/vsts/vsts-got-wrapper.spec.js new file mode 100644 index 0000000000..882739e165 --- /dev/null +++ b/test/platform/vsts/vsts-got-wrapper.spec.js @@ -0,0 +1,46 @@ +describe('platform/vsts/vsts-got-wrapper', () => { + let gitApi; + beforeEach(() => { + // clean up env + delete process.env.VSTS_TOKEN; + delete process.env.VSTS_ENDPOINT; + + // reset module + jest.resetModules(); + gitApi = require('../../../lib/platform/vsts/vsts-got-wrapper'); + }); + + describe('gitApi', () => { + it('should throw an error if no token is provided', async () => { + let err; + try { + await gitApi(); + } catch (e) { + err = e; + } + expect(err.message).toBe('No token found for vsts'); + }); + it('should throw an error if no endpoint is provided', async () => { + let err; + try { + process.env.VSTS_TOKEN = 'myToken'; + await gitApi(); + } catch (e) { + err = e; + } + expect(err.message).toBe( + `You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)` + ); + }); + it('should set token and endpoint', async () => { + process.env.VSTS_TOKEN = 'myToken'; + process.env.VSTS_ENDPOINT = 'myEndpoint'; + const res = await gitApi(); + + // We will track if the lib vso-node-api change + expect(res).toMatchSnapshot(); + expect(process.env.VSTS_TOKEN).toBe(`myToken`); + expect(process.env.VSTS_ENDPOINT).toBe(`myEndpoint`); + }); + }); +}); diff --git a/test/platform/vsts/vsts-helper.spec.js b/test/platform/vsts/vsts-helper.spec.js new file mode 100644 index 0000000000..f1b34991cf --- /dev/null +++ b/test/platform/vsts/vsts-helper.spec.js @@ -0,0 +1,295 @@ +describe('platform/vsts/helpers', () => { + let vstsHelper; + let gitApi; + + beforeEach(() => { + // clean up env + delete process.env.VSTS_TOKEN; + delete process.env.VSTS_ENDPOINT; + + // reset module + jest.resetModules(); + jest.mock('../../../lib/platform/vsts/vsts-got-wrapper'); + vstsHelper = require('../../../lib/platform/vsts/vsts-helper'); + gitApi = require('../../../lib/platform/vsts/vsts-got-wrapper'); + }); + + describe('getRepos', () => { + it('should throw an error if no token is provided', async () => { + let err; + try { + await vstsHelper.setTokenAndEndpoint(); + } catch (e) { + err = e; + } + expect(err.message).toBe('No token found for vsts'); + }); + it('should throw an error if no endpoint provided (with env variable on token)', async () => { + let err; + process.env.VSTS_TOKEN = 'token123'; + try { + await vstsHelper.setTokenAndEndpoint(); + } catch (e) { + err = e; + } + expect(err.message).toBe( + 'You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)' + ); + }); + it('should throw an error if no endpoint is provided', async () => { + let err; + try { + await vstsHelper.setTokenAndEndpoint('myToken'); + } catch (e) { + err = e; + } + expect(err.message).toBe( + `You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)` + ); + }); + it('should set token and endpoint', async () => { + await vstsHelper.setTokenAndEndpoint('myToken', 'myEndpoint'); + expect(process.env.VSTS_TOKEN).toBe(`myToken`); + expect(process.env.VSTS_ENDPOINT).toBe(`myEndpoint`); + }); + }); + + describe('getNewBranchName', () => { + it('should add refs/heads', () => { + const res = vstsHelper.getNewBranchName('testBB'); + expect(res).toBe(`refs/heads/testBB`); + }); + it('should be the same', () => { + const res = vstsHelper.getNewBranchName('refs/heads/testBB'); + expect(res).toBe(`refs/heads/testBB`); + }); + }); + + describe('getBranchNameWithoutRefsheadsPrefix', () => { + it('should be renamed', () => { + const res = vstsHelper.getBranchNameWithoutRefsheadsPrefix( + 'refs/heads/testBB' + ); + expect(res).toBe(`testBB`); + }); + it('should log error and return null', () => { + const res = vstsHelper.getBranchNameWithoutRefsheadsPrefix(); + expect(res).toBeNull(); + }); + it('should return the input', () => { + const res = vstsHelper.getBranchNameWithoutRefsheadsPrefix('testBB'); + expect(res).toBe('testBB'); + }); + }); + + describe('getRef', () => { + it('should get the ref', async () => { + gitApi.mockImplementationOnce(() => ({ + getRefs: jest.fn(() => [{ objectId: 132 }]), + })); + const res = await vstsHelper.getRefs('123', 'branch'); + expect(res).toMatchSnapshot(); + }); + it('should get 0 ref', async () => { + gitApi.mockImplementationOnce(() => ({ + getRefs: jest.fn(() => []), + })); + const res = await vstsHelper.getRefs('123'); + expect(res.length).toBe(0); + }); + it('should get the ref', async () => { + gitApi.mockImplementationOnce(() => ({ + getRefs: jest.fn(() => [{ objectId: '132' }]), + })); + const res = await vstsHelper.getRefs('123', 'refs/head/branch1'); + expect(res).toMatchSnapshot(); + }); + }); + + describe('getVSTSBranchObj', () => { + it('should be the branch object formated', async () => { + gitApi.mockImplementationOnce(() => ({ + getRefs: jest.fn(() => [{ objectId: '132' }]), + })); + const res = await vstsHelper.getVSTSBranchObj( + '123', + 'branchName', + 'base' + ); + expect(res).toMatchSnapshot(); + }); + it('should be the branch object formated', async () => { + gitApi.mockImplementationOnce(() => ({ + getRefs: jest.fn(() => []), + })); + const res = await vstsHelper.getVSTSBranchObj('123', 'branchName'); + expect(res).toMatchSnapshot(); + }); + }); + + describe('getVSTSCommitObj', () => { + it('should be get the commit obj formated (file to update)', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => ({ + readable: true, + read: jest.fn(() => + Buffer.from('{"hello": "test"}').toString('base64') + ), + })), + })); + + const res = await vstsHelper.getVSTSCommitObj( + 'Commit msg', + './myFilePath/test', + 'Hello world!', + '123', + 'repoName', + 'branchName' + ); + expect(res).toMatchSnapshot(); + }); + it('should be get the commit obj formated (file to create)', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => null), + })); + + const res = await vstsHelper.getVSTSCommitObj( + 'Commit msg', + './myFilePath/test', + 'Hello world!', + '123', + 'repoName', + 'branchName' + ); + expect(res).toMatchSnapshot(); + }); + }); + + describe('getFile', () => { + it('should return null error GitItemNotFoundException', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => ({ + readable: true, + read: jest.fn(() => + Buffer.from('{"typeKey": "GitItemNotFoundException"}').toString( + 'base64' + ) + ), + })), + })); + + const res = await vstsHelper.getFile( + '123', + 'repoName', + './myFilePath/test', + 'branchName' + ); + expect(res).toBeNull(); + }); + + it('should return null error GitUnresolvableToCommitException', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => ({ + readable: true, + read: jest.fn(() => + Buffer.from( + '{"typeKey": "GitUnresolvableToCommitException"}' + ).toString('base64') + ), + })), + })); + + const res = await vstsHelper.getFile( + '123', + 'repoName', + './myFilePath/test', + 'branchName' + ); + expect(res).toBeNull(); + }); + + it('should return the file content because it is not a json', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => ({ + readable: true, + read: jest.fn(() => + Buffer.from('{"hello"= "test"}').toString('base64') + ), + })), + })); + + const res = await vstsHelper.getFile( + '123', + 'repoName', + './myFilePath/test', + 'branchName' + ); + expect(res).toMatchSnapshot(); + }); + + it('should return null because the file is not readable', async () => { + gitApi.mockImplementationOnce(() => ({ + getItemText: jest.fn(() => ({ + readable: false, + })), + })); + + const res = await vstsHelper.getFile( + '123', + 'repoName', + './myFilePath/test', + 'branchName' + ); + expect(res).toBeNull(); + }); + }); + + describe('max4000Chars', () => { + it('should be the same', () => { + const res = vstsHelper.max4000Chars('Hello'); + expect(res).toMatchSnapshot(); + }); + it('should be truncated', () => { + let str = ''; + for (let i = 0; i < 5000; i += 1) { + str += 'a'; + } + const res = vstsHelper.max4000Chars(str); + expect(res.length).toBe(3999); + }); + }); + + describe('getRenovatePRFormat', () => { + it('should be formated (closed)', () => { + const res = vstsHelper.getRenovatePRFormat({ status: 2 }); + expect(res).toMatchSnapshot(); + }); + + it('should be formated (closed v2)', () => { + const res = vstsHelper.getRenovatePRFormat({ status: 3 }); + expect(res).toMatchSnapshot(); + }); + + it('should be formated (not closed)', () => { + const res = vstsHelper.getRenovatePRFormat({ status: 1 }); + expect(res).toMatchSnapshot(); + }); + + it('should be formated (isUnmergeable)', () => { + const res = vstsHelper.getRenovatePRFormat({ mergeStatus: 2 }); + expect(res).toMatchSnapshot(); + }); + }); + + describe('getCommitDetails', () => { + it('should get commit details', async () => { + gitApi.mockImplementationOnce(() => ({ + getCommit: jest.fn(() => ({ + parents: ['123456'], + })), + })); + const res = await vstsHelper.getCommitDetails('123', '123456'); + expect(res).toMatchSnapshot(); + }); + }); +}); diff --git a/test/workers/pr/__snapshots__/index.spec.js.snap b/test/workers/pr/__snapshots__/index.spec.js.snap index 91ae1f659f..ff05c02f7c 100644 --- a/test/workers/pr/__snapshots__/index.spec.js.snap +++ b/test/workers/pr/__snapshots__/index.spec.js.snap @@ -97,3 +97,29 @@ This PR has been generated by [Renovate Bot](https://renovateapp.com).", `; exports[`workers/pr ensurePr should return unmodified existing PR 1`] = `Array []`; + +exports[`workers/pr ensurePr should strip HTML PR for vsts 1`] = ` +Array [ + "renovate/dummy-1.x", + "Update dependency dummy to v1.1.0", + "This Pull Request updates dependency [dummy](https://github.com/renovateapp/dummy) from \`v1.0.0\` to \`v1.1.0\` + + +### Commits + + +**renovateapp/dummy** + +#### 1.1.0 +- [\`abcdefg\`](https://github.com/renovateapp/dummy/commit/abcdefghijklmnopqrstuvwxyz) foo [#3](https://github.com/renovateapp/dummy/issues/3) + + + + + +--- + +This PR has been generated by [Renovate Bot](https://renovateapp.com).", + Array [], +] +`; diff --git a/test/workers/pr/index.spec.js b/test/workers/pr/index.spec.js index 07d60b4bac..c5b17ec260 100644 --- a/test/workers/pr/index.spec.js +++ b/test/workers/pr/index.spec.js @@ -138,6 +138,15 @@ describe('workers/pr', () => { -1 ); }); + it('should strip HTML PR for vsts', async () => { + platform.getBranchStatus.mockReturnValueOnce('success'); + config.prCreation = 'status-success'; + config.isVsts = true; + const pr = await prWorker.ensurePr(config); + expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); + expect(platform.createPr.mock.calls[0]).toMatchSnapshot(); + expect(platform.createPr.mock.calls[0][2].indexOf('<details>')).toBe(-1); + }); it('should delete branch and return null if creating PR fails', async () => { platform.getBranchStatus.mockReturnValueOnce('success'); platform.createPr = jest.fn(); diff --git a/test/workers/repository/init/apis.spec.js b/test/workers/repository/init/apis.spec.js index 6b0feabfe9..12964958d7 100644 --- a/test/workers/repository/init/apis.spec.js +++ b/test/workers/repository/init/apis.spec.js @@ -26,5 +26,17 @@ describe('workers/repository/init/apis', () => { glGot.mockReturnValueOnce({ body: {} }); await initApis(config, 'some-token'); }); + it('runs vsts', async () => { + config.platform = 'vsts'; + config.repository = 'some/name'; + // config.endpoint = 'https://fabrikam.visualstudio.com/DefaultCollection'; + try { + await initApis(config, 'some-token'); + } catch (error) { + expect(error.message).toBe( + 'You need an endpoint with vsts. Something like this: https://{instance}.VisualStudio.com/{collection} (https://fabrikam.visualstudio.com/DefaultCollection)' + ); + } + }); }); }); diff --git a/yarn.lock b/yarn.lock index 2c118a1c38..aa77a5cf44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,8 +3,8 @@ "@semantic-release/commit-analyzer@^3.0.1": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-3.0.6.tgz#3020ca7030658f3f52fef14c78f7fcccb8a1b33a" + version "3.0.7" + resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-3.0.7.tgz#dc955444a6d3d2ae9b8e21f90c2c80c4e9142b2f" dependencies: "@semantic-release/error" "^2.0.0" conventional-changelog-angular "^1.4.0" @@ -14,18 +14,18 @@ pify "^3.0.0" "@semantic-release/condition-travis@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@semantic-release/condition-travis/-/condition-travis-6.1.0.tgz#7962c728f4c19389b57759c7ff9ee08df9b15795" + version "6.1.1" + resolved "https://registry.yarnpkg.com/@semantic-release/condition-travis/-/condition-travis-6.1.1.tgz#9a86e843b7d533ecfa5835d7a1534682ee6085a7" dependencies: "@semantic-release/error" "^2.0.0" - github "^11.0.0" + github "^12.0.0" parse-github-repo-url "^1.4.1" semver "^5.0.3" travis-deploy-once "^3.0.0" "@semantic-release/error@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-2.0.0.tgz#f156ecd509f5288c48bc7425a8abe22f975d1f8b" + version "2.1.0" + resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-2.1.0.tgz#44771f676f5b148da309111285a97901aa95a6e0" "@semantic-release/last-release-npm@^2.0.0": version "2.0.2" @@ -106,8 +106,8 @@ agentkeepalive@^3.3.0: humanize-ms "^1.2.1" ajv-keywords@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" ajv@^4.9.1: version "4.11.8" @@ -509,9 +509,9 @@ boom@5.x.x: dependencies: hoek "4.x.x" -boxen@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.1.tgz#0f11e7fe344edb9397977fc13ede7f64d956481d" +boxen@^1.0.0, boxen@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.2.tgz#3f1d4032c30ffea9d4b02c322eaf2ea741dcbce5" dependencies: ansi-align "^2.0.0" camelcase "^4.0.0" @@ -569,7 +569,25 @@ bunyan@1.8.12: mv "~2" safe-json-stringify "~1" -cacache@^9.2.9, cacache@~9.2.9: +cacache@^9.2.9: + version "9.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.3.0.tgz#9cd58f2dd0b8c8cacf685b7067b416d6d3cf9db1" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^4.1.6" + unique-filename "^1.1.0" + y18n "^3.2.1" + +cacache@~9.2.9: version "9.2.9" resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" dependencies: @@ -664,7 +682,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@2.3.0: +chalk@2.3.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -672,14 +690,6 @@ chalk@2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - changelog@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/changelog/-/changelog-1.4.1.tgz#82eab50891fb6b8a150a176e48654daa1cfc845c" @@ -877,8 +887,8 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" content-type-parser@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" conventional-changelog-angular@^1.4.0: version "1.5.1" @@ -1058,7 +1068,7 @@ dateformat@^1.0.11, dateformat@^1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2, debug@^2.2.0, debug@^2.4.1, debug@^2.6.3, debug@^2.6.8: +debug@2, debug@^2.2.0, debug@^2.4.1, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1070,7 +1080,7 @@ debug@2.2.0: dependencies: ms "0.7.1" -debug@^3.0.1: +debug@^3.0.1, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -1166,8 +1176,8 @@ dezalgo@^1.0.0, dezalgo@~1.0.3: wrappy "1" diff@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" doctrine@1.5.0: version "1.5.0" @@ -1595,6 +1605,12 @@ follow-redirects@0.0.7: debug "^2.2.0" stream-consume "^0.1.0" +follow-redirects@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.5.tgz#ffd3e14cbdd5eaa72f61b6368c1f68516c2a26cc" + dependencies: + debug "^2.6.9" + for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1855,6 +1871,15 @@ github@^11.0.0: mime "^1.2.11" netrc "^0.1.4" +github@^12.0.0: + version "12.0.1" + resolved "https://registry.yarnpkg.com/github/-/github-12.0.1.tgz#4f7467434d8d01152782e669e925b3115aa0b219" + dependencies: + follow-redirects "1.2.5" + https-proxy-agent "^2.1.0" + mime "^2.0.3" + netrc "^0.1.4" + github@~0.1.10: version "0.1.16" resolved "https://registry.yarnpkg.com/github/-/github-0.1.16.tgz#895d2a85b0feb7980d89ac0ce4f44dcaa03f17b5" @@ -1900,6 +1925,12 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.0.tgz#10d34039e0df04272e262cf24224f7209434df4f" + dependencies: + ini "^1.3.4" + global-modules@1.0.0, global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -2109,14 +2140,14 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.4.2, hosted-git-info@~2.5.0: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" html-encoding-sniffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" dependencies: whatwg-encoding "^1.0.1" http-cache-semantics@^3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.7.3.tgz#2f35c532ecd29f1e5413b9af833b724a3c6f7f72" + version "3.8.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.0.tgz#1e3ce248730e189ac692a6697b9e3fdea2ff8da3" http-proxy-agent@^2.0.0: version "2.0.0" @@ -2149,7 +2180,7 @@ https-proxy-agent@^1.0.0: debug "2" extend "3" -https-proxy-agent@^2.0.0: +https-proxy-agent@^2.0.0, https-proxy-agent@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.0.tgz#1391bee7fd66aeabc0df2a1fa90f58954f43e443" dependencies: @@ -2162,11 +2193,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" - -iconv-lite@^0.4.17, iconv-lite@~0.4.13: +iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -2174,15 +2201,15 @@ iferr@^0.1.5, iferr@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" -ignore-walk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.0.tgz#e407919edee5c47c63473b319bfe3ea4a771a57e" +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" dependencies: minimatch "^3.0.4" ignore@^3.3.3: - version "3.3.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" import-from@^2.1.0: version "2.1.0" @@ -2270,8 +2297,8 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" is-builtin-module@^1.0.0: version "1.0.0" @@ -2331,6 +2358,13 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" @@ -2463,17 +2497,17 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" istanbul-api@^1.1.1: - version "1.1.14" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680" + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620" dependencies: async "^2.1.4" fileset "^2.0.2" istanbul-lib-coverage "^1.1.1" - istanbul-lib-hook "^1.0.7" - istanbul-lib-instrument "^1.8.0" - istanbul-lib-report "^1.1.1" - istanbul-lib-source-maps "^1.2.1" - istanbul-reports "^1.1.2" + istanbul-lib-hook "^1.1.0" + istanbul-lib-instrument "^1.9.1" + istanbul-lib-report "^1.1.2" + istanbul-lib-source-maps "^1.2.2" + istanbul-reports "^1.1.3" js-yaml "^3.7.0" mkdirp "^0.5.1" once "^1.4.0" @@ -2482,15 +2516,15 @@ istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" -istanbul-lib-hook@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" +istanbul-lib-hook@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b" dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532" +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e" dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" @@ -2500,28 +2534,28 @@ istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-ins istanbul-lib-coverage "^1.1.1" semver "^5.3.0" -istanbul-lib-report@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" +istanbul-lib-report@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425" dependencies: istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" +istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c" dependencies: - debug "^2.6.3" + debug "^3.1.0" istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" rimraf "^2.6.1" source-map "^0.5.3" -istanbul-reports@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f" +istanbul-reports@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10" dependencies: handlebars "^4.0.3" @@ -2773,8 +2807,8 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" jschardet@^1.4.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + version "1.6.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.6.0.tgz#c7d1a71edcff2839db2f9ec30fc5d5ebd3c1a678" jsdom@^9.12.0: version "9.12.0" @@ -3218,10 +3252,10 @@ lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@~4.1.1: yallist "^2.1.2" make-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" dependencies: - pify "^2.3.0" + pify "^3.0.0" make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0: version "2.5.0" @@ -3310,6 +3344,10 @@ mime@^1.2.11: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.0.3.tgz#4353337854747c48ea498330dc034f9f4bbbcc0b" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -3336,17 +3374,17 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" -minipass@^2.0.0, minipass@^2.0.2: +minipass@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f" dependencies: yallist "^3.0.0" -minizlib@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.3.tgz#d5c1abf77be154619952e253336eccab9b2a32f5" +minizlib@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.4.tgz#8ebb51dd8bbe40b0126b5633dbb36b284a2f523c" dependencies: - minipass "^2.0.0" + minipass "^2.2.1" mississippi@^1.2.0, mississippi@^1.3.0, mississippi@~1.3.0: version "1.3.0" @@ -3392,8 +3430,8 @@ moment@2.19.2: resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" "moment@>= 2.9.0", moment@^2.10.6: - version "2.18.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + version "2.19.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" move-concurrently@^1.0.1, move-concurrently@~1.0.1: version "1.0.1" @@ -3571,10 +3609,10 @@ npm-lifecycle@~1.0.3: validate-npm-package-name "^3.0.0" npm-packlist@^1.1.6, npm-packlist@~1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.9.tgz#bd24a0b7a31a307315b07c2e54f4888f10577548" + version "1.1.10" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" dependencies: - ignore-walk "^3.0.0" + ignore-walk "^3.0.1" npm-bundled "^1.0.1" npm-pick-manifest@^1.0.4: @@ -3585,8 +3623,8 @@ npm-pick-manifest@^1.0.4: semver "^5.3.0" npm-profile@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-2.0.4.tgz#148070c0da22b512bf61a4a87758b957fdb4bbe7" + version "2.0.5" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-2.0.5.tgz#0e61b8f1611bd19d1eeff5e3d5c82e557da3b9d7" dependencies: aproba "^1.1.2" make-fetch-happen "^2.5.0" @@ -3745,8 +3783,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" "nwmatcher@>= 1.3.9 < 2.0.0": - version "1.4.2" - resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.2.tgz#c5e545ab40d22a56b0326531c4beaed7a888b3ea" + version "1.4.3" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" @@ -4057,8 +4095,8 @@ pretty-format@^21.2.1: ansi-styles "^3.2.0" private@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" process-nextick-args@~1.0.6: version "1.0.7" @@ -4131,8 +4169,8 @@ q@1.4.1: resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" q@^1.4.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" qrcode-terminal@~0.11.0: version "0.11.0" @@ -4155,8 +4193,8 @@ qs@~6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" query-string@~5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.0.tgz#fbdf7004b4d2aff792f9871981b7a2794f555947" + version "5.0.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" dependencies: decode-uri-component "^0.2.0" object-assign "^4.1.0" @@ -4177,16 +4215,7 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" -rc@^1.0.1, rc@^1.1.6: - version "1.2.1" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -rc@^1.1.7: +rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: version "1.2.2" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" dependencies: @@ -4714,8 +4743,8 @@ sntp@1.x.x: hoek "2.x.x" sntp@2.x.x: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" dependencies: hoek "4.x.x" @@ -4821,8 +4850,8 @@ stream-consume@^0.1.0: resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" stream-each@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.0.tgz#1e95d47573f580d814dc0ff8cd0f66f1ce53c991" + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" dependencies: end-of-stream "^1.1.0" stream-shift "^1.0.0" @@ -4925,8 +4954,8 @@ supports-color@^3.1.2: has-flag "^1.0.0" supports-color@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" dependencies: has-flag "^2.0.0" @@ -4967,12 +4996,12 @@ tar@^2.0.0, tar@^2.2.1: inherits "2" tar@^4.0.0, tar@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.1.tgz#3f5b2e5289db30c2abe4c960f43d0d9fff96aaf0" + version "4.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.2.tgz#e8e22bf3eec330e5c616d415a698395e294e8fad" dependencies: chownr "^1.0.1" - minipass "^2.0.2" - minizlib "^1.0.3" + minipass "^2.2.1" + minizlib "^1.0.4" mkdirp "^0.5.0" yallist "^3.0.2" @@ -5104,6 +5133,10 @@ tunnel-agent@~0.4.1: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" +tunnel@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -5118,6 +5151,13 @@ type-detect@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" +typed-rest-client@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-0.12.0.tgz#6376f5527f427da121dcafdfd7e41e1321e0720c" + dependencies: + tunnel "0.0.4" + underscore "1.8.3" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5151,6 +5191,10 @@ underscore.string@~2.2.0rc: version "2.2.1" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.2.1.tgz#d7c0fa2af5d5a1a67f4253daee98132e733f0f19" +underscore@1.8.3, underscore@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + unique-filename@^1.1.0, unique-filename@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" @@ -5181,7 +5225,21 @@ unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -update-notifier@^2.2.0, update-notifier@~2.2.0: +update-notifier@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451" + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +update-notifier@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" dependencies: @@ -5237,6 +5295,14 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vso-node-api@6.2.8-preview: + version "6.2.8-preview" + resolved "https://registry.yarnpkg.com/vso-node-api/-/vso-node-api-6.2.8-preview.tgz#99902e626c408716ab90b042705452c88ec1c2f0" + dependencies: + tunnel "0.0.4" + typed-rest-client "^0.12.0" + underscore "^1.8.3" + walk@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b" @@ -5271,10 +5337,10 @@ webidl-conversions@^4.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" whatwg-encoding@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + version "1.0.3" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3" dependencies: - iconv-lite "0.4.13" + iconv-lite "0.4.19" whatwg-url@^4.3.0: version "4.8.0" @@ -5322,8 +5388,8 @@ wordwrap@~0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" worker-farm@^1.3.1, worker-farm@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + version "1.5.1" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.1.tgz#8e9f4a7da4f3c595aa600903051b969390423fa1" dependencies: errno "^0.1.4" xtend "^4.0.1" -- GitLab