| 1 | import type * as Redacted from "effect/Redacted"; |
| 2 | |
| 3 | export interface ImageRegistry { |
| 4 | /** Registry host, e.g. `ghcr.io`. */ |
| 5 | server: string; |
| 6 | /** Registry username. */ |
| 7 | username: string; |
| 8 | /** Registry password. Use `Redacted.make(...)` or `Config.Redacted(...)`. */ |
| 9 | password: Redacted.Redacted<string>; |
| 10 | } |
| 11 | |
| 12 | /** Strips the tag and digest from an image reference, leaving the repository. */ |
| 13 | export const repositoryFromImageRef = (imageRef: string): string => { |
| 14 | const withoutDigest = imageRef.includes("@") |
| 15 | ? imageRef.slice(0, imageRef.indexOf("@")) |
| 16 | : imageRef; |
| 17 | const tagSeparator = withoutDigest.lastIndexOf(":"); |
| 18 | const pathSeparator = withoutDigest.lastIndexOf("/"); |
| 19 | return tagSeparator > pathSeparator |
| 20 | ? withoutDigest.slice(0, tagSeparator) |
| 21 | : withoutDigest; |
| 22 | }; |
| 23 | |
| 24 | /** |
| 25 | * Prefixes an image reference with the registry host unless the reference |
| 26 | * already carries a registry prefix (a dotted host, a host:port, or `localhost`). |
| 27 | */ |
| 28 | export const withRegistryHost = ( |
| 29 | imageRef: string, |
| 30 | registry: { server: string }, |
| 31 | ): string => { |
| 32 | const registryHost = registry.server.replace(/\/$/, ""); |
| 33 | const firstSegment = imageRef.split("/")[0]; |
| 34 | const hasRegistryPrefix = |
| 35 | imageRef.includes("/") && |
| 36 | (firstSegment.includes(".") || |
| 37 | firstSegment.includes(":") || |
| 38 | firstSegment === "localhost"); |
| 39 | return hasRegistryPrefix ? imageRef : `${registryHost}/${imageRef}`; |
| 40 | }; |
| 41 | |
| 42 | /** Extracts the `repository@sha256:...` digest from `docker push` output. */ |
| 43 | export const parseRepoDigest = ( |
| 44 | imageRef: string, |
| 45 | output: string, |
| 46 | ): string | undefined => { |
| 47 | const match = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/i.exec(output); |
| 48 | if (!match) return undefined; |
| 49 | return `${repositoryFromImageRef(imageRef)}@${match[1]}`; |
| 50 | }; |
| 51 | |
| 52 | /** |
| 53 | * Parses an image's RFC 3339 `Created` timestamp into epoch milliseconds. |
| 54 | * |
| 55 | * Docker only reports `Created` when the image config carries a creation time: |
| 56 | * API >= 1.44 omits it, older APIs backfill the year-1 zero value |
| 57 | * (`0001-01-01T00:00:00Z`), and 25.0.0–25.0.3 returned an empty string. In any |
| 58 | * of those cases there is no real build time, so we fall back to the wall clock. |
| 59 | */ |
| 60 | export const parseCreatedAt = (created: string | undefined): number => { |
| 61 | const parsed = created ? Date.parse(created) : Number.NaN; |
| 62 | return Number.isNaN(parsed) || parsed <= 0 ? Date.now() : parsed; |
| 63 | }; |