Skip to content

fix: re-push image even when only the dockerfile has changed #10248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 11, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/heavy-phones-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

fix: re-push container images on deploy even if the only change was to the Dockerfile
36 changes: 19 additions & 17 deletions packages/wrangler/src/__tests__/cloudchamber/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
dockerLoginManagedRegistry,
getCloudflareContainerRegistry,
runDockerCmd,
runDockerCmdWithOutput,
} from "@cloudflare/containers-shared";
import { UserError } from "../../errors";
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
Expand All @@ -17,6 +18,7 @@ vi.mock("@cloudflare/containers-shared", async (importOriginal) => {
return Object.assign({}, actual, {
dockerLoginManagedRegistry: vi.fn(),
runDockerCmd: vi.fn(),
runDockerCmdWithOutput: vi.fn(),
dockerBuild: vi.fn(() => ({ abort: () => {}, ready: Promise.resolve() })),
dockerImageInspect: vi.fn(),
});
Expand All @@ -37,6 +39,9 @@ describe("buildAndMaybePush", () => {
.mockResolvedValueOnce("[]")
// return image size and number of layers
.mockResolvedValueOnce("53387881 2");
vi.mocked(runDockerCmdWithOutput).mockReturnValueOnce(
'{"Descriptor":{"digest":"config-sha"}}'
);
mkdirSync("./container-context");

writeFileSync("./container-context/Dockerfile", dockerfile);
Expand Down Expand Up @@ -71,7 +76,7 @@ describe("buildAndMaybePush", () => {
"/custom/docker/path",
{
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
formatString: "{{json .RepoDigests}}",
formatString: "{{ json .RepoDigests }} {{ .Id }}",
}
);
expect(dockerImageInspect).toHaveBeenNthCalledWith(
Expand Down Expand Up @@ -130,7 +135,7 @@ describe("buildAndMaybePush", () => {
expect(dockerImageInspect).toHaveBeenCalledTimes(2);
expect(dockerImageInspect).toHaveBeenNthCalledWith(1, "docker", {
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
formatString: "{{json .RepoDigests}}",
formatString: "{{ json .RepoDigests }} {{ .Id }}",
});
expect(dockerImageInspect).toHaveBeenNthCalledWith(2, "docker", {
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
Expand All @@ -147,7 +152,7 @@ describe("buildAndMaybePush", () => {
vi.mocked(dockerImageInspect).mockReset();
vi.mocked(dockerImageInspect)
.mockResolvedValueOnce(
'["registry.cloudflare.com/test-app@sha256:three"]'
'["registry.cloudflare.com/test-app@sha256:three"] config-sha'
)
.mockResolvedValueOnce("53387881 2");
await runWrangler(
Expand All @@ -167,26 +172,23 @@ describe("buildAndMaybePush", () => {
],
dockerfile,
});
expect(runDockerCmd).toHaveBeenCalledTimes(2);
expect(runDockerCmd).toHaveBeenNthCalledWith(
1,
"docker",
[
"manifest",
"inspect",
`${getCloudflareContainerRegistry()}/some-account-id/test-app@sha256:three`,
],
"ignore"
);
expect(runDockerCmd).toHaveBeenNthCalledWith(2, "docker", [
expect(runDockerCmdWithOutput).toHaveBeenCalledOnce();
expect(runDockerCmdWithOutput).toHaveBeenCalledWith("docker", [
"manifest",
"inspect",
"-v",
`${getCloudflareContainerRegistry()}/some-account-id/test-app@sha256:three`,
]);
expect(runDockerCmd).toHaveBeenCalledTimes(1);
expect(runDockerCmd).toHaveBeenCalledWith("docker", [
"image",
"rm",
`${getCloudflareContainerRegistry()}/test-app:tag`,
]);
expect(dockerImageInspect).toHaveBeenCalledTimes(2);
expect(dockerImageInspect).toHaveBeenNthCalledWith(1, "docker", {
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
formatString: "{{json .RepoDigests}}",
formatString: "{{ json .RepoDigests }} {{ .Id }}",
});
expect(dockerImageInspect).toHaveBeenNthCalledWith(2, "docker", {
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
Expand All @@ -212,7 +214,7 @@ describe("buildAndMaybePush", () => {
],
dockerfile,
});
expect(dockerImageInspect).toHaveBeenCalledOnce();
expect(dockerImageInspect).not.toHaveBeenCalledOnce();
expect(dockerLoginManagedRegistry).not.toHaveBeenCalled();
});

Expand Down
118 changes: 48 additions & 70 deletions packages/wrangler/src/__tests__/containers/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
import * as fs from "node:fs";
import { PassThrough, Writable } from "node:stream";
import {
Expand Down Expand Up @@ -111,19 +111,19 @@ describe("wrangler deploy with containers", () => {
await runWrangler("deploy index.js");

expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your Worker has access to the following bindings:
Binding Resource
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your Worker has access to the following bindings:
Binding Resource
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object

Uploaded test-name (TIMINGS)
Building image my-container:Galaxy
Image does not exist remotely, pushing: registry.cloudflare.com/some-account-id/my-container:Galaxy
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
Uploaded test-name (TIMINGS)
Building image my-container:Galaxy
Image does not exist remotely, pushing: registry.cloudflare.com/some-account-id/my-container:Galaxy
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`""`);
expect(cliStd.stdout).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -448,19 +448,19 @@ describe("wrangler deploy with containers", () => {
await runWrangler("deploy --cwd src");

expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your Worker has access to the following bindings:
Binding Resource
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object

Uploaded test-name (TIMINGS)
Building image my-container:Galaxy
Image does not exist remotely, pushing: registry.cloudflare.com/some-account-id/my-container:Galaxy
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your Worker has access to the following bindings:
Binding Resource
env.EXAMPLE_DO_BINDING (ExampleDurableObject) Durable Object

Uploaded test-name (TIMINGS)
Building image my-container:Galaxy
Image does not exist remotely, pushing: registry.cloudflare.com/some-account-id/my-container:Galaxy
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`""`);
});
Expand Down Expand Up @@ -599,7 +599,7 @@ describe("wrangler deploy with containers", () => {
│ [containers.constraints]
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -740,7 +740,7 @@ describe("wrangler deploy with containers", () => {
│ [containers.constraints]
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -908,7 +908,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -954,7 +954,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -1012,7 +1012,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -1070,7 +1070,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -1124,7 +1124,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -1183,7 +1183,7 @@ describe("wrangler deploy with containers", () => {
│ tier = 1
│ SUCCESS Modified application my-container
│ SUCCESS Modified application my-container (Application ID: abc)
╰ Applied changes

Expand Down Expand Up @@ -1366,7 +1366,6 @@ describe("wrangler deploy with containers dry run", () => {
mockDockerImageInspectDigests("my-container", "worker")
)
.mockImplementationOnce(mockDockerLogin("mockpassword"))
.mockImplementationOnce(mockDockerManifestInspect("my-container", true))
.mockImplementationOnce(mockDockerPush("my-container", "worker"));

vi.stubEnv("WRANGLER_DOCKER_BIN", "/usr/bin/docker");
Expand Down Expand Up @@ -1412,7 +1411,7 @@ function createDockerMockChain(
mockDockerImageInspectDigests(containerName, tag),
mockDockerImageInspectSize(containerName, tag),
mockDockerLogin("mockpassword"),
mockDockerManifestInspect("some-account-id/" + containerName, true),
// Skip manifest inspect mock - it's not being called due to empty repoDigests
mockDockerTag(containerName, "some-account-id/" + containerName, tag),
mockDockerPush("some-account-id/" + containerName, tag),
mockDockerImageDelete("some-account-id/" + containerName, tag),
Expand Down Expand Up @@ -1441,8 +1440,17 @@ function setupDockerMocks(
.mockImplementationOnce(mocks[4])
.mockImplementationOnce(mocks[5])
.mockImplementationOnce(mocks[6])
.mockImplementationOnce(mocks[7])
.mockImplementationOnce(mocks[8]);
.mockImplementationOnce(mocks[7]);
// Default mock for execFileSync to handle docker verification and other calls
vi.mocked(execFileSync).mockImplementation(
(_file: string, args?: readonly string[]) => {
// Handle docker info calls (for verification)
if (args && args[0] === "manifest") {
return "i promise I am an unsuccessful docker manifest call";
}
return "";
}
);
}

// Common test setup
Expand Down Expand Up @@ -1677,7 +1685,7 @@ function mockDockerImageInspectDigests(containerName: string, tag: string) {
"inspect",
`${getCloudflareContainerRegistry()}/${containerName}:${tag}`,
"--format",
"{{json .RepoDigests}}",
"{{ json .RepoDigests }} {{ .Id }}",
]);

const stdout = new PassThrough();
Expand All @@ -1697,7 +1705,7 @@ function mockDockerImageInspectDigests(containerName: string, tag: string) {
setImmediate(() => {
stdout.emit(
"data",
`["${getCloudflareContainerRegistry()}/${containerName}@sha256:three"]`
`["${getCloudflareContainerRegistry()}/${containerName}@sha256:three"] config-sha`
);
});

Expand Down Expand Up @@ -1774,36 +1782,6 @@ function mockDockerLogin(expectedPassword: string) {
};
}

function mockDockerManifestInspect(containerName: string, shouldFail = true) {
return (cmd: string, args: readonly string[]) => {
expect(cmd).toBe("/usr/bin/docker");
expect(args[0]).toBe("manifest");
expect(args[1]).toBe("inspect");
expect(args[2]).toEqual(`${containerName}@three`);
expect(args).toEqual([
"manifest",
"inspect",
`${getCloudflareContainerRegistry()}/${containerName}@three`,
]);
const readable = new Writable({
write() {},
final() {},
});
return {
stdout: Buffer.from(
"i promise I am an unsuccessful docker manifest call"
),
stdin: readable,
on: function (reason: string, cbPassed: (code: number) => unknown) {
if (reason === "close") {
cbPassed(shouldFail ? 1 : 0);
}
return this;
},
} as unknown as ChildProcess;
};
}

function mockDockerPush(containerName: string, tag: string) {
return (cmd: string, args: readonly string[]) => {
expect(cmd).toBe("/usr/bin/docker");
Expand Down
Loading
Loading