Files
ClawdDotNet/deploy/publish.py
T
RichardandClaude Opus 5 6581fd0187 Befunde aus dem Testbetrieb auf der Linux-VM behoben (#572)
- #578: Abgebrochene Laeufe (Schrittgrenze, Zeitueberschreitung, Abbruch,
  Ausnahme) tragen ihre bezahlten Token - vorher 0 in RunUsage, fuer das
  Budget unsichtbar.
- #580: WebFetch prueft die aufgeloeste Zieladresse beim Verbinden; ein
  erlaubter Name, der auf 127.0.0.1 aufloest, wird gesperrt.
- #575: Dateien entstehen unter Unix mit 0600 (AtomicFile, Protokolle,
  FileRW-Anhaengen, state.db), Protokollverzeichnisse mit 0700.
- #576: SIGTERM/SIGINT beenden auch mit Fenster geordnet.
- #593: OpenRouter-Statusdienst wird gestartet.
- #589: Auftragsnachricht nennt aktuelle Zeit, Zeitzone und Termin.
- #581: Unvollstaendiges Argument-JSON erreicht das Werkzeug nicht mehr;
  das Modell bekommt eine verstaendliche Rueckmeldung statt Parsertext.
- #591: Weboberflaeche liest die Felder, die API und Ereignisstrom liefern
  (Freigaben, Protokoll, Taskboard mit Termin, Werkzeug-Einstellungen,
  Tokennutzung); Ereignisstrom in camelCase.
- #592: Chat zeigt Nachrichten nicht mehr doppelt.
- #536 (teilweise): A2A-Antworten tragen Absender und Empfaenger, Parser
  wirft nicht mehr; Datums- und Volltextfilter in Agenten-Chats.
- deploy/publish.py: Packager-Pfad net10.0, Schalter --agent-file.
- Version 0.1.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:40:56 +02:00

