From c30472dd4370b21abd481293540673c90dc3232c Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Wed, 5 Jul 2017 07:02:25 +0200 Subject: [PATCH] feat: Add ability to skip status checks for automerge (#438) This starts off [#359](https://github.com/singapore/renovate/issues/359) (configurable status checks) but only implements one sub-feature of it: disabling status checks to allow automerge if tests fail or if no tests are present. --- docs/configuration.md | 12 ++++++-- lib/api/github.js | 15 +++++++++- lib/api/gitlab.js | 15 +++++++++- lib/config/definitions.js | 9 ++++++ lib/workers/branch/index.js | 5 +++- lib/workers/pr/index.js | 19 ++++++++++--- test/api/github.spec.js | 28 +++++++++++++------ test/api/gitlab.spec.js | 20 +++++++++---- .../package/__snapshots__/index.spec.js.snap | 1 + 9 files changed, 101 insertions(+), 23 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6f416fe4d7..01555b1bd5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -394,6 +394,14 @@ Obviously, you can't set repository or package file location with this method. <td>`RENOVATE_AUTOMERGE_TYPE`</td> <td>`--automerge-type`<td> </tr> +<tr> + <td>`requiredStatusChecks`</td> + <td>List of status checks that must pass before automerging. Set to null to enable automerging without tests.</td> + <td>list</td> + <td><pre>[]</pre></td> + <td></td> + <td><td> +</tr> <tr> <td>`branchName`</td> <td>Branch name template</td> @@ -422,7 +430,7 @@ Obviously, you can't set repository or package file location with this method. <td>`prBody`</td> <td>Pull Request body template</td> <td>string</td> - <td><pre>"This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates dependency [{{depName}}]({{repositoryUrl}}) from version `{{currentVersion}}` to `{{newVersion}}`\n{{#if releases.length}}\n\n### Commits\n\n<details>\n<summary>{{githubName}}</summary>\n\n{{#each releases as |release|}}\n#### {{release.version}}\n{{#each release.commits as |commit|}}\n- [`{{commit.shortSha}}`]({{commit.url}}) {{commit.message}}\n{{/each}}\n{{/each}}\n\n</details>\n{{/if}}\n<br />\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://keylocation.sg/our-tech/renovate)."</pre></td> + <td><pre>"This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates dependency [{{depName}}]({{repositoryUrl}}) from version `{{currentVersion}}` to `{{newVersion}}`\n{{#if releases.length}}\n\n### Commits\n\n<details>\n<summary>{{githubName}}</summary>\n\n{{#each releases as |release|}}\n#### {{release.version}}\n{{#each release.commits as |commit|}}\n- [`{{commit.shortSha}}`]({{commit.url}}) {{commit.message}}\n{{/each}}\n{{/each}}\n\n</details>\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://keylocation.sg/our-tech/renovate)."</pre></td> <td>`RENOVATE_PR_BODY`</td> <td><td> </tr> @@ -483,7 +491,7 @@ Obviously, you can't set repository or package file location with this method. "branchName": "renovate/{{groupSlug}}", "commitMessage": "{{semanticPrefix}}Renovate {{groupName}} packages", "prTitle": "{{semanticPrefix}}Renovate {{groupName}} packages", - "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \"{{groupName}}\".\n\n{{#each upgrades as |upgrade|}}\n- [{{upgrade.depName}}]({{upgrade.repositoryUrl}}): from `{{upgrade.currentVersion}}` to `{{upgrade.newVersion}}`\n{{/each}}\n\n{{#unless isPin}}\n### Commits\n\n{{#each upgrades as |upgrade|}}\n{{#if upgrade.releases.length}}\n<details>\n<summary>{{upgrade.githubName}}</summary>\n{{#each upgrade.releases as |release|}}\n\n#### {{release.version}}\n{{#each release.commits as |commit|}}\n- [`{{commit.shortSha}}`]({{commit.url}}){{commit.message}}\n{{/each}}\n{{/each}}\n\n</details>\n{{/if}}\n{{/each}}\n{{/unless}}\n<br />\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://keylocation.sg/our-tech/renovate)." + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \"{{groupName}}\".\n\n{{#each upgrades as |upgrade|}}\n- [{{upgrade.depName}}]({{upgrade.repositoryUrl}}): from `{{upgrade.currentVersion}}` to `{{upgrade.newVersion}}`\n{{/each}}\n\n{{#unless isPin}}\n### Commits\n\n{{#each upgrades as |upgrade|}}\n{{#if upgrade.releases.length}}\n<details>\n<summary>{{upgrade.githubName}}</summary>\n{{#each upgrade.releases as |release|}}\n\n#### {{release.version}}\n{{#each release.commits as |commit|}}\n- [`{{commit.shortSha}}`]({{commit.url}}){{commit.message}}\n{{/each}}\n{{/each}}\n\n</details>\n{{/if}}\n{{/each}}\n{{/unless}}\n<br />\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://keylocation.sg/our-tech/renovate)." }</pre></td> <td></td> <td><td> diff --git a/lib/api/github.js b/lib/api/github.js index a3ba3eaf9d..04528826b5 100644 --- a/lib/api/github.js +++ b/lib/api/github.js @@ -241,8 +241,21 @@ async function getBranchPr(branchName) { } // Returns the combined status for a branch. -async function getBranchStatus(branchName) { +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( + `Unsupported requiredStatusChecks: ${JSON.stringify( + requiredStatusChecks + )}` + ); + return 'failed'; + } const gotString = `repos/${config.repoName}/commits/${branchName}/status`; logger.debug(gotString); const res = await ghGot(gotString); diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js index 2c539ca0b3..5fe2e45552 100644 --- a/lib/api/gitlab.js +++ b/lib/api/gitlab.js @@ -173,8 +173,21 @@ async function getBranchPr(branchName) { } // Returns the combined status for a branch. -async function getBranchStatus(branchName) { +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( + `Unsupported requiredStatusChecks: ${JSON.stringify( + requiredStatusChecks + )}` + ); + return 'failed'; + } // First, get the branch to find the commit SHA let url = `projects/${config.repoName}/repository/branches/${branchName}`; let res = await glGot(url); diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 61ac9e863b..d686f8ba84 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -245,6 +245,15 @@ const options = [ default: 'pr', onboarding: false, }, + { + name: 'requiredStatusChecks', + description: + 'List of status checks that must pass before automerging. Set to null to enable automerging without tests.', + type: 'list', + onboarding: false, + cli: false, + env: false, + }, // Default templates { name: 'branchName', diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js index 02c97d3de9..5121c3c016 100644 --- a/lib/workers/branch/index.js +++ b/lib/workers/branch/index.js @@ -171,7 +171,10 @@ async function ensureBranch(config) { return true; } logger.debug('Checking if we can automerge branch'); - const branchStatus = await api.getBranchStatus(branchName); + const branchStatus = await api.getBranchStatus( + branchName, + config.requiredStatusChecks + ); if (branchStatus === 'success') { logger.info(`Automerging branch`); try { diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js index 3bad4d5a84..25d0dabd6e 100644 --- a/lib/workers/pr/index.js +++ b/lib/workers/pr/index.js @@ -18,7 +18,10 @@ async function ensurePr(upgrades, logger, errors, warnings) { config.upgrades = []; const branchName = handlebars.compile(config.branchName)(config); - const branchStatus = await config.api.getBranchStatus(branchName); + const branchStatus = await config.api.getBranchStatus( + branchName, + config.requiredStatusChecks + ); // Only create a PR if a branch automerge has failed if (config.automergeEnabled && config.automergeType.startsWith('branch')) { @@ -150,16 +153,24 @@ async function ensurePr(upgrades, logger, errors, warnings) { } async function checkAutoMerge(pr, config, logger) { + logger.trace({ config }, 'checkAutoMerge'); logger.debug(`Checking #${pr.number} for automerge`); if (config.automergeEnabled && config.automergeType === 'pr') { logger.info('PR is configured for automerge'); // Return if PR not ready for automerge - if (pr.mergeable !== true || pr.mergeable_state === 'unstable') { - logger.info('PR is not ready for merge'); + if (pr.mergeable !== true) { + logger.info('PR is not mergeable'); + return; + } + if (config.requiredStatusChecks && pr.mergeable_state === 'unstable') { + logger.info('PR mergeable state is unstable'); return; } // Check branch status - const branchStatus = await config.api.getBranchStatus(pr.head.ref); + const branchStatus = await config.api.getBranchStatus( + pr.head.ref, + config.requiredStatusChecks + ); logger.debug(`branchStatus=${branchStatus}`); if (branchStatus !== 'success') { logger.info('Branch status is not "success"'); diff --git a/test/api/github.spec.js b/test/api/github.spec.js index 825a4ef1cb..02b8db1c06 100644 --- a/test/api/github.spec.js +++ b/test/api/github.spec.js @@ -500,26 +500,36 @@ describe('api/github', () => { expect(pr).toMatchSnapshot(); }); }); - describe('getBranchStatus(branchName)', () => { - it('should return true', async () => { + describe('getBranchStatus(branchName, requiredStatusChecks)', () => { + it('returne success if requiredStatusChecks null', async () => { + await initRepo('some/repo', 'token'); + const res = await github.getBranchStatus('somebranch', null); + expect(res).toEqual('success'); + }); + it('return failed if unsupported requiredStatusChecks', async () => { + await initRepo('some/repo', 'token'); + const res = await github.getBranchStatus('somebranch', ['foo']); + expect(res).toEqual('failed'); + }); + it('should pass through success', async () => { await initRepo('some/repo', 'token'); ghGot.mockImplementationOnce(() => ({ body: { - state: true, + state: 'success', }, })); - const res = await github.getBranchStatus('somebranch'); - expect(res).toEqual(true); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('success'); }); - it('should return false', async () => { + it('should pass through failed', async () => { await initRepo('some/repo', 'token'); ghGot.mockImplementationOnce(() => ({ body: { - state: false, + state: 'failed', }, })); - const res = await github.getBranchStatus('somebranch'); - expect(res).toEqual(false); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('failed'); }); }); describe('mergeBranch(branchName, mergeType)', () => { diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js index e3142fe5a5..7f5c520679 100644 --- a/test/api/gitlab.spec.js +++ b/test/api/gitlab.spec.js @@ -262,7 +262,7 @@ describe('api/gitlab', () => { expect(pr).toMatchSnapshot(); }); }); - describe('getBranchStatus(branchName)', () => { + describe('getBranchStatus(branchName, requiredStatusChecks)', () => { beforeEach(() => { glGot.mockReturnValueOnce({ body: { @@ -272,32 +272,42 @@ describe('api/gitlab', () => { }, }); }); + it('returns success if requiredStatusChecks null', async () => { + await initRepo('some/repo', 'token'); + const res = await gitlab.getBranchStatus('somebranch', null); + expect(res).toEqual('success'); + }); + it('return failed if unsupported requiredStatusChecks', async () => { + await initRepo('some/repo', 'token'); + const res = await gitlab.getBranchStatus('somebranch', ['foo']); + expect(res).toEqual('failed'); + }); it('returns pending if no results', async () => { glGot.mockReturnValueOnce({ body: [], }); - const res = await gitlab.getBranchStatus('some-branch'); + const res = await gitlab.getBranchStatus('somebranch', []); expect(res).toEqual('pending'); }); it('returns success if all are success', async () => { glGot.mockReturnValueOnce({ body: [{ status: 'success' }, { status: 'success' }], }); - const res = await gitlab.getBranchStatus('some-branch'); + const res = await gitlab.getBranchStatus('somebranch', []); expect(res).toEqual('success'); }); it('returns failure if any are failed', async () => { glGot.mockReturnValueOnce({ body: [{ status: 'success' }, { status: 'failed' }], }); - const res = await gitlab.getBranchStatus('some-branch'); + const res = await gitlab.getBranchStatus('somebranch', []); expect(res).toEqual('failure'); }); it('returns custom statuses', async () => { glGot.mockReturnValueOnce({ body: [{ status: 'success' }, { status: 'foo' }], }); - const res = await gitlab.getBranchStatus('some-branch'); + const res = await gitlab.getBranchStatus('somebranch', []); expect(res).toEqual('foo'); }); }); diff --git a/test/workers/package/__snapshots__/index.spec.js.snap b/test/workers/package/__snapshots__/index.spec.js.snap index 7c7e3242ee..95d4a96df9 100644 --- a/test/workers/package/__snapshots__/index.spec.js.snap +++ b/test/workers/package/__snapshots__/index.spec.js.snap @@ -9,6 +9,7 @@ Array [ "prCreation", "automerge", "automergeType", + "requiredStatusChecks", "branchName", "commitMessage", "prTitle", -- GitLab