@@ -13,7 +13,22 @@ const CONVENTIONAL_PATTERN = new RegExp(
1313 String . raw `(feat|fix|docs|style|refactor|perf|test|bench|build|ci|chore)(?:\(([^)]+)\))?(!)?: (.+)$`
1414)
1515const PULL_REQUEST_PATTERN = / \s + \( # ( [ 0 - 9 ] + ) \) $ /
16- const INTERNAL_TYPES = new Set ( [ 'bench' , 'build' , 'chore' , 'ci' , 'refactor' , 'style' , 'test' ] )
16+ const REFERENCE_PATTERN = / # ( [ 0 - 9 ] + ) / g
17+ const GITHUB_URL = 'https://github.com'
18+ const REPO_URL = `${ GITHUB_URL } /DataDog/dd-trace-js`
19+ const UNCATEGORIZED_PRODUCT = 'Other'
20+ const DEPENDENCY_PRODUCT = 'Dependencies'
21+ // Dependabot tags the commit scope `deps-dev` for development dependencies and
22+ // `deps` for production ones, but the `deps` manifests under test/benchmark/docs
23+ // directories are not shipped. The shipped manifests are the repo root and the
24+ // bundled `/vendor` tree; only those (and the matching production groups from
25+ // `.github/dependabot.yml`) reach customers. Keep this set in sync with the
26+ // `dependency-type: "production"` groups there.
27+ const PRODUCTION_DEPENDENCY_GROUPS = new Set ( [
28+ 'runtime-minor-and-patch-dependencies' ,
29+ 'vendor-minor-and-patch-dependencies' ,
30+ 'security-production' ,
31+ ] )
1732
1833const CATEGORY_BY_TYPE = {
1934 docs : 'Documentation' ,
@@ -120,9 +135,9 @@ for (const [product, scopes] of PRODUCTS) {
120135 * @property {string } category
121136 * @property {string } product
122137 * @property {string } subject
123- * @property {string } pr
124- * @property {boolean } internal
138+ * @property {string } pr Bare pull request number, e.g. `8012`, or `''` when absent.
125139 * @property {boolean } revert
140+ * @property {boolean } [drop] Set when the entry is intentionally omitted from the changelog.
126141 * @property {string } [warning]
127142 */
128143
@@ -139,6 +154,7 @@ function createReleaseChangelog (entries) {
139154 for ( const entry of entries ) {
140155 const change = parseChange ( entry )
141156
157+ if ( change . drop ) continue
142158 if ( change . warning ) warnings . push ( change . warning )
143159 if ( change . category === 'Features' && ! change . revert ) isMinor = true
144160 if ( entry . author ) contributors . add ( entry . author )
@@ -169,29 +185,51 @@ function parseChange (entry) {
169185 if ( ! parsed ) {
170186 return {
171187 category : INTERNAL_CATEGORY ,
172- product : 'Other' ,
188+ product : UNCATEGORIZED_PRODUCT ,
173189 subject : subjectWithPullRequest . subject ,
174190 pr : subjectWithPullRequest . pr ,
175- internal : true ,
176191 revert : false ,
177192 warning : `Non-conventional release-note subject for ${ entry . sha } : ${ entry . subject } ` ,
178193 }
179194 }
180195
181- const category = CATEGORY_BY_TYPE [ parsed . type ] || INTERNAL_CATEGORY
182- const internal = INTERNAL_TYPES . has ( parsed . type )
183- const product = selectProduct ( parsed . scopes )
196+ const dependency = classifyDependencyBump ( parsed . scopes , parsed . subject )
197+ if ( dependency === 'other' ) {
198+ return { drop : true }
199+ }
184200
185201 return {
186- category,
187- product,
202+ category : CATEGORY_BY_TYPE [ parsed . type ] || INTERNAL_CATEGORY ,
203+ product : dependency === 'production' ? DEPENDENCY_PRODUCT : selectProduct ( parsed . scopes ) ,
188204 subject : parsed . subject ,
189205 pr : subjectWithPullRequest . pr ,
190- internal,
191206 revert : parsed . isRevert ,
192207 }
193208}
194209
210+ /**
211+ * Classify a Dependabot dependency bump as shipped (`production`) or not
212+ * (`other`, dropped from the changelog), or `undefined` when the commit is not
213+ * a dependency bump at all. Development dependencies and the instrumented-library
214+ * support ranges under test/benchmark/docs directories never reach customers.
215+ *
216+ * @param {string[] } scopes
217+ * @param {string } subject
218+ * @returns {'production'|'other'|undefined }
219+ */
220+ function classifyDependencyBump ( scopes , subject ) {
221+ if ( ! scopes . includes ( 'deps' ) && ! scopes . includes ( 'deps-dev' ) ) return
222+ if ( scopes . includes ( 'deps-dev' ) ) return 'other'
223+
224+ const directory = subject . match ( / \b i n ( \/ \S + ) / )
225+ if ( directory && directory [ 1 ] !== '/vendor' ) return 'other'
226+
227+ const group = subject . match ( / \b t h e ( \S + ) g r o u p \b / )
228+ if ( group && ! PRODUCTION_DEPENDENCY_GROUPS . has ( group [ 1 ] ) ) return 'other'
229+
230+ return 'production'
231+ }
232+
195233/**
196234 * @param {string } subject
197235 */
@@ -203,7 +241,7 @@ function parsePullRequest (subject) {
203241
204242 return {
205243 subject : subject . slice ( 0 , match . index ) ,
206- pr : `# ${ match [ 1 ] } ` ,
244+ pr : match [ 1 ] ,
207245 }
208246}
209247
@@ -299,11 +337,8 @@ function renderMarkdown (sections, contributors) {
299337 }
300338
301339 if ( contributors . size > 0 ) {
302- lines . push ( '<b>Contributors</b>' )
303- for ( const contributor of [ ...contributors ] . sort ( compareContributors ) ) {
304- lines . push ( `- ${ contributor } ` )
305- }
306- lines . push ( '' )
340+ const badges = [ ...contributors ] . sort ( compareContributors ) . map ( renderContributor )
341+ lines . push ( '### Contributors' , '' , badges . join ( ' ' ) , '' )
307342 }
308343
309344 return lines . join ( '\n' )
@@ -314,24 +349,21 @@ function renderMarkdown (sections, contributors) {
314349 */
315350function renderHeading ( category ) {
316351 if ( category === INTERNAL_CATEGORY ) {
317- return `<b> ${ category } </b> (CI, Testing, Benchmarking)`
352+ return `### ${ category } (CI, Testing, Benchmarking)`
318353 }
319354
320- return `<b> ${ category } </b> `
355+ return `### ${ category } `
321356}
322357
323358/**
324- * Groups same-product entries together; the internal section carries no product,
325- * so it falls through to a plain subject sort.
359+ * Groups same-product entries together, then orders by subject within a product.
326360 *
327361 * @param {Change } a
328362 * @param {Change } b
329363 */
330364function compareChanges ( a , b ) {
331- if ( ! a . internal ) {
332- const byProduct = a . product . toLowerCase ( ) . localeCompare ( b . product . toLowerCase ( ) )
333- if ( byProduct !== 0 ) return byProduct
334- }
365+ const byProduct = a . product . toLowerCase ( ) . localeCompare ( b . product . toLowerCase ( ) )
366+ if ( byProduct !== 0 ) return byProduct
335367
336368 return a . subject . toLowerCase ( ) . localeCompare ( b . subject . toLowerCase ( ) )
337369}
@@ -344,16 +376,49 @@ function compareContributors (a, b) {
344376 return a . toLowerCase ( ) . localeCompare ( b . toLowerCase ( ) )
345377}
346378
379+ /**
380+ * Renders a GitHub avatar that links to the contributor's profile. Display
381+ * strings that are not a `@handle` (a plain git author name) have no profile to
382+ * link, so they render verbatim.
383+ *
384+ * @param {string } contributor
385+ */
386+ function renderContributor ( contributor ) {
387+ if ( ! contributor . startsWith ( '@' ) ) return contributor
388+
389+ const login = contributor . slice ( 1 )
390+ return `[<img src="${ GITHUB_URL } /${ login } .png?size=48" width="24" height="24" ` +
391+ `alt="${ contributor } " title="${ contributor } " />](${ GITHUB_URL } /${ login } )`
392+ }
393+
347394/**
348395 * @param {Change } change
349396 */
350397function renderChange ( change ) {
351- const suffix = change . pr ? ` ${ change . pr } ` : ''
352- if ( change . internal ) {
353- return `- ${ change . subject } ${ suffix } `
398+ const subject = linkifyReferences ( change . subject )
399+ const suffix = change . pr ? ` ${ renderPullRequest ( change . pr ) } ` : ''
400+ if ( change . product === UNCATEGORIZED_PRODUCT ) {
401+ return `- ${ subject } ${ suffix } `
354402 }
355403
356- return `- <b>${ change . product } </b> ${ change . subject } ${ suffix } `
404+ return `- **${ change . product } :** ${ subject } ${ suffix } `
405+ }
406+
407+ /**
408+ * Wraps inline `#1234` references in an explicit link so GitHub renders them as
409+ * plain links instead of expanding each one into a pull request preview.
410+ *
411+ * @param {string } text
412+ */
413+ function linkifyReferences ( text ) {
414+ return text . replaceAll ( REFERENCE_PATTERN , ( _ , number ) => renderPullRequest ( number ) )
415+ }
416+
417+ /**
418+ * @param {string } number Bare pull request number.
419+ */
420+ function renderPullRequest ( number ) {
421+ return `[#${ number } ](${ REPO_URL } /pull/${ number } )`
357422}
358423
359424module . exports = {
0 commit comments