From 0a6421ae719cb4be07387fbb6494801501eefc6c Mon Sep 17 00:00:00 2001 From: castrojo Date: Tue, 8 Sep 2026 15:32:26 +0000 Subject: [PATCH] fix(cards): parse HTML release-feed tables before generating Bluefin cards card-feed-parser.mjs only recognized Markdown release tables. When fetch-feeds.js falls back to the GitHub Atom feed (no GITHUB_TOKEN), the feed content is GitHub-rendered HTML instead of raw Markdown, so parseFeedItem() returned null for every Bluefin/Bluefin LTS item and generate-card-images.mjs logged "no release found", skipping both Bluefin card slugs. Add extractSectionsHtml()/splitHtmlRow()/stripHtml() to parse the HTML table representation of the same '### Heading' + table sections, and have parseFeedItem() choose the HTML or Markdown extractor based on the feed content. Cover it with a fixture built from the real HTML GitHub renders for release bodies. Signed-off-by: castrojo --- scripts/generate-card-images.test.js | 64 ++++++++++++++++++++++++++ scripts/lib/card-feed-parser.mjs | 69 +++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/scripts/generate-card-images.test.js b/scripts/generate-card-images.test.js index 5c579bf61..d9d9699f4 100644 --- a/scripts/generate-card-images.test.js +++ b/scripts/generate-card-images.test.js @@ -53,6 +53,70 @@ test("parseFeedItem extracts markdown package, diff, and commit data", async () }); }); +test("parseFeedItem extracts HTML release-table data (Atom feed fallback)", async () => { + const { parseFeedItem } = await import("./lib/card-feed-parser.mjs"); + + // Real shape produced when GitHub renders release Markdown to HTML, as + // returned by the Atom feed fallback (fetch-feeds.js) when GITHUB_TOKEN + // is unavailable and the REST API can't be used. + const item = { + title: "stable-20260401 (F43.20260401, #123)", + pubDate: "2026-04-01T00:00:00Z", + link: "https://example.com/release", + content: [ + "

Major packages

", + "", + "", + "", + "", + "", + "", + "
NameVersion
Kernel6.14.0 ➡️ 6.15.0
Mesa25.0
", + "

Major DX packages

", + "", + "", + "", + "", + "", + "
NameVersion
Devpod0.5 ➡️ 0.6
", + "

All Images

", + "", + "", + "", + "", + "", + "", + "", + "
NamePreviousNew
ghcr.io/example/new
🔄ghcr.io/example/changed
ghcr.io/example/removed
", + "

Commits

", + "", + "", + "", + "", + "", + "", + "
HashMessage
abc123Update kernel
def456Update mesa
", + ].join("\n"), + }; + + assert.deepEqual(parseFeedItem(item, "stable"), { + stream: "stable", + tag: "stable-20260401", + fedoraVersion: "43", + centosVersion: null, + majorPackages: [ + { name: "Kernel", version: "6.15.0", prevVersion: "6.14.0" }, + { name: "Mesa", version: "25.0", prevVersion: null }, + ], + dxPackages: [{ name: "Devpod", version: "0.6", prevVersion: "0.5" }], + gdxPackages: [], + diffStats: { added: 1, changed: 1, removed: 1 }, + commitCount: 2, + dateMs: Date.parse("2026-04-01T00:00:00Z"), + link: "https://example.com/release", + }); +}); + test("parseFeedItem returns null when markdown tables are missing major packages", async () => { const { parseFeedItem } = await import("./lib/card-feed-parser.mjs"); diff --git a/scripts/lib/card-feed-parser.mjs b/scripts/lib/card-feed-parser.mjs index fa3500d18..9fa69acb7 100644 --- a/scripts/lib/card-feed-parser.mjs +++ b/scripts/lib/card-feed-parser.mjs @@ -72,12 +72,77 @@ export function parseCommitRows(rows) { return rows.filter((cells) => cells.length >= 2 && cells[0] && cells[0] !== "Hash").length; } +/** + * Strip HTML tags and decode the handful of entities GitHub uses when + * rendering release notes (Atom feed content, or the release page itself). + */ +export function stripHtml(text) { + return text + .replace(/<[^>]+>/g, "") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/�?39;/g, "'") + .trim(); +} + +export function splitHtmlRow(rowHtml) { + const cells = []; + const cellRe = /]*>([\s\S]*?)<\/t[hd]>/gi; + let match; + while ((match = cellRe.exec(rowHtml))) { + cells.push(stripHtml(match[1])); + } + return cells; +} + +/** + * Extract `### Heading` -> table-rows sections from an HTML release body + * (e.g. the GitHub Atom feed, which renders Markdown to HTML). Each heading + * is paired with the next that appears before the following heading. + */ +export function extractSectionsHtml(content) { + const sections = new Map(); + const headingRe = /]*>([\s\S]*?)<\/h[1-6]>/gi; + const headings = []; + let hm; + while ((hm = headingRe.exec(content))) { + headings.push({ index: hm.index, end: headingRe.lastIndex, text: stripHtml(hm[1]) }); + } + + const tableRe = /]*>([\s\S]*?)<\/table>/gi; + const tables = []; + let tm; + while ((tm = tableRe.exec(content))) { + tables.push({ index: tm.index, body: tm[1] }); + } + + for (let i = 0; i < headings.length; i++) { + const heading = headings[i]; + const nextHeadingIndex = i + 1 < headings.length ? headings[i + 1].index : Infinity; + const table = tables.find((t) => t.index > heading.end && t.index < nextHeadingIndex); + if (!table) continue; + + const rowRe = /]*>([\s\S]*?)<\/tr>/gi; + const rows = []; + let rm; + while ((rm = rowRe.exec(table.body))) { + const cells = splitHtmlRow(rm[1]); + if (cells.length > 0) rows.push(cells); + } + if (rows.length > 0) sections.set(heading.text, rows); + } + return sections; +} + export function parseFeedItem(item, streamHint) { const content = item.content ?? ""; const isMarkdown = /^\|[\s|:-]*---[\s|:-]*\|/m.test(content); - if (!isMarkdown) return null; + const isHtml = !isMarkdown && /]*>[\s\S]*<\/table>/i.test(content); + if (!isMarkdown && !isHtml) return null; - const sections = extractSectionsMd(content); + const sections = isHtml ? extractSectionsHtml(content) : extractSectionsMd(content); const majorPackages = parseTwoColTableMd(sections.get("Major packages") ?? []); const dxPackages = parseTwoColTableMd(sections.get("Major DX packages") ?? []); const gdxPackages = parseTwoColTableMd(sections.get("Major GDX packages") ?? []);