Support branch automerging (#274)

Add support for automerging without PR, Closes #177 

* update definitions and docs


* Add mergeBranch api

* support merge commit

* set automergeType

* Update API

* Refactor merge commit

* branch-push working

* Refactor branch

* Add back base tree

* Fix failing tests

* Update definitions and docs

* Fix automerge logic

* Test isBranchStale

* start mergeBranch test

* test mergeBranch branch-push throws

* more tests

* test unknown commit type

* pr tests

* Detect automerge in versions helper

* update tests for new automergeEnabled flag

* refactor pr logic

* complete pr worker tests

* branch automerge tests

* Update docs

* refactor branch automerge check
This commit is contained in:
Rhys Arkins 2017-06-08 06:18:21 +02:00 committed by GitHub
parent f3ff65e4fa
commit 112ff0b410
13 changed files with 650 additions and 114 deletions

View file

@ -89,6 +89,7 @@ $ node renovate --help
--rebase-stale-prs [boolean] Rebase stale PRs (GitHub only)
--pr-creation <string> When to create the PR for a branch. Values: immediate, not-pending, status-success.
--automerge <string> What types of upgrades to merge to base branch automatically. Values: none, minor or any
--automerge-type <string> How to automerge - "branch-merge-commit", "branch-push" or "pr". Branch support is GitHub-only
--yarn-cache-folder <string> Location of yarn cache folder to use. Set to empty string to disable
--maintain-yarn-lock [boolean] Keep yarn.lock files updated in base branch
--lazy-grouping [boolean] Use group names only when multiple dependencies upgraded
@ -152,6 +153,7 @@ Obviously, you can't set repository or package file location with this method.
| `rebaseStalePrs` | Rebase stale PRs (GitHub only) | boolean | `false` | `RENOVATE_REBASE_STALE_PRS` | `--rebase-stale-prs` |
| `prCreation` | When to create the PR for a branch. Values: immediate, not-pending, status-success. | string | `"immediate"` | `RENOVATE_PR_CREATION` | `--pr-creation` |
| `automerge` | What types of upgrades to merge to base branch automatically. Values: none, minor or any | string | `"none"` | `RENOVATE_AUTOMERGE` | `--automerge` |
| `automergeType` | How to automerge - "branch-merge-commit", "branch-push" or "pr". Branch support is GitHub-only | string | `"pr"` | `RENOVATE_AUTOMERGE_TYPE` | `--automerge-type` |
| `branchName` | Branch name template | string | `"renovate/{{depName}}-{{newVersionMajor}}.x"` | `RENOVATE_BRANCH_NAME` | |
| `commitMessage` | Commit message template | string | `"Update dependency {{depName}} to version {{newVersion}}"` | `RENOVATE_COMMIT_MESSAGE` | |
| `prTitle` | Pull Request title template | string | `"{{#if isPin}}Pin{{else}}Update{{/if}} dependency {{depName}} to version {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}{{newVersionMajor}}.x{{else}}{{newVersion}}{{/if}}{{/if}}"` | `RENOVATE_PR_TITLE` | |

View file

@ -15,9 +15,11 @@ module.exports = {
findFilePaths,
// Branch
branchExists,
isBranchStale,
getBranchPr,
getBranchStatus,
deleteBranch,
mergeBranch,
// issue
addAssignees,
addReviewers,
@ -209,6 +211,19 @@ async function branchExists(branchName) {
}
}
async function isBranchStale(branchName) {
// Check if branch's parent SHA = master SHA
logger.debug(`isBranchStale(${branchName})`);
const branchCommit = await getBranchCommit(branchName);
logger.debug(`branchCommit=${branchCommit}`);
const commitDetails = await getCommitDetails(branchCommit);
logger.debug(`commitDetails=${JSON.stringify(commitDetails)}`);
const parentSha = commitDetails.parents[0].sha;
logger.debug(`parentSha=${parentSha}`);
// Return true if the SHAs don't match
return parentSha !== config.baseCommitSHA;
}
// Returns the Pull Request for a branch. Null if not exists.
async function getBranchPr(branchName) {
logger.debug(`getBranchPr(${branchName})`);
@ -236,6 +251,46 @@ async function deleteBranch(branchName) {
await ghGot.delete(`repos/${config.repoName}/git/refs/heads/${branchName}`);
}
async function mergeBranch(branchName, mergeType) {
logger.debug(`mergeBranch(${branchName}, ${mergeType})`);
if (mergeType === 'branch-push') {
const url = `repos/${config.repoName}/git/refs/heads/${config.defaultBranch}`;
const options = {
body: {
sha: await getBranchCommit(branchName),
},
};
try {
await ghGot.patch(url, options);
} catch (err) {
logger.error(`Error pushing branch merge for ${branchName}`);
logger.debug(JSON.stringify(err));
throw new Error('branch-push failed');
}
} else if (mergeType === 'branch-merge-commit') {
const url = `repos/${config.repoName}/merges`;
const options = {
body: {
base: config.defaultBranch,
head: branchName,
},
};
try {
await ghGot.post(url, options);
} catch (err) {
logger.error(`Error pushing branch merge for ${branchName}`);
logger.debug(JSON.stringify(err));
throw new Error('branch-push failed');
}
} else {
throw new Error(`Unsupported branch merge type: ${mergeType}`);
}
// Update base commit
config.baseCommitSHA = await getBranchCommit(config.defaultBranch);
// Delete branch
await deleteBranch(branchName);
}
// Issue
async function addAssignees(issueNo, assignees) {
@ -522,6 +577,12 @@ async function getBranchCommit(branchName) {
.body.object.sha;
}
async function getCommitDetails(commit) {
logger.debug(`getCommitDetails(${commit})`);
const results = await ghGot(`repos/${config.repoName}/git/commits/${commit}`);
return results.body;
}
// Return the tree SHA for a commit
async function getCommitTree(commit) {
logger.debug(`getCommitTree(${commit})`);

View file

@ -131,6 +131,13 @@ const options = [
type: 'string',
default: 'none',
},
{
name: 'automergeType',
description:
'How to automerge - "branch-merge-commit", "branch-push" or "pr". Branch support is GitHub-only',
type: 'string',
default: 'pr',
},
// String templates
{
name: 'branchName',

View file

@ -79,12 +79,16 @@ function determineUpgrades(dep, currentVersion, config) {
? 'major'
: 'minor';
const changeLogToVersion = newVersion;
const automergeEnabled =
config.automerge === 'any' ||
(config.automerge === 'minor' && upgradeType === 'minor');
allUpgrades[upgradeKey] = {
upgradeType,
newVersion,
newVersionMajor,
changeLogFromVersion,
changeLogToVersion,
automergeEnabled,
};
}
});

View file

@ -16,6 +16,18 @@ async function getParentBranch(branchName, config) {
return undefined;
}
logger.debug(`${branchName} already exists`);
// Check if needs rebasing
if (
(config.automergeEnabled && config.automergeType === 'branch-push') ||
config.rebaseStalePrs
) {
const isBranchStale = await config.api.isBranchStale(branchName);
if (isBranchStale) {
logger.debug(`Branch ${branchName} is stale and needs rebasing`);
return undefined;
}
}
// Check for existing PR
const pr = await config.api.getBranchPr(branchName);
// Decide if we need to rebase
@ -35,17 +47,6 @@ async function getParentBranch(branchName, config) {
// Don't do anything different, but warn
logger.verbose(`Cannot rebase branch ${branchName}`);
}
if (pr.isStale && config.rebaseStalePrs) {
logger.verbose(`Existing PR for ${branchName} is stale`);
if (pr.canRebase) {
// Only supported by GitHub
// Setting parentBranch back to undefined means that we'll use the default branch
logger.debug(`Rebasing branch ${branchName}`);
return undefined;
}
// Don't do anything different, but warn
logger.verbose(`Cannot rebase branch ${branchName}`);
}
logger.debug(`Existing ${branchName} does not need rebasing`);
return branchName;
}
@ -156,8 +157,34 @@ async function ensureBranch(upgrades) {
commitMessage,
parentBranch
);
} else {
logger.debug(`No files to commit to branch ${branchName}`);
}
if (!api.branchExists(branchName)) {
// Return now if no branch exists
return false;
}
const config = upgrades[0];
if (config.automergeEnabled === false || config.automergeType === 'pr') {
// No branch automerge
return true;
}
logger.debug(`No files to commit to branch ${branchName}`);
return api.branchExists(branchName);
logger.debug('Checking if we can automerge branch');
const branchStatus = await api.getBranchStatus(branchName);
if (branchStatus === 'success') {
logger.info(`Automerging branch ${branchName}`);
try {
await api.mergeBranch(branchName, config.automergeType);
} catch (err) {
logger.error(`Failed to automerge branch ${branchName}`);
logger.debug(JSON.stringify(err));
throw err;
}
} else {
logger.debug(
`Branch status is "${branchStatus}" - skipping branch automerge`
);
}
// Return true as branch exists
return true;
}

