From 3eed65b68eba1c52c3211fbf2e38fb6af33ff8e5 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 6 Jul 2021 13:29:57 +0100 Subject: [PATCH 01/14] Initial pass at grouping by feature --- bin/plugin/commands/changelog.js | 57 +++++++++++++++++++++++++++ bin/plugin/commands/test/changelog.js | 41 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 9384bc6540f43..418adf6a22c72 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -92,6 +92,21 @@ const LABEL_TYPE_MAPPING = { '[Type] Security': 'Security', }; +const LABEL_FEATURE_MAPPING = { + '[Package] Block editor': 'Block Editor', + '[Package] Editor': 'Editor', + '[Package] Edit Widgets': 'Widgets', + '[Package] Widgets Customizer': 'Widgets', + '[Feature] Widgets Screen': 'Widgets', + '[Block] Legacy Widget': 'Widgets', + '[Package] Components': 'Components', + '[Feature] UI Components': 'Components', + '[Feature] Component System': 'Components', + '[Package] Block Library': 'Block Library', + '[Feature] Blocks': 'Block Library', + 'New Block': 'Block Library', +}; + /** * Order in which to print group titles. A value of `undefined` is used as slot * in which unrecognized headings are to be inserted. @@ -152,6 +167,24 @@ function getTypesByLabels( labels ) { ); } +function getIssuesByFeature( labels ) { + return uniq( + labels + .filter( ( label ) => + Object.keys( LABEL_FEATURE_MAPPING ).includes( label ) + ) + .map( ( label ) => LABEL_FEATURE_MAPPING[ label ] ) + ); +} + +function getBlockSpecificIssues( labels ) { + return uniq( + labels + .filter( ( label ) => label.startsWith( '[Block] ' ) ) + .map( () => 'Block Library' ) + ); +} + /** * Returns type candidates based on given issue title. * @@ -180,6 +213,7 @@ function getTypesByTitle( title ) { */ function getIssueType( issue ) { const labels = issue.labels.map( ( { name } ) => name ); + const candidates = [ ...getTypesByLabels( labels ), ...getTypesByTitle( issue.title ), @@ -188,6 +222,17 @@ function getIssueType( issue ) { return candidates.length ? candidates.sort( sortType )[ 0 ] : 'Various'; } +function getIssueFeature( issue ) { + const labels = issue.labels.map( ( { name } ) => name ); + + const candidates = [ + ...getIssuesByFeature( labels ), + ...getBlockSpecificIssues( labels ), + ]; + + return candidates.length ? candidates.sort( sortType )[ 0 ] : 'Unknown'; +} + /** * Sort comparator, comparing two label candidates. * @@ -485,6 +530,7 @@ async function getChangelog( settings ) { let changelog = ''; const groupedPullRequests = groupBy( pullRequests, getIssueType ); + const sortedGroups = Object.keys( groupedPullRequests ).sort( sortGroup ); for ( const group of sortedGroups ) { const groupPullRequests = groupedPullRequests[ group ]; @@ -495,6 +541,16 @@ async function getChangelog( settings ) { if ( ! groupEntries.length ) { continue; } + // console.log( groupPullRequests ); + console.log( + groupBy( + groupPullRequests.map( ( item ) => ( { + title: item.title, + labels: item.labels, + } ) ), + getIssueFeature + ) + ); changelog += '### ' + group + '\n\n'; groupEntries.forEach( ( entry ) => ( changelog += entry + '\n' ) ); @@ -562,6 +618,7 @@ async function getReleaseChangelog( options ) { getNormalizedTitle, getReleaseChangelog, getIssueType, + getIssueFeature, sortGroup, getTypesByLabels, getTypesByTitle, diff --git a/bin/plugin/commands/test/changelog.js b/bin/plugin/commands/test/changelog.js index bdde2e92926c9..1e0aceaaae1dc 100644 --- a/bin/plugin/commands/test/changelog.js +++ b/bin/plugin/commands/test/changelog.js @@ -12,6 +12,7 @@ import { sortGroup, getTypesByLabels, getTypesByTitle, + getIssueFeature, } from '../changelog'; describe( 'getNormalizedTitle', () => { @@ -172,6 +173,46 @@ describe( 'getIssueType', () => { } ); } ); +describe( 'getIssueFeature', () => { + it( 'returns "Unknown" if unable to find appropriate feature by label', () => { + const result = getIssueFeature( { labels: [] } ); + + expect( result ).toBe( 'Unknown' ); + } ); + + it( 'returns "Block Library" for all PRs that have a block specific label', () => { + const result = getIssueFeature( { + labels: [ + { + name: '[Block] Some Block', + }, + ], + } ); + + expect( result ).toEqual( 'Block Library' ); + } ); + + it.each( [ + { + labels: [ + { + name: '[Package] Edit Widgets', + }, + ], + feature: 'Widgets', + }, + ] )( + 'returns appropriate feature for all PRs that have a specific label', + ( testData ) => { + const result = getIssueFeature( { + labels: testData.labels, + } ); + + expect( result ).toEqual( testData.feature ); + } + ); +} ); + describe( 'sortGroup', () => { it( 'returns groups in order', () => { const result = [ From 75539a9627361a86ed65ea72ce864f212464e3ce Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 6 Jul 2021 16:59:02 +0100 Subject: [PATCH 02/14] Basic sort by feature implementation --- bin/plugin/commands/changelog.js | 70 +++++++++++++++++---------- bin/plugin/commands/test/changelog.js | 63 ++++++++++++++++-------- 2 files changed, 87 insertions(+), 46 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 418adf6a22c72..d713a0d555e29 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -1,7 +1,7 @@ /** * External dependencies */ -const { groupBy, escapeRegExp, uniq } = require( 'lodash' ); +const { countBy, groupBy, escapeRegExp, uniq } = require( 'lodash' ); const Octokit = require( '@octokit/rest' ); const { sprintf } = require( 'sprintf-js' ); const semver = require( 'semver' ); @@ -93,7 +93,7 @@ const LABEL_TYPE_MAPPING = { }; const LABEL_FEATURE_MAPPING = { - '[Package] Block editor': 'Block Editor', + '[Package] Block Editor': 'Block Editor', '[Package] Editor': 'Editor', '[Package] Edit Widgets': 'Widgets', '[Package] Widgets Customizer': 'Widgets', @@ -167,17 +167,15 @@ function getTypesByLabels( labels ) { ); } -function getIssuesByFeature( labels ) { - return uniq( - labels - .filter( ( label ) => - Object.keys( LABEL_FEATURE_MAPPING ).includes( label ) - ) - .map( ( label ) => LABEL_FEATURE_MAPPING[ label ] ) - ); +function getIssueFeatures( labels ) { + return labels + .filter( ( label ) => + Object.keys( LABEL_FEATURE_MAPPING ).includes( label ) + ) + .map( ( label ) => LABEL_FEATURE_MAPPING[ label ] ); } -function getBlockSpecificIssues( labels ) { +function getIsBlockSpecificIssue( labels ) { return uniq( labels .filter( ( label ) => label.startsWith( '[Block] ' ) ) @@ -225,12 +223,31 @@ function getIssueType( issue ) { function getIssueFeature( issue ) { const labels = issue.labels.map( ( { name } ) => name ); + const blockSpecificLabels = getIsBlockSpecificIssue( labels ); + + if ( blockSpecificLabels.length ) { + return blockSpecificLabels[ 0 ]; + } + const candidates = [ - ...getIssuesByFeature( labels ), - ...getBlockSpecificIssues( labels ), + ...blockSpecificLabels, + ...getIssueFeatures( labels ), ]; - return candidates.length ? candidates.sort( sortType )[ 0 ] : 'Unknown'; + if ( ! candidates.length ) { + return 'Unknown'; + } + + // Get occurances of the feature labels. + const featureCounts = countBy( candidates ); + + // Check which matching label occurs most often. + const rankedFeatures = Object.keys( featureCounts ).sort( + ( a, b ) => featureCounts[ b ] - featureCounts[ a ] + ); + + // Return the one that appeared most often. + return rankedFeatures[ 0 ]; } /** @@ -541,19 +558,22 @@ async function getChangelog( settings ) { if ( ! groupEntries.length ) { continue; } - // console.log( groupPullRequests ); - console.log( - groupBy( - groupPullRequests.map( ( item ) => ( { - title: item.title, - labels: item.labels, - } ) ), - getIssueFeature - ) - ); + + const featureGroups = groupBy( groupPullRequests, getIssueFeature ); changelog += '### ' + group + '\n\n'; - groupEntries.forEach( ( entry ) => ( changelog += entry + '\n' ) ); + + Object.keys( featureGroups ).forEach( ( feature ) => { + const featureGroup = featureGroups[ feature ]; + changelog += '- ' + feature + '\n'; + const featureGroupEntries = featureGroup + .map( getEntry ) + .filter( Boolean ); + featureGroupEntries.forEach( + ( entry ) => ( changelog += ` ${ entry }\n` ) + ); + changelog += '\n'; + } ); changelog += '\n'; } diff --git a/bin/plugin/commands/test/changelog.js b/bin/plugin/commands/test/changelog.js index 1e0aceaaae1dc..b2f145c492b53 100644 --- a/bin/plugin/commands/test/changelog.js +++ b/bin/plugin/commands/test/changelog.js @@ -174,43 +174,64 @@ describe( 'getIssueType', () => { } ); describe( 'getIssueFeature', () => { - it( 'returns "Unknown" if unable to find appropriate feature by label', () => { + it( 'returns "Unknown" as feature if unable to find appropriate feature by label', () => { const result = getIssueFeature( { labels: [] } ); expect( result ).toBe( 'Unknown' ); } ); - it( 'returns "Block Library" for all PRs that have a block specific label', () => { + it( 'returns "Block Library" as feature for all PRs that have a block specific label', () => { + const widgetFavouringLabels = [ + { + name: '[Block] Some Block', + }, + { + name: '[Package] Edit Widgets', + }, + { + name: '[Feature] Widgets Screen', + }, + { + name: '[Package] Widgets Customizer', + }, + ]; + const result = getIssueFeature( { - labels: [ - { - name: '[Block] Some Block', - }, - ], + labels: widgetFavouringLabels, } ); expect( result ).toEqual( 'Block Library' ); } ); - it.each( [ - { + it( 'returns a single best match feature for a given issue', () => { + const result = getIssueFeature( { labels: [ + { + name: '[Package] Components', + }, { name: '[Package] Edit Widgets', }, + { + name: '[Package] Block Editor', + }, + { + name: '[Package] Editor', + }, + { + name: '[Feature] UI Components', + }, + { + name: '[Feature] Component System', + }, + { + name: '[Package] Block Library', // 2. mapped to "Block Library" + }, ], - feature: 'Widgets', - }, - ] )( - 'returns appropriate feature for all PRs that have a specific label', - ( testData ) => { - const result = getIssueFeature( { - labels: testData.labels, - } ); - - expect( result ).toEqual( testData.feature ); - } - ); + } ); + + expect( result ).toEqual( 'Components' ); + } ); } ); describe( 'sortGroup', () => { From 4ae6a6edb566f9b10310bd8e6b2985b336e23421 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 6 Jul 2021 17:15:35 +0100 Subject: [PATCH 03/14] Prefer Group by feature label if present --- bin/plugin/commands/changelog.js | 10 ++++++++++ bin/plugin/commands/test/changelog.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index d713a0d555e29..c612158f34587 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -183,6 +183,10 @@ function getIsBlockSpecificIssue( labels ) { ); } +function getFeatureSpecificLabel( labels ) { + return labels.find( ( label ) => label.startsWith( '[Feature] ' ) ); +} + /** * Returns type candidates based on given issue title. * @@ -223,6 +227,12 @@ function getIssueType( issue ) { function getIssueFeature( issue ) { const labels = issue.labels.map( ( { name } ) => name ); + const featureSpecificLabel = getFeatureSpecificLabel( labels ); + + if ( featureSpecificLabel ) { + return featureSpecificLabel.replace( '[Feature] ', '' ); + } + const blockSpecificLabels = getIsBlockSpecificIssue( labels ); if ( blockSpecificLabels.length ) { diff --git a/bin/plugin/commands/test/changelog.js b/bin/plugin/commands/test/changelog.js index b2f145c492b53..41c52ab7fe68b 100644 --- a/bin/plugin/commands/test/changelog.js +++ b/bin/plugin/commands/test/changelog.js @@ -180,6 +180,27 @@ describe( 'getIssueFeature', () => { expect( result ).toBe( 'Unknown' ); } ); + it( 'returns the feature label for any PRs marked with a specific feature label', () => { + const result = getIssueFeature( { + labels: [ + { + name: '[Block] Some Block', + }, + { + name: '[Package] Edit Widgets', + }, + { + name: '[Feature] Widgets Screen', + }, + { + name: '[Package] Widgets Customizer', + }, + ], + } ); + + expect( result ).toEqual( 'Widgets Screen' ); + } ); + it( 'returns "Block Library" as feature for all PRs that have a block specific label', () => { const widgetFavouringLabels = [ { From b3addc0042e60633b84f5fda21e5c298bb5aa87e Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 6 Jul 2021 17:27:39 +0100 Subject: [PATCH 04/14] Sort Unknown to be the last feature group --- bin/plugin/commands/changelog.js | 37 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index c612158f34587..68a9eb991e5bf 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -19,6 +19,8 @@ const config = require( '../config' ); // @ts-ignore const manifest = require( '../../../package.json' ); +const UNKNOWN = 'Unknown'; + /** @typedef {import('@octokit/rest')} GitHub */ /** @typedef {import('@octokit/rest').IssuesListForRepoResponseItem} IssuesListForRepoResponseItem */ /** @typedef {import('@octokit/rest').IssuesListMilestonesForRepoResponseItem} OktokitIssuesListMilestonesForRepoResponseItem */ @@ -245,7 +247,7 @@ function getIssueFeature( issue ) { ]; if ( ! candidates.length ) { - return 'Unknown'; + return UNKNOWN; } // Get occurances of the feature labels. @@ -573,17 +575,28 @@ async function getChangelog( settings ) { changelog += '### ' + group + '\n\n'; - Object.keys( featureGroups ).forEach( ( feature ) => { - const featureGroup = featureGroups[ feature ]; - changelog += '- ' + feature + '\n'; - const featureGroupEntries = featureGroup - .map( getEntry ) - .filter( Boolean ); - featureGroupEntries.forEach( - ( entry ) => ( changelog += ` ${ entry }\n` ) - ); - changelog += '\n'; - } ); + Object.keys( featureGroups ) + .sort() // natural sort first + .sort( ( a, b ) => { + // sort "Unknown" to always be at the end + if ( a === 'Unknown' ) { + return 1; + } else if ( b === 'Unknown' ) { + return -1; + } + return 0; + } ) + .forEach( ( feature ) => { + const featureGroup = featureGroups[ feature ]; + changelog += '- ' + feature + '\n'; + const featureGroupEntries = featureGroup + .map( getEntry ) + .filter( Boolean ); + featureGroupEntries.forEach( + ( entry ) => ( changelog += ` ${ entry }\n` ) + ); + changelog += '\n'; + } ); changelog += '\n'; } From 902a1302a906aea5f6583c6a0b614dd9e1df15b3 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 6 Jul 2021 17:31:47 +0100 Subject: [PATCH 05/14] Strip feature name from entry --- bin/plugin/commands/changelog.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 68a9eb991e5bf..bcd785c86e70f 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -588,13 +588,19 @@ async function getChangelog( settings ) { } ) .forEach( ( feature ) => { const featureGroup = featureGroups[ feature ]; + // Start new markdown list changelog += '- ' + feature + '\n'; const featureGroupEntries = featureGroup .map( getEntry ) .filter( Boolean ); - featureGroupEntries.forEach( - ( entry ) => ( changelog += ` ${ entry }\n` ) - ); + featureGroupEntries.forEach( ( entry ) => { + // Strip feature name from entry if present. + entry = entry && entry.replace( `[${ feature } - `, '[' ); + + // Add a new bullet point to the list. + changelog += ` ${ entry }\n`; + } ); + // End the markdown list. changelog += '\n'; } ); changelog += '\n'; From 1cbf284352a5c1bfc07d47d0b05ba0f87c519cc7 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 19 Jul 2021 16:03:45 +0100 Subject: [PATCH 06/14] Improve function naming and simplify --- bin/plugin/commands/changelog.js | 71 ++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index bcd785c86e70f..44f001c52f276 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -169,7 +169,15 @@ function getTypesByLabels( labels ) { ); } -function getIssueFeatures( labels ) { +/** + * Returns candidates by retrieving the appropriate mapping + * from the label -> feature lookup. + * + * @param {string[]} labels Label names. + * + * @return {string[]} Feature candidates. + */ +function mapLabelsToFeatures( labels ) { return labels .filter( ( label ) => Object.keys( LABEL_FEATURE_MAPPING ).includes( label ) @@ -177,15 +185,26 @@ function getIssueFeatures( labels ) { .map( ( label ) => LABEL_FEATURE_MAPPING[ label ] ); } +/** + * Returns whether not the given labels contain the block specific + * label "block library". + * + * @param {string[]} labels Label names. + * + * @return {boolean} whether or not the issue's is labbeled as block specific + */ function getIsBlockSpecificIssue( labels ) { - return uniq( - labels - .filter( ( label ) => label.startsWith( '[Block] ' ) ) - .map( () => 'Block Library' ) - ); + return !! labels.find( ( label ) => label.startsWith( '[Block] ' ) ); } -function getFeatureSpecificLabel( labels ) { +/** + * Returns the first feature specific label from the given labels. + * + * @param {string[]} labels Label names. + * + * @return {string|undefined} the feature specific label. + */ +function getFeatureSpecificLabels( labels ) { return labels.find( ( label ) => label.startsWith( '[Feature] ' ) ); } @@ -226,32 +245,50 @@ function getIssueType( issue ) { return candidates.length ? candidates.sort( sortType )[ 0 ] : 'Various'; } +/** + * Removes any `[Feature]` prefix from a given label. + * + * @param {string} label The label potentially containing a prefix. + * + * @return {string} the label without the prefix. + */ +function stripFeaturePrefix( label ) { + return label.replace( '[Feature] ', '' ); +} + +/** + * Returns the most appropriate feature category for the given issue based + * on a basic heuristic. + * + * @param {IssuesListForRepoResponseItem} issue Issue object. + * + * @return {string} the feature name. + */ function getIssueFeature( issue ) { const labels = issue.labels.map( ( { name } ) => name ); - const featureSpecificLabel = getFeatureSpecificLabel( labels ); + const featureSpecificLabel = getFeatureSpecificLabels( labels ); + // If it's marked as as `[Feature]` label then use that feature name. if ( featureSpecificLabel ) { - return featureSpecificLabel.replace( '[Feature] ', '' ); + return stripFeaturePrefix( featureSpecificLabel ); } const blockSpecificLabels = getIsBlockSpecificIssue( labels ); - if ( blockSpecificLabels.length ) { - return blockSpecificLabels[ 0 ]; + // If it's block specific issue then use the block specific label. + if ( blockSpecificLabels ) { + return 'Block Library'; } - const candidates = [ - ...blockSpecificLabels, - ...getIssueFeatures( labels ), - ]; + const featureCandidates = mapLabelsToFeatures( labels ); - if ( ! candidates.length ) { + if ( ! featureCandidates.length ) { return UNKNOWN; } // Get occurances of the feature labels. - const featureCounts = countBy( candidates ); + const featureCounts = countBy( featureCandidates ); // Check which matching label occurs most often. const rankedFeatures = Object.keys( featureCounts ).sort( From f5e8939355d3b792d3e91126f4ef32ebc7799258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor?= <27339341+priethor@users.noreply.github.com> Date: Thu, 8 Jul 2021 17:59:53 +0200 Subject: [PATCH 07/14] Update "Widgets" changelog mapping name to "Widgets Editor" Co-authored-by: Dave Smith --- bin/plugin/commands/changelog.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 44f001c52f276..ff407fcdcfc52 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -97,10 +97,10 @@ const LABEL_TYPE_MAPPING = { const LABEL_FEATURE_MAPPING = { '[Package] Block Editor': 'Block Editor', '[Package] Editor': 'Editor', - '[Package] Edit Widgets': 'Widgets', - '[Package] Widgets Customizer': 'Widgets', - '[Feature] Widgets Screen': 'Widgets', - '[Block] Legacy Widget': 'Widgets', + '[Package] Edit Widgets': 'Widgets Editor', + '[Package] Widgets Customizer': 'Widgets Editor', + '[Feature] Widgets Screen': 'Widgets Editor', + '[Block] Legacy Widget': 'Widgets Editor', '[Package] Components': 'Components', '[Feature] UI Components': 'Components', '[Feature] Component System': 'Components', From 2929c67eca993c92714e9f6e1e7f77e0b892a86a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 19 Jul 2021 16:18:54 +0100 Subject: [PATCH 08/14] Add feature mapping as provided by Hector See https://github.com/WordPress/gutenberg/pull/33229#pullrequestreview-701879448 --- bin/plugin/commands/changelog.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index ff407fcdcfc52..27147b9661e1b 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -94,18 +94,37 @@ const LABEL_TYPE_MAPPING = { '[Type] Security': 'Security', }; +/** + * Mapping of label names to arbitary features in the release notes. + * + * Mapping a given label to a feature will guarantee it will be categorised + * under that feature name in the changelog within each section. + * + * @type {Record} + */ const LABEL_FEATURE_MAPPING = { + '[Feature] Widgets Screen': 'Widgets Editor', + '[Feature] Design Tools': 'Design Tools', + '[Feature] UI Components': 'Components', + '[Feature] Component System': 'Components', + '[Feature] Template Editing Mode': 'Template Editor', + '[Feature] Blocks': 'Block Library', + '[Feature] Inserter': 'Block Editor', + '[Feature] Drag and Drop': 'Block Editor', + '[Feature] Block Multi Selection': 'Block Editor', + '[Feature] Link Editing': 'Block Editor', + '[Package] Edit Post': 'Post Editor', + '[Package] Icons': 'Icons', '[Package] Block Editor': 'Block Editor', '[Package] Editor': 'Editor', '[Package] Edit Widgets': 'Widgets Editor', '[Package] Widgets Customizer': 'Widgets Editor', - '[Feature] Widgets Screen': 'Widgets Editor', - '[Block] Legacy Widget': 'Widgets Editor', '[Package] Components': 'Components', - '[Feature] UI Components': 'Components', - '[Feature] Component System': 'Components', '[Package] Block Library': 'Block Library', - '[Feature] Blocks': 'Block Library', + '[Package] Rich text': 'Block Editor', + '[Package] Data': 'Data Layer', + '[Block] Legacy Widget': 'Widgets Editor', + 'REST API Interaction': 'REST API', 'New Block': 'Block Library', }; From 0f27dc39c57b5469a4f19cf591c430c52eb45e80 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 19 Jul 2021 16:56:23 +0100 Subject: [PATCH 09/14] Give priority to explicit label to feature mapping and update tests --- bin/plugin/commands/changelog.js | 38 ++++++------- bin/plugin/commands/test/changelog.js | 78 ++++++++++++++------------- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 27147b9661e1b..355cc62345b92 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -286,36 +286,38 @@ function stripFeaturePrefix( label ) { function getIssueFeature( issue ) { const labels = issue.labels.map( ( { name } ) => name ); + const featureCandidates = mapLabelsToFeatures( labels ); + + // 1. Prefer explicit mapping of label to feature. + if ( featureCandidates.length ) { + // Get occurances of the feature labels. + const featureCounts = countBy( featureCandidates ); + + // Check which matching label occurs most often. + const rankedFeatures = Object.keys( featureCounts ).sort( + ( a, b ) => featureCounts[ b ] - featureCounts[ a ] + ); + + // Return the one that appeared most often. + return rankedFeatures[ 0 ]; + } + + // 2. `[Feature]` labels const featureSpecificLabel = getFeatureSpecificLabels( labels ); - // If it's marked as as `[Feature]` label then use that feature name. if ( featureSpecificLabel ) { return stripFeaturePrefix( featureSpecificLabel ); } + // 3. Block specific labels. const blockSpecificLabels = getIsBlockSpecificIssue( labels ); - // If it's block specific issue then use the block specific label. if ( blockSpecificLabels ) { return 'Block Library'; } - const featureCandidates = mapLabelsToFeatures( labels ); - - if ( ! featureCandidates.length ) { - return UNKNOWN; - } - - // Get occurances of the feature labels. - const featureCounts = countBy( featureCandidates ); - - // Check which matching label occurs most often. - const rankedFeatures = Object.keys( featureCounts ).sort( - ( a, b ) => featureCounts[ b ] - featureCounts[ a ] - ); - - // Return the one that appeared most often. - return rankedFeatures[ 0 ]; + // Fallback - if we couldn't find a good match. + return UNKNOWN; } /** diff --git a/bin/plugin/commands/test/changelog.js b/bin/plugin/commands/test/changelog.js index 41c52ab7fe68b..d348d90436985 100644 --- a/bin/plugin/commands/test/changelog.js +++ b/bin/plugin/commands/test/changelog.js @@ -174,84 +174,90 @@ describe( 'getIssueType', () => { } ); describe( 'getIssueFeature', () => { - it( 'returns "Unknown" as feature if unable to find appropriate feature by label', () => { + it( 'returns "Unknown" as feature if there are no labels', () => { const result = getIssueFeature( { labels: [] } ); expect( result ).toBe( 'Unknown' ); } ); - it( 'returns the feature label for any PRs marked with a specific feature label', () => { + it( 'falls by to "Unknown" as the feature if unable to classify by other means', () => { const result = getIssueFeature( { labels: [ { - name: '[Block] Some Block', - }, - { - name: '[Package] Edit Widgets', + name: 'Some Label', }, { - name: '[Feature] Widgets Screen', + name: '[Package] Example Package', // 1. has explicit mapping }, { - name: '[Package] Widgets Customizer', + name: '[Package] Another One', }, ], } ); - expect( result ).toEqual( 'Widgets Screen' ); + expect( result ).toEqual( 'Unknown' ); } ); - it( 'returns "Block Library" as feature for all PRs that have a block specific label', () => { - const widgetFavouringLabels = [ - { - name: '[Block] Some Block', - }, - { - name: '[Package] Edit Widgets', - }, - { - name: '[Feature] Widgets Screen', - }, - { - name: '[Package] Widgets Customizer', - }, - ]; - + it( 'gives precedence to manual feature mapping', () => { const result = getIssueFeature( { - labels: widgetFavouringLabels, + labels: [ + { + name: '[Block] Some Block', // 3. Block-specific label + }, + { + name: '[Package] Edit Widgets', // 1. has explicit mapping + }, + { + name: '[Feature] Some Feature', // 2. Feature label. + }, + { + name: '[Package] Another One', + }, + ], } ); - expect( result ).toEqual( 'Block Library' ); + const mappingForPackageEditWidgets = 'Widgets Editor'; + + expect( result ).toEqual( mappingForPackageEditWidgets ); } ); - it( 'returns a single best match feature for a given issue', () => { + it( 'gives secondary priority to feature labels when manually mapped label is not present', () => { const result = getIssueFeature( { labels: [ { - name: '[Package] Components', + name: '[Block] Some Block', // block specific label }, { - name: '[Package] Edit Widgets', + name: '[Package] This package', }, { - name: '[Package] Block Editor', + name: '[Feature] Cool Feature', // should have priority despite prescence of block specific label }, { - name: '[Package] Editor', + name: '[Package] Another One', }, + ], + } ); + + expect( result ).toEqual( 'Cool Feature' ); + } ); + + it( 'gives tertiary priority to "Block Library" as feature for all PRs that have a block specific label (and where manually mapped or feature label not present)', () => { + const result = getIssueFeature( { + labels: [ { - name: '[Feature] UI Components', + name: '[Block] Some Block', }, { - name: '[Feature] Component System', + name: '[Package] This package', }, { - name: '[Package] Block Library', // 2. mapped to "Block Library" + name: '[Package] Another One', }, ], } ); - expect( result ).toEqual( 'Components' ); + expect( result ).toEqual( 'Block Library' ); } ); } ); From c39c778d01d495ec28c36833276bfab9dc9e255f Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 19 Jul 2021 17:05:36 +0100 Subject: [PATCH 10/14] Sort features within section by total number of PR entries --- bin/plugin/commands/changelog.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 355cc62345b92..2732602533ae4 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -630,11 +630,12 @@ async function getChangelog( settings ) { } const featureGroups = groupBy( groupPullRequests, getIssueFeature ); - changelog += '### ' + group + '\n\n'; Object.keys( featureGroups ) - .sort() // natural sort first + .sort( ( a, b ) => { + return featureGroups[ b ].length - featureGroups[ a ].length; + } ) .sort( ( a, b ) => { // sort "Unknown" to always be at the end if ( a === 'Unknown' ) { From 9e19907d6db5427e83dfbe98e1451f2c0ad7b234 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 19 Jul 2021 17:14:24 +0100 Subject: [PATCH 11/14] Simplify and unify sorting mechanic --- bin/plugin/commands/changelog.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 2732602533ae4..1691a4d4bc7b0 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -108,6 +108,7 @@ const LABEL_FEATURE_MAPPING = { '[Feature] UI Components': 'Components', '[Feature] Component System': 'Components', '[Feature] Template Editing Mode': 'Template Editor', + '[Feature] Writing Flow': 'Block Editor', '[Feature] Blocks': 'Block Library', '[Feature] Inserter': 'Block Editor', '[Feature] Drag and Drop': 'Block Editor', @@ -633,9 +634,6 @@ async function getChangelog( settings ) { changelog += '### ' + group + '\n\n'; Object.keys( featureGroups ) - .sort( ( a, b ) => { - return featureGroups[ b ].length - featureGroups[ a ].length; - } ) .sort( ( a, b ) => { // sort "Unknown" to always be at the end if ( a === 'Unknown' ) { @@ -643,7 +641,7 @@ async function getChangelog( settings ) { } else if ( b === 'Unknown' ) { return -1; } - return 0; + return featureGroups[ b ].length - featureGroups[ a ].length; } ) .forEach( ( feature ) => { const featureGroup = featureGroups[ feature ]; From b5cda0c638885040f80771cc41801f722c3b4aad Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 20 Jul 2021 10:03:49 +0100 Subject: [PATCH 12/14] Improve code formatting and update UNKNOWN type --- bin/plugin/commands/changelog.js | 92 ++++++++++++++++++--------- bin/plugin/commands/test/changelog.js | 4 +- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 1691a4d4bc7b0..5d4752364a251 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -19,7 +19,7 @@ const config = require( '../config' ); // @ts-ignore const manifest = require( '../../../package.json' ); -const UNKNOWN = 'Unknown'; +const UNKNOWN_FEATURE_FALLBACK_NAME = 'Uncategorized'; /** @typedef {import('@octokit/rest')} GitHub */ /** @typedef {import('@octokit/rest').IssuesListForRepoResponseItem} IssuesListForRepoResponseItem */ @@ -318,7 +318,7 @@ function getIssueFeature( issue ) { } // Fallback - if we couldn't find a good match. - return UNKNOWN; + return UNKNOWN_FEATURE_FALLBACK_NAME; } /** @@ -620,6 +620,7 @@ async function getChangelog( settings ) { const groupedPullRequests = groupBy( pullRequests, getIssueType ); const sortedGroups = Object.keys( groupedPullRequests ).sort( sortGroup ); + for ( const group of sortedGroups ) { const groupPullRequests = groupedPullRequests[ group ]; const groupEntries = groupPullRequests @@ -630,42 +631,75 @@ async function getChangelog( settings ) { continue; } - const featureGroups = groupBy( groupPullRequests, getIssueFeature ); + // Start a new section within the changelog. changelog += '### ' + group + '\n\n'; - Object.keys( featureGroups ) - .sort( ( a, b ) => { - // sort "Unknown" to always be at the end - if ( a === 'Unknown' ) { - return 1; - } else if ( b === 'Unknown' ) { - return -1; - } - return featureGroups[ b ].length - featureGroups[ a ].length; - } ) - .forEach( ( feature ) => { - const featureGroup = featureGroups[ feature ]; - // Start new markdown list - changelog += '- ' + feature + '\n'; - const featureGroupEntries = featureGroup - .map( getEntry ) - .filter( Boolean ); - featureGroupEntries.forEach( ( entry ) => { - // Strip feature name from entry if present. - entry = entry && entry.replace( `[${ feature } - `, '[' ); - - // Add a new bullet point to the list. - changelog += ` ${ entry }\n`; - } ); - // End the markdown list. - changelog += '\n'; + // Group PRs within this section into "Features". + const featureGroups = groupBy( groupPullRequests, getIssueFeature ); + + const featuredGroupNames = sortFeatureGroups( featureGroups ); + + // Start output of Features within the section. + featuredGroupNames.forEach( ( featureName ) => { + const featureGroupPRs = featureGroups[ featureName ]; + + const featureGroupEntries = featureGroupPRs + .map( getEntry ) + .filter( Boolean ); + + // Don't create feature sections when there are no PRs. + if ( ! featureGroupEntries.length ) { + return; + } + + // Start new
    for the Feature group. + changelog += '- ' + featureName + '\n'; + + // Add a
  • for each PR in the Feature. + featureGroupEntries.forEach( ( entry ) => { + // Strip feature name from entry if present. + entry = entry && entry.replace( `[${ featureName } - `, '[' ); + + // Add a new bullet point to the list. + changelog += ` ${ entry }\n`; } ); + + // Close the
      for the Feature group. + changelog += '\n'; + } ); + changelog += '\n'; } return changelog; } +/** + * Sorts the feature groups by the feature which contains the greatest number of PRs + * ready for output into the changelog. + * + * @param {Object[]} featureGroups feature specific PRs keyed by feature name. + * @return {string[]} sorted list of feature names. + */ +function sortFeatureGroups( featureGroups ) { + return Object.keys( featureGroups ).sort( + ( featureAName, featureBName ) => { + // Sort "Unknown" to always be at the end + if ( featureAName === UNKNOWN_FEATURE_FALLBACK_NAME ) { + return 1; + } else if ( featureBName === UNKNOWN_FEATURE_FALLBACK_NAME ) { + return -1; + } + + // Sort by greatest number of PRs in the group first. + return ( + featureGroups[ featureBName ].length - + featureGroups[ featureAName ].length + ); + } + ); +} + /** * Generates and logs changelog for a milestone. * diff --git a/bin/plugin/commands/test/changelog.js b/bin/plugin/commands/test/changelog.js index d348d90436985..773609949873b 100644 --- a/bin/plugin/commands/test/changelog.js +++ b/bin/plugin/commands/test/changelog.js @@ -177,7 +177,7 @@ describe( 'getIssueFeature', () => { it( 'returns "Unknown" as feature if there are no labels', () => { const result = getIssueFeature( { labels: [] } ); - expect( result ).toBe( 'Unknown' ); + expect( result ).toBe( 'Uncategorized' ); } ); it( 'falls by to "Unknown" as the feature if unable to classify by other means', () => { @@ -195,7 +195,7 @@ describe( 'getIssueFeature', () => { ], } ); - expect( result ).toEqual( 'Unknown' ); + expect( result ).toEqual( 'Uncategorized' ); } ); it( 'gives precedence to manual feature mapping', () => { From 68abcf31d55894941c37f5ee56bbb1cf3a84d1bc Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 20 Jul 2021 10:08:27 +0100 Subject: [PATCH 13/14] Normalize function name --- bin/plugin/commands/changelog.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 5d4752364a251..39021ce9ba83d 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -265,17 +265,6 @@ function getIssueType( issue ) { return candidates.length ? candidates.sort( sortType )[ 0 ] : 'Various'; } -/** - * Removes any `[Feature]` prefix from a given label. - * - * @param {string} label The label potentially containing a prefix. - * - * @return {string} the label without the prefix. - */ -function stripFeaturePrefix( label ) { - return label.replace( '[Feature] ', '' ); -} - /** * Returns the most appropriate feature category for the given issue based * on a basic heuristic. @@ -307,7 +296,7 @@ function getIssueFeature( issue ) { const featureSpecificLabel = getFeatureSpecificLabels( labels ); if ( featureSpecificLabel ) { - return stripFeaturePrefix( featureSpecificLabel ); + return removeFeaturePrefix( featureSpecificLabel ); } // 3. Block specific labels. @@ -458,6 +447,17 @@ function removeRedundantTypePrefix( title, issue ) { ); } +/** + * Removes any `[Feature] ` prefix from a given string. + * + * @param {string} text The string of text potentially containing a prefix. + * + * @return {string} the text without the prefix. + */ +function removeFeaturePrefix( text ) { + return text.replace( '[Feature] ', '' ); +} + /** * Array of normalizations applying to title, each returning a new string, or * undefined to indicate an entry which should be omitted. From 08ace56fa354577c1828d75b4825738c042f3822 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 20 Jul 2021 10:16:47 +0100 Subject: [PATCH 14/14] Fix typings --- bin/plugin/commands/changelog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 39021ce9ba83d..0e769daab44f1 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -678,7 +678,7 @@ async function getChangelog( settings ) { * Sorts the feature groups by the feature which contains the greatest number of PRs * ready for output into the changelog. * - * @param {Object[]} featureGroups feature specific PRs keyed by feature name. + * @param {Object.} featureGroups feature specific PRs keyed by feature name. * @return {string[]} sorted list of feature names. */ function sortFeatureGroups( featureGroups ) {