fix: dedupe calendar stable releases

This commit is contained in:
lucaronin
2026-05-13 14:35:13 +02:00
parent d6ad2ff836
commit b2d23a8559
2 changed files with 117 additions and 0 deletions

View File

@@ -121,6 +121,52 @@ describe('buildReleaseHistoryPage', () => {
expect(html.indexOf('Tolaria Alpha 2026.4.20.10')).toBeLessThan(html.indexOf('Tolaria Alpha 2026.4.20.9'))
})
it('deduplicates equivalent stable calendar tags and keeps the richer notes', () => {
const html = buildReleaseHistoryPage([
{
assets: [
{
browser_download_url: 'https://example.com/Tolaria_2026.5.13_macOS_Silicon.dmg',
name: 'Tolaria_2026.5.13_macOS_Silicon.dmg',
},
],
body: '## What&apos;s Changed\n\n<ul><li></li></ul>',
body_html: '<h2>What&apos;s Changed</h2><ul><li></li></ul>',
html_url: 'https://github.com/refactoringhq/tolaria/releases/tag/stable-v2026.5.13',
name: 'Tolaria 2026.5.13',
prerelease: false,
published_at: '2026-05-13T09:30:44Z',
tag_name: 'stable-v2026.5.13',
},
{
assets: [
{
browser_download_url: 'https://example.com/Tolaria_2026.5.13_macOS_Silicon.dmg',
name: 'Tolaria_2026.5.13_macOS_Silicon.dmg',
},
],
body_html: [
'<h2>What&apos;s Changed</h2>',
'<h3>Features</h3>',
'<ul>',
'<li>Add AI visibility setting</li>',
'<li>Support mounted vault workspaces</li>',
'</ul>',
].join(''),
html_url: 'https://github.com/refactoringhq/tolaria/releases/tag/v2026-05-13',
name: 'Tolaria v2026-05-13',
prerelease: false,
published_at: '2026-05-13T09:21:30Z',
tag_name: 'v2026-05-13',
},
])
expect(html).toContain('Stable<span class="tab-count">1</span>')
expect(html).toContain('Tolaria v2026-05-13')
expect(html).toContain('Add AI visibility setting')
expect(html).not.toContain('Tolaria 2026.5.13')
})
it('filters draft releases and shows an empty state for channels without published builds', () => {
const html = buildReleaseHistoryPage([
{

View File

@@ -516,6 +516,10 @@ const RELEASE_CHANNEL_LABELS: Record<ReleaseChannel, string> = {
const RELEASE_CHANNEL_LABELS_BY_CHANNEL = new Map<ReleaseChannel, string>(
Object.entries(RELEASE_CHANNEL_LABELS) as Array<[ReleaseChannel, string]>,
)
const STABLE_TAG_DATE_PATTERNS = [
/^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$/,
/^v(\d{4})-(\d{1,2})-(\d{1,2})$/,
]
function escapeMarkupText(value: string): string {
return value
@@ -635,6 +639,71 @@ function readableNotesUrlForRelease(channel: ReleaseChannel, tagName: string): s
return `release-notes/${encodeURIComponent(tagName)}.md`
}
function normalizeStableTagDate(tagName: string): string | null {
for (const pattern of STABLE_TAG_DATE_PATTERNS) {
const match = tagName.match(pattern)
if (!match) continue
const [, year, month, day] = match
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
}
return null
}
function stableReleaseDedupeKey(release: ReleaseEntry): string {
const stableDate = normalizeStableTagDate(release.tagName)
return stableDate === null ? `tag:${release.tagName}` : `date:${stableDate}`
}
function releaseNotesListItemCount(notesHtml: string): number {
return notesHtml.match(/<li\b/gi)?.length ?? 0
}
function releasePreferenceScore(release: ReleaseEntry): number {
return releaseNotesListItemCount(release.notesHtml) + release.downloads.length
}
function mergeReleaseDownloads(primary: ReleaseDownload[], secondary: ReleaseDownload[]): ReleaseDownload[] {
const seenUrls = new Set<string>()
const downloads: ReleaseDownload[] = []
for (const download of [...primary, ...secondary]) {
if (seenUrls.has(download.url)) continue
seenUrls.add(download.url)
downloads.push(download)
}
return downloads
}
function preferredRelease(left: ReleaseEntry, right: ReleaseEntry): ReleaseEntry {
const leftScore = releasePreferenceScore(left)
const rightScore = releasePreferenceScore(right)
const selectedRelease = rightScore !== leftScore
? (rightScore > leftScore ? right : left)
: (right.publishedTimestamp > left.publishedTimestamp ? right : left)
const fallbackRelease = selectedRelease === right ? left : right
return {
...selectedRelease,
downloads: mergeReleaseDownloads(selectedRelease.downloads, fallbackRelease.downloads),
}
}
function deduplicateStableReleases(releases: ReleaseEntry[]): ReleaseEntry[] {
const releasesByDate = new Map<string, ReleaseEntry>()
for (const release of releases) {
const key = stableReleaseDedupeKey(release)
const existingRelease = releasesByDate.get(key)
releasesByDate.set(key, existingRelease ? preferredRelease(existingRelease, release) : release)
}
return Array.from(releasesByDate.values())
}
function normalizeReleaseEntry(release: GitHubReleasePayload): [ReleaseChannel, ReleaseEntry] | null {
if (release.draft === true) return null
@@ -678,6 +747,8 @@ function collectReleaseSections(payload: unknown): ReleaseSections {
appendReleaseSection(sections, normalizedRelease)
}
sections.stable = deduplicateStableReleases(sections.stable)
for (const channel of ['stable', 'alpha'] as const) {
const section = Reflect.get(sections, channel) as ReleaseEntry[]
section.sort((left, right) => right.publishedTimestamp - left.publishedTimestamp)