Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions scripts/fetch-github-images.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ async function inspectImage(imageRef, tag) {
return JSON.parse(stdout);
}

// A product's primary stream tag (e.g. Utah's `testing`) is only considered
// published once it actually shows up in the registry's tag list. This keeps
// buildTopStreams' "no exact match" fallback from fabricating a switch
// command for a tag that has never been pushed at all.
function isImagePublished(spec, tagSet) {
return spec.streamOrder.some((tag) => tagSet.has(tag));
}

function buildTopStreams(spec, tagSet) {
const top = [];
for (const tag of spec.streamOrder) {
Expand Down Expand Up @@ -597,6 +605,7 @@ async function buildProduct(spec, feeds, cachedById, ageHours, sbomCache) {
tags = existing?.allTags || [];
}
const tagSet = new Set(tags);
const imagePublished = isImagePublished(spec, tagSet);

let nvidiaTagSet = null;
if (spec.nvidiaPackage) {
Expand All @@ -610,16 +619,16 @@ async function buildProduct(spec, feeds, cachedById, ageHours, sbomCache) {
}
}

const streams = attachNvidiaCommands(
buildTopStreams(spec, tagSet),
spec,
nvidiaTagSet,
);
const testingStreams = attachNvidiaTestingCommands(
buildTestingStreams(spec, tags),
spec,
nvidiaTagSet,
);
const streams = imagePublished
? attachNvidiaCommands(buildTopStreams(spec, tagSet), spec, nvidiaTagSet)
: [];
const testingStreams = imagePublished
? attachNvidiaTestingCommands(
buildTestingStreams(spec, tags),
spec,
nvidiaTagSet,
)
: [];

for (const stream of streams) {
stream.versions = await buildStreamVersionInfo(
Expand Down Expand Up @@ -717,8 +726,17 @@ async function buildProduct(spec, feeds, cachedById, ageHours, sbomCache) {
metadata,
metadataSource,
versions,
security: buildSecurityInfo(spec, inspectTag),
security: imagePublished
? buildSecurityInfo(spec, inspectTag)
: {
cosignKeyUrl: null,
verifyCommand: null,
attestCommand: null,
hasAttestation: false,
sbomCommand: null,
},
inspectTag,
imagePublished,
lastPublishedAt: lastPublishedAt,
stale,
keepEvenIfStale: Boolean(spec.keepEvenIfStale),
Expand Down Expand Up @@ -866,6 +884,7 @@ module.exports = {
handleUnavailableCache,
hasUsableSbomData,
isCurrentImageCatalog,
isImagePublished,
main,
normalizeTestingTag,
reportMainError,
Expand Down
9 changes: 9 additions & 0 deletions scripts/fetch-github-images.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
cacheAgeHours,
handleUnavailableCache,
isCurrentImageCatalog,
isImagePublished,
main,
normalizeTestingTag,
reportMainError,
Expand Down Expand Up @@ -358,6 +359,14 @@ test("buildSecurityInfo returns keyless verification commands for keyless repos"
assert.match(info.attestCommand, /https:\/\/slsa\.dev\/provenance\/v1/);
});

test("isImagePublished is false when the registry has no matching stream tag", () => {
const spec = { streamOrder: ["testing"] };

assert.equal(isImagePublished(spec, new Set()), false);
assert.equal(isImagePublished(spec, new Set(["unstable", "latest"])), false);
assert.equal(isImagePublished(spec, new Set(["testing"])), true);
});

test("buildSecurityInfo returns keyless verification commands for Utah", () => {
const info = buildSecurityInfo(
{
Expand Down
204 changes: 115 additions & 89 deletions src/components/ImagesCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface Product {
summary: string;
artwork: "bluefin" | "achillobator" | "dakotaraptor";
supportedArches?: string[] | null;
imageRef?: string;
downloads?: {
display: string;
source: "live" | "cache" | "unavailable";
Expand Down Expand Up @@ -94,6 +95,7 @@ interface Product {
sbomCommand?: string | null;
} | null;
lastPublishedAt?: string | null;
imagePublished?: boolean;
}

interface ImagesCatalog {
Expand Down Expand Up @@ -341,6 +343,7 @@ export default function ImagesCatalogComponent({
const releaseUrl = assetsLink(product.versions?.release?.url);
const lastValidated = formatDate(catalog.generatedAt || null);
const lastPublished = formatDate(product.lastPublishedAt || null);
const isAwaitingInitialRelease = product.imagePublished === false;
const hasNvidiaVariant =
product.streams.some((entry) => Boolean(entry.nvidiaCommand)) ||
product.testingStreams.some((entry) => Boolean(entry.nvidiaCommand));
Expand Down Expand Up @@ -484,7 +487,13 @@ export default function ImagesCatalogComponent({
)}
</div>

{product.streams.length > 0 ? (
{isAwaitingInitialRelease ? (
<p className={styles.emptyText}>
Awaiting initial release: no image has been published to{" "}
<code>{product.imageRef}</code> yet. Switch commands will
appear here once the first build ships.
</p>
) : product.streams.length > 0 ? (
<Tabs
groupId={`streams-${product.id}`}
values={product.streams.map((entry) => ({
Expand Down Expand Up @@ -532,15 +541,17 @@ export default function ImagesCatalogComponent({
<p className={styles.emptyText}>No active tags.</p>
)}

<details className={styles.testingDetails}>
<summary>
Testing Branches ({product.testingStreams.length})
</summary>
<StreamList
streams={product.testingStreams}
preferNvidia={nvidiaEnabled}
/>
</details>
{!isAwaitingInitialRelease && (
<details className={styles.testingDetails}>
<summary>
Testing Branches ({product.testingStreams.length})
</summary>
<StreamList
streams={product.testingStreams}
preferNvidia={nvidiaEnabled}
/>
</details>
)}
</section>

<section
Expand All @@ -549,91 +560,106 @@ export default function ImagesCatalogComponent({
<Heading as="h3" className={styles.sectionTitle}>
Signing and SBOM
</Heading>
{product.security?.cosignKeyUrl ? (
{isAwaitingInitialRelease ? (
<p className={styles.securityText}>
Key: <code>{product.security.cosignKeyUrl}</code>
Signing, provenance, and SBOM commands will appear here once{" "}
<code>{product.imageRef}</code> has a published image to
verify.
</p>
) : (
<p className={styles.securityText}>
No published cosign key URL in this catalog.
</p>
)}

<Tabs
groupId={`security-${product.id}`}
values={[
{ label: "Verify Signature", value: "verify-signature" },
{ label: "Verify Provenance", value: "verify-provenance" },
{ label: "Inspect SBOM", value: "generate-sbom" },
]}
>
<TabItem value="verify-signature">
<p className={styles.tabCopy}>
Signature verification confirms this image was signed by the
expected maintainers and helps detect tampering before
deployment.{" "}
<Link
to="https://docs.sigstore.dev/cosign/verifying/verify/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
{product.security?.verifyCommand && (
<CodeBlock language="bash">
{product.security.verifyCommand}
</CodeBlock>
)}
</TabItem>
<TabItem value="verify-provenance">
<p className={styles.tabCopy}>
Provenance attestation lets you validate how the image was
built in CI so you can make trust decisions from evidence.{" "}
<Link
to="https://slsa.dev/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
{product.security?.attestCommand && (
<CodeBlock language="bash">
{product.security.attestCommand}
</CodeBlock>
<>
{product.security?.cosignKeyUrl ? (
<p className={styles.securityText}>
Key: <code>{product.security.cosignKeyUrl}</code>
</p>
) : (
<p className={styles.securityText}>
No published cosign key URL in this catalog.
</p>
)}
{product.security?.attestCommand &&
product.security.hasAttestation === false && (

<Tabs
groupId={`security-${product.id}`}
values={[
{ label: "Verify Signature", value: "verify-signature" },
{
label: "Verify Provenance",
value: "verify-provenance",
},
{ label: "Inspect SBOM", value: "generate-sbom" },
]}
>
<TabItem value="verify-signature">
<p className={styles.tabCopy}>
Note: attestations are not yet published for this image.
The command is provided for when they are.
Signature verification confirms this image was signed by
the expected maintainers and helps detect tampering
before deployment.{" "}
<Link
to="https://docs.sigstore.dev/cosign/verifying/verify/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
)}
</TabItem>
<TabItem value="generate-sbom">
<p className={styles.tabCopy}>
SBOMs are published alongside each image as OCI referrers.
Use oras to inspect attached artifacts and pull the SBOM for
audits, policy checks, and vulnerability triage.{" "}
<Link
to="https://oras.land/docs/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
{product.security?.sbomCommand && (
<CodeBlock language="bash">
{product.security.sbomCommand}
</CodeBlock>
)}
</TabItem>
</Tabs>
{product.security?.verifyCommand && (
<CodeBlock language="bash">
{product.security.verifyCommand}
</CodeBlock>
)}
</TabItem>
<TabItem value="verify-provenance">
<p className={styles.tabCopy}>
Provenance attestation lets you validate how the image
was built in CI so you can make trust decisions from
evidence.{" "}
<Link
to="https://slsa.dev/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
{product.security?.attestCommand && (
<CodeBlock language="bash">
{product.security.attestCommand}
</CodeBlock>
)}
{product.security?.attestCommand &&
product.security.hasAttestation === false && (
<p className={styles.tabCopy}>
Note: attestations are not yet published for this
image. The command is provided for when they are.
</p>
)}
</TabItem>
<TabItem value="generate-sbom">
<p className={styles.tabCopy}>
SBOMs are published alongside each image as OCI
referrers. Use oras to inspect attached artifacts and
pull the SBOM for audits, policy checks, and
vulnerability triage.{" "}
<Link
to="https://oras.land/docs/"
target="_blank"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
{product.security?.sbomCommand && (
<CodeBlock language="bash">
{product.security.sbomCommand}
</CodeBlock>
)}
</TabItem>
</Tabs>
</>
)}
</section>
</article>
);
Expand Down
Loading