- #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>
167 lines
6.0 KiB
Python
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()
|