diff --git a/docs/configuration.md b/docs/configuration.md index ef22de91871b95eb62218d8bdc2e0d1807e0ae17..3151874fea7b3186c0598805185d039b2d9611b2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -96,6 +96,7 @@ $ node renovate --help --semantic-prefix <string> Prefix to use if semantic commits are enabled --recreate-closed [boolean] Recreate PRs even if same ones were closed previously --rebase-stale-prs [boolean] Rebase stale PRs (GitHub only) + --unpublish-safe [boolean] Set a status check for unpublish-safe upgrades --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, patch, minor or any --automerge-type <string> How to automerge - "branch-merge-commit", "branch-push" or "pr". Branch support is GitHub-only @@ -482,6 +483,14 @@ Obviously, you can't set repository or package file location with this method. <td>`RENOVATE_REBASE_STALE_PRS`</td> <td>`--rebase-stale-prs`<td> </tr> +<tr> + <td>`unpublishSafe`</td> + <td>Set a status check for unpublish-safe upgrades</td> + <td>boolean</td> + <td><pre>true</pre></td> + <td>`RENOVATE_UNPUBLISH_SAFE`</td> + <td>`--unpublish-safe`<td> +</tr> <tr> <td>`prCreation`</td> <td>When to create the PR for a branch. Values: immediate, not-pending, status-success.</td> diff --git a/docs/status-checks.md b/docs/status-checks.md new file mode 100644 index 0000000000000000000000000000000000000000..ead279856944a1d4c5964f6a8521f293077fb942 --- /dev/null +++ b/docs/status-checks.md @@ -0,0 +1,9 @@ +# Status Checks + +## unpublish-safe + +Renovate includes a status showing whether upgrades are "unpublish-safe". This is because [packages less than 24 hours old may be unpublished by their authors](https://docs.npmjs.com/cli/unpublish). We recommend you wait for this status check to pass before merging unless the upgrade is urgent, otherwise you may find that packages simply disappear from the npm registry, breaking your build. + +If you would like to disable this status check, add `"unpublishSafe": false` to your config. + +If you would like to delay creation of Pull Requests until after this check passes, then add `"prCreation": "not-pending"` to your config. This way the PR will only be created once the upgrades in the branch are at least 24 hours old because Renovate sets the status check to "pending" in the meantime. diff --git a/lib/api/github.js b/lib/api/github.js index 490d79333acc07ce9d1fffbab2a25a16111a875a..9baacf02b6530dc161e4ff05e47bceff64e9e86b 100644 --- a/lib/api/github.js +++ b/lib/api/github.js @@ -59,6 +59,7 @@ module.exports = { isBranchStale, getBranchPr, getBranchStatus, + setBranchStatus, deleteBranch, mergeBranch, // issue @@ -363,6 +364,26 @@ async function getBranchStatus(branchName, requiredStatusChecks) { return res.body.state; } +async function setBranchStatus( + branchName, + context, + description, + state, + targetUrl +) { + const branchCommit = await getBranchCommit(branchName); + const url = `repos/${config.repoName}/statuses/${branchCommit}`; + const options = { + state, + description, + context, + }; + if (targetUrl) { + options.target_url = targetUrl; + } + await ghGotRetry.post(url, { body: options }); +} + async function deleteBranch(branchName) { await ghGotRetry.delete( `repos/${config.repoName}/git/refs/heads/${branchName}` diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js index 581029c28aaee407450ec05c4b79285ce117ba21..6d40ab9388e024f13221112383c4ee81222baf35 100644 --- a/lib/api/gitlab.js +++ b/lib/api/gitlab.js @@ -15,6 +15,7 @@ module.exports = { getBranch, getBranchPr, getBranchStatus, + setBranchStatus, deleteBranch, // issue addAssignees, @@ -221,6 +222,30 @@ async function getBranchStatus(branchName, requiredStatusChecks) { return status; } +async function setBranchStatus( + branchName, + context, + description, + state, + targetUrl +) { + // First, get the branch to find the commit SHA + let url = `projects/${config.repoName}/repository/branches/${branchName}`; + const res = await glGot(url); + const branchSha = res.body.commit.id; + // Now, check the statuses for that commit + url = `projects/${config.repoName}/statuses/${branchSha}`; + const options = { + state, + description, + context, + }; + if (targetUrl) { + options.target_url = targetUrl; + } + await glGot.post(url, { body: options }); +} + async function deleteBranch(branchName) { await glGot.delete( `projects/${config.repoName}/repository/branches/${branchName}` diff --git a/lib/api/npm.js b/lib/api/npm.js index 58b31fda8054c3b170c83183219f114852d4892b..7eba27749e18d722653891957ed12490ed314aa9 100644 --- a/lib/api/npm.js +++ b/lib/api/npm.js @@ -73,7 +73,7 @@ async function getDependency(name, logger) { }; Object.keys(dep.versions).forEach(version => { // We don't use any of the version payload currently - dep.versions[version] = {}; + dep.versions[version] = { time: res.body.time[version] }; }); npmCache[cacheKey] = dep; logger.trace({ dependency: dep }, 'dependency'); diff --git a/lib/config/definitions.js b/lib/config/definitions.js index e686c3614418af5db4d8a7425509e4736a3a48e6..12e37e15d3217d950e1eb46ba580f4b80c8cc8bd 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -338,6 +338,11 @@ const options = [ type: 'boolean', default: false, }, + { + name: 'unpublishSafe', + description: 'Set a status check for unpublish-safe upgrades', + type: 'boolean', + }, { name: 'prCreation', description: diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js index e2d0b321a1c32e7347085aee57f56509af7d53ae..438d49bab206acefe1215b0864203e758652fc25 100644 --- a/lib/workers/branch/index.js +++ b/lib/workers/branch/index.js @@ -88,7 +88,15 @@ async function ensureBranch(config) { const api = config.api; const packageFiles = {}; const commitFiles = []; + let unpublishable; for (const upgrade of config.upgrades) { + if (typeof upgrade.unpublishable !== 'undefined') { + if (typeof unpublishable !== 'undefined') { + unpublishable = unpublishable && upgrade.unpublishable; + } else { + unpublishable = upgrade.unpublishable; + } + } if (upgrade.type === 'lockFileMaintenance') { logger.debug('branch lockFileMaintenance'); try { @@ -189,6 +197,20 @@ async function ensureBranch(config) { // Return now if no branch exists return false; } + // Set unpublishable status check + if (config.unpublishSafe && typeof unpublishable !== 'undefined') { + const state = unpublishable ? 'success' : 'pending'; + const description = unpublishable + ? 'Packages are at least 24 hours old' + : 'Packages < 24 hours old can be unpublished'; + await api.setBranchStatus( + branchName, + 'renovate/unpublish-safe', + description, + state, + 'https://github.com/singapore/renovate/blob/master/docs/status-checks#unpublish-safe.md' + ); + } if (config.automergeEnabled === false || config.automergeType === 'pr') { // No branch automerge return true; diff --git a/lib/workers/package/versions.js b/lib/workers/package/versions.js index 92b5cfb61926a563890ba5a226221c8ce595c2a4..0c4c3a59ae337344a709cf4185a9d012b9bbca3c 100644 --- a/lib/workers/package/versions.js +++ b/lib/workers/package/versions.js @@ -3,6 +3,7 @@ const semver = require('semver'); const stable = require('semver-stable'); const _ = require('lodash'); const semverUtils = require('semver-utils'); +const moment = require('moment'); module.exports = { determineUpgrades, @@ -141,6 +142,13 @@ function determineUpgrades(npmDep, config) { }); // Return only the values - we don't need the keys anymore const upgrades = Object.keys(allUpgrades).map(key => allUpgrades[key]); + for (const upgrade of upgrades) { + const elapsed = moment().diff( + moment(versions[upgrade.newVersion].time), + 'days' + ); + upgrade.unpublishable = elapsed > 0; + } // Return now if array is empty, or we can keep pinned version upgrades if (upgrades.length === 0 || config.pinVersions || !isRange(currentVersion)) { diff --git a/test/api/__snapshots__/npm.spec.js.snap b/test/api/__snapshots__/npm.spec.js.snap index 6a180eed7ae7ac4853cf88534c36574ea01bf958..9618f3e9d86c26ac4032be192beb58cbffc05a68 100644 --- a/test/api/__snapshots__/npm.spec.js.snap +++ b/test/api/__snapshots__/npm.spec.js.snap @@ -7,7 +7,9 @@ Object { "name": undefined, "repositoryUrl": "https://github.com/renovateapp/dummy", "versions": Object { - "0.0.1": Object {}, + "0.0.1": Object { + "time": "", + }, }, } `; @@ -29,7 +31,9 @@ Object { "name": undefined, "repositoryUrl": "https://google.com", "versions": Object { - "0.0.1": Object {}, + "0.0.1": Object { + "time": "", + }, }, } `; @@ -53,7 +57,9 @@ Object { "name": undefined, "repositoryUrl": "https://google.com", "versions": Object { - "0.0.1": Object {}, + "0.0.1": Object { + "time": "", + }, }, } `; @@ -77,7 +83,9 @@ Object { "name": undefined, "repositoryUrl": "https://google.com", "versions": Object { - "0.0.1": Object {}, + "0.0.1": Object { + "time": "", + }, }, } `; diff --git a/test/api/github.spec.js b/test/api/github.spec.js index e3d215a60f32d46b66eeb5a6fc927688ac064079..6662ed1034a72b15db0382bac9d94e60d1e4312d 100644 --- a/test/api/github.spec.js +++ b/test/api/github.spec.js @@ -823,6 +823,27 @@ describe('api/github', () => { expect(res).toEqual('failed'); }); }); + describe('setBranchStatus', () => { + it('sets branch status', async () => { + await initRepo('some/repo', 'token'); + // getBranchCommit + ghGot.mockImplementationOnce(() => ({ + body: { + object: { + sha: '1235', + }, + }, + })); + await github.setBranchStatus( + 'some-branch', + 'some-context', + 'some-description', + 'some-state', + 'some-url' + ); + expect(ghGot.post.mock.calls).toHaveLength(1); + }); + }); describe('mergeBranch(branchName, mergeType)', () => { it('should perform a branch-push merge', async () => { await initRepo('some/repo', 'token'); diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js index 48813649d750c3ef9f2a04c55059db2c5947e654..792785944aef48fd5cd894cf02ff3e2738f37b77 100644 --- a/test/api/gitlab.spec.js +++ b/test/api/gitlab.spec.js @@ -326,6 +326,27 @@ describe('api/gitlab', () => { expect(res).toEqual('foo'); }); }); + describe('setBranchStatus', () => { + it('sets branch status', async () => { + await initRepo('some/repo', 'token'); + // getBranchCommit + glGot.mockReturnValueOnce({ + body: { + commit: { + id: 1, + }, + }, + }); + await gitlab.setBranchStatus( + 'some-branch', + 'some-context', + 'some-description', + 'some-state', + 'some-url' + ); + expect(glGot.post.mock.calls).toHaveLength(1); + }); + }); describe('deleteBranch(branchName)', () => { it('should send delete', async () => { glGot.delete = jest.fn(); diff --git a/test/api/npm.spec.js b/test/api/npm.spec.js index 10a39cbcca87a516c8f6324dc5a12fbe4842ff88..48263f190e5168dbc22d2e7765ab362d7d154764 100644 --- a/test/api/npm.spec.js +++ b/test/api/npm.spec.js @@ -19,6 +19,9 @@ const npmResponse = { type: 'git', url: 'git://github.com/renovateapp/dummy.git', }, + time: { + '0.0.1': '', + }, }, }; diff --git a/test/workers/branch/__snapshots__/index.spec.js.snap b/test/workers/branch/__snapshots__/index.spec.js.snap index 48df282e6c8d4d3d2d9039bb8569683f13eae4a7..2ad706187a1755f3b773bbabadd887d9f2b080da 100644 --- a/test/workers/branch/__snapshots__/index.spec.js.snap +++ b/test/workers/branch/__snapshots__/index.spec.js.snap @@ -15,6 +15,7 @@ Object { "getBranchStatus": [Function], "getFileContent": [Function], "mergeBranch": [Function], + "setBranchStatus": [Function], }, ], } @@ -39,6 +40,7 @@ Object { "getBranchStatus": [Function], "getFileContent": [Function], "mergeBranch": [Function], + "setBranchStatus": [Function], }, ], } diff --git a/test/workers/branch/index.spec.js b/test/workers/branch/index.spec.js index 973c92ca9d59267f39ce737280a4bfaff39ce92f..c0b57d4d53510785fa079b4a0332ade78c236042 100644 --- a/test/workers/branch/index.spec.js +++ b/test/workers/branch/index.spec.js @@ -119,6 +119,7 @@ describe('workers/branch', () => { config.api.commitFilesToBranch = jest.fn(); config.api.getFileContent.mockReturnValueOnce('old content'); config.api.getBranchStatus = jest.fn(); + config.api.setBranchStatus = jest.fn(); config.depName = 'dummy'; config.currentVersion = '1.0.0'; config.newVersion = '1.1.0'; @@ -160,6 +161,25 @@ describe('workers/branch', () => { expect(npm.getLockFile.mock.calls.length).toBe(1); expect(yarn.getLockFile.mock.calls.length).toBe(1); expect(config.api.commitFilesToBranch.mock.calls[0][1].length).toBe(1); + expect(config.api.setBranchStatus.mock.calls).toHaveLength(0); + }); + it('sets branch status pending', async () => { + branchWorker.getParentBranch.mockReturnValueOnce('dummy branch'); + packageJsonHelper.setNewValue.mockReturnValueOnce('new content'); + config.api.branchExists.mockReturnValueOnce(true); + config.upgrades[0].unpublishable = true; + config.upgrades.push({ ...config }); + config.upgrades[1].unpublishable = false; + expect(await branchWorker.ensureBranch(config)).toBe(true); + expect(config.api.setBranchStatus.mock.calls).toHaveLength(1); + }); + it('sets branch status success', async () => { + branchWorker.getParentBranch.mockReturnValueOnce('dummy branch'); + packageJsonHelper.setNewValue.mockReturnValueOnce('new content'); + config.api.branchExists.mockReturnValueOnce(true); + config.upgrades[0].unpublishable = true; + expect(await branchWorker.ensureBranch(config)).toBe(true); + expect(config.api.setBranchStatus.mock.calls).toHaveLength(1); }); it('automerges successful branches', async () => { branchWorker.getParentBranch.mockReturnValueOnce('dummy branch'); diff --git a/test/workers/package/__snapshots__/index.spec.js.snap b/test/workers/package/__snapshots__/index.spec.js.snap index 78a9ad39ffd65777de058705a5b19d9d626c0243..c033b470314a3337fd68fab89289715db071f81c 100644 --- a/test/workers/package/__snapshots__/index.spec.js.snap +++ b/test/workers/package/__snapshots__/index.spec.js.snap @@ -9,6 +9,7 @@ Array [ "semanticPrefix", "recreateClosed", "rebaseStalePrs", + "unpublishSafe", "prCreation", "automerge", "automergeType", @@ -40,6 +41,7 @@ Array [ "semanticPrefix", "recreateClosed", "rebaseStalePrs", + "unpublishSafe", "prCreation", "automerge", "automergeType", @@ -207,6 +209,7 @@ This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](ht "semanticPrefix": "chore(deps):", "timezone": null, "type": "error", + "unpublishSafe": true, }, ] `; diff --git a/test/workers/package/__snapshots__/versions.spec.js.snap b/test/workers/package/__snapshots__/versions.spec.js.snap index 7352538b6f6d83acfc0bc60a38e4590433a502f7..6e47c491e009fcb5644568074654b884ba661cd3 100644 --- a/test/workers/package/__snapshots__/versions.spec.js.snap +++ b/test/workers/package/__snapshots__/versions.spec.js.snap @@ -13,6 +13,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -23,6 +24,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -38,6 +40,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, ] `; @@ -60,6 +63,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -72,6 +76,7 @@ Array [ "newVersion": "0.9.7", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, ] `; @@ -89,6 +94,7 @@ Array [ "newVersion": "1.4.1", "newVersionMajor": 1, "type": "pin", + "unpublishable": false, }, ] `; @@ -127,6 +133,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -137,6 +144,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -149,6 +157,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, ] `; @@ -164,6 +173,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -174,6 +184,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -191,6 +202,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -201,6 +213,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -218,6 +231,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -228,6 +242,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -243,6 +258,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -253,6 +269,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -263,6 +280,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 8, "type": "patch", + "unpublishable": false, }, ] `; @@ -278,6 +296,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -288,6 +307,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "patch", + "unpublishable": false, }, ] `; @@ -303,6 +323,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -313,6 +334,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "patch", + "unpublishable": false, }, ] `; @@ -328,6 +350,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 1, "type": "minor", + "unpublishable": false, }, ] `; @@ -340,6 +363,7 @@ Object { "newVersionMajor": 1, "semanticPrefix": "fix(deps):", "type": "rollback", + "unpublishable": false, } `; @@ -355,6 +379,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 0, "type": "minor", + "unpublishable": false, }, ] `; @@ -370,6 +395,7 @@ Array [ "newVersionMajor": 2, "newVersionMinor": 0, "type": "major", + "unpublishable": false, }, ] `; @@ -387,6 +413,7 @@ Array [ "newVersion": "2.0.3", "newVersionMajor": 2, "type": "pin", + "unpublishable": false, }, ] `; @@ -402,6 +429,7 @@ Array [ "newVersionMajor": 2, "newVersionMinor": 0, "type": "major", + "unpublishable": false, }, ] `; @@ -417,6 +445,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -427,6 +456,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -439,6 +469,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, ] `; @@ -454,6 +485,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -464,6 +496,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -476,6 +509,7 @@ Array [ "newVersion": "0.4.4", "newVersionMajor": 0, "type": "pin", + "unpublishable": false, }, ] `; @@ -492,6 +526,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -507,6 +542,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -519,6 +555,7 @@ Array [ "newVersion": "1.3.0", "newVersionMajor": 1, "type": "pin", + "unpublishable": false, }, ] `; @@ -535,6 +572,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, ] `; @@ -551,6 +589,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -562,6 +601,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -577,6 +617,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -589,6 +630,7 @@ Array [ "newVersion": "1.0.1", "newVersionMajor": 1, "type": "pin", + "unpublishable": false, }, ] `; @@ -605,6 +647,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -616,6 +659,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -632,6 +676,7 @@ Array [ "newVersionMajor": 0, "newVersionMinor": 9, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": false, @@ -643,6 +688,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -659,6 +705,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "major", + "unpublishable": false, }, ] `; @@ -675,6 +722,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, ] `; @@ -690,6 +738,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, Object { "automergeEnabled": true, @@ -702,6 +751,7 @@ Array [ "newVersion": "1.3.0", "newVersionMajor": 1, "type": "pin", + "unpublishable": false, }, ] `; @@ -718,6 +768,7 @@ Array [ "newVersionMajor": 1, "newVersionMinor": 4, "type": "minor", + "unpublishable": false, }, ] `;