From d7a6bbe36708cc8c1db04d6b32cb4b996bd31630 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Sun, 6 Aug 2017 15:38:10 +0200 Subject: [PATCH] feat: unpublish-safe status check (#635) Renovate now adds a status check renovate/unpublish-safe that has the following behaviour: If any upgrade in the branch is < 24 hours old then the status check state is "pending" If all upgrades in the branch are 24 hours or more old then the status check state is "success" This is able to be disabled via a new option "unpublishSafe". Closes #494 --- docs/configuration.md | 9 ++++ docs/status-checks.md | 9 ++++ lib/api/github.js | 21 ++++++++ lib/api/gitlab.js | 25 +++++++++ lib/api/npm.js | 2 +- lib/config/definitions.js | 5 ++ lib/workers/branch/index.js | 22 ++++++++ lib/workers/package/versions.js | 8 +++ test/api/__snapshots__/npm.spec.js.snap | 16 ++++-- test/api/github.spec.js | 21 ++++++++ test/api/gitlab.spec.js | 21 ++++++++ test/api/npm.spec.js | 3 ++ .../branch/__snapshots__/index.spec.js.snap | 2 + test/workers/branch/index.spec.js | 20 ++++++++ .../package/__snapshots__/index.spec.js.snap | 3 ++ .../__snapshots__/versions.spec.js.snap | 51 +++++++++++++++++++ 16 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 docs/status-checks.md diff --git a/docs/configuration.md b/docs/configuration.md index ef22de9187..3151874fea 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 0000000000..ead2798569 --- /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 490d79333a..9baacf02b6 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 581029c28a..6d40ab9388 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 58b31fda80..7eba27749e 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 e686c36144..12e37e15d3 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 e2d0b321a1..438d49bab2 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 92b5cfb619..0c4c3a59ae 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 6a180eed7a..9618f3e9d8 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 e3d215a60f..6662ed1034 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 48813649d7..792785944a 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 10a39cbcca..48263f190e 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 48df282e6c..2ad706187a 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 973c92ca9d..c0b57d4d53 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 78a9ad39ff..c033b47031 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 7352538b6f..6e47c491e0 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, }, ] `; -- GitLab