diff --git a/docs/skills/variant-docs-pages.md b/docs/skills/variant-docs-pages.md index 430b4955c..b11336cb5 100644 --- a/docs/skills/variant-docs-pages.md +++ b/docs/skills/variant-docs-pages.md @@ -106,13 +106,15 @@ a unified 5-part composition: identity headers and leading `# Title` by `scripts/fetch-readmes.mjs`. 3. `## Downloads & Image Versions`: `` displays major component cards with bump badges, plus available ISO cards (Stable - and/or Testing depending on image type, or a DDI notice for server). + and/or Testing depending on image type) or versioned installer/DDI/netboot cards + for Bluefin Server. Installer publication channel differs from embedded payload channel (e.g. Dakota stable payload served from the testing CDN). 4. `## Usage`: `` embeds first-party daily active systems with per-family filtering and explicit unavailable fallbacks. 5. `## Release Assets & Attestations`: `` renders - downloadable release artifacts, bootc/DDI switches, and provenance verification. + downloadable release artifacts, bootc/DDI switches, and provenance verification, + with a left-rail timeline of the latest releases for Server. Keep headings as Markdown `##` in the `.mdx` file so Docusaurus's build-time AST populates the right-hand Table of Contents. @@ -166,6 +168,7 @@ card entry from `IMAGES` in `ImageCards.tsx`. explicit, and link build-time recipes to upstream repositories. When headings are simplified, place explicit HTML anchors (``) so legacy links do not 404. + ## Common Rationalizations | Rationalization | Reality | diff --git a/scripts/catalog-components.test.js b/scripts/catalog-components.test.js index e3b1e9b28..7c76ff7af 100644 --- a/scripts/catalog-components.test.js +++ b/scripts/catalog-components.test.js @@ -47,6 +47,16 @@ function loadComponent(tsxPath, overrides = {}) { if (id.startsWith("@site/")) { return require(path.join(__dirname, "..", id.replace(/^@site\//, ""))); } + if (id.startsWith("./") || id.startsWith("../")) { + const resolvedPath = path.resolve(path.dirname(tsxPath), id); + if (fs.existsSync(resolvedPath + ".tsx")) { + return loadComponent(resolvedPath + ".tsx", overrides); + } + if (fs.existsSync(resolvedPath + ".ts")) { + return loadComponent(resolvedPath + ".ts", overrides); + } + return require(resolvedPath); + } return require(id); }, mod, @@ -543,3 +553,55 @@ test("DriverVersionsCatalog renders bluefin-server with systemd, sysexts, and sy "sysupdate deployment note rendered instead of bootc switch", ); }); +const ImageProductSection = loadComponent( + path.join(COMPONENTS_DIR, "ImageProductSection.tsx"), +); + +test("ProductDownloadsAndVersions and ProductReleaseAssets render server download card and 7-release timeline", () => { + const { ProductDownloadsAndVersions, ProductReleaseAssets } = + ImageProductSection; + + const downloadsHtml = render(ProductDownloadsAndVersions, { + product: "server", + }); + + assert.ok( + downloadsHtml.includes("BLUEFIN SERVER"), + "server download card title rendered", + ); + assert.ok( + downloadsHtml.includes("Download Installer"), + "installer action button rendered", + ); + assert.ok( + downloadsHtml.includes("Download DDI"), + "ddi action button rendered", + ); + assert.ok( + downloadsHtml.includes("Download UKI"), + "netboot UKI action button rendered", + ); + + const assetsHtml = render(ProductReleaseAssets, { + product: "server", + }); + + // Verify left-rail timeline markers + assert.ok( + assetsHtml.includes("Now"), + "archive rail Now label rendered for timeline", + ); + assert.ok( + assetsHtml.includes("Past"), + "archive rail Past label rendered for timeline", + ); + + // Count rendered release cards for server (should be exactly 7) + const serverTagMatches = assetsHtml.match(/v26\.10\.\d+/g) || []; + const uniqueTags = [...new Set(serverTagMatches)]; + assert.equal( + uniqueTags.length, + 7, + "timeline displays exactly the last 7 releases", + ); +}); diff --git a/scripts/fetch-image-releases.mjs b/scripts/fetch-image-releases.mjs index e9d7f1a92..810c955df 100755 --- a/scripts/fetch-image-releases.mjs +++ b/scripts/fetch-image-releases.mjs @@ -133,9 +133,9 @@ function recentReleases(releases, source) { const buckets = (families.length ? families : ["unknown"]).map( (family) => `${family}:${stream}`, ); - const keep = buckets.some( - (bucket) => (counts.get(bucket) ?? 0) < PER_STREAM, - ); + const limit = + source.repository === PRODUCT_SOURCES.server.repository ? 7 : PER_STREAM; + const keep = buckets.some((bucket) => (counts.get(bucket) ?? 0) < limit); for (const bucket of buckets) counts.set(bucket, (counts.get(bucket) ?? 0) + 1); return keep; @@ -533,6 +533,14 @@ if ( assert.equal(row.assets[0].size, 0); assert.equal(row.assets[0].name, "SHA256SUMS.gpg"); assert.equal(row.versions, null); + const serverReleases = Array.from({ length: 10 }, (_, i) => ({ + tag_name: `v26.10.${1200 + i}`, + draft: false, + })); + assert.equal( + recentReleases(serverReleases, PRODUCT_SOURCES.server).length, + 7, + ); console.log("fetch-image-releases: metadata self-check passed"); } else if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { try { diff --git a/src/components/DownloadCard.module.css b/src/components/DownloadCard.module.css index eb5739c28..040548894 100644 --- a/src/components/DownloadCard.module.css +++ b/src/components/DownloadCard.module.css @@ -37,6 +37,9 @@ .cardDakota::after { background-image: url("/img/characters/dakotaraptor.webp"); } +.cardServer::after { + background-image: url("/img/portal/characters/alamosaurus.webp"); +} /* ── Stream accent borders ── */ @@ -46,6 +49,9 @@ .cardDakota { border-left: 3px solid #7c3aed; } +.cardServer { + border-left: 3px solid #10b981; +} /* ── Header ── */ diff --git a/src/components/DownloadCard.tsx b/src/components/DownloadCard.tsx index e6744df86..267a74fad 100644 --- a/src/components/DownloadCard.tsx +++ b/src/components/DownloadCard.tsx @@ -7,6 +7,7 @@ export interface DownloadEntry { isoFilename: string; torrentUrl?: string; checksumUrl: string; + buttonLabel?: string; } export interface EntryGroup { @@ -15,7 +16,7 @@ export interface EntryGroup { } export interface DownloadCardProps { - variant: "bluefin" | "dakotaraptor"; + variant: "bluefin" | "dakotaraptor" | "server"; title: string; description: React.ReactNode; entries: DownloadEntry[]; @@ -28,6 +29,7 @@ export interface DownloadCardProps { const VARIANT_CLASS: Record = { bluefin: styles.cardBluefin, dakotaraptor: styles.cardDakota, + server: styles.cardServer, }; const LOGO_MAP: Record = { @@ -83,7 +85,7 @@ function EntryRow({ download={entry.isoFilename} className={styles.downloadButton} > - 📥 Download ISO + {entry.buttonLabel ? `📥 ${entry.buttonLabel}` : "📥 Download ISO"} {entry.torrentUrl ? ( diff --git a/src/components/ImageProductSection.module.css b/src/components/ImageProductSection.module.css index d431c46f5..f82e39bee 100644 --- a/src/components/ImageProductSection.module.css +++ b/src/components/ImageProductSection.module.css @@ -146,3 +146,104 @@ color: var(--ifm-color-emphasis-600); font-style: italic; } + +.timelinePage { + position: relative; + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + padding-left: 2.35rem; + margin-top: 1.25rem; +} + +.archiveRail { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 1.9rem; + pointer-events: none; +} + +.archiveRail::before { + content: ""; + position: absolute; + left: 0.8rem; + top: 1.05rem; + bottom: 1.05rem; + width: 6px; + border-radius: 999px; + background: linear-gradient( + to bottom, + #10b981 0%, + #059669 25%, + var(--ifm-color-primary) 50%, + color-mix(in srgb, #22d3ee 70%, var(--ifm-color-primary)) 75%, + color-mix(in srgb, #22d3ee 30%, transparent) 90%, + transparent 100% + ); + box-shadow: 0 0 12px 2px color-mix(in srgb, #10b981 40%, transparent); +} + +.archiveNow, +.archivePast { + position: absolute; + left: 0; + font-size: 0.66rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ifm-color-emphasis-700); +} + +.archiveNow { + top: 0; +} + +.archivePast { + bottom: 0; +} + +.releaseDivider { + height: 2px; + margin: 0.4rem 0; + border-radius: 999px; + background: linear-gradient( + to right, + transparent 0%, + color-mix(in srgb, #10b981 40%, transparent) 10%, + color-mix(in srgb, #4a9eff 80%, transparent) 40%, + color-mix(in srgb, #4a9eff 95%, transparent) 50%, + color-mix(in srgb, #4a9eff 80%, transparent) 60%, + color-mix(in srgb, #10b981 40%, transparent) 90%, + transparent 100% + ); + box-shadow: 0 0 8px 1px color-mix(in srgb, #10b981 25%, transparent); +} + +[data-theme="dark"] .releaseDivider { + background: linear-gradient( + to right, + transparent 0%, + color-mix(in srgb, #34d399 35%, transparent) 10%, + color-mix(in srgb, #7ec8ff 70%, transparent) 40%, + color-mix(in srgb, #7ec8ff 90%, transparent) 50%, + color-mix(in srgb, #7ec8ff 70%, transparent) 60%, + color-mix(in srgb, #34d399 35%, transparent) 90%, + transparent 100% + ); + box-shadow: 0 0 10px 1px color-mix(in srgb, #34d399 30%, transparent); +} + +@media (max-width: 600px) { + .timelinePage { + padding-left: 1.75rem; + } + .archiveRail { + width: 1.4rem; + } + .archiveRail::before { + left: 0.55rem; + width: 5px; + } +} diff --git a/src/components/ImageProductSection.tsx b/src/components/ImageProductSection.tsx index 054e3df2d..1e1fc4582 100644 --- a/src/components/ImageProductSection.tsx +++ b/src/components/ImageProductSection.tsx @@ -316,11 +316,126 @@ export function ProductDownloadsAndVersions({ )} ) : product === "server" ? ( -
- Bluefin Server deploys and installs via raw - Discoverable Disk Images (DDI) and UEFI UKI binaries rather than an - Anaconda ISO installer. Download installer and netboot artifacts - below. +
+ {(() => { + const installerAsset = latestRelease?.assets?.find( + (a) => + a.name.startsWith("bluefin-server-installer_") && + a.name.endsWith(".raw"), + ); + const ddiAsset = latestRelease?.assets?.find( + (a) => + a.name.startsWith("bluefin-server_") && + a.name.endsWith(".raw") && + !a.name.includes(".usr"), + ); + const netbootUkiAsset = latestRelease?.assets?.find( + (a) => + a.name.startsWith("bluefin-server-netboot_") && + a.name.endsWith(".efi"), + ); + const netbootEspAsset = latestRelease?.assets?.find( + (a) => + a.name.startsWith("bluefin-server-netboot_") && + a.name.endsWith(".esp.raw"), + ); + const diskUkiAsset = latestRelease?.assets?.find( + (a) => + a.name.startsWith("bluefin-server-") && + a.name.endsWith(".efi") && + !a.name.includes("-netboot_"), + ); + const checksumAsset = latestRelease?.assets?.find( + (a) => a.name === "SHA256SUMS", + ); + const checksumUrl = + checksumAsset?.url ?? + (latestRelease + ? `https://github.com/projectbluefin/server/releases/download/${latestRelease.tag}/SHA256SUMS` + : "#"); + + const primaryEntries: DownloadEntry[] = [ + ...(installerAsset + ? [ + { + label: "USB Installer (x86_64 raw)", + isoUrl: installerAsset.url, + isoFilename: installerAsset.name, + checksumUrl, + buttonLabel: "Download Installer", + }, + ] + : []), + ...(ddiAsset + ? [ + { + label: "OS DDI Image (raw diskless)", + isoUrl: ddiAsset.url, + isoFilename: ddiAsset.name, + checksumUrl, + buttonLabel: "Download DDI", + }, + ] + : []), + ]; + + const netbootEntries: DownloadEntry[] = [ + ...(netbootUkiAsset + ? [ + { + label: "Netboot UKI (UEFI HTTP/PXE)", + isoUrl: netbootUkiAsset.url, + isoFilename: netbootUkiAsset.name, + checksumUrl, + buttonLabel: "Download UKI", + }, + ] + : []), + ...(netbootEspAsset + ? [ + { + label: "Netboot ESP (systemd-boot raw)", + isoUrl: netbootEspAsset.url, + isoFilename: netbootEspAsset.name, + checksumUrl, + buttonLabel: "Download ESP", + }, + ] + : []), + ...(diskUkiAsset + ? [ + { + label: "Disk UKI (installed node update)", + isoUrl: diskUkiAsset.url, + isoFilename: diskUkiAsset.name, + checksumUrl, + buttonLabel: "Download UKI", + }, + ] + : []), + ]; + + return ( + 0 + ? [ + { + label: "Netboot & UEFI Artifacts", + entries: netbootEntries, + }, + ] + : undefined + } + /> + ); + })()}
) : (

@@ -365,126 +480,156 @@ export function ProductReleaseAssets({ No published GitHub releases found for this product.

) : ( -
- {releases.map((release) => { - const isIso = release.source.kind === "iso"; - const isBootc = !isIso && Boolean(identities[product].image); - const isDdi = product === "server"; - - // Find matching SBOM entry if present - const sbomEntry = sbomStream?.releases - ? Object.values(sbomStream.releases).find((r) => - release.tag.includes(r.tag?.replace("stable-", "") ?? ""), - ) - : null; - - return ( -
-
-
- - - {release.tag} - - - - {isDdi - ? "DDI + UKI" - : isIso - ? "ISO installer" - : "bootc OCI"} - - {release.stream && ( - - :{release.stream} +
+ {product === "server" && ( + + )} + {(product === "server" ? releases.slice(0, 7) : releases).map( + (release, index) => { + const isIso = release.source.kind === "iso"; + const isBootc = !isIso && Boolean(identities[product].image); + const isDdi = product === "server"; + + // Find matching SBOM entry if present + const sbomEntry = sbomStream?.releases + ? Object.values(sbomStream.releases).find((r) => + release.tag.includes(r.tag?.replace("stable-", "") ?? ""), + ) + : null; + + return ( + + {product === "server" && index > 0 && ( +
+ )} +
+
+
+ + + {release.tag} + + + + {isDdi + ? "DDI + UKI" + : isIso + ? "ISO installer" + : "bootc OCI"} + + {release.stream && ( + + :{release.stream} + + )} +
+ + {formatDate(release.publishedAt)} +
+ + {/* Major component version boxes for server releases */} + {isDdi && release.versions && ( + )} -
- - {formatDate(release.publishedAt)} - -
- - {/* Supply Chain / Attestation Verification */} - {!isIso && ( -
- Supply chain & attestation: - {isDdi ? ( - - GPG-signed SHA256SUMS manifest & in-toto SLSA / SPDX - SBOM published with release. - - ) : sbomEntry?.attestation?.verified ? ( - - ✓ Provenance attestation verified ( - {sbomEntry.attestation.predicateType || "SLSA v1"}). - - ) : sbomEntry?.attestation?.present ? ( - - Provenance attestation present in GHCR referrer. - - ) : ( - - No attestation for this tag in the SBOM cache. - + + {/* Supply Chain / Attestation Verification */} + {!isIso && ( +
+ Supply chain & attestation: + {isDdi ? ( + + GPG-signed SHA256SUMS manifest & in-toto SLSA / + SPDX SBOM published with release. + + ) : sbomEntry?.attestation?.verified ? ( + + ✓ Provenance attestation verified ( + {sbomEntry.attestation.predicateType || "SLSA v1"}). + + ) : sbomEntry?.attestation?.present ? ( + + Provenance attestation present in GHCR referrer. + + ) : ( + + No attestation for this tag in the SBOM cache. + + )} +
)} -
- )} - - {/* Switch or deployment instructions */} - {isBootc && ( -
- - Bootc container switch: - - - {`sudo bootc switch --enforce-container-sigpolicy ${identities[product].image}:${release.stream ?? "stable"}`} - -
- )} - - {/* Downloadable assets table */} - {release.assets.length > 0 && ( -
- - - - - - - - - - {release.assets.map((asset) => ( - - - - - - ))} - -
Asset fileSizeAction
- {asset.name} - {formatBytes(asset.size)} - - Download - -
-
- )} -
- ); - })} + + {/* Switch or deployment instructions */} + {isBootc && ( +
+ + Bootc container switch: + + + {`sudo bootc switch --enforce-container-sigpolicy ${identities[product].image}:${release.stream ?? "stable"}`} + +
+ )} + + {/* Downloadable assets table */} + {release.assets.length > 0 && ( +
+ + + + + + + + + + {release.assets.map((asset) => ( + + + + + + ))} + +
Asset fileSizeAction
+ {asset.name} + {formatBytes(asset.size)} + + Download + +
+
+ )} + + + ); + }, + )}
)}