V6.1 runtime settings ->.env.production fix
Quality Gate / gate (push) Successful in 2m29s

This commit is contained in:
Jim Lancaster
2026-09-02 12:31:04 -05:00
parent 16391463d6
commit 15a4814e23
2 changed files with 32 additions and 1 deletions
@@ -6,6 +6,9 @@ import os
import re import re
import tempfile import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from errno import EBUSY
from errno import EPERM
from errno import EXDEV
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
@@ -518,6 +521,7 @@ def _upsert_env_key(lines: list[str], key: str, value: str) -> None:
def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None: def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
temp_path: Path | None = None
try: try:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
content = "\n".join(lines).rstrip("\n") content = "\n".join(lines).rstrip("\n")
@@ -533,7 +537,15 @@ def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
) as handle: ) as handle:
temp_path = Path(handle.name) temp_path = Path(handle.name)
handle.write(content) handle.write(content)
try:
temp_path.replace(path) temp_path.replace(path)
except OSError as exc:
# Single-file bind mounts can reject replace() (cross-device or busy mountpoint).
# Fallback to direct write so Runtime Settings can persist to mounted env files.
if exc.errno not in {EXDEV, EBUSY, EPERM}:
raise
with path.open("w", encoding="utf-8", newline="\n") as handle:
handle.write(content)
except OSError as exc: except OSError as exc:
raise AppError( raise AppError(
"Runtime settings file is not writable.", "Runtime settings file is not writable.",
@@ -541,6 +553,9 @@ def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
suggestion="Verify file path and write permissions, then retry.", suggestion="Verify file path and write permissions, then retry.",
detail=f"Failed writing runtime env file {path}: {type(exc).__name__}: {exc}", detail=f"Failed writing runtime env file {path}: {type(exc).__name__}: {exc}",
) from exc ) from exc
finally:
if temp_path is not None:
temp_path.unlink(missing_ok=True)
def _escape_json_string(value: str) -> str: def _escape_json_string(value: str) -> str:
+16
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import errno
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -132,3 +133,18 @@ def test_save_runtime_settings_surfaces_unwritable_target(tmp_path: Path, monkey
with pytest.raises(AppError) as exc: with pytest.raises(AppError) as exc:
save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path) save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
assert exc.value.message == "Runtime settings file is not writable." assert exc.value.message == "Runtime settings file is not writable."
def test_save_runtime_settings_falls_back_when_atomic_replace_is_unavailable(tmp_path: Path, monkeypatch):
settings = _settings_for_runtime_editing(tmp_path)
env_path = tmp_path / ".env.production"
env_path.write_text("OPENROUTER_API_KEY=test-key\nPORT=8000\n", encoding="utf-8")
def _replace_cross_device(_self: Path, _target: Path) -> Path:
raise OSError(errno.EXDEV, "Invalid cross-device link")
monkeypatch.setattr(Path, "replace", _replace_cross_device)
snapshot = save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
assert snapshot.env_file_path == env_path
assert "PORT=9001" in env_path.read_text(encoding="utf-8")