From 773067569b3da546536aa8a5a5a8255c4ebccdd6 Mon Sep 17 00:00:00 2001 From: "Thor (Guardrail)" Date: Wed, 22 Apr 2026 14:00:29 -0700 Subject: [PATCH] fix: swa env file restore, build-arg quoting, docker rollback cd_prefix, dead if-True Four bugs fixed: 1. providers/azure/swa.py: .env.production.local was deleted before build but never restored in the finally block. Now backs up the file and restores it (along with restoring/removing .env.production correctly when it didn't previously exist). 2. providers/azure/container.py + builders/docker.py: _build_args_str() emitted --build-arg KEY=VALUE without quoting values, causing build failures when values contain spaces or shell metacharacters. Now uses shlex.quote(). 3. cli.py: _check_access() had a bare 'if True:' guard around the access-granted message, which was dead code (always executed). Replaced with a direct call + explanatory comment. Behaviour is unchanged but the intent is explicit. 4. providers/docker/container.py: rollback()'s no-target path chained two docker compose commands with '&&' but the second command was missing cd_prefix, so it ran from the wrong directory on the remote host when project_dir is set. --- dds/builders/docker.py | 12 ++++++++++-- dds/cli.py | 8 ++++---- dds/providers/azure/container.py | 12 ++++++++++-- dds/providers/azure/swa.py | 15 +++++++++++++++ dds/providers/docker/container.py | 2 +- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/dds/builders/docker.py b/dds/builders/docker.py index d18a072..eeb5e5a 100644 --- a/dds/builders/docker.py +++ b/dds/builders/docker.py @@ -59,7 +59,15 @@ def resolve_image_tag( def _build_args_str(build_args: dict[str, str] | None) -> str: - """Format build args dict into Docker --build-arg flags.""" + """Format build args dict into Docker --build-arg flags. + + Values containing spaces or shell-special characters are quoted to prevent + word-splitting when the command string is passed to the shell. + """ if not build_args: return "" - return " ".join(f"--build-arg {k}={v}" for k, v in build_args.items()) + import shlex + + return " ".join( + f"--build-arg {k}={shlex.quote(str(v))}" for k, v in build_args.items() + ) diff --git a/dds/cli.py b/dds/cli.py index 36d1f7c..8497902 100644 --- a/dds/cli.py +++ b/dds/cli.py @@ -66,10 +66,10 @@ def _check_access(environment: str, env_cfg: dict) -> None: ) raise SystemExit(1) - if True: - console.print( - f" [dim]Access check: {deployer_email} ✓ ({environment})[/dim]" - ) + # Always emit a dim confirmation so operators have an audit trail. + console.print( + f" [dim]Access check: {deployer_email} ✓ ({environment})[/dim]" + ) def _make_ctx(click_ctx: click.Context, environment: str, service: str) -> DeployContext: diff --git a/dds/providers/azure/container.py b/dds/providers/azure/container.py index 8381636..deaa558 100644 --- a/dds/providers/azure/container.py +++ b/dds/providers/azure/container.py @@ -349,7 +349,15 @@ def _http_check(url: str, timeout: float = 10.0, verbose: bool = False) -> bool: def _build_args_str(build_args: dict[str, str] | None) -> str: - """Format build args dict into Docker --build-arg flags.""" + """Format build args dict into Docker --build-arg flags. + + Values containing spaces or shell-special characters are quoted to prevent + word-splitting when the command string is passed to the shell. + """ if not build_args: return "" - return " ".join(f"--build-arg {k}={v}" for k, v in build_args.items()) + import shlex + + return " ".join( + f"--build-arg {k}={shlex.quote(str(v))}" for k, v in build_args.items() + ) diff --git a/dds/providers/azure/swa.py b/dds/providers/azure/swa.py index 2d429ae..266b623 100644 --- a/dds/providers/azure/swa.py +++ b/dds/providers/azure/swa.py @@ -27,6 +27,7 @@ def deploy(self, ctx: DeployContext) -> None: env_production_path = os.path.join(project_dir, ".env.production") env_production_backup = None + env_prod_local_backup: str | None = None if env_file: env_file_path = os.path.join(project_dir, env_file) @@ -46,6 +47,8 @@ def deploy(self, ctx: DeployContext) -> None: env_prod_local = os.path.join(project_dir, ".env.production.local") if os.path.exists(env_prod_local): + with open(env_prod_local) as f: + env_prod_local_backup = f.read() os.remove(env_prod_local) try: @@ -107,6 +110,18 @@ def deploy(self, ctx: DeployContext) -> None: f.write(env_production_backup) if ctx.verbose: console.print(" Restored original .env.production") + elif env_file and os.path.exists(env_production_path): + # We created .env.production from scratch — remove it + os.remove(env_production_path) + if ctx.verbose: + console.print(" Removed temporary .env.production") + + env_prod_local_path = os.path.join(project_dir, ".env.production.local") + if env_prod_local_backup is not None: + with open(env_prod_local_path, "w") as f: + f.write(env_prod_local_backup) + if ctx.verbose: + console.print(" Restored original .env.production.local") def status(self, ctx: DeployContext) -> None: """Show status for a Static Web App.""" diff --git a/dds/providers/docker/container.py b/dds/providers/docker/container.py index 22f43f6..d38d826 100644 --- a/dds/providers/docker/container.py +++ b/dds/providers/docker/container.py @@ -179,7 +179,7 @@ def rollback(self, ctx: DeployContext, target_revision: str | None = None) -> bo ssh( host, f"{cd_prefix}docker compose -f {compose_file} down {service_name} && " - f"docker compose -f {compose_file} up -d {service_name}", + f"{cd_prefix}docker compose -f {compose_file} up -d {service_name}", verbose=ctx.verbose, )