publisher, deployer: report ssh failure instead of BrokenPipeError (0.9.8)
build / build (push) Successful in 21m56s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
das
2026-10-25 18:34:29 +02:00
co-authored by Claude Sonnet 5.5
parent 1a62ddaeb8
commit 8db767efb9
10 changed files with 43 additions and 13 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
# Das OS — що зробити далі
Стан на 2026-10-25: версія 0.9.7 (не закомічено), етапи 1–8 готові (live ISO, власний репозиторій, оформлення, інсталятор Calamares зі знімками Btrfs, Go-утиліти `das-update` і `das-welcome`, серверний образ з консольним інсталятором, менші ISO, консольні помічники, профіль розробника), CI (етап 9: пакети й обидва ISO збираються на власному runner, ~45 хв).
Стан на 2026-10-25: версія 0.9.8 (не закомічено), етапи 1–8 готові (live ISO, власний репозиторій, оформлення, інсталятор Calamares зі знімками Btrfs, Go-утиліти `das-update` і `das-welcome`, серверний образ з консольним інсталятором, менші ISO, консольні помічники, профіль розробника), CI (етап 9: пакети й обидва ISO збираються на власному runner, ~45 хв).
## Етап 5. Go-утиліти (готово, лишилися перевірки)
- [x] `das-update`: вікно, значок у треї (лише коли є оновлення), консольний режим `--cli`, знімок snapper pre/post, встановлення через pkexec, підказка про відкат.
@@ -120,6 +120,7 @@ Live ISO без графіки з консольним інсталятором:
- [x] Зміну шляхів конфігу та `history.log` перенесено в оригінальні `publisher` і `deployer` (`config.default_path()`, `pipeline._history_path()`): у checkout усе як було, у пакеті — `~/.config` і `~/.local/state`. Копії пакетів відрізняються від оригіналів лише значеннями за замовчуванням (localhost, user).
- [x] У десктопний образ і в `das-server` 0.5.0 додано `nano`: `publish` і `deploy` відкривають конфіг у редакторі за F2 (`$VISUAL`/`$EDITOR`, інакше `nano`), без нього завершувались; 0.9.6.
- [x] У десктопний образ додано `curl` (у `das-server` він уже був); 0.9.7.
- [x] `publish` і `deploy` падали з `BrokenPipeError`, коли `ssh` завершується одразу (недоступний хост): тепер це `RemoteError` з повідомленням `ssh` (код 255 завжди помилка), тест з фіктивним `ssh`; `publisher`/`deployer` 0.3.1, 0.9.8; виправлено й в оригіналах.
- [ ] Прибрати значки томів live-носія («Том 6,6 ГБ») з робочого столу live-сесії.
- [ ] Тема GRUB у встановленій системі (зараз стандартний чорний екран).
- [ ] Перевірити меню Whisker вручну.
+1 -1
View File
@@ -1 +1 @@
0.9.7
0.9.8
@@ -1,2 +1,2 @@
"""Deploy local projects to the home server over ssh + rsync."""
__version__ = "0.3.0"
__version__ = "0.3.1"
@@ -31,8 +31,11 @@ def _stream(cmd: list[str], log: Log | None, stdin: str | None = None, cwd=None,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env,
errors="replace")
if stdin is not None:
try:
proc.stdin.write(stdin)
proc.stdin.close()
except BrokenPipeError:
pass # ssh already exited; its message is in the output below
out = []
for line in proc.stdout:
line = line.rstrip("\n")
@@ -57,7 +60,7 @@ def run(server: Server, script: str, log: Log | None = None, check: bool = True,
"""Run a bash script on the server; values become shell variables in front of it."""
body = "set -uo pipefail\n" + shell_vars(**values) + script
rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body)
if check and rc != 0:
if rc == 255 or (check and rc != 0):
raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else ""))
return out
+12
View File
@@ -1,3 +1,4 @@
import os
from pathlib import Path
import pytest
@@ -175,3 +176,14 @@ def test_python_config_layout(tmp_path):
plan.dest = "/home/user/app/docker/stack"
plan.app = config.App(name="stack", kind="docker", src=str(tmp_path), dest="docker/stack")
assert get("docker").config_items(plan) == [(".env", "/home/user/app/configs/docker/stack.env")]
def test_ssh_exits_early(tmp_path, monkeypatch):
# ssh dies at once (e.g. unresolvable host): the broken pipe must surface as RemoteError with its message
fake = tmp_path / "ssh"
fake.write_text("#!/bin/sh\necho \"ssh: Could not resolve hostname\" >&2\nexit 255\n")
fake.chmod(0o755)
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
srv = config.Server(host="nohost.invalid", user="u")
with pytest.raises(remote.RemoteError, match="Could not resolve"):
remote.run(srv, "x" * 5_000_000, check=False)
+1 -1
View File
@@ -3,7 +3,7 @@
# Differences from the upstream: the config is in ~/.config/deployer (see bin/deploy) and
# history.log is in ~/.local/state/deployer.
pkgname=deployer
version=0.3.0
version=0.3.1
revision=1
create_wrksrc=yes
hostmakedepends="python3"
@@ -1,2 +1,2 @@
"""Publish apk, AppImage and binaries to the apps page of the home Tomcat over ssh + rsync."""
__version__ = "0.3.0"
__version__ = "0.3.1"
@@ -26,8 +26,11 @@ def _stream(cmd: list[str], log: Log | None, stdin: str | None = None, cwd=None,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env,
errors="replace")
if stdin is not None:
try:
proc.stdin.write(stdin)
proc.stdin.close()
except BrokenPipeError:
pass # ssh already exited; its message is in the output below
out = []
for line in proc.stdout:
line = line.rstrip("\n")
@@ -52,7 +55,7 @@ def run(server: Server, script: str, log: Log | None = None, check: bool = True,
"""Run a bash script on the server; values become shell variables in front of it."""
body = "set -uo pipefail\n" + shell_vars(**values) + script
rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body)
if check and rc != 0:
if rc == 255 or (check and rc != 0):
raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else ""))
return out
+12 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest
from publisher import config, detect, packages, pipeline
from publisher import config, detect, packages, pipeline, remote
from publisher.packages import BINARY, KINDS, Published
APK, APPIMAGE, TARGZ = KINDS[0], KINDS[1], KINDS[-1]
@@ -215,3 +215,14 @@ def test_detect_go(tmp_path):
def test_site_name_cleanup():
assert detect._site_name("Das Pressure 2") == "DasPressure2"
assert os.path.basename(detect._home(Path.home() / "a")) == "a"
def test_ssh_exits_early(tmp_path, monkeypatch):
# ssh dies at once (e.g. unresolvable host): the broken pipe must surface as RemoteError with its message
fake = tmp_path / "ssh"
fake.write_text("#!/bin/sh\necho \"ssh: Could not resolve hostname\" >&2\nexit 255\n")
fake.chmod(0o755)
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
srv = config.Server(host="nohost.invalid", user="u")
with pytest.raises(remote.RemoteError, match="Could not resolve"):
remote.run(srv, "x" * 5_000_000, check=False)
+1 -1
View File
@@ -3,7 +3,7 @@
# Differences from the upstream: the config is in ~/.config/publisher (see bin/publish) and
# history.log is in ~/.local/state/publisher.
pkgname=publisher
version=0.3.0
version=0.3.1
revision=1
create_wrksrc=yes
hostmakedepends="python3"