66 dockerImageInspect ,
77 dockerLoginManagedRegistry ,
88 getCloudflareRegistryWithAccountNamespace ,
9- getDockerImageDigest ,
109 isDir ,
1110 runDockerCmd ,
1211} from "@cloudflare/containers-shared" ;
@@ -79,7 +78,7 @@ export async function buildAndMaybePush(
7978 pathToDocker : string ,
8079 push : boolean ,
8180 containerConfig ?: ContainerApp
82- ) : Promise < string > {
81+ ) : Promise < { image : string ; pushed : boolean } > {
8382 try {
8483 // account is also used to check limits below, so it is better to just pull the entire
8584 // account information here
@@ -110,10 +109,11 @@ export async function buildAndMaybePush(
110109 // account's disk size limits
111110 const inspectOutput = await dockerImageInspect ( pathToDocker , {
112111 imageTag,
113- formatString : "{{ .Size }} {{ len .RootFS.Layers }}" ,
112+ formatString :
113+ "{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}" ,
114114 } ) ;
115115
116- const [ sizeStr , layerStr ] = inspectOutput . split ( " " ) ;
116+ const [ sizeStr , layerStr , repoDigests ] = inspectOutput . split ( " " ) ;
117117 const size = parseInt ( sizeStr , 10 ) ;
118118 const layers = parseInt ( layerStr , 10 ) ;
119119
@@ -126,18 +126,46 @@ export async function buildAndMaybePush(
126126 account : account ,
127127 containerApp : containerConfig ,
128128 } ) ;
129-
129+ let pushed = false ;
130130 if ( push ) {
131131 await dockerLoginManagedRegistry ( pathToDocker ) ;
132132 try {
133+ // We don't try to parse repoDigests until this point
134+ // because we don't want to fail on parse errors if we
135+ // won't be pushing the image anyways.
136+ //
137+ // A Docker image digest is a unique, cryptographic identifier (SHA-256 hash)
138+ // representing the content of a Docker image. Unlike tags, which can be reused
139+ // or changed, a digest is immutable and ensures that the exact same image is
140+ // pulled every time. This guarantees consistency across different environments
141+ // and deployments.
142+ // From: https://docs.docker.com/dhi/core-concepts/digests/
143+ const parsedDigests = JSON . parse ( repoDigests ) ;
144+
145+ if ( ! Array . isArray ( parsedDigests ) ) {
146+ // If it's not the format we expect, fall back to pushing
147+ // since it's annoying but safe.
148+ throw new Error (
149+ `Expected RepoDigests from docker inspect to be an array but got ${ JSON . stringify ( parsedDigests ) } `
150+ ) ;
151+ }
152+
133153 const repositoryOnly = imageTag . split ( ":" ) [ 0 ] ;
134154 // if this succeeds it means this image already exists remotely
135155 // if it fails it means it doesn't exist remotely and should be pushed.
136- const localDigest = await getDockerImageDigest ( pathToDocker , imageTag ) ;
137- const digest = repositoryOnly + "@" + localDigest ;
156+ const digests = parsedDigests . filter (
157+ ( d ) : d is string =>
158+ typeof d === "string" && d . split ( "@" ) [ 0 ] === repositoryOnly
159+ ) ;
160+ if ( digests . length !== 1 ) {
161+ throw new Error (
162+ `Expected there to only be 1 valid digests for this repository: ${ repositoryOnly } but there were ${ digests . length } `
163+ ) ;
164+ }
165+
138166 await runDockerCmd (
139167 pathToDocker ,
140- [ "manifest" , "inspect" , digest ] ,
168+ [ "manifest" , "inspect" , digests [ 0 ] ] ,
141169 "ignore"
142170 ) ;
143171
@@ -146,14 +174,20 @@ export async function buildAndMaybePush(
146174 `Untagging built image: ${ imageTag } since there was no change.`
147175 ) ;
148176 await runDockerCmd ( pathToDocker , [ "image" , "rm" , imageTag ] ) ;
149- return "" ;
177+ return { image : digests [ 0 ] , pushed : false } ;
150178 } catch ( error ) {
151179 logger . log ( `Image does not exist remotely, pushing: ${ imageTag } ` ) ;
180+ if ( error instanceof Error ) {
181+ logger . debug (
182+ `Checking for local image ${ imageTag } failed with error: ${ error . message } `
183+ ) ;
184+ }
152185
153186 await runDockerCmd ( pathToDocker , [ "push" , imageTag ] ) ;
187+ pushed = true ;
154188 }
155189 }
156- return imageTag ;
190+ return { image : imageTag , pushed : pushed } ;
157191 } catch ( error ) {
158192 if ( error instanceof Error ) {
159193 throw new UserError ( error . message ) ;
0 commit comments