Compare commits
4
Commits
396986aaf4
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8db767efb9 | ||
|
|
1a62ddaeb8 | ||
|
|
7e7bcbe017 | ||
|
|
60af93df74 |
@@ -1,6 +1,6 @@
|
|||||||
# Das OS — що зробити далі
|
# Das OS — що зробити далі
|
||||||
|
|
||||||
Стан на 2026-10-25: версія 0.9.5 (не закомічено), етапи 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-утиліти (готово, лишилися перевірки)
|
## Етап 5. Go-утиліти (готово, лишилися перевірки)
|
||||||
- [x] `das-update`: вікно, значок у треї (лише коли є оновлення), консольний режим `--cli`, знімок snapper pre/post, встановлення через pkexec, підказка про відкат.
|
- [x] `das-update`: вікно, значок у треї (лише коли є оновлення), консольний режим `--cli`, знімок snapper pre/post, встановлення через pkexec, підказка про відкат.
|
||||||
@@ -117,7 +117,10 @@ Live ISO без графіки з консольним інсталятором:
|
|||||||
- [x] Значки Wi-Fi і Bluetooth у треї лишались блідими: панельні значки Papirus (`#dfdfdf`) зроблені для темних панелей, а CSS їх не перефарбовує. Світла тема тепер на `Papirus-Light` (`#444`): `xsettings.xml`, `index.theme`, екран входу; `das-theme` 0.3.0, `das-settings` 0.6.0, 0.9.5; перевірити на ISO.
|
- [x] Значки Wi-Fi і Bluetooth у треї лишались блідими: панельні значки Papirus (`#dfdfdf`) зроблені для темних панелей, а CSS їх не перефарбовує. Світла тема тепер на `Papirus-Light` (`#444`): `xsettings.xml`, `index.theme`, екран входу; `das-theme` 0.3.0, `das-settings` 0.6.0, 0.9.5; перевірити на ISO.
|
||||||
- [x] `das-panel-icon help` (`-h`, `--help`) показує підказку, а не ставить значок «?»; невідомі `-ключі` відхиляються.
|
- [x] `das-panel-icon help` (`-h`, `--help`) показує підказку, а не ставить значок «?»; невідомі `-ключі` відхиляються.
|
||||||
- [x] На десктопний образ додано `publisher` (`publish`) і `deployer` (`deploy`): копії проєктів з `~/Dev/Projects/Python`, конфіг у `~/.config/<назва>/` (створюється з прикладу при першому запуску), історія в `~/.local/state/<назва>/`; перевірити на ISO.
|
- [x] На десктопний образ додано `publisher` (`publish`) і `deployer` (`deploy`): копії проєктів з `~/Dev/Projects/Python`, конфіг у `~/.config/<назва>/` (створюється з прикладу при першому запуску), історія в `~/.local/state/<назва>/`; перевірити на ISO.
|
||||||
- [ ] Перенести в оригінальні `publisher` і `deployer` зміну шляхів конфігу та `history.log` (зараз вони лише в копіях пакетів), щоб копії не розходились.
|
- [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-сесії.
|
- [ ] Прибрати значки томів live-носія («Том 6,6 ГБ») з робочого столу live-сесії.
|
||||||
- [ ] Тема GRUB у встановленій системі (зараз стандартний чорний екран).
|
- [ ] Тема GRUB у встановленій системі (зараз стандартний чорний екран).
|
||||||
- [ ] Перевірити меню Whisker вручну.
|
- [ ] Перевірити меню Whisker вручну.
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ das-theme
|
|||||||
das-tools
|
das-tools
|
||||||
das-helpers
|
das-helpers
|
||||||
das-shell
|
das-shell
|
||||||
|
nano
|
||||||
|
curl
|
||||||
publisher
|
publisher
|
||||||
deployer
|
deployer
|
||||||
xtools-minimal
|
xtools-minimal
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Template file for 'das-server'
|
# Template file for 'das-server'
|
||||||
pkgname=das-server
|
pkgname=das-server
|
||||||
version=0.4.0
|
version=0.5.0
|
||||||
revision=1
|
revision=1
|
||||||
depends="openssh chrony dhcpcd NetworkManager docker docker-compose nftables
|
depends="openssh chrony dhcpcd NetworkManager docker docker-compose nftables
|
||||||
fail2ban socklog-void python3 curl lazydocker lazygit mc tmux htop ncdu
|
fail2ban socklog-void python3 curl lazydocker lazygit mc tmux htop ncdu nano
|
||||||
das-snapshots das-update das-helpers das-shell xtools-minimal"
|
das-snapshots das-update das-helpers das-shell xtools-minimal"
|
||||||
short_desc="Das OS server: packages and secure defaults for SSH, firewall and Docker"
|
short_desc="Das OS server: packages and secure defaults for SSH, firewall and Docker"
|
||||||
maintainer="Das <anry.das@gmail.com>"
|
maintainer="Das <anry.das@gmail.com>"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
"""Deploy local projects to the home server over ssh + rsync."""
|
"""Deploy local projects to the home server over ssh + rsync."""
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ def main(argv=None) -> int:
|
|||||||
|
|
||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
color(sys.stdout.isatty() and not args.no_color)
|
color(sys.stdout.isatty() and not args.no_color)
|
||||||
cfg_path = Path(args.config) if args.config else Path(__file__).resolve().parent.parent / "deploy.toml"
|
cfg_path = Path(args.config) if args.config else config.default_path()
|
||||||
try:
|
try:
|
||||||
cfg = config.load(cfg_path)
|
cfg = config.load(cfg_path)
|
||||||
except (OSError, config.ConfigError) as e:
|
except (OSError, config.ConfigError) as e:
|
||||||
|
|||||||
@@ -211,3 +211,11 @@ def append_apps(path: Path, apps: list[App], drafts: list[App] = ()) -> None:
|
|||||||
block = "".join(f"# {line}\n" for line in app_to_toml(a).splitlines())
|
block = "".join(f"# {line}\n" for line in app_to_toml(a).splitlines())
|
||||||
text = text.rstrip("\n") + "\n\n# TODO: check src / build / dest, then uncomment\n" + block
|
text = text.rstrip("\n") + "\n\n# TODO: check src / build / dest, then uncomment\n" + block
|
||||||
path.write_text(text)
|
path.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def default_path() -> Path:
|
||||||
|
"""deploy.toml next to the sources (a checkout), else ~/.config/deployer/deploy.toml (an installed package)."""
|
||||||
|
local = Path(__file__).resolve().parent.parent / "deploy.toml"
|
||||||
|
if local.exists():
|
||||||
|
return local
|
||||||
|
return Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "deployer" / "deploy.toml"
|
||||||
|
|||||||
@@ -13,8 +13,17 @@ from . import kinds, remote, snapshots
|
|||||||
from .config import App, Config
|
from .config import App, Config
|
||||||
from .kinds import Options, Plan
|
from .kinds import Options, Plan
|
||||||
|
|
||||||
# Das OS package: /usr/lib is read-only, so the history goes to the user's state dir
|
|
||||||
HISTORY = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state") / "deployer" / "history.log"
|
|
||||||
|
def _history_path() -> Path:
|
||||||
|
"""history.log next to the sources (a checkout), else ~/.local/state/deployer/ (an installed package)."""
|
||||||
|
local = Path(__file__).resolve().parent.parent
|
||||||
|
if os.access(local, os.W_OK):
|
||||||
|
return local / "history.log"
|
||||||
|
return Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state") / "deployer" / "history.log"
|
||||||
|
|
||||||
|
|
||||||
|
HISTORY = _history_path()
|
||||||
|
|
||||||
|
|
||||||
class DeployError(RuntimeError):
|
class DeployError(RuntimeError):
|
||||||
|
|||||||
@@ -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,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env,
|
||||||
errors="replace")
|
errors="replace")
|
||||||
if stdin is not None:
|
if stdin is not None:
|
||||||
proc.stdin.write(stdin)
|
try:
|
||||||
proc.stdin.close()
|
proc.stdin.write(stdin)
|
||||||
|
proc.stdin.close()
|
||||||
|
except BrokenPipeError:
|
||||||
|
pass # ssh already exited; its message is in the output below
|
||||||
out = []
|
out = []
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = line.rstrip("\n")
|
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."""
|
"""Run a bash script on the server; values become shell variables in front of it."""
|
||||||
body = "set -uo pipefail\n" + shell_vars(**values) + script
|
body = "set -uo pipefail\n" + shell_vars(**values) + script
|
||||||
rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body)
|
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 ""))
|
raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else ""))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -175,3 +176,14 @@ def test_python_config_layout(tmp_path):
|
|||||||
plan.dest = "/home/user/app/docker/stack"
|
plan.dest = "/home/user/app/docker/stack"
|
||||||
plan.app = config.App(name="stack", kind="docker", src=str(tmp_path), dest="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")]
|
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)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Differences from the upstream: the config is in ~/.config/deployer (see bin/deploy) and
|
# Differences from the upstream: the config is in ~/.config/deployer (see bin/deploy) and
|
||||||
# history.log is in ~/.local/state/deployer.
|
# history.log is in ~/.local/state/deployer.
|
||||||
pkgname=deployer
|
pkgname=deployer
|
||||||
version=0.3.0
|
version=0.3.1
|
||||||
revision=1
|
revision=1
|
||||||
create_wrksrc=yes
|
create_wrksrc=yes
|
||||||
hostmakedepends="python3"
|
hostmakedepends="python3"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
"""Publish apk, AppImage and binaries to the apps page of the home Tomcat over ssh + rsync."""
|
"""Publish apk, AppImage and binaries to the apps page of the home Tomcat over ssh + rsync."""
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ def main(argv=None) -> int:
|
|||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
global COLOR
|
global COLOR
|
||||||
COLOR = sys.stdout.isatty() and not args.no_color
|
COLOR = sys.stdout.isatty() and not args.no_color
|
||||||
cfg_path = Path(args.config) if args.config else Path(__file__).resolve().parent.parent / "publish.toml"
|
cfg_path = Path(args.config) if args.config else config.default_path()
|
||||||
try:
|
try:
|
||||||
cfg = config.load(cfg_path)
|
cfg = config.load(cfg_path)
|
||||||
except (OSError, config.ConfigError) as e:
|
except (OSError, config.ConfigError) as e:
|
||||||
|
|||||||
@@ -185,3 +185,11 @@ def append_apps(path: Path, apps: list[App]) -> None:
|
|||||||
validate_app(a)
|
validate_app(a)
|
||||||
text = text.rstrip("\n") + "\n\n" + app_to_toml(a)
|
text = text.rstrip("\n") + "\n\n" + app_to_toml(a)
|
||||||
path.write_text(text)
|
path.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def default_path() -> Path:
|
||||||
|
"""publish.toml next to the sources (a checkout), else ~/.config/publisher/publish.toml (an installed package)."""
|
||||||
|
local = Path(__file__).resolve().parent.parent / "publish.toml"
|
||||||
|
if local.exists():
|
||||||
|
return local
|
||||||
|
return Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "publisher" / "publish.toml"
|
||||||
|
|||||||
@@ -13,8 +13,17 @@ from . import packages, remote
|
|||||||
from .config import App, Config
|
from .config import App, Config
|
||||||
from .packages import Kind, Local, Published, version_key
|
from .packages import Kind, Local, Published, version_key
|
||||||
|
|
||||||
# Das OS package: /usr/lib is read-only, so the history goes to the user's state dir
|
|
||||||
HISTORY = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state") / "publisher" / "history.log"
|
|
||||||
|
def _history_path() -> Path:
|
||||||
|
"""history.log next to the sources (a checkout), else ~/.local/state/publisher/ (an installed package)."""
|
||||||
|
local = Path(__file__).resolve().parent.parent
|
||||||
|
if os.access(local, os.W_OK):
|
||||||
|
return local / "history.log"
|
||||||
|
return Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state") / "publisher" / "history.log"
|
||||||
|
|
||||||
|
|
||||||
|
HISTORY = _history_path()
|
||||||
|
|
||||||
# Plan item actions; the first five change the server
|
# Plan item actions; the first five change the server
|
||||||
NEW, REPLACE, META, NOTES, DELETE, SAME, SKIP = "new", "replace", "meta", "notes", "delete", "same", "skip"
|
NEW, REPLACE, META, NOTES, DELETE, SAME, SKIP = "new", "replace", "meta", "notes", "delete", "same", "skip"
|
||||||
|
|||||||
@@ -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,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env,
|
||||||
errors="replace")
|
errors="replace")
|
||||||
if stdin is not None:
|
if stdin is not None:
|
||||||
proc.stdin.write(stdin)
|
try:
|
||||||
proc.stdin.close()
|
proc.stdin.write(stdin)
|
||||||
|
proc.stdin.close()
|
||||||
|
except BrokenPipeError:
|
||||||
|
pass # ssh already exited; its message is in the output below
|
||||||
out = []
|
out = []
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = line.rstrip("\n")
|
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."""
|
"""Run a bash script on the server; values become shell variables in front of it."""
|
||||||
body = "set -uo pipefail\n" + shell_vars(**values) + script
|
body = "set -uo pipefail\n" + shell_vars(**values) + script
|
||||||
rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body)
|
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 ""))
|
raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else ""))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from publisher import config, detect, packages, pipeline
|
from publisher import config, detect, packages, pipeline, remote
|
||||||
from publisher.packages import BINARY, KINDS, Published
|
from publisher.packages import BINARY, KINDS, Published
|
||||||
|
|
||||||
APK, APPIMAGE, TARGZ = KINDS[0], KINDS[1], KINDS[-1]
|
APK, APPIMAGE, TARGZ = KINDS[0], KINDS[1], KINDS[-1]
|
||||||
@@ -215,3 +215,14 @@ def test_detect_go(tmp_path):
|
|||||||
def test_site_name_cleanup():
|
def test_site_name_cleanup():
|
||||||
assert detect._site_name("Das Pressure 2") == "DasPressure2"
|
assert detect._site_name("Das Pressure 2") == "DasPressure2"
|
||||||
assert os.path.basename(detect._home(Path.home() / "a")) == "a"
|
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)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Differences from the upstream: the config is in ~/.config/publisher (see bin/publish) and
|
# Differences from the upstream: the config is in ~/.config/publisher (see bin/publish) and
|
||||||
# history.log is in ~/.local/state/publisher.
|
# history.log is in ~/.local/state/publisher.
|
||||||
pkgname=publisher
|
pkgname=publisher
|
||||||
version=0.3.0
|
version=0.3.1
|
||||||
revision=1
|
revision=1
|
||||||
create_wrksrc=yes
|
create_wrksrc=yes
|
||||||
hostmakedepends="python3"
|
hostmakedepends="python3"
|
||||||
|
|||||||
Reference in New Issue
Block a user