Kernel<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "HWE Kernel",
- pattern: / | HWE Kernel<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "GNOME",
- pattern: / | (?:Gnome|GNOME)<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "Mesa",
- pattern: / | Mesa<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "Podman",
- pattern: / | Podman<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "NVIDIA",
- pattern: / | Nvidia<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "Docker",
- pattern: / | Docker<\/strong><\/td>\s*| ([^<]+)/,
- },
- {
- name: "systemd",
- pattern: / | 🔄<\/td>\s* | systemd<\/td>\s* | [^<]*<\/td>\s* | ([^<]+)/,
- changePattern:
- / | 🔄<\/td>\s* | systemd<\/td>\s* | ([^<]+)<\/td>\s* | ([^<]+)/,
- },
- {
- name: "bootc",
- pattern: / | 🔄<\/td>\s* | bootc<\/td>\s* | [^<]*<\/td>\s* | ([^<]+)/,
- changePattern:
- / | 🔄<\/td>\s* | bootc<\/td>\s* | ([^<]+)<\/td>\s* | ([^<]+)/,
- },
-];
-
-/**
- * Extracts package version from content using the provided pattern.
- * Handles both upgrade arrows (old ➡️ new) and static versions.
- *
- * @param content - HTML content from changelog
- * @param pattern - Regex pattern to match package version
- * @returns Latest version string or null if not found
- */
-export function extractPackageVersion(
- content: string,
- pattern: RegExp,
-): string | null {
- const match = content.match(pattern);
- if (match) {
- const versionText = match[1].trim();
- // Extract the latest version if there's an arrow (6.14.11-300 ➡️ 6.15.9-201)
- return versionText.includes("➡️")
- ? versionText.split("➡️")[1].trim()
- : versionText;
- }
- return null;
-}
-
-/**
- * Extracts version change information for FeedItems component.
- * Only returns changes that have upgrade arrows, not static versions.
- *
- * @param content - HTML content from changelog
- * @param packageConfig - Package configuration with patterns
- * @returns Version change info or null if no upgrade found
- */
-export function extractVersionChange(
- content: string,
- packageConfig: PackagePattern,
-): { name: string; change: string } | null {
- // For packages with changePattern (systemd, bootc), use the special pattern
- if (packageConfig.changePattern) {
- const match = content.match(packageConfig.changePattern);
- if (match) {
- const fromVersion = match[1].trim();
- const toVersion = match[2].trim();
- return {
- name: packageConfig.name,
- change: `${fromVersion} → ${toVersion}`,
- };
- }
- return null;
- }
-
- // For standard format patterns, look for upgrade arrows
- const match = content.match(packageConfig.pattern);
- if (match) {
- const versionText = match[1].trim();
- if (versionText.includes("➡️")) {
- // Only show upgrades (with arrow), not static versions
- const [fromVersion, toVersion] = versionText
- .split("➡️")
- .map((v) => v.trim());
- return {
- name: packageConfig.name,
- change: `${fromVersion} → ${toVersion}`,
- };
- }
- }
-
- return null;
-}
diff --git a/src/lib/sbom-adapter.ts b/src/lib/sbom-adapter.ts
deleted file mode 100644
index 3e6e3e606..000000000
--- a/src/lib/sbom-adapter.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * SBOM-first data adapter.
- *
- * Single entry point for components that need release, version, or package
- * data. Reads from sbom-attestations-frontend.json and exposes a clean API
- * so that components never import raw JSON directly.
- *
- * Design goals:
- * 1. One canonical import for release data across the site
- * 2. Lazy loading — the 104 KB SBOM payload is parsed on first access
- * 3. Typed interfaces that match what components actually render
- * 4. Adapter layer isolates components from upstream SBOM schema changes
- */
-
-// ── Types ────────────────────────────────────────────────────────────────────
-
-export interface PackageVersions {
- kernel?: string | null;
- gnome?: string | null;
- mesa?: string | null;
- podman?: string | null;
- systemd?: string | null;
- [key: string]: string | null | undefined;
-}
-
-export interface Attestation {
- present: boolean | null;
- verified: boolean;
- predicateType: string | null;
- slsaType?: string;
- errorKind?: string;
- error: string | null;
-}
-
-export interface Release {
- tag: string;
- imageRef: string;
- digest: string | null;
- attestation: Attestation;
- packageVersions: PackageVersions | null;
- checkedAt: string;
-}
-
-export interface Stream {
- id: string;
- label: string;
- org: string;
- package: string;
- streamPrefix: string;
- keyRepo: string;
- keyless: boolean;
- releases: Record;
-}
-
-export interface SbomData {
- generatedAt: string;
- lookbackDays: number;
- maxReleasesPerStream: number;
- streams: Record;
-}
-
-export interface LatestRelease {
- stream: string;
- tag: string;
- date: string;
- kernel: string;
- gnome: string;
- mesa: string;
- digest: string;
- imageRef: string;
- allVersions: PackageVersions;
-}
-
-export interface StreamSummary {
- id: string;
- label: string;
- releaseCount: number;
- latestTag: string | null;
- latestDate: string | null;
-}
-
-// ── Lazy loader ──────────────────────────────────────────────────────────────
-
-let _cache: SbomData | null = null;
-
-function load(): SbomData {
- if (_cache) return _cache;
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- _cache = require("@site/static/data/sbom-attestations-frontend.json") as SbomData;
- return _cache;
-}
-
-// ── Public API ───────────────────────────────────────────────────────────────
-
-/** Get the raw SBOM data (lazy-loaded). */
-export function getSbomData(): SbomData {
- return load();
-}
-
-/** List all stream IDs. */
-export function getStreamIds(): string[] {
- return Object.keys(load().streams);
-}
-
-/** Get a single stream by ID. */
-export function getStream(streamId: string): Stream | undefined {
- return load().streams[streamId];
-}
-
-/** Get the latest release for a stream. Returns null if stream has no releases. */
-export function getLatestRelease(streamId: string): LatestRelease | null {
- const stream = getStream(streamId);
- if (!stream) return null;
-
- const entries = Object.entries(stream.releases);
- if (entries.length === 0) return null;
-
- // Releases are ordered newest-first by convention
- const [tag, release] = entries[0];
- const pv = release.packageVersions || {};
-
- return {
- stream: streamId,
- tag,
- date: release.checkedAt,
- kernel: pv.kernel ?? "unknown",
- gnome: pv.gnome ?? "unknown",
- mesa: pv.mesa ?? "unknown",
- digest: release.digest ?? "unknown",
- imageRef: release.imageRef,
- allVersions: pv,
- };
-}
-
-/** Get latest releases for all streams. Streams with no releases are skipped. */
-export function getAllLatestReleases(): LatestRelease[] {
- return getStreamIds()
- .map(getLatestRelease)
- .filter((r): r is LatestRelease => r !== null);
-}
-
-/** Get a summary of all streams (id, label, release count, latest tag). */
-export function getStreamSummaries(): StreamSummary[] {
- const data = load();
- return Object.values(data.streams).map((s) => {
- const tags = Object.keys(s.releases);
- return {
- id: s.id,
- label: s.label,
- releaseCount: tags.length,
- latestTag: tags[0] ?? null,
- latestDate: tags[0] ? s.releases[tags[0]].checkedAt : null,
- };
- });
-}
-
-/** Get all releases for a stream, sorted newest-first. */
-export function getReleases(streamId: string): Release[] {
- const stream = getStream(streamId);
- if (!stream) return [];
- return Object.values(stream.releases);
-}
-
-/** Check SBOM freshness. Returns hours since generation. */
-export function getSbomAgeHours(): number {
- const gen = new Date(load().generatedAt);
- return (Date.now() - gen.getTime()) / (1000 * 60 * 60);
-}
-
-/**
- * Find which streams contain a specific package version.
- * Useful for "which images ship kernel X.Y.Z?" queries.
- */
-export function findStreamsByPackageVersion(
- packageName: string,
- version: string,
-): string[] {
- const data = load();
- const matches: string[] = [];
- for (const [streamId, stream] of Object.entries(data.streams)) {
- for (const release of Object.values(stream.releases)) {
- if (release.packageVersions?.[packageName] === version) {
- matches.push(streamId);
- break;
- }
- }
- }
- return matches;
-}
diff --git a/src/lib/sbom-feed-adapter.ts b/src/lib/sbom-feed-adapter.ts
deleted file mode 100644
index 2e283d4c1..000000000
--- a/src/lib/sbom-feed-adapter.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * SBOM → Feed compatibility adapter.
- *
- * Bridges the gap between the SBOM data model (streams/releases/packages)
- * and the legacy feed card format that FeedItems.tsx and FirehoseFeed.tsx
- * currently render. This allows a gradual migration: components can switch
- * to sbom-feed-adapter imports without rewriting their rendering logic.
- *
- * Once components are fully migrated, this adapter can be removed and
- * components can consume sbom-adapter.ts directly.
- */
-
-import type { Release } from "./sbom-adapter";
-import { getStream, getStreamIds, getSbomData } from "./sbom-adapter";
-
-// ── Feed-compatible types ────────────────────────────────────────────────────
-
-export interface VersionChip {
- label: string;
- value: string;
-}
-
-export interface FeedCard {
- id: string;
- title: string;
- stream: string;
- tag: string;
- date: string;
- imageRef: string;
- digest: string;
- chips: VersionChip[];
- kind: "release";
-}
-
-// ── Helpers ──────────────────────────────────────────────────────────────────
-
-const CHIP_KEYS = ["kernel", "gnome", "mesa", "podman", "systemd"] as const;
-
-function releaseToCard(streamId: string, tag: string, release: Release): FeedCard {
- const pv = release.packageVersions || {};
- const chips: VersionChip[] = CHIP_KEYS
- .filter((k) => pv[k])
- .map((k) => ({ label: k.charAt(0).toUpperCase() + k.slice(1), value: pv[k]! }));
-
- const streamLabel = streamId
- .replace(/-/g, " ")
- .replace(/\b\w/g, (c) => c.toUpperCase());
-
- return {
- id: `${streamId}/${tag}`,
- title: `${streamLabel} — ${tag}`,
- stream: streamId,
- tag,
- date: release.checkedAt,
- imageRef: release.imageRef,
- digest: release.digest ?? "",
- chips,
- kind: "release",
- };
-}
-
-// ── Public API ───────────────────────────────────────────────────────────────
-
-/**
- * Get all SBOM releases as feed cards, newest-first across all streams.
- * Optionally filter by stream prefix (e.g., "bluefin-stable").
- */
-export function getSbomFeedCards(streamFilter?: string): FeedCard[] {
- const ids = streamFilter
- ? getStreamIds().filter((id) => id.startsWith(streamFilter))
- : getStreamIds();
-
- const cards: FeedCard[] = [];
- for (const streamId of ids) {
- const stream = getStream(streamId);
- if (!stream) continue;
- for (const [tag, release] of Object.entries(stream.releases)) {
- cards.push(releaseToCard(streamId, tag, release));
- }
- }
-
- // Sort newest-first by date
- cards.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
- return cards;
-}
-
-/**
- * Get the latest release card per stream (one card per stream).
- * Useful for the "pinned" cards at the top of the changelogs page.
- */
-export function getPinnedCards(streamIds?: string[]): FeedCard[] {
- const ids = streamIds ?? getStreamIds();
- const cards: FeedCard[] = [];
-
- for (const streamId of ids) {
- const stream = getStream(streamId);
- if (!stream) continue;
- const tags = Object.keys(stream.releases);
- if (tags.length === 0) continue;
- const tag = tags[0];
- cards.push(releaseToCard(streamId, tag, stream.releases[tag]));
- }
-
- return cards;
-}
-
-/**
- * Get the SBOM generation timestamp as an ISO string.
- */
-export function getSbomTimestamp(): string {
- return getSbomData().generatedAt;
-}
diff --git a/src/types/sbom.ts b/src/types/sbom.ts
index c99a1a956..12e4517cc 100644
--- a/src/types/sbom.ts
+++ b/src/types/sbom.ts
@@ -1,7 +1,7 @@
/**
* TypeScript interfaces for the SBOM attestation cache.
* Written to static/data/sbom-attestations.json by scripts/fetch-github-sbom.js
- * and consumed by ImageCards.tsx and FeedItems.tsx.
+ * and consumed by ImageProductSection.tsx and FirehoseFeed.tsx.
*/
export interface AttestationResult {
@@ -97,8 +97,8 @@ export interface SbomStream {
keyless: boolean;
/**
* Map of cache key → release attestation result.
- * Cache keys match the format produced by extractReleaseTag() in utils/sbomRelease.ts:
- * stable-YYYYMMDD, gts-YYYYMMDD, etc.
+ * Cache keys match the format produced by sbomKeyForRelease() in
+ * utils/firehoseHelpers.ts: stable-YYYYMMDD, stable-daily-YYYYMMDD, etc.
*/
releases: Record;
}
diff --git a/src/types/theme.d.ts b/src/types/theme.d.ts
deleted file mode 100644
index 8f7154e83..000000000
--- a/src/types/theme.d.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-declare module "@theme/useStoredFeed" {
- interface FeedItem {
- title: string;
- link:
- | string
- | { href?: string }
- | Array<{
- href?: string;
- rel?: string;
- $?: { href?: string; type?: string };
- }>;
- description?: string;
- pubDate?: string;
- updated?: string;
- guid?: string;
- id?: string;
- author?: string | { name?: string };
- content?: { value?: string } | string;
- }
-
- interface ParsedFeed {
- // RSS feed structure
- rss?: {
- channel?: {
- item?: FeedItem | FeedItem[];
- };
- };
- // Alternative RSS structure
- channel?: {
- item?: FeedItem | FeedItem[];
- };
- // Atom feed structure
- feed?: {
- entry?: FeedItem | FeedItem[];
- };
- }
-
- function useStoredFeed(key: string): ParsedFeed | null;
- export default useStoredFeed;
-}
diff --git a/src/utils/sbomRelease.ts b/src/utils/sbomRelease.ts
deleted file mode 100644
index 7a07f4080..000000000
--- a/src/utils/sbomRelease.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-/**
- * Utility functions for extracting release data from the SBOM attestation cache.
- *
- * These helpers bridge between RSS feed item titles (which contain release tags
- * like "stable-20260501") and the structured SBOM data that contains package
- * versions, attestation state, and supply chain information.
- */
-
-import type {
- SbomAttestationsData,
- SbomRelease,
- PackageVersions,
-} from "../types/sbom";
-
-// ─── Constants ───────────────────────────────────────────────────────────────
-
-/** Maps FeedItems feedId to the corresponding SBOM stream key. */
-export const SBOM_STREAM_BY_FEED_ID: Record = {
- bluefinReleases: "bluefin-stable",
-};
-
-// ─── Tag Extraction ──────────────────────────────────────────────────────────
-
-/**
- * Extract the release tag (e.g. "stable-20260501") from a feed item title.
- * The tag is both the SBOM cache key and the GHCR registry tag.
- * Returns null if no recognizable tag pattern is found.
- */
-export const extractReleaseTag = (title: string): string | null => {
- const tagMatch = title.match(/(stable-\d{8}|beta-\d{8}|latest-\d{8})/i);
- return tagMatch ? tagMatch[1].toLowerCase() : null;
-};
-
-/**
- * Parse a release tag's date component into ISO date format (YYYY-MM-DD).
- */
-export const getReleaseDateFromTag = (
- releaseTag: string | null,
-): string | null => {
- if (!releaseTag) return null;
- const match = releaseTag.match(/(\d{8})$/);
- if (!match) return null;
- return match[1].replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3");
-};
-
-// ─── SBOM Lookup ─────────────────────────────────────────────────────────────
-
-/**
- * Look up an SBOM release entry by feed ID and item title.
- * Returns the SbomRelease if found, or null.
- */
-export const getSbomRelease = (
- cache: SbomAttestationsData,
- feedId: string,
- title: string,
-): SbomRelease | null => {
- const streamId = SBOM_STREAM_BY_FEED_ID[feedId];
- const cacheKey = extractReleaseTag(title);
- if (!streamId || !cacheKey) return null;
-
- return cache.streams?.[streamId]?.releases?.[cacheKey] ?? null;
-};
-
-/**
- * Get the package versions for a release, or null if not available.
- */
-export const getPackageVersions = (
- cache: SbomAttestationsData,
- feedId: string,
- title: string,
-): PackageVersions | null => {
- const release = getSbomRelease(cache, feedId, title);
- return release?.packageVersions ?? null;
-};
-
-// ─── Supply Chain ────────────────────────────────────────────────────────────
-
-export interface SupplyChainLinks {
- packageTagUrl: string | null;
- attestationVerified: boolean | null;
- attestationPresent: boolean | null;
-}
-
-/**
- * Build supply-chain link information for a given release title.
- * Looks up attestation state from the SBOM cache stream matching the feed.
- */
-export const getSupplyChainLinks = (
- cache: SbomAttestationsData,
- title: string,
- feedId?: string,
-): SupplyChainLinks => {
- const releaseTag = extractReleaseTag(title);
-
- if (!releaseTag) {
- return {
- packageTagUrl: null,
- attestationVerified: null,
- attestationPresent: null,
- };
- }
-
- let attestationVerified: boolean | null = null;
- let attestationPresent: boolean | null = null;
-
- const streamId =
- feedId && feedId in SBOM_STREAM_BY_FEED_ID
- ? SBOM_STREAM_BY_FEED_ID[feedId]
- : undefined;
- if (streamId && cache?.streams && streamId in cache.streams) {
- const stream = cache.streams[streamId];
- const entry = stream.releases?.[releaseTag];
- if (entry) {
- attestationVerified = entry.attestation?.verified ?? null;
- attestationPresent = entry.attestation?.present ?? null;
- }
- }
-
- return {
- packageTagUrl: `https://github.com/orgs/ublue-os/packages/container/bluefin?tag=${encodeURIComponent(releaseTag)}`,
- attestationVerified,
- attestationPresent,
- };
-};
-
-// ─── Version Summary ─────────────────────────────────────────────────────────
-
-export interface VersionChange {
- name: string;
- change: string;
-}
-
-/**
- * Build a concise version summary (Kernel, GNOME, Mesa, etc.) for a release.
- * Reads directly from the SBOM packageVersions.
- *
- * @param nvidiaVersion Optional NVIDIA version from an external source (not in SBOM)
- */
-export const extractVersionSummary = (
- cache: SbomAttestationsData,
- title: string,
- feedId: string,
- nvidiaVersion?: string | null,
-): VersionChange[] => {
- const packages = getPackageVersions(cache, feedId, title);
- if (!packages) return [];
-
- const changes: VersionChange[] = [];
- if (packages.kernel)
- changes.push({ name: "Kernel", change: packages.kernel });
- if (packages.gnome) changes.push({ name: "GNOME", change: packages.gnome });
- if (packages.mesa) changes.push({ name: "Mesa", change: packages.mesa });
- if (packages.podman)
- changes.push({ name: "Podman", change: packages.podman });
- if (packages.systemd)
- changes.push({ name: "systemd", change: packages.systemd });
- if (packages.bootc) changes.push({ name: "bootc", change: packages.bootc });
- if (nvidiaVersion) changes.push({ name: "NVIDIA", change: nvidiaVersion });
-
- return changes;
-};
-
-// ─── Package Diff (computed from SBOM) ───────────────────────────────────────
-
-export interface PackageDiffEntry {
- name: string;
- newVersion: string | null;
- oldVersion: string | null;
-}
-
-export interface PackageDiff {
- added: PackageDiffEntry[];
- changed: PackageDiffEntry[];
- removed: PackageDiffEntry[];
-}
-
-export interface MajorVersionBump {
- name: string;
- from: string;
- to: string;
-}
-
-export interface ReleaseSummary {
- packageUpdates: number;
- newPackages: number;
- removedPackages: number;
- majorBumps: number;
-}
-
-/**
- * Compute a package diff between two consecutive releases by comparing
- * their allPackages maps from the SBOM data.
- */
-export const computePackageDiff = (
- current: PackageVersions | null,
- previous: PackageVersions | null,
-): PackageDiff | null => {
- const currentPkgs = current?.allPackages;
- const previousPkgs = previous?.allPackages;
-
- // If either release lacks allPackages, we can't compute a diff
- if (!currentPkgs || !previousPkgs) return null;
-
- const added: PackageDiffEntry[] = [];
- const changed: PackageDiffEntry[] = [];
- const removed: PackageDiffEntry[] = [];
-
- // Find added and changed packages
- for (const [name, newVersion] of Object.entries(currentPkgs)) {
- const oldVersion = previousPkgs[name];
- if (oldVersion === undefined) {
- added.push({ name, newVersion, oldVersion: null });
- } else if (oldVersion !== newVersion) {
- changed.push({ name, newVersion, oldVersion });
- }
- }
-
- // Find removed packages
- for (const name of Object.keys(previousPkgs)) {
- if (currentPkgs[name] === undefined) {
- removed.push({ name, newVersion: null, oldVersion: previousPkgs[name] });
- }
- }
-
- return { added, changed, removed };
-};
-
-/**
- * Get the previous release in the same SBOM stream (by tag sort order).
- * Release tags sort lexicographically as YYYYMMDD.
- */
-export const getPreviousRelease = (
- cache: SbomAttestationsData,
- feedId: string,
- title: string,
-): SbomRelease | null => {
- const streamId = SBOM_STREAM_BY_FEED_ID[feedId];
- const cacheKey = extractReleaseTag(title);
- if (!streamId || !cacheKey) return null;
-
- const stream = cache.streams?.[streamId];
- if (!stream?.releases) return null;
-
- // Sort release keys descending (newest first)
- const sortedKeys = Object.keys(stream.releases).sort().reverse();
- const currentIdx = sortedKeys.indexOf(cacheKey);
- if (currentIdx < 0 || currentIdx >= sortedKeys.length - 1) return null;
-
- const previousKey = sortedKeys[currentIdx + 1];
- return stream.releases[previousKey] ?? null;
-};
-
-/**
- * Parse the major version number from a version string.
- */
-export const parseMajorVersion = (value: string | null): number | null => {
- if (!value) return null;
- const match = value.match(/\d+/);
- if (!match) return null;
- return Number.parseInt(match[0], 10);
-};
-
-/**
- * Extract major version bumps from a package diff.
- */
-export const extractMajorVersionBumps = (
- diff: PackageDiff | null,
-): MajorVersionBump[] => {
- if (!diff) return [];
- return diff.changed
- .flatMap((pkg) => {
- const fromMajor = parseMajorVersion(pkg.oldVersion);
- const toMajor = parseMajorVersion(pkg.newVersion);
- if (fromMajor === null || toMajor === null || toMajor <= fromMajor) {
- return [];
- }
- return [
- {
- name: pkg.name,
- from: pkg.oldVersion || "unknown",
- to: pkg.newVersion || "unknown",
- },
- ];
- })
- .slice(0, 10);
-};
-
-/**
- * Build a release summary from a computed package diff.
- */
-export const buildReleaseSummary = (
- diff: PackageDiff | null,
- majorVersionBumps: MajorVersionBump[],
-): ReleaseSummary | null => {
- if (!diff) return null;
- return {
- packageUpdates: diff.changed.length,
- newPackages: diff.added.length,
- removedPackages: diff.removed.length,
- majorBumps: majorVersionBumps.length,
- };
-};
| | | | | | |