UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4
Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag. Oberflaeche - Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml (eigene ControlThemes statt Fluent umzufaerben). - Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite statt eigenem Fenster, Texteditor-Fenster. - ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt. Rocket.Chat - Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird nur, wenn wirklich etwas anliegt. - send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist. - Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei Annahmen waren falsch und sind korrigiert. Deploymentcenter - DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json) erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke. - AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"_comment": "Vorlage fuer deploy/packager.config.json (per .gitignore ausgeschlossen). Aufruf: pack-and-deploy --config deploy/packager.config.json ... Siehe docs/Deploymentcenter-2.4-Integrationsplan.md",
|
||||
|
||||
"_ftp_comment": "Dieselben Zugangsdaten wie Deploymentcenter/scripts/deploy_config.json - derselbe Server, anderes Zielverzeichnis. NICHT deploy.py verwenden: das spiegelt den Deploymentcenter-Projektbaum in die FTP-Wurzel und hat mit dem Veroeffentlichen eines Releases nichts zu tun.",
|
||||
"ftpHost": "",
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "",
|
||||
"ftpPass": "",
|
||||
|
||||
"_remote_comment": "Auf diesem Server liegt die Release-Ablage auf der FTP-Wurzel, nicht unter public/ oder public_html/ - geprueft am 2026-08-13. Die Beispielvorlage des Packagers nennt /public_html/releases, das passt hier nicht.",
|
||||
"ftpRemoteBaseDir": "/releases",
|
||||
|
||||
"apiBaseUrl": "https://dc.mhdf.de",
|
||||
|
||||
"_apiToken_comment": "Token mit dem Recht updateservice:publish, im WebUI unter Token-Verwaltung erzeugen. Ohne ihn laedt der Packager das Paket zwar hoch, meldet es aber nicht an, es entsteht KEINE Signatur, und der Rueckgabewert ist 2.",
|
||||
"apiToken": "",
|
||||
|
||||
"_excludePatterns_comment": "Kommt gar nicht erst ins Paket. Echte Globs: * innerhalb eines Ordners, ** ueber Ordnergrenzen.",
|
||||
"excludePatterns": [
|
||||
"*.pdb",
|
||||
"*.xml",
|
||||
"*.log",
|
||||
"logs/**",
|
||||
"Logs/**",
|
||||
"Instances/**",
|
||||
"tools/**",
|
||||
"*.tmp"
|
||||
],
|
||||
|
||||
"_preservePatterns_comment": "Wird ausgeliefert, ersetzt am Ziel aber nie eine vorhandene Datei. Bei uns bewusst leer: Die Konfiguration liegt in AppPaths.ConfigDirectory (%APPDATA% bzw. $XDG_CONFIG_HOME) und damit ausserhalb des Installationsverzeichnisses - ein Update kann sie gar nicht erreichen. setup.json gehoert NICHT hierher: sie ist eine Beschreibung, keine eingerichtete Datei, und soll mit jedem Release aktualisiert werden.",
|
||||
"preservePatterns": []
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/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/net8.0/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) -> 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.
|
||||
"""
|
||||
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)")
|
||||
|
||||
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("--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)
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user