167 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Baut ClawdDotNet fuer eine Plattform und veroeffentlicht es im Deploymentcenter.
python deploy/publish.py --rid win-x64 --channel dev
python deploy/publish.py --rid linux-x64 --channel dev --dry-run
Drei Schritte, die einzeln zu leicht vergessen werden:
1. dotnet publish in ein frisches Verzeichnis. Frisch, weil der Packager alles
einpackt, was er vorfindet - Reste eines aelteren Laufs landeten sonst mit
im Paket.
2. update-agent dazulegen. Ohne ihn findet ResolveAgentPath() nichts, und die
Anwendung kann sich nicht selbst aktualisieren. Die Erstinstallation legt
ihn NICHT ins Zielverzeichnis - sie laeuft von dort, wo der Benutzer sie
hingelegt hat.
3. pack-and-deploy aufrufen.
Die Version kommt aus Directory.Build.props. Sie hier noch einmal anzugeben
waere eine zweite Pflegestelle - und der Packager bricht bei einer Abweichung
zur Assembly ohnehin ab.
"""
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PROJECT = REPO / "src" / "ClawdDotNet.Desktop" / "ClawdDotNet.Desktop.csproj"
CONFIG = REPO / "deploy" / "packager.config.json"
PACKAGER = Path(
"J:/Softwareprojekte/Deploymentcenter/client-dotnet/Deploymentcenter.Packager"
"/bin/Release/net10.0/win-x64/pack-and-deploy.exe"
)
INSTALLER_BASE = "https://dc.mhdf.de/installer"
def read_version() -> str:
text = (REPO / "Directory.Build.props").read_text(encoding="utf-8")
match = re.search(r"<Version>([^<]+)</Version>", text)
if not match:
sys.exit("Directory.Build.props enthaelt kein <Version>.")
return match.group(1).strip()
def run(cmd: list[str], **kw) -> None:
print(" $", " ".join(str(c) for c in cmd))
result = subprocess.run(cmd, **kw)
if result.returncode != 0:
sys.exit(f"Abgebrochen (Rueckgabewert {result.returncode}): {cmd[0]}")
def publish(rid: str, out_dir: Path) -> None:
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True)
run([
"dotnet", "publish", str(PROJECT),
"-c", "Release", "-r", rid, "--self-contained", "false",
"-o", str(out_dir), "--nologo", "-v", "q",
])
def fetch_agent(rid: str, out_dir: Path, agent_file: Path | None = None) -> None:
"""Holt den update-agent und prueft die Pruefsumme.
Bewusst das ausgelieferte Binary statt eines selbst gebauten: Es ist
dasselbe, das die Erstinstallation verwendet, und wird zentral gepflegt.
Ohne Pruefsummenvergleich waere das ein Download, der spaeter fremden Code
ausfuehrt - genau der Pfad, den eine Signatur schuetzen soll.
agent_file ersetzt nur den Download, nicht die Pruefung: Die Ablage liefert
zeitweise um 100 KB/s, und urllib blieb dabei am 2026-09-14 ohne Meldung
stehen. Eine vorhandene Kopie muss dieselbe Pruefsumme tragen wie das
Manifest - sonst ist sie nicht das ausgelieferte Binary.
"""
with urllib.request.urlopen(f"{INSTALLER_BASE}/installer.json", timeout=30) as r:
manifest = json.load(r)
entry = next((b for b in manifest.get("binaries", []) if b.get("platform") == rid), None)
if entry is None:
sys.exit(f"Kein update-agent fuer {rid} im Installer-Manifest.")
print(f" update-agent {manifest.get('version')} fuer {rid} ({entry['sizeBytes']} Bytes)")
if agent_file is not None:
print(f" aus Datei: {agent_file}")
payload = agent_file.read_bytes()
else:
with urllib.request.urlopen(f"{INSTALLER_BASE}/{entry['file']}", timeout=180) as r:
payload = r.read()
actual = hashlib.sha256(payload).hexdigest()
if actual != entry["sha256"].lower():
sys.exit(f"Pruefsumme weicht ab!\n erwartet: {entry['sha256']}\n erhalten: {actual}")
target = out_dir / ("update-agent.exe" if rid.startswith("win") else "update-agent")
target.write_bytes(payload)
if not rid.startswith("win"):
target.chmod(0o755)
print(f" Pruefsumme in Ordnung -> {target.name}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--rid", default="win-x64", help="win-x64, linux-x64, ...")
parser.add_argument("--channel", default="dev", choices=["dev", "beta", "prod"])
parser.add_argument("--changelog", default="")
parser.add_argument("--no-agent", action="store_true",
help="update-agent nicht mitliefern")
parser.add_argument("--agent-file", type=Path,
help="vorhandene Kopie des update-agent statt Download; "
"die Pruefsumme wird trotzdem gegen installer.json geprueft")
parser.add_argument("--dry-run", action="store_true",
help="nur bauen und pruefen, nicht hochladen")
args = parser.parse_args()
version = read_version()
print(f"ClawdDotNet {version} | {args.rid} | Kanal {args.channel}")
out_dir = Path(tempfile.gettempdir()) / f"clawddotnet-publish-{args.rid}"
print("\n[1/3] dotnet publish")
publish(args.rid, out_dir)
print("\n[2/3] update-agent")
if args.no_agent:
print(" uebersprungen (--no-agent)")
else:
fetch_agent(args.rid, out_dir, args.agent_file)
total = sum(f.stat().st_size for f in out_dir.rglob("*") if f.is_file())
print(f"\n {len(list(out_dir.rglob('*')))} Eintraege, {total / 1_048_576:.1f} MB")
if args.dry_run:
print(f"\n[3/3] uebersprungen (--dry-run). Ergebnis liegt in {out_dir}")
return
if not CONFIG.exists():
sys.exit(f"{CONFIG} fehlt. Vorlage: deploy/packager.config.example.json")
print("\n[3/3] pack-and-deploy")
cmd = [
str(PACKAGER), "--config", str(CONFIG),
"--project", "clawddotnet", "--version", version,
"--channel", args.channel, "--platform", args.rid,
"--publish-dir", str(out_dir),
]
if args.changelog:
cmd += ["--changelog", args.changelog]
run(cmd)
if __name__ == "__main__":
main()