From c66fd80dd14c0bbf750c97e7c74038862ef35c99 Mon Sep 17 00:00:00 2001 From: Tyler <53561637+im-tyler@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:59:43 -0700 Subject: [PATCH] fix(build): always docker build --pull so base images track current registry tags A stale build-host cache resolves FROM tags to old images indefinitely. That shipped a pre-security-release Node (CVE-2026-111497 window, 2026-10-22) from a runner that had last pulled weeks earlier. --pull on every local and remote docker build makes a tag mean what the registry serves now. Command-pin tests updated to match. --- internal/build/build.go | 12 ++++++++---- internal/build/build_test.go | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 2117c9a..1c316d4 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -131,9 +131,12 @@ func (b *Builder) Build(ctx context.Context, cfg BuildConfig) (string, error) { } func (b *Builder) buildDockerfile(ctx context.Context, tag, buildDir, contextSub, dockerfile, platform string) error { - // The plain build keeps its historical byte-identical command; - // BuildKit is only forced on for --secret mounts (D4 build_secrets). - cmd := "docker build -t " + ssh.ShellQuote(tag) + // --pull always: a base-image tag must resolve to what the registry + // serves now, never what the build host happens to have cached (a + // stale runner cache once shipped a pre-security-release Node, + // 2026-10-22). BuildKit is only forced on for --secret mounts (D4 + // build_secrets). + cmd := "docker build --pull -t " + ssh.ShellQuote(tag) if len(b.secrets) > 0 { cmd = "DOCKER_BUILDKIT=1 " + cmd } @@ -261,7 +264,8 @@ func LocalBuild(ctx context.Context, cfg LocalBuildConfig, stdout io.Writer) (st } func localBuildDockerfile(ctx context.Context, tag, dir, contextSub, dockerfile, platform string, stdout io.Writer) error { - args := []string{"build", "-t", tag} + // --pull: same stale-base-image rule as the remote build. + args := []string{"build", "--pull", "-t", tag} // Explicit config, else the shared local-build default (Apple silicon // cross-compiles for linux/amd64) — one rule for the build and its diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 43476ee..4ed1041 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -63,7 +63,7 @@ func TestBuild_Dockerfile(t *testing.T) { if len(mock.Calls) != 1 { t.Fatalf("expected 1 call, got %d", len(mock.Calls)) } - if !strings.Contains(mock.Calls[0], "docker build -t 'myapp-build-abc1234' /deployments/myapp/build") { + if !strings.Contains(mock.Calls[0], "docker build --pull -t 'myapp-build-abc1234' /deployments/myapp/build") { t.Errorf("unexpected command: %s", mock.Calls[0]) } } @@ -496,7 +496,7 @@ func TestBuild_SubdirDockerfileCommand(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - want := "docker build -t 'myapp-build-abc1234' -f '/deployments/myapp/build/server/monolith/Dockerfile' /deployments/myapp/build" + want := "docker build --pull -t 'myapp-build-abc1234' -f '/deployments/myapp/build/server/monolith/Dockerfile' /deployments/myapp/build" if !strings.Contains(mock.Calls[0], want) { t.Errorf("unexpected command:\n got: %s\nwant contains: %s", mock.Calls[0], want) } @@ -541,7 +541,7 @@ func TestBuild_DockerfileWithSecrets(t *testing.T) { }); err != nil { t.Fatal(err) } - want := "DOCKER_BUILDKIT=1 docker build -t 'myapp-build-abc1234' --secret id='NPM_TOKEN',src='/run/secrets/NPM_TOKEN' /deployments/myapp/build" + want := "DOCKER_BUILDKIT=1 docker build --pull -t 'myapp-build-abc1234' --secret id='NPM_TOKEN',src='/run/secrets/NPM_TOKEN' /deployments/myapp/build" if got := exec.Calls[len(exec.Calls)-1]; got != want { t.Fatalf("build command = %q, want %q", got, want) }