View file

@ -15,10 +15,19 @@ async function ensurePr(upgrades) {
logger.debug('Ensuring PR');
const branchName = handlebars.compile(config.branchName)(config);
const branchStatus = await config.api.getBranchStatus(branchName);
// Only create a PR if a branch automerge has failed
if (config.automergeEnabled && config.automergeType.startsWith('branch')) {
logger.debug(`Branch is configured for branch automerge`);
if (branchStatus === 'failed') {
logger.debug(`Branch tests failed, so will create PR`);
} else {
return null;
}
}
if (config.prCreation === 'status-success') {
logger.debug('Checking branch combined status');
const branchStatus = await config.api.getBranchStatus(branchName);
if (branchStatus !== 'success') {
logger.debug(`Branch status is "${branchStatus}" - not creating PR`);
return null;
@ -26,7 +35,6 @@ async function ensurePr(upgrades) {
logger.debug('Branch status success');
} else if (config.prCreation === 'not-pending') {
logger.debug('Checking branch combined status');
const branchStatus = await config.api.getBranchStatus(branchName);
if (branchStatus === 'pending' || branchStatus === 'running') {
logger.debug(`Branch status is "${branchStatus}" - not creating PR`);
return null;
@ -74,21 +82,18 @@ async function ensurePr(upgrades) {
if (config.labels.length > 0) {
await config.api.addLabels(pr.number, config.labels);
}
// Don't assign or review if automerging
if (
config.automerge === 'none' ||
(config.automerge === 'minor' && config.upgradeType !== 'minor')
) {
// Skip assign and review if automerging PR
if (config.automergeEnabled && config.automergeType === 'pr') {
logger.debug(
`Skipping assignees and reviewers as automerge=${config.automerge}`
);
} else {
if (config.assignees.length > 0) {
await config.api.addAssignees(pr.number, config.assignees);
}
if (config.reviewers.length > 0) {
await config.api.addReviewers(pr.number, config.reviewers);
}
} else {
logger.debug(
`Skipping assignees and reviewers as automerge=${config.automerge}`
);
}
logger.info(`Created ${pr.displayNumber}`);
return pr;
@ -100,10 +105,7 @@ async function ensurePr(upgrades) {
async function checkAutoMerge(pr, config) {
logger.debug(`Checking #${pr.number} for automerge`);
if (
config.automerge === 'any' ||
(config.automerge === 'minor' && config.upgradeType === 'minor')
) {
if (config.automergeEnabled && config.automergeType === 'pr') {
logger.verbose('PR is configured for automerge');
logger.debug(JSON.stringify(pr));
// Return if PR not ready for automerge

View file

@ -19,7 +19,7 @@
"test-dirty": "git diff --exit-code",
"test": "npm run lint && npm run jest",
"transpile": "rimraf dist && mkdirp dist && babel lib --out-dir dist",
"update-docs": "bash bin/update-docs.sh"
"update-docs": "npm run transpile && bash bin/update-docs.sh"
},
"repository": {
"type": "git",

View file

@ -771,6 +771,177 @@ Object {
}
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-merge-commit merge 1`] = `
Array [
Array [
"repos/some/repo",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-merge-commit merge 2`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-merge-commit merge 3`] = `
Array [
Array [
"repos/some/repo/merges",
Object {
"body": Object {
"base": "master",
"head": "thebranchname",
},
},
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-merge-commit merge 4`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-merge-commit merge 5`] = `
Array [
Array [
"repos/some/repo/git/refs/heads/thebranchname",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-push merge 1`] = `
Array [
Array [
"repos/some/repo",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
Array [
"repos/some/repo/git/refs/heads/thebranchname",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-push merge 2`] = `
Array [
Array [
"repos/some/repo/git/refs/heads/master",
Object {
"body": Object {
"sha": "1235",
},
},
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-push merge 3`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-push merge 4`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should perform a branch-push merge 5`] = `
Array [
Array [
"repos/some/repo/git/refs/heads/thebranchname",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 1`] = `[Error: branch-push failed]`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 2`] = `
Array [
Array [
"repos/some/repo",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 3`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 4`] = `
Array [
Array [
"repos/some/repo/merges",
Object {
"body": Object {
"base": "master",
"head": "thebranchname",
},
},
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 5`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-merge-commit throws 6`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 1`] = `[Error: branch-push failed]`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 2`] = `
Array [
Array [
"repos/some/repo",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
Array [
"repos/some/repo/git/refs/heads/thebranchname",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 3`] = `
Array [
Array [
"repos/some/repo/git/refs/heads/master",
Object {
"body": Object {
"sha": "1235",
},
},
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 4`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 5`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if branch-push merge throws 6`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 1`] = `[Error: Unsupported branch merge type: wrong-merge-type]`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 2`] = `
Array [
Array [
"repos/some/repo",
],
Array [
"repos/some/repo/git/refs/heads/master",
],
]
`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 3`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 4`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 5`] = `Array []`;
exports[`api/github mergeBranch(branchName, mergeType) should throw if unknown merge type 6`] = `Array []`;
exports[`api/github updatePr(prNo, title, body) should update the PR 1`] = `
Array [
Array [

View file

@ -407,6 +407,52 @@ describe('api/github', () => {
expect(err.message).toBe('Something went wrong');
});
});
describe('isBranchStale(branchName)', () => {
it('should return false if same SHA as master', async () => {
await initRepo('some/repo', 'token');
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
// getCommitDetails - same as master
ghGot.mockImplementationOnce(() => ({
body: {
parents: [
{
sha: '1234',
},
],
},
}));
expect(await github.isBranchStale('thebranchname')).toBe(false);
});
it('should return true if SHA different from master', async () => {
await initRepo('some/repo', 'token');
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
// getCommitDetails - different
ghGot.mockImplementationOnce(() => ({
body: {
parents: [
{
sha: '12345678',
},
],
},
}));
expect(await github.isBranchStale('thebranchname')).toBe(true);
});
});
describe('getBranchPr(branchName)', () => {
it('should return null if no PR exists', async () => {
await initRepo('some/repo', 'token');
@ -460,6 +506,112 @@ describe('api/github', () => {
expect(res).toEqual(false);
});
});
describe('mergeBranch(branchName, mergeType)', () => {
it('should perform a branch-push merge', async () => {
await initRepo('some/repo', 'token');
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
ghGot.patch.mockImplementationOnce();
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
// deleteBranch
ghGot.delete.mockImplementationOnce();
await github.mergeBranch('thebranchname', 'branch-push');
expect(ghGot.mock.calls).toMatchSnapshot();
expect(ghGot.patch.mock.calls).toMatchSnapshot();
expect(ghGot.post.mock.calls).toMatchSnapshot();
expect(ghGot.put.mock.calls).toMatchSnapshot();
expect(ghGot.delete.mock.calls).toMatchSnapshot();
});
it('should throw if branch-push merge throws', async () => {
await initRepo('some/repo', 'token');
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
ghGot.patch.mockImplementationOnce(() => {
throw new Error('branch-push failed');
});
let e;
try {
await github.mergeBranch('thebranchname', 'branch-push');
} catch (err) {
e = err;
}
expect(e).toMatchSnapshot();
expect(ghGot.mock.calls).toMatchSnapshot();
expect(ghGot.patch.mock.calls).toMatchSnapshot();
expect(ghGot.post.mock.calls).toMatchSnapshot();
expect(ghGot.put.mock.calls).toMatchSnapshot();
expect(ghGot.delete.mock.calls).toMatchSnapshot();
});
it('should perform a branch-merge-commit merge', async () => {
await initRepo('some/repo', 'token');
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
await github.mergeBranch('thebranchname', 'branch-merge-commit');
expect(ghGot.mock.calls).toMatchSnapshot();
expect(ghGot.patch.mock.calls).toMatchSnapshot();
expect(ghGot.post.mock.calls).toMatchSnapshot();
expect(ghGot.put.mock.calls).toMatchSnapshot();
expect(ghGot.delete.mock.calls).toMatchSnapshot();
});
it('should throw if branch-merge-commit throws', async () => {
await initRepo('some/repo', 'token');
ghGot.post.mockImplementationOnce(() => {
throw new Error('branch-push failed');
});
let e;
try {
await github.mergeBranch('thebranchname', 'branch-merge-commit');
} catch (err) {
e = err;
}
expect(e).toMatchSnapshot();
expect(ghGot.mock.calls).toMatchSnapshot();
expect(ghGot.patch.mock.calls).toMatchSnapshot();
expect(ghGot.post.mock.calls).toMatchSnapshot();
expect(ghGot.put.mock.calls).toMatchSnapshot();
expect(ghGot.delete.mock.calls).toMatchSnapshot();
});
it('should throw if unknown merge type', async () => {
await initRepo('some/repo', 'token');
let e;
try {
await github.mergeBranch('thebranchname', 'wrong-merge-type');
} catch (err) {
e = err;
}
expect(e).toMatchSnapshot();
expect(ghGot.mock.calls).toMatchSnapshot();
expect(ghGot.patch.mock.calls).toMatchSnapshot();
expect(ghGot.post.mock.calls).toMatchSnapshot();
expect(ghGot.put.mock.calls).toMatchSnapshot();
expect(ghGot.delete.mock.calls).toMatchSnapshot();
});
});
describe('addAssignees(issueNo, assignees)', () => {
it('should add the given assignees to the issue', async () => {
await initRepo('some/repo', 'token');
@ -708,6 +860,14 @@ describe('api/github', () => {
},
},
}));
// getBranchCommit
ghGot.mockImplementationOnce(() => ({
body: {
object: {
sha: '1235',
},
},
}));
return github.initRepo(...args);
}
await guessInitRepo('some/repo', 'token');
@ -723,7 +883,6 @@ describe('api/github', () => {
await github.mergePr(pr);
expect(ghGot.put.mock.calls).toHaveLength(1);
expect(ghGot.delete.mock.calls).toHaveLength(1);
expect(ghGot.mock.calls).toHaveLength(3);
});
it('should try squash after rebase', async () => {
const pr = {
@ -738,7 +897,6 @@ describe('api/github', () => {
await github.mergePr(pr);
expect(ghGot.put.mock.calls).toHaveLength(2);
expect(ghGot.delete.mock.calls).toHaveLength(1);
expect(ghGot.mock.calls).toHaveLength(3);
});
it('should try merge after squash', async () => {
const pr = {
@ -756,7 +914,6 @@ describe('api/github', () => {
await github.mergePr(pr);
expect(ghGot.put.mock.calls).toHaveLength(3);
expect(ghGot.delete.mock.calls).toHaveLength(1);
expect(ghGot.mock.calls).toHaveLength(3);
});
it('should give up', async () => {
const pr = {
@ -777,7 +934,6 @@ describe('api/github', () => {
await github.mergePr(pr);
expect(ghGot.put.mock.calls).toHaveLength(3);
expect(ghGot.delete.mock.calls).toHaveLength(0);
expect(ghGot.mock.calls).toHaveLength(2);
});
});
describe('getFile(filePatch, branchName)', () => {

View file

@ -3,6 +3,7 @@
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) disables major release separation (major) 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -15,6 +16,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) disables major release separation (minor) 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.0.0",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -27,6 +29,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) ignores pinning for ranges when other upgrade exists 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.9.7",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -49,6 +52,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) returns both updates if automerging minor 1`] = `
Array [
Object {
"automergeEnabled": true,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "0.9.7",
"newVersion": "0.9.7",
@ -56,6 +60,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -68,6 +73,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) returns only one update if automerging any 1`] = `
Array [
Object {
"automergeEnabled": true,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -80,6 +86,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) returns only one update if grouping 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -92,6 +99,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) should allow unstable versions if the current version is unstable 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.0.0-beta",
"changeLogToVersion": "1.1.0-beta",
"newVersion": "1.1.0-beta",
@ -104,6 +112,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) should treat zero zero caret ranges as pinned 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.0.34",
"changeLogToVersion": "0.0.35",
"isRange": true,
@ -117,6 +126,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) supports > latest versions if configured 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.4.1",
"changeLogToVersion": "2.0.1",
"newVersion": "2.0.1",
@ -139,6 +149,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) supports future versions if configured 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.4.1",
"changeLogToVersion": "2.0.3",
"newVersion": "2.0.3",
@ -151,6 +162,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) supports minor and major upgrades for ranged versions 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "0.9.7",
"newVersion": "0.9.7",
@ -158,6 +170,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -170,6 +183,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) supports minor and major upgrades for tilde ranges 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "0.9.7",
"newVersion": "0.9.7",
@ -177,6 +191,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.4.4",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -189,6 +204,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades .x major ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.9.7",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -202,6 +218,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades .x minor ranges 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.3.0",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -214,6 +231,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades .x minor ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.3.0",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -227,6 +245,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades less than equal ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "0.9.7",
"isRange": true,
@ -235,6 +254,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -248,6 +268,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades minor ranged versions 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.0.1",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -260,6 +281,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades multiple caret ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "0.9.7",
"isRange": true,
@ -268,6 +290,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -281,6 +304,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades multiple tilde ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "0.9.7",
"isRange": true,
@ -289,6 +313,7 @@ Array [
"upgradeType": "minor",
},
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.7.2",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -302,6 +327,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades shorthand major ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "0.9.7",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -315,6 +341,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades shorthand minor ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.3.0",
"changeLogToVersion": "1.4.1",
"isRange": true,
@ -328,6 +355,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades tilde ranges 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.3.0",
"changeLogToVersion": "1.4.1",
"newVersion": "1.4.1",
@ -340,6 +368,7 @@ Array [
exports[`helpers/versions .determineUpgrades(dep, currentVersion, defaultConfig) upgrades tilde ranges without pinning 1`] = `
Array [
Object {
"automergeEnabled": false,
"changeLogFromVersion": "1.3.0",
"changeLogToVersion": "1.4.1",
"isRange": true,

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`workers/branch ensureBranch(config) automerges successful branches 1`] = `
Object {
"calls": Array [
Array [
"renovate/dummy-1.x",
"branch-push",
],
],
"instances": Array [
Object {
"branchExists": [Function],
"commitFilesToBranch": [Function],
"getBranchStatus": [Function],
"getFileContent": [Function],
"mergeBranch": [Function],
},
],
}
`;
exports[`workers/branch ensureBranch(config) throws if automerge throws 1`] = `[Error: automerge failed]`;
exports[`workers/branch ensureBranch(config) throws if automerge throws 2`] = `
Object {
"calls": Array [
Array [
"renovate/dummy-1.x",
"branch-push",
],
],
"instances": Array [
Object {
"branchExists": [Function],
"commitFilesToBranch": [Function],
"getBranchStatus": [Function],
"getFileContent": [Function],
"mergeBranch": [Function],
},
],
}
`;

View file

@ -16,6 +16,8 @@ describe('workers/branch', () => {
api: {
branchExists: jest.fn(() => true),
getBranchPr: jest.fn(),
getBranchStatus: jest.fn(),
isBranchStale: jest.fn(() => false),
},
};
});
@ -31,7 +33,7 @@ describe('workers/branch', () => {
branchName
);
});
it('returns false if does not need rebaseing', async () => {
it('returns branchName if does not need rebaseing', async () => {
config.api.getBranchPr.mockReturnValue({
isUnmergeable: false,
});
@ -39,7 +41,7 @@ describe('workers/branch', () => {
branchName
);
});
it('returns false if unmergeable and cannot rebase', async () => {
it('returns branchName if unmergeable and cannot rebase', async () => {
config.api.getBranchPr.mockReturnValue({
isUnmergeable: true,
canRebase: false,
@ -48,7 +50,7 @@ describe('workers/branch', () => {
branchName
);
});
it('returns true if unmergeable and can rebase', async () => {
it('returns undefined if unmergeable and can rebase', async () => {
config.api.getBranchPr.mockReturnValue({
isUnmergeable: true,
canRebase: true,
@ -57,35 +59,17 @@ describe('workers/branch', () => {
undefined
);
});
it('returns false if stale but not configured to rebase', async () => {
config.api.getBranchPr.mockReturnValue({
isUnmergeable: false,
isStale: true,
canRebase: true,
});
config.rebaseStalePrs = false;
it('returns branchName if automerge branch-push and not stale', async () => {
config.automergeEnabled = true;
config.automergeType = 'branch-push';
expect(await branchWorker.getParentBranch(branchName, config)).toBe(
branchName
);
});
it('returns false if stale but cannot rebase', async () => {
config.api.getBranchPr.mockReturnValueOnce({
isUnmergeable: false,
isStale: true,
canRebase: false,
});
config.rebaseStalePrs = true;
expect(await branchWorker.getParentBranch(branchName, config)).toBe(
branchName
);
});
it('returns true if stale and can rebase', async () => {
config.api.getBranchPr.mockReturnValueOnce({
isUnmergeable: false,
isStale: true,
canRebase: true,
});
config.rebaseStalePrs = true;
it('returns undefined if automerge branch-push and stale', async () => {
config.automergeEnabled = true;
config.automergeType = 'branch-push';
config.api.isBranchStale.mockReturnValueOnce(true);
expect(await branchWorker.getParentBranch(branchName, config)).toBe(
undefined
);
@ -105,15 +89,17 @@ describe('workers/branch', () => {
config.api.branchExists = jest.fn();
config.api.commitFilesToBranch = jest.fn();
config.api.getFileContent.mockReturnValueOnce('old content');
config.api.getBranchStatus = jest.fn();
config.depName = 'dummy';
config.currentVersion = '1.0.0';
config.newVersion = '1.1.0';
config.newVersionMajor = 1;
});
it('returns if new content matches old', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('old content');
config.api.branchExists.mockReturnValueOnce(false);
await branchWorker.ensureBranch([config]);
expect(await branchWorker.ensureBranch([config])).toBe(false);
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(0);
@ -122,13 +108,86 @@ describe('workers/branch', () => {
it('commits one file if no yarn lock or package-lock.json found', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
await branchWorker.ensureBranch([config]);
config.api.branchExists.mockReturnValueOnce(true);
expect(await branchWorker.ensureBranch([config])).toBe(true);
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(1);
expect(yarnHelper.getLockFile.mock.calls.length).toBe(1);
expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1);
});
it('returns true if automerging pr', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
config.api.branchExists.mockReturnValueOnce(true);
config.automergeEnabled = true;
config.automergeType = 'pr';
expect(await branchWorker.ensureBranch([config])).toBe(true);
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(1);
expect(yarnHelper.getLockFile.mock.calls.length).toBe(1);
expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1);
});
it('automerges successful branches', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
config.api.branchExists.mockReturnValueOnce(true);
config.api.getBranchStatus.mockReturnValueOnce('success');
config.api.mergeBranch = jest.fn();
config.automergeEnabled = true;
config.automergeType = 'branch-push';
expect(await branchWorker.ensureBranch([config])).toBe(true);
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(config.api.getBranchStatus.mock.calls.length).toBe(1);
expect(config.api.mergeBranch.mock).toMatchSnapshot();
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(1);
expect(yarnHelper.getLockFile.mock.calls.length).toBe(1);
expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1);
});
it('skips automerge if status not success', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
config.api.branchExists.mockReturnValueOnce(true);
config.api.getBranchStatus.mockReturnValueOnce('pending');
config.api.mergeBranch = jest.fn();
config.automergeEnabled = true;
config.automergeType = 'branch-push';
expect(await branchWorker.ensureBranch([config])).toBe(true);
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(config.api.getBranchStatus.mock.calls.length).toBe(1);
expect(config.api.mergeBranch.mock.calls.length).toBe(0);
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(1);
expect(yarnHelper.getLockFile.mock.calls.length).toBe(1);
expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1);
});
it('throws if automerge throws', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
config.api.branchExists.mockReturnValueOnce(true);
config.api.getBranchStatus.mockReturnValueOnce('success');
config.automergeEnabled = true;
config.automergeType = 'branch-push';
config.api.mergeBranch = jest.fn(() => {
throw new Error('automerge failed');
});
let e;
try {
await branchWorker.ensureBranch([config]);
} catch (err) {
e = err;
}
expect(e).toMatchSnapshot();
expect(branchWorker.getParentBranch.mock.calls.length).toBe(1);
expect(config.api.getBranchStatus.mock.calls.length).toBe(1);
expect(config.api.mergeBranch.mock).toMatchSnapshot();
expect(packageJsonHelper.setNewValue.mock.calls.length).toBe(1);
expect(npmHelper.getLockFile.mock.calls.length).toBe(1);
expect(yarnHelper.getLockFile.mock.calls.length).toBe(1);
expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1);
});
it('commits two files if yarn lock found', async () => {
branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
yarnHelper.getLockFile.mockReturnValueOnce('non null response');

View file

@ -27,48 +27,33 @@ describe('workers/pr', () => {
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(0);
});
it('should automerge if any and pr is mergeable', async () => {
config.automerge = 'any';
it('should automerge if enabled and pr is mergeable', async () => {
config.automergeEnabled = true;
pr.mergeable = true;
config.api.getBranchStatus.mockReturnValueOnce('success');
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(1);
});
it('should not automerge if any and pr is mergeable but branch status is not success', async () => {
config.automerge = 'any';
it('should not automerge if enabled and pr is mergeable but branch status is not success', async () => {
config.automergeEnabled = true;
pr.mergeable = true;
config.api.getBranchStatus.mockReturnValueOnce('pending');
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(0);
});
it('should not automerge if any and pr is mergeable but unstable', async () => {
config.automerge = 'any';
it('should not automerge if enabled and pr is mergeable but unstable', async () => {
config.automergeEnabled = true;
pr.mergeable = true;
pr.mergeable_state = 'unstable';
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(0);
});
it('should not automerge if any and pr is unmergeable', async () => {
config.automerge = 'any';
it('should not automerge if enabled and pr is unmergeable', async () => {
config.automergeEnabled = true;
pr.mergeable = false;
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(0);
});
it('should automerge if minor and upgradeType is minor', async () => {
config.automerge = 'minor';
config.upgradeType = 'minor';
pr.mergeable = true;
config.api.getBranchStatus.mockReturnValueOnce('success');
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(1);
});
it('should not automerge if minor and upgradeType is major', async () => {
config.automerge = 'minor';
config.upgradeType = 'major';
pr.mergeable = true;
await prWorker.checkAutoMerge(pr, config);
expect(config.api.mergePr.mock.calls.length).toBe(0);
});
});
describe('ensurePr(upgrades)', () => {
let config;
@ -77,6 +62,7 @@ describe('workers/pr', () => {
config = Object.assign({}, defaultConfig);
config.api = {
createPr: jest.fn(() => ({ displayNumber: 'New Pull Request' })),
getBranchStatus: jest.fn(),
};
existingPr = {
title: 'Update dependency dummy to version 1.1.0',
@ -150,44 +136,18 @@ describe('workers/pr', () => {
expect(config.api.addAssignees.mock.calls.length).toBe(1);
expect(config.api.addReviewers.mock.calls.length).toBe(1);
});
it('should not add assignees and reviewers to new PR if automerging any', async () => {
it('should not add assignees and reviewers to new PR if automerging enabled', async () => {
config.api.getBranchPr = jest.fn();
config.api.addAssignees = jest.fn();
config.api.addReviewers = jest.fn();
config.assignees = ['bar'];
config.reviewers = ['baz'];
config.automerge = 'any';
config.automergeEnabled = true;
const pr = await prWorker.ensurePr([config]);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
expect(config.api.addAssignees.mock.calls.length).toBe(0);
expect(config.api.addReviewers.mock.calls.length).toBe(0);
});
it('should not add assignees and reviewers to new PR if automerging minor', async () => {
config.api.getBranchPr = jest.fn();
config.api.addAssignees = jest.fn();
config.api.addReviewers = jest.fn();
config.assignees = ['bar'];
config.reviewers = ['baz'];
config.upgradeType = 'minor';
config.automerge = 'minor';
const pr = await prWorker.ensurePr([config]);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
expect(config.api.addAssignees.mock.calls.length).toBe(0);
expect(config.api.addReviewers.mock.calls.length).toBe(0);
});
it('should add assignees and reviewers to new PR if automerging minor and its major', async () => {
config.api.getBranchPr = jest.fn();
config.api.addAssignees = jest.fn();
config.api.addReviewers = jest.fn();
config.assignees = ['bar'];
config.reviewers = ['baz'];
config.upgradeType = 'major';
config.automerge = 'minor';
const pr = await prWorker.ensurePr([config]);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
expect(config.api.addAssignees.mock.calls.length).toBe(1);
expect(config.api.addReviewers.mock.calls.length).toBe(1);
});
it('should return unmodified existing PR', async () => {
config.depName = 'dummy';
config.currentVersion = '1.0.0';
@ -210,5 +170,20 @@ describe('workers/pr', () => {
});
expect(pr).toMatchObject(updatedPr);
});
it('should create PR if branch automerging failed', async () => {
config.automergeEnabled = true;
config.automergeType = 'branch-push';
config.api.getBranchStatus.mockReturnValueOnce('failed');
config.api.getBranchPr = jest.fn();
const pr = await prWorker.ensurePr([config]);
expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
});
it('should return null if branch automerging not failed', async () => {
config.automergeEnabled = true;
config.automergeType = 'branch-push';
config.api.getBranchStatus.mockReturnValueOnce('pending');
const pr = await prWorker.ensurePr([config]);
expect(pr).toBe(null);
});
});
});