diff --git a/docs/configuration.md b/docs/configuration.md index 6f416fe4d755a2d348d653829ae4e12562cee4bc..01555b1bd55585caf70b77fcabe9caee10612750 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 a3ba3eaf9d5008fe3973bd388508e6c0f24d99f4..04528826b563806379a9a9bfddff8547f4133432 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 2c539ca0b378b2b9c0e9c31f99e1d72137c93d9e..5fe2e455528b43497325ad4eda849d50effaad9a 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 61ac9e863bd4cee66df3c8a39b7b165b190bad59..d686f8ba84e36b0e4f06255013060b7dfb20d4b7 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 02c97d3de9b6ddac14bbf32e97d227602364f95f..5121c3c0164b15f4c787fe33015c422726723da3 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 3bad4d5a848640407a183b3a3fda4e8bc183036e..25d0dabd6e430064ee30638bf515ddaf08afde26 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 825a4ef1cbb254a3f56b69c65a20eb3e756e8f18..02b8db1c0689d391515f9560af7492c6f50c3b10 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 e3142fe5a53892d9beb7bbd5b573ac8f6e248a91..7f5c52067968430026a23c6e0bf86f16969c9d7e 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 7c7e3242ee49867c6d0041b6b43002c78883514f..95d4a96df920a2c69919741d9c2df6ae564750ab 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",