(
buckets: CategoryBuckets,
opts: { scope: 'user' | 'all'; heading: string; range: string },
)
| 122 | }; |
| 123 | |
| 124 | export function formatMarkdown( |
| 125 | buckets: CategoryBuckets, |
| 126 | opts: { scope: 'user' | 'all'; heading: string; range: string }, |
| 127 | ): string { |
| 128 | const showInternal = opts.scope === 'all'; |
| 129 | const order: ReleaseCategory[] = showInternal |
| 130 | ? ['breaking', 'features', 'fixes', 'perf', 'docs', 'internal', 'other'] |
| 131 | : ['breaking', 'features', 'fixes', 'perf']; |
| 132 | |
| 133 | const lines: string[] = [`## ${opts.heading}`, '', `_Range: \`${opts.range}\`_`, '']; |
| 134 | let any = false; |
| 135 | for (const cat of order) { |
| 136 | const items = buckets[cat]; |
| 137 | if (items.length === 0) continue; |
| 138 | any = true; |
| 139 | lines.push(`### ${SECTION_LABELS[cat]}`); |
| 140 | for (const it of items) { |
| 141 | const scopeTag = it.scope ? `**${it.scope}:** ` : ''; |
| 142 | lines.push(`- ${scopeTag}${stripConvPrefix(it.subject)} (\`${it.sha}\`)`); |
| 143 | } |
| 144 | lines.push(''); |
| 145 | } |
| 146 | if (!any) lines.push('_No user-facing changes in this range._', ''); |
| 147 | return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'; |
| 148 | } |
| 149 | |
| 150 | function stripConvPrefix(subject: string): string { |
| 151 | return subject.replace(CONV_RE, (_, ..._args) => { |
no test coverage detected