From 7185047eb724362590407bb0fae21c0a3f6821d7 Mon Sep 17 00:00:00 2001 From: lear Date: Wed, 4 Mar 2026 11:07:42 +0300 Subject: [PATCH 1/2] fix: add docker login before rollback and fix execAsyncRemote argument order --- packages/server/src/services/rollbacks.ts | 31 ++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 00c60ebc8..301438fed 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => { const command = `docker image rm ${image} --force`; if (serverId) { - await execAsyncRemote(command, serverId); + await execAsyncRemote(serverId, command); } else { await execAsync(command); } @@ -171,6 +171,27 @@ export const rollback = async (rollbackId: string) => { ); }; +const dockerLoginForRegistry = async ( + registry: Registry, + serverId?: string | null, +) => { + const escapedRegistry = shEscape(registry.registryUrl); + const escapedUser = shEscape(registry.username); + const escapedPassword = shEscape(registry.password); + const loginCommand = `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`; + + if (serverId) { + await execAsyncRemote(serverId, loginCommand); + } else { + await execAsync(loginCommand); + } +}; + +function shEscape(s: string | undefined): string { + if (!s) return "''"; + return `'${s.replace(/'/g, `'\\''`)}'`; +} + const rollbackApplication = async ( appName: string, image: string, @@ -188,6 +209,14 @@ const rollbackApplication = async ( throw new Error("Full context is required for rollback"); } + // Ensure Docker daemon is authenticated with the rollback registry + // before updating the swarm service. The authconfig in CreateServiceOptions + // alone is not sufficient — Docker Swarm also relies on the daemon's + // cached credentials (~/.docker/config.json) to distribute auth to nodes. + if (fullContext.rollbackRegistry) { + await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId); + } + const docker = await getRemoteDocker(serverId); // Use the same configuration as mechanizeDockerContainer From d2fabc998dff5c4da58ee5bc5f657a05001f1c2b Mon Sep 17 00:00:00 2001 From: lear Date: Wed, 4 Mar 2026 12:45:57 +0300 Subject: [PATCH 2/2] refactor: reuse safeDockerLoginCommand from registry.ts instead of duplicating shEscape --- packages/server/src/services/registry.ts | 2 +- packages/server/src/services/rollbacks.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index f9d3977b9..ad18e0b66 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string { return `'${s.replace(/'/g, `'\\''`)}'`; } -function safeDockerLoginCommand( +export function safeDockerLoginCommand( registry: string | undefined, user: string | undefined, pass: string | undefined, diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 301438fed..51d978572 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment"; import type { Mount } from "./mount"; import type { Port } from "./port"; import type { Project } from "./project"; -import type { Registry } from "./registry"; +import { type Registry, safeDockerLoginCommand } from "./registry"; export const createRollback = async ( input: z.infer, @@ -175,10 +175,11 @@ const dockerLoginForRegistry = async ( registry: Registry, serverId?: string | null, ) => { - const escapedRegistry = shEscape(registry.registryUrl); - const escapedUser = shEscape(registry.username); - const escapedPassword = shEscape(registry.password); - const loginCommand = `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`; + const loginCommand = safeDockerLoginCommand( + registry.registryUrl, + registry.username, + registry.password, + ); if (serverId) { await execAsyncRemote(serverId, loginCommand); @@ -187,11 +188,6 @@ const dockerLoginForRegistry = async ( } }; -function shEscape(s: string | undefined): string { - if (!s) return "''"; - return `'${s.replace(/'/g, `'\\''`)}'`; -} - const rollbackApplication = async ( appName: string, image: string,