From 2f92e3825294200431cd63ef36190da7b1ea0a0f Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Tue, 31 Mar 2026 18:13:56 -0400 Subject: [PATCH 1/4] fix(ci): store SBOM data in GHA cache, remove seed file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite update-sbom-cache.yml: use actions/cache/save instead of git commit+push; drop contents:write permission (now contents:read) - Add incremental restore of existing SBOM cache before fetch so only changed streams are re-fetched - Update pages.yml restore-keys: add github-data-sbom- prefix so the site build picks up SBOM data from the update-sbom-cache workflow - Remove /static/data/sbom-attestations.json seed file exception from .gitignore — data files are runtime-only, not committed - Add src/types/sbom-attestations.d.ts ambient module declaration so tsc resolves @site/static/data/sbom-attestations.json without the file existing locally Assisted-by: Claude Sonnet 4.6 via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pages.yml | 1 + .github/workflows/update-sbom-cache.yml | 27 ++++++++++++++----------- .gitignore | 2 -- src/types/sbom-attestations.d.ts | 15 ++++++++++++++ static/data/sbom-attestations.json | 1 - 5 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/types/sbom-attestations.d.ts delete mode 100644 static/data/sbom-attestations.json diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a644cf5e..6f3099e1 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -50,6 +50,7 @@ jobs: path: static/data key: github-data-${{ hashFiles('scripts/fetch-*.js') }} restore-keys: | + github-data-sbom- github-data- - name: Install dependencies diff --git a/.github/workflows/update-sbom-cache.yml b/.github/workflows/update-sbom-cache.yml index ec549df5..67cef484 100644 --- a/.github/workflows/update-sbom-cache.yml +++ b/.github/workflows/update-sbom-cache.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: - contents: write # for git commit + push + contents: read jobs: update-sbom-cache: @@ -46,6 +46,15 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci + - name: Restore existing SBOM data cache (for incremental updates) + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: static/data + key: github-data-sbom-${{ github.run_id }} + restore-keys: | + github-data-sbom- + github-data- + - name: Install cosign uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 @@ -65,14 +74,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.PROJECT_READ_TOKEN }} run: npm run fetch-sbom - - name: Commit updated cache - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add static/data/sbom-attestations.json - if git diff --cached --quiet; then - echo "No changes to sbom-attestations.json — skipping commit." - else - git commit -m "chore(sbom): update attestation cache $(date -u +%Y-%m-%d)" - git push - fi + - name: Save SBOM data cache + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: static/data + key: github-data-sbom-${{ github.run_id }} diff --git a/.gitignore b/.gitignore index cdf57fbf..15c8e52c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,6 @@ /docs/contributing.md /static/feeds/*.json /static/data/*.json -# Seed file for SBOM attestation cache — must exist at build time -!/static/data/sbom-attestations.json # Misc .DS_Store diff --git a/src/types/sbom-attestations.d.ts b/src/types/sbom-attestations.d.ts new file mode 100644 index 00000000..ff2fd796 --- /dev/null +++ b/src/types/sbom-attestations.d.ts @@ -0,0 +1,15 @@ +/** + * Type declaration for the SBOM attestation cache data file. + * + * static/data/sbom-attestations.json is generated at CI time by + * scripts/fetch-github-sbom.js and restored from GitHub Actions cache before + * the build runs. It is gitignored and will not exist during local `tsc` + * typechecks — this ambient declaration satisfies the compiler without + * requiring the file to be present on disk. + */ + +declare module "@site/static/data/sbom-attestations.json" { + import type { SbomAttestationsData } from "@site/src/types/sbom"; + const data: SbomAttestationsData; + export default data; +} diff --git a/static/data/sbom-attestations.json b/static/data/sbom-attestations.json deleted file mode 100644 index fcb6585c..00000000 --- a/static/data/sbom-attestations.json +++ /dev/null @@ -1 +0,0 @@ -{ "generatedAt": null, "streams": {} } From 98cb43d7f86d18a8e4323b5c0be01b46cb787e77 Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Tue, 31 Mar 2026 18:18:39 -0400 Subject: [PATCH 2/4] fix(sbom): fix cosign error semantics, NDJSON parsing, and canonical tag enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verifyAttestation: unexpected tooling/auth failures now return present:null with errorKind:'tooling' instead of present:true — callers can distinguish 'no attestation' (present:false) from tooling errors (present:null) - verifyAttestation: parse only stdout for NDJSON attestation bundles; stderr carries status text ('Verification OK') that must not be treated as JSON - fetchAllPackageVersions: rethrow Packages API errors so callers can fall back to the existing cache instead of silently receiving an empty version list and wiping all releases for that stream - findRecentTagsForStream: enforce canonical tag matching — only exact '-YYYYMMDD' tags are accepted; non-canonical variants like lts-hwe-testing-20260331 previously collapsed into the canonical cache key and could silently overwrite valid entries Assisted-by: Claude Sonnet 4.6 via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/fetch-github-sbom.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts/fetch-github-sbom.js b/scripts/fetch-github-sbom.js index b3041ee2..c63dcd99 100644 --- a/scripts/fetch-github-sbom.js +++ b/scripts/fetch-github-sbom.js @@ -159,8 +159,11 @@ async function fetchAllPackageVersions(org, pkg) { try { batch = await fetchJson(url); } catch (err) { - console.warn(` Packages API page ${page} failed: ${err.message}`); - break; + // Rethrow so the caller (main) can preserve existing cache for this package + // rather than silently returning an empty result and clearing all releases. + throw new Error( + `Packages API page ${page} for ${org}/${pkg} failed: ${err.message}`, + ); } if (!Array.isArray(batch) || batch.length === 0) break; versions.push(...batch); @@ -254,16 +257,21 @@ async function verifyAttestation(imageRef, spec) { error: "no attestation", }; } + // Unexpected tooling/registry/auth failure — do NOT claim present: true. + // Callers use present: false (no attestation) vs present: null (error/unknown) + // to distinguish absence from verification failure. return { - present: true, + present: null, verified: false, predicateType: null, + errorKind: "tooling", error: String(err.stderr || err.message), }; } - // cosign outputs NDJSON (one JSON object per line) - const lines = (stdout + stderr) + // cosign outputs NDJSON (one JSON object per line) on stdout only; + // stderr carries status messages ("Verification OK") that must not be parsed. + const lines = stdout .split("\n") .map((l) => l.trim()) .filter((l) => l.startsWith("{")); @@ -547,6 +555,12 @@ function findRecentTagsForStream(allVersions, spec) { const dateStr = extractDateFromTag(normalised); if (!dateStr) continue; + // Enforce canonical tag: only exact `-YYYYMMDD` is accepted. + // Non-canonical variants like lts-hwe-testing-20260331 would normalise to + // lts-20260331 and silently overwrite valid canonical entries. + const expectedCanonical = `${spec.streamPrefix}-${dateStr}`; + if (normalised !== expectedCanonical) continue; + found.push({ tag: normalised, cacheKey: buildCacheKey(spec.streamPrefix, dateStr), From f1416526cf3167731d61f17baa05cf0fdcb331cb Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Tue, 31 Mar 2026 18:19:33 -0400 Subject: [PATCH 3/4] fix(sbom): fix stable-daily feed aliasing; fix MD040 fence language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch-github-images.js: latestFeedItem() was aliasing stream 'stable-daily' to 'stable', causing stable-daily image cards to show stable release metadata. Now returns null so callers render unknown values — no feed exists for daily. - docs/devcontainers.md: add 'json' language specifier to fenced code block (MD040 lint fix) for the Dev Containers podman settings snippet. Assisted-by: Claude Sonnet 4.6 via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/devcontainers.md | 4 +++- scripts/fetch-github-images.js | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/devcontainers.md b/docs/devcontainers.md index 57b040ac..925dcbd8 100644 --- a/docs/devcontainers.md +++ b/docs/devcontainers.md @@ -40,11 +40,13 @@ docker --version By default, the Dev Containers extension uses docker. To switch to podman, make the following changes in Dev Containers settings: -``` + +```json "dev.containers.dockerComposePath": "podman-compose" "dev.containers.dockerPath": "podman" "dev.containers.dockerSocketPath": "/run/user/1000/podman/podman.sock" ``` + You can use `systemctl --user status podman.socket` to find the socket path corresponding to your user id. Or if you want to run podman in rootful mode, use `/run/podman/podman.sock` instead. ## Getting Started diff --git a/scripts/fetch-github-images.js b/scripts/fetch-github-images.js index 55091406..282fce90 100644 --- a/scripts/fetch-github-images.js +++ b/scripts/fetch-github-images.js @@ -210,8 +210,12 @@ function parseFedoraFromImageVersion(imageVersion) { function latestFeedItem(feeds, source) { if (!source) return null; + // stable-daily has no dedicated release feed — returning stable metadata would + // misrepresent daily-only images as stable releases. Return null so callers + // render unknown values instead. + if (source.stream === "stable-daily") return null; const items = source.feed === "lts" ? feeds.lts.items : feeds.bluefin.items; - const stream = source.stream === "stable-daily" ? "stable" : source.stream; + const stream = source.stream; const match = items.find((item) => { const title = (item.title || "").toLowerCase(); From 9a69a6365a2e394360eb54e8fb1616e5eda76f9c Mon Sep 17 00:00:00 2001 From: "Jorge O. Castro" Date: Tue, 31 Mar 2026 18:35:08 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix(sbom):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20preserve=20cache=20on=20API=20failure;=20fix=20AttestationRe?= =?UTF-8?q?sult=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1: fetchAllPackageVersions error path previously set allVersionsByPackage to an empty array, causing processStream to emit zero releases and clear the cache for all streams. Fix: leave the key absent on failure; processStream now detects the missing key and returns the existing cache entry instead. Issue 2: verifyAttestation returns present:null and errorKind:"tooling" for tooling/registry/auth failures, but AttestationResult had present:boolean (no null) and no errorKind field. The processStream reconstruction also silently dropped errorKind. Fix: widen present to boolean|null, add optional errorKind:"tooling" field to the interface, and propagate errorKind through the reconstruction spread. Assisted-by: Claude Sonnet 4.6 via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/fetch-github-sbom.js | 19 +++++++++++++++++-- src/types/sbom.ts | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/scripts/fetch-github-sbom.js b/scripts/fetch-github-sbom.js index c63dcd99..8611dc5d 100644 --- a/scripts/fetch-github-sbom.js +++ b/scripts/fetch-github-sbom.js @@ -591,7 +591,18 @@ function findRecentTagsForStream(allVersions, spec) { */ async function processStream(spec, allVersionsByPackage, existing) { const pkgKey = `${spec.org}/${spec.package}`; - const allVersions = allVersionsByPackage.get(pkgKey) || []; + // If the key is absent the Packages API fetch failed — preserve existing cache. + if (!allVersionsByPackage.has(pkgKey)) { + if (existing?.streams?.[spec.id]) { + console.log( + ` ${spec.id}: Packages API unavailable — keeping existing cache`, + ); + return existing.streams[spec.id]; + } + // No existing cache to fall back to; return empty stream. + return { ...spec, releases: {} }; + } + const allVersions = allVersionsByPackage.get(pkgKey); const recentTags = findRecentTagsForStream(allVersions, spec); console.log( @@ -630,6 +641,9 @@ async function processStream(spec, allVersionsByPackage, existing) { verified: attestation.verified, predicateType: attestation.predicateType, slsaType: SLSA_TYPE, + ...(attestation.errorKind !== undefined && { + errorKind: attestation.errorKind, + }), error: attestation.error, }; } @@ -728,7 +742,8 @@ async function main() { console.log(` ${versions.length} versions fetched`); } catch (err) { console.error(` Failed to fetch versions for ${key}: ${err.message}`); - allVersionsByPackage.set(key, []); + // Do NOT set an empty array — leave the key absent so processStream + // detects the fetch failure and preserves the existing cache instead. } } diff --git a/src/types/sbom.ts b/src/types/sbom.ts index 5ac53f21..23502f21 100644 --- a/src/types/sbom.ts +++ b/src/types/sbom.ts @@ -7,19 +7,29 @@ export interface AttestationResult { /** * Whether a SLSA provenance attestation was found for this image. + * true = attestation present and verified. * false = no attestation published (the command will fail if run). + * null = verification could not complete due to a tooling/registry/auth + * error; attestation existence is unknown. */ - present: boolean; + present: boolean | null; /** * Whether the attestation passed cosign signature verification. - * false with present:true = attestation exists but verification failed. + * false with present:true = attestation exists but verification failed. * false with present:false = attestation does not exist. + * false with present:null = verification could not be attempted. */ verified: boolean; /** SLSA predicate type URI, populated when present:true */ predicateType: string | null; /** SLSA type URL used during verification */ slsaType: string; + /** + * Error classification for present:null results. + * "tooling" = cosign/oras/registry error (transient or auth failure). + * Absent (undefined) for present:true/false results. + */ + errorKind?: "tooling"; /** Human-readable error string when present:false or verified:false */ error: string | null; }