Merge pull request #11 from dependabot/brrygrdn/output-single-dependency

Clarify action naming, add usage examples for actions + gh cli
This commit is contained in:
Barry Gordon
2021-06-01 15:14:27 +01:00
committed by GitHub
12 changed files with 900 additions and 8420 deletions

View File

@@ -3,6 +3,7 @@ on: [ pull_request ]
jobs:
# test action works running from the graph
if: ${{ github.actor == 'dependabot[bot]' }}
run-action:
runs-on: ubuntu-latest
steps:

109
README.md
View File

@@ -2,12 +2,15 @@
<img src="https://s3.eu-west-2.amazonaws.com/dependabot-images/logo-with-name-horizontal.svg?v5" alt="Dependabot" width="336">
</p>
# Dependabot Pull Request Action
# Fetch Metadata Action
**Name:** `dependabot/fetch-metadata`
Extract information about the dependencies being updated by a Dependabot-generated PR.
**Name:** `dependabot/pull-request-action`
## Usage instructions
Create a workflow file that contains a step that uses: dependabot/pull-request-action@v1`, e.g.
Create a workflow file that contains a step that uses: dependabot/fetch-metadata@v1`, e.g.
```yaml
-- .github/workflows/dependabot-prs.yml
@@ -20,38 +23,96 @@ jobs:
steps:
- name: Fetch Dependabot metadata
id: dependabot-metadata
uses: dependabot/pull-request-action
uses: dependabot/fetch-metadata
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
```
Subsequent actions will have access to `steps.dependabot-metadata.outputs.updated-dependencies` which will contain a
JSON object with information about the changes, e.g.
Subsequent actions will have access to the following outputs:
```json
[
{
"dependencyName": "dependabot-core",
"dependencyType": "direct:production",
"updateType": "version-update:semver-major"
}
]
```
- `steps.dependabot-metadata.outputs.dependency-names`
- A comma-separated list of the package names updated by the PR.
- `steps.dependabot-metadata.outputs.dependency-type`
- The type of dependency has determined this PR to be, e.g. `direct:production`. For all possible values, see [the `allow` documentation](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#allow).
- `steps.dependabot-metadata.outputs.update-type`
- The highest semver change being made by this PR, e.g. `version-update:semver-major`. For all possible values, see [the `ignore` documentation](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#ignore).
- `steps.dependabot-metadata.outputs.updated-dependencies-json`
- A JSON string containing the full information about each updated Dependency.
**Note:** This output will only be populated if the target Pull Request was opened by Dependabot and contains **only** Dependabot-created commits.
**Note:** These outputs will only be populated if the target Pull Request was opened by Dependabot and contains
**only** Dependabot-created commits.
This metadata can be used along with Action's [expression syntax](https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#functions) and the [GitHub CLI](https://github.com/cli/cli) to create
useful automation for your Dependabot PRs.
### Auto-approving
NYI
Since the `dependabot/fetch-metadata` Action will set a failure code if it cannot find any metadata, you can
have a permissive auto-approval on all Dependabot PRs like so:
```yaml
name: Dependabot auto-approve
description: Auto-approve Dependabot PRs
on: pull_request_target
permissions:
pull-requests: write
jobs:
dependabot:
# Checking the actor will prevent your Action run failing on non-Dependabot PRs
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata
- name: Approve a PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```
### Enabling GitHub automerge
NYI
```yaml
name: Dependabot auto-merge
description: Enable GitHub Automerge for patch updates on `bar`
on: pull_request_target
permissions:
pull-requests: write
jobs:
dependabot:
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata
- name: Enable auto-merge for Dependabot PRs # respects branch protection rules
if: ${{contains(steps.metadata.outputs.dependency-names, "bar") && steps.metadata.outputs.update-type == "version-update:semver-patch"}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```
## Why?
### Labelling
NYI
## Development and release process
NYI
```yaml
name: Dependabot auto-label
description: Label all production dependencies with the "production" label
on: pull_request_target
permissions:
pull-requests: write
jobs:
dependabot:
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata
- name: Add a label for all production dependencies
if: ${{ steps.metadata.outputs.dependency-type == "direct:production" }}
run: gh pr edit "$PR_URL" --add-label "production"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```

View File

@@ -1,12 +1,18 @@
name: 'Dependabot PR Automation'
description: 'Parse Dependabot commit metadata to automate PR handling'
name: 'Fetch Metadata from Dependabot PRs'
description: 'Extract information from about the dependency being updated by a Dependabot-generated PR'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
outputs:
updated-dependencies:
description: 'A JSON serialised hash of any metadata found in verified Dependabot commits in the PR.'
dependency-names:
description: 'A comma-separated list of all package names updated.'
dependency-type:
description: 'The type of dependency has determined this PR to be, e.g. "direct:production".'
update-type:
description: 'The highest semver change being made by this PR, e.g. "version-update:semver-major"'
updated-dependencies-json:
description: 'A JSON string containing the full information about each updated Dependency.'
runs:
using: 'node12'
main: 'dist/index.js'

562
dist/index.js generated vendored
View File

@@ -5642,6 +5642,508 @@ function onceStrict (fn) {
}
/***/ }),
/***/ 2522:
/***/ (function(module) {
/* global define */
(function (root, pluralize) {
/* istanbul ignore else */
if (true) {
// Node.
module.exports = pluralize();
} else {}
})(this, function () {
// Rule storage - pluralize and singularize need to be run sequentially,
// while other rules can be optimized using an object for instant lookups.
var pluralRules = [];
var singularRules = [];
var uncountables = {};
var irregularPlurals = {};
var irregularSingles = {};
/**
* Sanitize a pluralization rule to a usable regular expression.
*
* @param {(RegExp|string)} rule
* @return {RegExp}
*/
function sanitizeRule (rule) {
if (typeof rule === 'string') {
return new RegExp('^' + rule + '$', 'i');
}
return rule;
}
/**
* Pass in a word token to produce a function that can replicate the case on
* another word.
*
* @param {string} word
* @param {string} token
* @return {Function}
*/
function restoreCase (word, token) {
// Tokens are an exact match.
if (word === token) return token;
// Lower cased words. E.g. "hello".
if (word === word.toLowerCase()) return token.toLowerCase();
// Upper cased words. E.g. "WHISKY".
if (word === word.toUpperCase()) return token.toUpperCase();
// Title cased words. E.g. "Title".
if (word[0] === word[0].toUpperCase()) {
return token.charAt(0).toUpperCase() + token.substr(1).toLowerCase();
}
// Lower cased words. E.g. "test".
return token.toLowerCase();
}
/**
* Interpolate a regexp string.
*
* @param {string} str
* @param {Array} args
* @return {string}
*/
function interpolate (str, args) {
return str.replace(/\$(\d{1,2})/g, function (match, index) {
return args[index] || '';
});
}
/**
* Replace a word using a rule.
*
* @param {string} word
* @param {Array} rule
* @return {string}
*/
function replace (word, rule) {
return word.replace(rule[0], function (match, index) {
var result = interpolate(rule[1], arguments);
if (match === '') {
return restoreCase(word[index - 1], result);
}
return restoreCase(match, result);
});
}
/**
* Sanitize a word by passing in the word and sanitization rules.
*
* @param {string} token
* @param {string} word
* @param {Array} rules
* @return {string}
*/
function sanitizeWord (token, word, rules) {
// Empty string or doesn't need fixing.
if (!token.length || uncountables.hasOwnProperty(token)) {
return word;
}
var len = rules.length;
// Iterate over the sanitization rules and use the first one to match.
while (len--) {
var rule = rules[len];
if (rule[0].test(word)) return replace(word, rule);
}
return word;
}
/**
* Replace a word with the updated word.
*
* @param {Object} replaceMap
* @param {Object} keepMap
* @param {Array} rules
* @return {Function}
*/
function replaceWord (replaceMap, keepMap, rules) {
return function (word) {
// Get the correct token and case restoration functions.
var token = word.toLowerCase();
// Check against the keep object map.
if (keepMap.hasOwnProperty(token)) {
return restoreCase(word, token);
}
// Check against the replacement map for a direct word replacement.
if (replaceMap.hasOwnProperty(token)) {
return restoreCase(word, replaceMap[token]);
}
// Run all the rules against the word.
return sanitizeWord(token, word, rules);
};
}
/**
* Check if a word is part of the map.
*/
function checkWord (replaceMap, keepMap, rules, bool) {
return function (word) {
var token = word.toLowerCase();
if (keepMap.hasOwnProperty(token)) return true;
if (replaceMap.hasOwnProperty(token)) return false;
return sanitizeWord(token, token, rules) === token;
};
}
/**
* Pluralize or singularize a word based on the passed in count.
*
* @param {string} word The word to pluralize
* @param {number} count How many of the word exist
* @param {boolean} inclusive Whether to prefix with the number (e.g. 3 ducks)
* @return {string}
*/
function pluralize (word, count, inclusive) {
var pluralized = count === 1
? pluralize.singular(word) : pluralize.plural(word);
return (inclusive ? count + ' ' : '') + pluralized;
}
/**
* Pluralize a word.
*
* @type {Function}
*/
pluralize.plural = replaceWord(
irregularSingles, irregularPlurals, pluralRules
);
/**
* Check if a word is plural.
*
* @type {Function}
*/
pluralize.isPlural = checkWord(
irregularSingles, irregularPlurals, pluralRules
);
/**
* Singularize a word.
*
* @type {Function}
*/
pluralize.singular = replaceWord(
irregularPlurals, irregularSingles, singularRules
);
/**
* Check if a word is singular.
*
* @type {Function}
*/
pluralize.isSingular = checkWord(
irregularPlurals, irregularSingles, singularRules
);
/**
* Add a pluralization rule to the collection.
*
* @param {(string|RegExp)} rule
* @param {string} replacement
*/
pluralize.addPluralRule = function (rule, replacement) {
pluralRules.push([sanitizeRule(rule), replacement]);
};
/**
* Add a singularization rule to the collection.
*
* @param {(string|RegExp)} rule
* @param {string} replacement
*/
pluralize.addSingularRule = function (rule, replacement) {
singularRules.push([sanitizeRule(rule), replacement]);
};
/**
* Add an uncountable word rule.
*
* @param {(string|RegExp)} word
*/
pluralize.addUncountableRule = function (word) {
if (typeof word === 'string') {
uncountables[word.toLowerCase()] = true;
return;
}
// Set singular and plural references for the word.
pluralize.addPluralRule(word, '$0');
pluralize.addSingularRule(word, '$0');
};
/**
* Add an irregular word definition.
*
* @param {string} single
* @param {string} plural
*/
pluralize.addIrregularRule = function (single, plural) {
plural = plural.toLowerCase();
single = single.toLowerCase();
irregularSingles[single] = plural;
irregularPlurals[plural] = single;
};
/**
* Irregular rules.
*/
[
// Pronouns.
['I', 'we'],
['me', 'us'],
['he', 'they'],
['she', 'they'],
['them', 'them'],
['myself', 'ourselves'],
['yourself', 'yourselves'],
['itself', 'themselves'],
['herself', 'themselves'],
['himself', 'themselves'],
['themself', 'themselves'],
['is', 'are'],
['was', 'were'],
['has', 'have'],
['this', 'these'],
['that', 'those'],
// Words ending in with a consonant and `o`.
['echo', 'echoes'],
['dingo', 'dingoes'],
['volcano', 'volcanoes'],
['tornado', 'tornadoes'],
['torpedo', 'torpedoes'],
// Ends with `us`.
['genus', 'genera'],
['viscus', 'viscera'],
// Ends with `ma`.
['stigma', 'stigmata'],
['stoma', 'stomata'],
['dogma', 'dogmata'],
['lemma', 'lemmata'],
['schema', 'schemata'],
['anathema', 'anathemata'],
// Other irregular rules.
['ox', 'oxen'],
['axe', 'axes'],
['die', 'dice'],
['yes', 'yeses'],
['foot', 'feet'],
['eave', 'eaves'],
['goose', 'geese'],
['tooth', 'teeth'],
['quiz', 'quizzes'],
['human', 'humans'],
['proof', 'proofs'],
['carve', 'carves'],
['valve', 'valves'],
['looey', 'looies'],
['thief', 'thieves'],
['groove', 'grooves'],
['pickaxe', 'pickaxes'],
['passerby', 'passersby']
].forEach(function (rule) {
return pluralize.addIrregularRule(rule[0], rule[1]);
});
/**
* Pluralization rules.
*/
[
[/s?$/i, 's'],
[/[^\u0000-\u007F]$/i, '$0'],
[/([^aeiou]ese)$/i, '$1'],
[/(ax|test)is$/i, '$1es'],
[/(alias|[^aou]us|t[lm]as|gas|ris)$/i, '$1es'],
[/(e[mn]u)s?$/i, '$1s'],
[/([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i, '$1'],
[/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1i'],
[/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'],
[/(seraph|cherub)(?:im)?$/i, '$1im'],
[/(her|at|gr)o$/i, '$1oes'],
[/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i, '$1a'],
[/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, '$1a'],
[/sis$/i, 'ses'],
[/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'],
[/([^aeiouy]|qu)y$/i, '$1ies'],
[/([^ch][ieo][ln])ey$/i, '$1ies'],
[/(x|ch|ss|sh|zz)$/i, '$1es'],
[/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'],
[/\b((?:tit)?m|l)(?:ice|ouse)$/i, '$1ice'],
[/(pe)(?:rson|ople)$/i, '$1ople'],
[/(child)(?:ren)?$/i, '$1ren'],
[/eaux$/i, '$0'],
[/m[ae]n$/i, 'men'],
['thou', 'you']
].forEach(function (rule) {
return pluralize.addPluralRule(rule[0], rule[1]);
});
/**
* Singularization rules.
*/
[
[/s$/i, ''],
[/(ss)$/i, '$1'],
[/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i, '$1fe'],
[/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
[/ies$/i, 'y'],
[/\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i, '$1ie'],
[/\b(mon|smil)ies$/i, '$1ey'],
[/\b((?:tit)?m|l)ice$/i, '$1ouse'],
[/(seraph|cherub)im$/i, '$1'],
[/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i, '$1'],
[/(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i, '$1sis'],
[/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
[/(test)(?:is|es)$/i, '$1is'],
[/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1us'],
[/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i, '$1um'],
[/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i, '$1on'],
[/(alumn|alg|vertebr)ae$/i, '$1a'],
[/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
[/(matr|append)ices$/i, '$1ix'],
[/(pe)(rson|ople)$/i, '$1rson'],
[/(child)ren$/i, '$1'],
[/(eau)x?$/i, '$1'],
[/men$/i, 'man']
].forEach(function (rule) {
return pluralize.addSingularRule(rule[0], rule[1]);
});
/**
* Uncountable rules.
*/
[
// Singular words with no plurals.
'adulthood',
'advice',
'agenda',
'aid',
'aircraft',
'alcohol',
'ammo',
'analytics',
'anime',
'athletics',
'audio',
'bison',
'blood',
'bream',
'buffalo',
'butter',
'carp',
'cash',
'chassis',
'chess',
'clothing',
'cod',
'commerce',
'cooperation',
'corps',
'debris',
'diabetes',
'digestion',
'elk',
'energy',
'equipment',
'excretion',
'expertise',
'firmware',
'flounder',
'fun',
'gallows',
'garbage',
'graffiti',
'hardware',
'headquarters',
'health',
'herpes',
'highjinks',
'homework',
'housework',
'information',
'jeans',
'justice',
'kudos',
'labour',
'literature',
'machinery',
'mackerel',
'mail',
'media',
'mews',
'moose',
'music',
'mud',
'manga',
'news',
'only',
'personnel',
'pike',
'plankton',
'pliers',
'police',
'pollution',
'premises',
'rain',
'research',
'rice',
'salmon',
'scissors',
'series',
'sewage',
'shambles',
'shrimp',
'software',
'species',
'staff',
'swine',
'tennis',
'traffic',
'transportation',
'trout',
'tuna',
'wealth',
'welfare',
'whiting',
'wildebeest',
'wildlife',
'you',
/pok[eé]mon$/i,
// Regexes.
/[^aeiou]ese$/i, // "chinese", "japanese"
/deer$/i, // "deer", "reindeer"
/fish$/i, // "fish", "blowfish", "angelfish"
/measles$/i,
/o[iu]s$/i, // "carnivorous"
/pox$/i, // "chickpox", "smallpox"
/sheep$/i
].forEach(pluralize.addUncountableRule);
return pluralize;
});
/***/ }),
/***/ 4294:
@@ -12766,6 +13268,18 @@ module.exports = require("zlib");;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __nccwpck_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __nccwpck_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
@@ -12890,6 +13404,46 @@ function parse(commitMessage) {
return [];
}
// EXTERNAL MODULE: ./node_modules/pluralize/pluralize.js
var pluralize = __nccwpck_require__(2522);
var pluralize_default = /*#__PURE__*/__nccwpck_require__.n(pluralize);
;// CONCATENATED MODULE: ./src/dependabot/output.ts
const DEPENDENCY_TYPES_PRIORITY = [
'direct:production',
'direct:development',
'indirect'
];
const UPDATE_TYPES_PRIORITY = [
'version-update:semver-major',
'version-update:semver-minor',
'version-update:semver-patch'
];
function set(updatedDependencies) {
core.info(`Outputting metadata for ${pluralize_default()('updated dependency', updatedDependencies.length, true)}`);
core.setOutput('updated-dependencies-json', updatedDependencies);
core.setOutput('dependency-names', updatedDependencies.map(dependency => {
return dependency.dependencyName;
}).join(', '));
core.setOutput('dependency-type', maxDependencyTypes(updatedDependencies));
core.setOutput('update-type', maxSemver(updatedDependencies));
}
function maxDependencyTypes(updatedDependencies) {
const dependencyTypes = updatedDependencies.reduce(function (dependencyTypes, dependency) {
dependencyTypes.add(dependency.dependencyType);
return dependencyTypes;
}, new Set());
return DEPENDENCY_TYPES_PRIORITY.find(dependencyType => dependencyTypes.has(dependencyType)) || 'unknown';
}
function maxSemver(updatedDependencies) {
const semverLevels = updatedDependencies.reduce(function (semverLevels, dependency) {
semverLevels.add(dependency.updateType);
return semverLevels;
}, new Set());
return UPDATE_TYPES_PRIORITY.find(semverLevel => semverLevels.has(semverLevel)) || null;
}
;// CONCATENATED MODULE: ./src/main.ts
var main_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@@ -12904,6 +13458,7 @@ var main_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arg
function run() {
return main_awaiter(this, void 0, void 0, function* () {
const token = core.getInput('github-token');
@@ -12921,15 +13476,14 @@ function run() {
core.info('Parsing Dependabot metadata/');
const updatedDependencies = parse(commitMessage);
if (updatedDependencies.length > 0) {
core.info("Outputting metadata to 'updated-dependencies'.");
core.setOutput('updated-dependencies', updatedDependencies);
set(updatedDependencies);
}
else {
core.info('PR does not contain metadata, nothing to do.');
core.setFailed('PR does not contain metadata, nothing to do.');
}
}
else {
core.info('PR is not from Dependabot, nothing to do.');
core.setFailed('PR is not from Dependabot, nothing to do.');
}
});
}

8385
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,8 @@
"license": "MIT",
"dependencies": {
"@actions/core": "^1.3.0",
"@actions/github": "^5.0.0"
"@actions/github": "^5.0.0",
"pluralize": "^8.0.0"
},
"devDependencies": {
"@types/jest": "^26.0.23",

View File

@@ -0,0 +1,119 @@
import * as core from '@actions/core'
import * as Output from './output'
beforeEach(() => {
jest.restoreAllMocks()
jest.spyOn(core, 'setOutput').mockImplementation(jest.fn())
jest.spyOn(core, 'info').mockImplementation(jest.fn())
})
test('when given a single dependency it sets its values', async () => {
const updatedDependencies = [
{
dependencyName: 'coffee-rails',
dependencyType: 'direct:production',
updateType: 'version-update:semver-minor'
}
]
Output.set(updatedDependencies)
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining('Outputting metadata for 1 updated dependency')
)
expect(core.setOutput).toHaveBeenCalledWith('updated-dependencies-json', updatedDependencies)
expect(core.setOutput).toBeCalledWith('dependency-names', 'coffee-rails')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:production')
expect(core.setOutput).toBeCalledWith('update-type', 'version-update:semver-minor')
})
test('when given a multiple dependencies, it uses the highest values for types', async () => {
const updatedDependencies = [
{
dependencyName: 'rspec',
dependencyType: 'direct:development',
updateType: 'version-update:semver-minor'
},
{
dependencyName: 'coffee-rails',
dependencyType: 'indirect',
updateType: 'version-update:semver-minor'
},
{
dependencyName: 'coffeescript',
dependencyType: 'indirect',
updateType: 'version-update:semver-major'
},
{
dependencyName: 'rspec-coffeescript',
dependencyType: 'indirect',
updateType: 'version-update:semver-patch'
}
]
Output.set(updatedDependencies)
expect(core.setOutput).toHaveBeenCalledWith('updated-dependencies-json', updatedDependencies)
expect(core.setOutput).toBeCalledWith('dependency-names', 'rspec, coffee-rails, coffeescript, rspec-coffeescript')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:development')
expect(core.setOutput).toBeCalledWith('update-type', 'version-update:semver-major')
})
test('when the dependency has no update type', async () => {
const updatedDependencies = [
{
dependencyName: 'coffee-rails',
dependencyType: 'direct:production',
updateType: ''
}
]
Output.set(updatedDependencies)
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining('Outputting metadata for 1 updated dependency')
)
expect(core.setOutput).toHaveBeenCalledWith('updated-dependencies-json', updatedDependencies)
expect(core.setOutput).toBeCalledWith('dependency-names', 'coffee-rails')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:production')
expect(core.setOutput).toBeCalledWith('update-type', null)
})
test('when given a multiple dependencies, and some do not have update types', async () => {
const updatedDependencies = [
{
dependencyName: 'rspec',
dependencyType: 'direct:development',
updateType: ''
},
{
dependencyName: 'coffee-rails',
dependencyType: 'indirect',
updateType: 'version-update:semver-minor'
},
{
dependencyName: 'coffeescript',
dependencyType: 'indirect',
updateType: ''
},
{
dependencyName: 'rspec-coffeescript',
dependencyType: 'indirect',
updateType: 'version-update:semver-patch'
}
]
Output.set(updatedDependencies)
expect(core.setOutput).toHaveBeenCalledWith('updated-dependencies-json', updatedDependencies)
expect(core.setOutput).toBeCalledWith('dependency-names', 'rspec, coffee-rails, coffeescript, rspec-coffeescript')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:development')
expect(core.setOutput).toBeCalledWith('update-type', 'version-update:semver-minor')
})

44
src/dependabot/output.ts Normal file
View File

@@ -0,0 +1,44 @@
import Pluralize from 'pluralize'
import * as core from '@actions/core'
import { updatedDependency } from './update_metadata'
const DEPENDENCY_TYPES_PRIORITY = [
'direct:production',
'direct:development',
'indirect'
]
const UPDATE_TYPES_PRIORITY = [
'version-update:semver-major',
'version-update:semver-minor',
'version-update:semver-patch'
]
export function set (updatedDependencies: Array<updatedDependency>): void {
core.info(`Outputting metadata for ${Pluralize('updated dependency', updatedDependencies.length, true)}`)
core.setOutput('updated-dependencies-json', updatedDependencies)
core.setOutput('dependency-names', updatedDependencies.map(dependency => {
return dependency.dependencyName
}).join(', '))
core.setOutput('dependency-type', maxDependencyTypes(updatedDependencies))
core.setOutput('update-type', maxSemver(updatedDependencies))
}
function maxDependencyTypes (updatedDependencies: Array<updatedDependency>): string {
const dependencyTypes = updatedDependencies.reduce(function (dependencyTypes, dependency) {
dependencyTypes.add(dependency.dependencyType)
return dependencyTypes
}, new Set())
return DEPENDENCY_TYPES_PRIORITY.find(dependencyType => dependencyTypes.has(dependencyType)) || 'unknown'
}
function maxSemver (updatedDependencies: Array<updatedDependency>): string | null {
const semverLevels = updatedDependencies.reduce(function (semverLevels, dependency) {
semverLevels.add(dependency.updateType)
return semverLevels
}, new Set())
return UPDATE_TYPES_PRIORITY.find(semverLevel => semverLevels.has(semverLevel)) || null
}

View File

@@ -53,7 +53,7 @@ test('it supports multiple dependencies within a single fragment', async () => {
' dependency-type: direct:production\n' +
' update-type: version-update:semver-minor\n' +
'- dependency-name: coffeescript\n' +
' dependency-type: indirect:production\n' +
' dependency-type: indirect\n' +
' update-type: version-update:semver-patch\n' +
'...\n' +
'\n' +
@@ -68,7 +68,7 @@ test('it supports multiple dependencies within a single fragment', async () => {
expect(updatedDependencies[0].updateType).toEqual('version-update:semver-minor')
expect(updatedDependencies[1].dependencyName).toEqual('coffeescript')
expect(updatedDependencies[1].dependencyType).toEqual('indirect:production')
expect(updatedDependencies[1].dependencyType).toEqual('indirect')
expect(updatedDependencies[1].updateType).toEqual('version-update:semver-patch')
})
@@ -89,7 +89,7 @@ test('it only returns information within the first fragment if there are multipl
'---\n' +
'updated-dependencies:\n' +
'- dependency-name: coffeescript\n' +
' dependency-type: indirect:production\n' +
' dependency-type: indirect\n' +
' update-type: version-update:semver-patch\n' +
'...\n' +
'\n' +

View File

@@ -1,6 +1,6 @@
import * as YAML from 'yaml'
interface updatedDependency {
export interface updatedDependency {
dependencyName: string,
dependencyType: string,
updateType: string,

View File

@@ -30,7 +30,7 @@ test('it does nothing if the PR is not verified as from Dependabot', async () =>
await run()
expect(core.info).toHaveBeenCalledWith(
expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('PR is not from Dependabot, nothing to do.')
)
})
@@ -43,12 +43,12 @@ test('it does nothing if there is no metadata in the commit', async () => {
await run()
expect(core.info).toHaveBeenCalledWith(
expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('PR does not contain metadata, nothing to do.')
)
})
test('it sets the updated dependencies as an output for subsequent actions', async () => {
test('it sets the updated dependency as an output for subsequent actions', async () => {
const mockCommitMessage =
'Bumps [coffee-rails](https://github.com/rails/coffee-rails) from 4.0.1 to 4.2.2.\n' +
'- [Release notes](https://github.com/rails/coffee-rails/releases)\n' +
@@ -73,10 +73,11 @@ test('it sets the updated dependencies as an output for subsequent actions', asy
await run()
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining('Outputting metadata')
expect.stringContaining('Outputting metadata for 1 updated dependency')
)
expect(core.setOutput).toHaveBeenCalledWith(
'updated-dependencies',
'updated-dependencies-json',
[
{
dependencyName: 'coffee-rails',
@@ -85,4 +86,60 @@ test('it sets the updated dependencies as an output for subsequent actions', asy
}
]
)
expect(core.setOutput).toBeCalledWith('dependency-names', 'coffee-rails')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:production')
expect(core.setOutput).toBeCalledWith('update-type', 'version-update:semver-minor')
})
test('if there are multiple dependencies, it summarizes them', async () => {
const mockCommitMessage =
'Bumps [coffee-rails](https://github.com/rails/coffee-rails) from 4.0.1 to 4.2.2.\n' +
'- [Release notes](https://github.com/rails/coffee-rails/releases)\n' +
'- [Changelog](https://github.com/rails/coffee-rails/blob/master/CHANGELOG.md)\n' +
'- [Commits](rails/coffee-rails@v4.0.1...v4.2.2)\n' +
'\n' +
'---\n' +
'updated-dependencies:\n' +
'- dependency-name: coffee-rails\n' +
' dependency-type: direct:production\n' +
' update-type: version-update:semver-minor\n' +
'- dependency-name: coffeescript\n' +
' dependency-type: indirect\n' +
' update-type: version-update:semver-major\n' +
'...\n' +
'\n' +
'Signed-off-by: dependabot[bot] <support@github.com>'
jest.spyOn(core, 'getInput').mockReturnValue('mock-token')
jest.spyOn(dependabotCommits, 'getMessage').mockImplementation(jest.fn(
() => Promise.resolve(mockCommitMessage)
))
jest.spyOn(core, 'setOutput').mockImplementation(jest.fn())
await run()
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining('Outputting metadata for 2 updated dependencies')
)
expect(core.setOutput).toHaveBeenCalledWith(
'updated-dependencies-json',
[
{
dependencyName: 'coffee-rails',
dependencyType: 'direct:production',
updateType: 'version-update:semver-minor'
},
{
dependencyName: 'coffeescript',
dependencyType: 'indirect',
updateType: 'version-update:semver-major'
}
]
)
expect(core.setOutput).toBeCalledWith('dependency-names', 'coffee-rails, coffeescript')
expect(core.setOutput).toBeCalledWith('dependency-type', 'direct:production')
expect(core.setOutput).toBeCalledWith('update-type', 'version-update:semver-major')
})

View File

@@ -2,6 +2,7 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import * as verifiedCommits from './dependabot/verified_commits'
import * as updateMetadata from './dependabot/update_metadata'
import * as output from './dependabot/output'
export async function run (): Promise<void> {
const token = core.getInput('github-token')
@@ -27,13 +28,12 @@ export async function run (): Promise<void> {
const updatedDependencies = updateMetadata.parse(commitMessage)
if (updatedDependencies.length > 0) {
core.info("Outputting metadata to 'updated-dependencies'.")
core.setOutput('updated-dependencies', updatedDependencies)
output.set(updatedDependencies)
} else {
core.info('PR does not contain metadata, nothing to do.')
core.setFailed('PR does not contain metadata, nothing to do.')
}
} else {
core.info('PR is not from Dependabot, nothing to do.')
core.setFailed('PR is not from Dependabot, nothing to do.')
}
}