From 07b9e875a6c130f884c5f333c5fe7eba0311d8da Mon Sep 17 00:00:00 2001 From: Guilherme Kammsetzer Date: Wed, 5 Oct 2022 16:31:39 -0400 Subject: [PATCH 1/2] feat: add support to dependabot's compatibility score --- action.yml | 6 +++ dist/index.js | 73 +++++++++++++++++------------------ src/action.js | 15 ++++++++ src/util.js | 1 + test/action.test.js | 93 +++++++++++++++++++++++++++++++++++++++++++++ test/util.test.js | 10 +++++ 6 files changed, 162 insertions(+), 36 deletions(-) diff --git a/action.yml b/action.yml index ae1c1e7e..436d3a7a 100644 --- a/action.yml +++ b/action.yml @@ -28,6 +28,9 @@ inputs: pr-number: description: 'A pull request number, only required if triggered from a workflow_dispatch event' required: false + compatibility-score: + description: 'The minimum compatibility score required for the PR to be merged (0-100)' + required: false runs: using: 'composite' @@ -36,6 +39,8 @@ runs: id: dependabot-metadata uses: dependabot/fetch-metadata@v1 if: ${{ github.actor == 'dependabot[bot]' }} + with: + compat-lookup: true - name: Merge/approve PR uses: actions/github-script@v6 if: ${{ github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' }} @@ -51,6 +56,7 @@ runs: updateType: '${{ steps.dependabot-metadata.outputs.update-type }}', dependencyType:'${{ steps.dependabot-metadata.outputs.dependency-type }}', dependencyNames: '${{ steps.dependabot-metadata.outputs.dependency-names }}', + compatibilityScore: '${{ steps.dependabot-metadata.outputs.compatibility-score }}', } }) diff --git a/dist/index.js b/dist/index.js index 40c6914c..46b75eb2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -140,6 +140,7 @@ const file_command_1 = __nccwpck_require__(717); const utils_1 = __nccwpck_require__(278); const os = __importStar(__nccwpck_require__(37)); const path = __importStar(__nccwpck_require__(17)); +const uuid_1 = __nccwpck_require__(974); const oidc_utils_1 = __nccwpck_require__(41); /** * The code to exit an action @@ -169,9 +170,20 @@ function exportVariable(name, val) { process.env[name] = convertedVal; const filePath = process.env['GITHUB_ENV'] || ''; if (filePath) { - return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val)); + const delimiter = `ghadelimiter_${uuid_1.v4()}`; + // These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter. + if (name.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedVal.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); } - command_1.issueCommand('set-env', { name }, convertedVal); } exports.exportVariable = exportVariable; /** @@ -189,7 +201,7 @@ exports.setSecret = setSecret; function addPath(inputPath) { const filePath = process.env['GITHUB_PATH'] || ''; if (filePath) { - file_command_1.issueFileCommand('PATH', inputPath); + file_command_1.issueCommand('PATH', inputPath); } else { command_1.issueCommand('add-path', {}, inputPath); @@ -229,10 +241,7 @@ function getMultilineInput(name, options) { const inputs = getInput(name, options) .split('\n') .filter(x => x !== ''); - if (options && options.trimWhitespace === false) { - return inputs; - } - return inputs.map(input => input.trim()); + return inputs; } exports.getMultilineInput = getMultilineInput; /** @@ -265,12 +274,8 @@ exports.getBooleanInput = getBooleanInput; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function setOutput(name, value) { - const filePath = process.env['GITHUB_OUTPUT'] || ''; - if (filePath) { - return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value)); - } process.stdout.write(os.EOL); - command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(value)); + command_1.issueCommand('set-output', { name }, value); } exports.setOutput = setOutput; /** @@ -399,11 +404,7 @@ exports.group = group; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function saveState(name, value) { - const filePath = process.env['GITHUB_STATE'] || ''; - if (filePath) { - return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value)); - } - command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(value)); + command_1.issueCommand('save-state', { name }, value); } exports.saveState = saveState; /** @@ -469,14 +470,13 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +exports.issueCommand = void 0; // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ const fs = __importStar(__nccwpck_require__(147)); const os = __importStar(__nccwpck_require__(37)); -const uuid_1 = __nccwpck_require__(974); const utils_1 = __nccwpck_require__(278); -function issueFileCommand(command, message) { +function issueCommand(command, message) { const filePath = process.env[`GITHUB_${command}`]; if (!filePath) { throw new Error(`Unable to find environment variable for file command ${command}`); @@ -488,22 +488,7 @@ function issueFileCommand(command, message) { encoding: 'utf8' }); } -exports.issueFileCommand = issueFileCommand; -function prepareKeyValueMessage(key, value) { - const delimiter = `ghadelimiter_${uuid_1.v4()}`; - const convertedValue = utils_1.toCommandValue(value); - // These should realistically never happen, but just in case someone finds a - // way to exploit uuid generation let's not allow keys or values that contain - // the delimiter. - if (key.includes(delimiter)) { - throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); - } - if (convertedValue.includes(delimiter)) { - throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); - } - return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; -} -exports.prepareKeyValueMessage = prepareKeyValueMessage; +exports.issueCommand = issueCommand; //# sourceMappingURL=file-command.js.map /***/ }), @@ -2760,6 +2745,7 @@ module.exports = async function run({ APPROVE_ONLY, TARGET, PR_NUMBER, + COMPATIBILITY_SCORE, } = getInputs(inputs) try { @@ -2794,6 +2780,20 @@ module.exports = async function run({ ) } + const targetScore = +COMPATIBILITY_SCORE + const compatScore = +dependabotMetadata.compatibilityScore + + if ( + !isNaN(targetScore) && + !isNaN(compatScore) && + compatScore < targetScore + ) { + core.setFailed( + `Compatibility score is lower than allowed. Expected at least ${targetScore} but received ${compatScore}` + ) + return + } + if ( TARGET !== updateTypes.any && updateTypesPriority.indexOf(updateType) > @@ -3056,6 +3056,7 @@ exports.getInputs = inputs => { APPROVE_ONLY: /true/i.test(inputs['approve-only']), TARGET: mapUpdateType(inputs['target']), PR_NUMBER: inputs['pr-number'], + COMPATIBILITY_SCORE: inputs['compatibility-score'], } } diff --git a/src/action.js b/src/action.js index c30a6503..4de5043d 100644 --- a/src/action.js +++ b/src/action.js @@ -30,6 +30,7 @@ module.exports = async function run({ APPROVE_ONLY, TARGET, PR_NUMBER, + COMPATIBILITY_SCORE, } = getInputs(inputs) try { @@ -64,6 +65,20 @@ module.exports = async function run({ ) } + const targetScore = +COMPATIBILITY_SCORE + const compatScore = +dependabotMetadata.compatibilityScore + + if ( + !isNaN(targetScore) && + !isNaN(compatScore) && + compatScore < targetScore + ) { + core.setFailed( + `Compatibility score is lower than allowed. Expected at least ${targetScore} but received ${compatScore}` + ) + return + } + if ( TARGET !== updateTypes.any && updateTypesPriority.indexOf(updateType) > diff --git a/src/util.js b/src/util.js index 28a04b41..c1659460 100644 --- a/src/util.js +++ b/src/util.js @@ -45,5 +45,6 @@ exports.getInputs = inputs => { APPROVE_ONLY: /true/i.test(inputs['approve-only']), TARGET: mapUpdateType(inputs['target']), PR_NUMBER: inputs['pr-number'], + COMPATIBILITY_SCORE: inputs['compatibility-score'], } } diff --git a/test/action.test.js b/test/action.test.js index 601d5aaf..8dcea889 100644 --- a/test/action.test.js +++ b/test/action.test.js @@ -466,3 +466,96 @@ tap.test('should forbid minor when target is patch', async () => { sinon.assert.notCalled(stubs.approveStub) sinon.assert.notCalled(stubs.mergeStub) }) + +tap.test( + 'should not allow merge with compatibility score lower than target score', + async () => { + const PR_NUMBER = Math.random() + + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + number: PR_NUMBER, + user: { login: BOT_NAME }, + }, + }, + inputs: { + PR_NUMBER, + 'compatibility-score': 90, + }, + dependabotMetadata: createDependabotMetadata({ + updateType: updateTypes.minor, + compatibilityScore: 80, + }), + }) + + await action() + + sinon.assert.calledWithExactly( + stubs.coreStub.setFailed, + `Compatibility score is lower than allowed. Expected at least 90 but received 80` + ) + sinon.assert.notCalled(stubs.approveStub) + sinon.assert.notCalled(stubs.mergeStub) + } +) + +tap.test( + 'should allow merge with compatibility score higher than target score', + async () => { + const PR_NUMBER = Math.random() + + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + number: PR_NUMBER, + user: { login: BOT_NAME }, + }, + }, + inputs: { + PR_NUMBER, + 'compatibility-score': 90, + }, + dependabotMetadata: createDependabotMetadata({ + updateType: updateTypes.minor, + compatibilityScore: 91, + }), + }) + + await action() + + sinon.assert.notCalled(stubs.coreStub.setFailed) + sinon.assert.called(stubs.approveStub) + sinon.assert.called(stubs.mergeStub) + } +) + +tap.test( + 'should allow merge when compatScore is equal to targetScore', + async () => { + const PR_NUMBER = Math.random() + + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + number: PR_NUMBER, + user: { login: BOT_NAME }, + }, + }, + inputs: { + PR_NUMBER, + 'compatibility-score': 90, + }, + dependabotMetadata: createDependabotMetadata({ + updateType: updateTypes.minor, + compatibilityScore: 90, + }), + }) + + await action() + + sinon.assert.notCalled(stubs.coreStub.setFailed) + sinon.assert.called(stubs.approveStub) + sinon.assert.called(stubs.mergeStub) + } +) diff --git a/test/util.test.js b/test/util.test.js index 435e927d..50539f5b 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -91,6 +91,16 @@ tap.test('getInputs', async t => { t.test('PR_NUMBER', async t => { t.equal(getInputs({ 'pr-number': '10' }).PR_NUMBER, '10') }) + t.test('COMPATIBILITY_SCORE', async t => { + t.equal( + getInputs({ 'compatibility-score': '10' }).COMPATIBILITY_SCORE, + '10' + ) + t.equal( + getInputs({ 'compatibility-score': '10' }).COMPATIBILITY_SCORE, + '10' + ) + }) } ) }) From 923d35a9dc9d7de840784a1780dd50fbccac16ea Mon Sep 17 00:00:00 2001 From: Guilherme Kammsetzer Date: Wed, 5 Oct 2022 16:43:39 -0400 Subject: [PATCH 2/2] docs: add compatibility score input to readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 32c516f5..6149fb92 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ An example of a non-semantic version is a commit hash when using git submodules. _Optional_ A pull request number, only required if triggered from a workflow_dispatch event. Typically this would be triggered by a script running in a seperate CI provider. See [Trigger action from workflow_dispatch event](#trigger-action-from-workflow_dispatch-event) +### `compatibility-score` + +_Optional_ A minimum [Compatibility score](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates#about-compatibility-scores) needed for the PR to be merged. + ## Usage Configure this action in your workflows providing the inputs described above.