diff --git a/docs/configuration.md b/docs/configuration.md index 25a10f35e35dda783fe079fe5814c4a05b9bd6ce..0b28c3763f70a098db3836fb716a441b82cdfff8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -88,6 +88,7 @@ $ node renovate --help --ignore-deps <list> Dependencies to ignore --pin-versions [boolean] Convert ranged versions in package.json to pinned versions --separate-major-releases [boolean] If set to false, it will upgrade dependencies to latest release only, and not separate major/minor branches + --separate-patch-releases [boolean] If set to true, it will separate minor and patch updates into separate branches --ignore-future [boolean] Ignore versions tagged as "future" --ignore-unstable [boolean] Ignore versions with unstable semver --respect-latest [boolean] Ignore versions newer than npm "latest" version @@ -332,6 +333,14 @@ Obviously, you can't set repository or package file location with this method. <td>`RENOVATE_SEPARATE_MAJOR_RELEASES`</td> <td>`--separate-major-releases`<td> </tr> +<tr> + <td>`separatePatchReleases`</td> + <td>If set to true, it will separate minor and patch updates into separate branches</td> + <td>boolean</td> + <td><pre>false</pre></td> + <td>`RENOVATE_SEPARATE_PATCH_RELEASES`</td> + <td>`--separate-patch-releases`<td> +</tr> <tr> <td>`ignoreFuture`</td> <td>Ignore versions tagged as "future"</td> @@ -356,6 +365,30 @@ Obviously, you can't set repository or package file location with this method. <td>`RENOVATE_RESPECT_LATEST`</td> <td>`--respect-latest`<td> </tr> +<tr> + <td>`major`</td> + <td>Configuration to apply when an update type is major</td> + <td>json</td> + <td><pre>{}</pre></td> + <td>`RENOVATE_MAJOR`</td> + <td><td> +</tr> +<tr> + <td>`minor`</td> + <td>Configuration to apply when an update type is minor</td> + <td>json</td> + <td><pre>{}</pre></td> + <td>`RENOVATE_MINOR`</td> + <td><td> +</tr> +<tr> + <td>`patch`</td> + <td>Configuration to apply when an update type is patch. Only applies if `separatePatchReleases` is set to true</td> + <td>json</td> + <td><pre>{"branchName": "renovate/{{depName}}-{{newVersionMajor}}.{newVersionMinor}}.x"}</pre></td> + <td>`RENOVATE_PATCH`</td> + <td><td> +</tr> <tr> <td>`semanticCommits`</td> <td>Enable semantic commit prefixes for commits and PR titles</td> @@ -448,7 +481,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 {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency [{{depName}}]({{repositoryUrl}}) from version `{{currentVersion}}` to `{{newVersion}}`{{#if isRollback}}. This is necessary and important because version `{{currentVersion}}` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}\n{{#if releases.length}}\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\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://renovateapp.com)."</pre></td> + <td><pre>"This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}`{{depName}}`{{/if}} from version `{{currentVersion}}` to `{{newVersion}}`{{#if isRollback}}. This is necessary and important because version `{{currentVersion}}` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}\n{{#if releases.length}}\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\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://renovateapp.com)."</pre></td> <td>`RENOVATE_PR_BODY`</td> <td><td> </tr> @@ -510,7 +543,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{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\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://renovateapp.com)." + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \"{{groupName}}\".\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#each upgrades as |upgrade|}}\n- {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{depName}}`{{/if}}: 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://renovateapp.com)." }</pre></td> <td></td> <td><td> diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 4af6f08dbd4bda7713e2f12f3ab75f17b9312e24..554240c8be50946b7f2cd987f257400af8804a0a 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -185,6 +185,14 @@ const options = [ stage: 'package', type: 'boolean', }, + { + name: 'separatePatchReleases', + description: + 'If set to true, it will separate minor and patch updates into separate branches', + stage: 'package', + type: 'boolean', + default: false, + }, { name: 'ignoreFuture', description: 'Ignore versions tagged as "future"', @@ -203,6 +211,38 @@ const options = [ stage: 'package', type: 'boolean', }, + // Major/Minor/Patch + { + name: 'major', + description: 'Configuration to apply when an update type is major', + stage: 'package', + type: 'json', + default: {}, + cli: false, + mergeable: true, + }, + { + name: 'minor', + description: 'Configuration to apply when an update type is minor', + stage: 'package', + type: 'json', + default: {}, + cli: false, + mergeable: true, + }, + { + name: 'patch', + description: + 'Configuration to apply when an update type is patch. Only applies if `separatePatchReleases` is set to true', + stage: 'package', + type: 'json', + default: { + branchName: + 'renovate/{{depName}}-{{newVersionMajor}}.{{newVersionMinor}}.x', + }, + cli: false, + mergeable: true, + }, // Semantic commit / Semantic release { name: 'semanticCommits', diff --git a/lib/workers/package/index.js b/lib/workers/package/index.js index 5494ba9500c3ca4f193af3269abd9bdd8253ed18..20b506433dfcb8cbd37188c3092440acda7cf7eb 100644 --- a/lib/workers/package/index.js +++ b/lib/workers/package/index.js @@ -49,7 +49,14 @@ async function renovatePackage(config) { logger.debug({ results }, `${config.depName} lookup results`); // Flatten the result on top of config, add repositoryUrl return results.map(result => { - const upg = configParser.mergeChildConfig(config, result); + let upg = configParser.mergeChildConfig(config, result); + if (upg.isMajor) { + upg = configParser.mergeChildConfig(upg, upg.major); + } else if (upg.isMinor) { + upg = configParser.mergeChildConfig(upg, upg.minor); + } else if (upg.isPatch) { + upg = configParser.mergeChildConfig(upg, upg.patch); + } upg.repositoryUrl = npmDep && npmDep.repositoryUrl && npmDep.repositoryUrl.length ? npmDep.repositoryUrl diff --git a/lib/workers/package/versions.js b/lib/workers/package/versions.js index 8a0b7ca023fd4f19afe494d98005c44302a85662..bd7dae0f44320f66909501ee5d83053b7bcefa2b 100644 --- a/lib/workers/package/versions.js +++ b/lib/workers/package/versions.js @@ -87,24 +87,38 @@ function determineUpgrades(npmDep, config) { .forEach(newVersion => { // Group by major versions const newVersionMajor = semver.major(newVersion); - // Only split majors if configured to do so, and no group or 'any' automerge - const separateMajors = - config.separateMajorReleases && - !config.groupName && - config.automerge !== 'any'; - const upgradeKey = separateMajors ? newVersionMajor : 'latest'; + const newVersionMinor = semver.minor(newVersion); + let type; + if (newVersionMajor > semver.major(changeLogFromVersion)) { + type = 'major'; + } else if ( + newVersionMinor === semver.minor(changeLogFromVersion) && + (config.separatePatchReleases || config.automerge === 'patch') + ) { + // Only use patch if configured to + type = 'patch'; + } else { + type = 'minor'; + } + let upgradeKey; + if ( + !config.separateMajorReleases || + config.groupName || + config.automerge === 'any' + ) { + // If we're not separating releases then we use a common lookup key + upgradeKey = 'latest'; + } else if (type === 'patch') { + upgradeKey = `{{newVersionMajor}}.{{newVersionMinor}}`; + } else { + // Use major version as lookup key + upgradeKey = newVersionMajor; + } // Save this, if it's a new major version or greater than the previous greatest if ( !allUpgrades[upgradeKey] || semver.gt(newVersion, allUpgrades[upgradeKey].newVersion) ) { - const newVersionMinor = semver.minor(newVersion); - const type = - newVersionMajor > semver.major(changeLogFromVersion) // eslint-disable-line no-nested-ternary - ? 'major' - : newVersionMinor > semver.minor(changeLogFromVersion) - ? 'minor' - : 'patch'; const changeLogToVersion = newVersion; const automergeEnabled = isAutomergeEnabled(config.automerge, type); allUpgrades[upgradeKey] = { diff --git a/test/workers/package/__snapshots__/index.spec.js.snap b/test/workers/package/__snapshots__/index.spec.js.snap index 4160c5b53dec5990edc9b18eb1deb5faa0cb27d4..06814c44298c60ce1d653b7e9657b0b30dc99a1d 100644 --- a/test/workers/package/__snapshots__/index.spec.js.snap +++ b/test/workers/package/__snapshots__/index.spec.js.snap @@ -1,5 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`lib/workers/package/index renovatePackage(config) merges type 1`] = ` +Array [ + "timezone", + "schedule", + "semanticCommits", + "semanticPrefix", + "recreateClosed", + "rebaseStalePrs", + "prCreation", + "automerge", + "automergeType", + "requiredStatusChecks", + "branchName", + "commitMessage", + "prTitle", + "prBody", + "lazyGrouping", + "groupName", + "groupSlug", + "group", + "labels", + "assignees", + "reviewers", + "depName", + "currentVersion", + "isMajor", + "repositoryUrl", +] +`; + exports[`lib/workers/package/index renovatePackage(config) returns array if upgrades found 1`] = ` Array [ "timezone", diff --git a/test/workers/package/__snapshots__/versions.spec.js.snap b/test/workers/package/__snapshots__/versions.spec.js.snap index d91ab568238a0a198a4a7ef1dd5b477ab58cde1c..a941f3bf26182994b8eb5acb616326c3f4be3899 100644 --- a/test/workers/package/__snapshots__/versions.spec.js.snap +++ b/test/workers/package/__snapshots__/versions.spec.js.snap @@ -153,17 +153,17 @@ Array [ ] `; -exports[`workers/package/versions .determineUpgrades(npmDep, config) returns both updates if automerging patch 1`] = ` +exports[`workers/package/versions .determineUpgrades(npmDep, config) returns minor update if separate patches not configured 1`] = ` Array [ Object { - "automergeEnabled": true, + "automergeEnabled": false, "changeLogFromVersion": "0.9.0", "changeLogToVersion": "0.9.7", - "isPatch": true, + "isMinor": true, "newVersion": "0.9.7", "newVersionMajor": 0, "newVersionMinor": 9, - "type": "patch", + "type": "minor", }, Object { "automergeEnabled": false, @@ -232,6 +232,91 @@ Array [ ] `; +exports[`workers/package/versions .determineUpgrades(npmDep, config) returns patch minor and major 1`] = ` +Array [ + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.8.0", + "changeLogToVersion": "0.9.7", + "isMinor": true, + "newVersion": "0.9.7", + "newVersionMajor": 0, + "newVersionMinor": 9, + "type": "minor", + }, + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.8.0", + "changeLogToVersion": "1.4.1", + "isMajor": true, + "newVersion": "1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "major", + }, + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.8.0", + "changeLogToVersion": "0.8.12", + "isPatch": true, + "newVersion": "0.8.12", + "newVersionMajor": 0, + "newVersionMinor": 8, + "type": "patch", + }, +] +`; + +exports[`workers/package/versions .determineUpgrades(npmDep, config) returns patch update if automerging patch 1`] = ` +Array [ + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.9.0", + "changeLogToVersion": "1.4.1", + "isMajor": true, + "newVersion": "1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "major", + }, + Object { + "automergeEnabled": true, + "changeLogFromVersion": "0.9.0", + "changeLogToVersion": "0.9.7", + "isPatch": true, + "newVersion": "0.9.7", + "newVersionMajor": 0, + "newVersionMinor": 9, + "type": "patch", + }, +] +`; + +exports[`workers/package/versions .determineUpgrades(npmDep, config) returns patch update if separatePatchReleases 1`] = ` +Array [ + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.9.0", + "changeLogToVersion": "1.4.1", + "isMajor": true, + "newVersion": "1.4.1", + "newVersionMajor": 1, + "newVersionMinor": 4, + "type": "major", + }, + Object { + "automergeEnabled": false, + "changeLogFromVersion": "0.9.0", + "changeLogToVersion": "0.9.7", + "isPatch": true, + "newVersion": "0.9.7", + "newVersionMajor": 0, + "newVersionMinor": 9, + "type": "patch", + }, +] +`; + exports[`workers/package/versions .determineUpgrades(npmDep, config) should allow unstable versions if the current version is unstable 1`] = ` Array [ Object { @@ -264,12 +349,12 @@ Array [ "automergeEnabled": false, "changeLogFromVersion": "0.0.34", "changeLogToVersion": "0.0.35", - "isPatch": true, + "isMinor": true, "isRange": true, "newVersion": "^0.0.35", "newVersionMajor": 0, "newVersionMinor": 0, - "type": "patch", + "type": "minor", }, ] `; diff --git a/test/workers/package/index.spec.js b/test/workers/package/index.spec.js index ac175021ce17b59872e37bf9f1313807418611d6..74d0793ec29de1d973893bbe89669f88ad3f6f27 100644 --- a/test/workers/package/index.spec.js +++ b/test/workers/package/index.spec.js @@ -53,5 +53,19 @@ describe('lib/workers/package/index', () => { expect(res).toHaveLength(1); expect(Object.keys(res[0])).toMatchSnapshot(); }); + it('merges type', async () => { + npmApi.getDependency.mockReturnValueOnce({}); + versions.determineUpgrades = jest.fn(() => [ + { isMajor: true }, + { isMinor: true }, + { isPatch: true }, + ]); + const res = await pkgWorker.renovatePackage(config); + expect(res).toHaveLength(3); + expect(Object.keys(res[0])).toMatchSnapshot(); + expect(res[0].branchName.indexOf('newVersionMinor')).toBe(-1); + expect(res[1].branchName.indexOf('newVersionMinor')).toBe(-1); + expect(res[2].branchName.indexOf('newVersionMinor')).not.toBe(-1); + }); }); }); diff --git a/test/workers/package/versions.spec.js b/test/workers/package/versions.spec.js index e7fabd074497e134e04ac398e57ac0d7ba0a0f0f..09c3dee43472ee0b66f6b363acbd3b6e4d4716fe 100644 --- a/test/workers/package/versions.spec.js +++ b/test/workers/package/versions.spec.js @@ -49,11 +49,27 @@ describe('workers/package/versions', () => { config.currentVersion = '^0.4.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); - it('returns both updates if automerging patch', () => { + it('returns minor update if separate patches not configured', () => { + config.currentVersion = '0.9.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('returns patch update if automerging patch', () => { config.automerge = 'patch'; config.currentVersion = '0.9.0'; expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); }); + it('returns patch update if separatePatchReleases', () => { + config.separatePatchReleases = true; + config.currentVersion = '0.9.0'; + expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot(); + }); + it('returns patch minor and major', () => { + config.separatePatchReleases = true; + config.currentVersion = '0.8.0'; + const res = versions.determineUpgrades(qJson, config); + expect(res).toHaveLength(3); + expect(res).toMatchSnapshot(); + }); it('disables major release separation (major)', () => { config.separateMajorReleases = false; config.currentVersion = '^0.4.0';