From ca34a5f866cacce6a684afb82d6db0599fa0aadb2c3329cc7f3670b1cf3fa595 Mon Sep 17 00:00:00 2001 From: gc-dev-afjd Date: Sat, 11 Jul 2026 17:34:45 +0200 Subject: [PATCH] Versions-Definition: --version mit Remote-Check + README-Mismatch-Warnung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Program.cs: PrintVersion() vergleicht Assembly-Version mit README.md (Block-Quote '> **Version: X.Y.Z**') und warnt bei Mismatch. Remote-Check via 'git ls-remote origin HEAD' vergleicht lokalen mit remote Commit-Hash; Hinweis 'UPDATE AVAILABLE' bei Differenz. Copy-Installationen (kein .git) erhalten Hinweis auf ./install.sh. - README.md: Versions-Block-Quote unter dem Titel (kanonisch), Update-Check-Doku, drei synchrone Versionsquellen dokumentiert. - AGENTS.md: Versions-Definition als Definition of Done für Releases (README + csproj + Assembly müssen synchron sein). --- AGENTS.md | 18 ++++++++ README.md | 44 ++++++++++++++++++- src/Program.cs | 117 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d4963d6..c34abb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,3 +34,21 @@ dotnet bin/Release/net10.0/webstatic.dll serve --port 8000 # lokale V - **Kein Commit ohne expliziten Wunsch.** Bei Release: signierte Commits. - `dist/`, `bin/`, `obj/`, `.translation-drafts/` sind gitignored. - Machine-Translation liefert nur **Entwürfe** (`.translation-drafts/`), nie Auto-Überschreibung. + +## Versions-Definition (Definition of Done für Releases) +Die Versionsnummer wird an **drei Stellen synchron** gehalten — ein Release-Commit +muss alle drei aktualisieren, andernfalls warnt `webstatic --version` vor einem +Mismatch: + +1. **`README.md`** — Block-Quote `> **Version: X.Y.Z**` direkt unter dem Titel + (kanonisch; per `git show origin/main:README.md` remote sichtbar) +2. **`GCBoK.WebStaticBuilder.csproj`** — `X.Y.Z` (Assembly) +3. **`webstatic --version`** — gibt Assembly-Version aus, vergleicht mit README + und remote Commit-Hash (`git ls-remote origin HEAD`) + +Vor einem Release sind lokal und remote die Version feststellbar, ohne die CLI +aufzurufen: +```bash +git show HEAD:README.md | head -3 # lokal +git show origin/main:README.md | head -3 # remote +``` diff --git a/README.md b/README.md index c6c98be..d59d763 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # GCBoK.WebStaticBuilder +> **Version: 1.0.0** — Diese Versionsnummer steht in `README.md` (kanonisch), +> in `GCBoK.WebStaticBuilder.csproj` (``) und wird von `webstatic --version` +> ausgegeben. Alle drei Quellen müssen synchron gehalten werden. + Eigenständiger, quelloffener **Static-Site-Generator** der GitCover Commons für mehrsprachige, SEO-optimierte, compliance-freundliche Websites. Ersetzt den früheren C#-Builderskript der GCBoK-Website durch ein generisches, für KMU @@ -69,8 +73,46 @@ Siehe `webstatic.example/webstatic.json` und `docs/getting-started.md`. ## Version & Update +Die Versionsnummer wird an drei Stellen synchron gehalten (Definition of Done +für jeden Release-Commit): + +1. **`README.md`** — Block-Quote direkt unter dem Titel (kanonisch, per Remote + via `git show origin/main:README.md` sichtbar) +2. **`GCBoK.WebStaticBuilder.csproj`** — `1.0.0` (Assembly) +3. **`webstatic --version`** — gibt die Assembly-Version aus + +So kann vor einem Update sowohl lokal (im Installations-Repo) als auch remote +(im Upstream-Repo) die Version festgestellt werden, ohne die CLI aufzurufen: + +```bash +# Lokal installierte Version (Clone-Installation) +git -C /opt/GitCover/webstatic show HEAD:README.md | head -3 + +# Remote verfügbare Version (vor Pull) +git -C /opt/GitCover/webstatic show origin/main:README.md | head -3 +``` + +### Update-Check via CLI + +`webstatic --version` führt automatisch einen Remote-Check durch +(`git ls-remote origin HEAD`) und vergleicht lokalen mit remote Commit-Hash: + +```bash +webstatic --version +# GCBoK.WebStaticBuilder 1.0.0.0 +# commit: 6ca8167 (local) +# remote: ff634a3 +# +# UPDATE AVAILABLE +# Run: webstatic update +``` + +Bei Copy-Installationen (kein `.git`) entfällt der Remote-Check; das Tool weist +auf `./install.sh` als Update-Weg hin. + +### Update durchführen + ```bash -webstatic --version # Assembly-Version + Git-Commit des Installations-Repos webstatic update # Selbstaktualisierung (Clone-Installation): lokale # Änderungen werden gestasht, git pull, gemerged, neu gebaut ``` diff --git a/src/Program.cs b/src/Program.cs index aaf659e..353bfe5 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -24,10 +24,7 @@ if (argsList.Contains("--help") || argsList.Contains("-h") || argsList.Contains( } if (argsList.Contains("--version")) { - var v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - Console.WriteLine($"GCBoK.WebStaticBuilder {v?.ToString() ?? "0.0.0"}"); - var hash = GitShortHash(AppContext.BaseDirectory); - if (hash != null) Console.WriteLine($"commit: {hash}"); + PrintVersion(verbose); return; } @@ -411,3 +408,115 @@ static void ShowStatus(string rootDir) var markdown = File.ReadAllText(readme); MarkdownConsoleRenderer.Render(markdown, Console.Out); } + +static void PrintVersion(bool verbose) +{ + var v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + var asmVersion = v?.ToString() ?? "0.0.0"; + Console.WriteLine($"GCBoK.WebStaticBuilder {asmVersion}"); + + // README.md version check — canonical source is the block-quote + // "> **Version: X.Y.Z**" directly under the title. + var readmeVersion = ExtractReadmeVersion(AppContext.BaseDirectory); + if (readmeVersion != null) + { + // Assembly versions are 4-part (1.0.0.0); README uses 3-part (1.0.0). + // Compare the first 3 components to avoid trailing-zero noise. + var asmShort = v?.ToString(3) ?? asmVersion; + if (!string.Equals(asmShort, readmeVersion, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"WARNING: version mismatch — README.md says {readmeVersion}, assembly says {asmShort}"); + Console.WriteLine(" Update README.md and/or in the .csproj to keep them in sync."); + } + } + + var repo = FindRepoRoot(AppContext.BaseDirectory); + if (repo == null) + { + Console.WriteLine("install: copy (no git repo — version check skipped)"); + Console.WriteLine("update: run ./install.sh again to update"); + return; + } + + var localHash = GitShortHash(AppContext.BaseDirectory); + if (localHash != null) Console.WriteLine($"commit: {localHash} (local)"); + + // Remote check (best-effort, non-blocking on failure) + var remoteShort = CaptureRemoteHeadShort(repo); + if (remoteShort == null) + { + if (verbose) Console.WriteLine("(remote check failed — no network or no upstream)"); + return; + } + + Console.WriteLine($"remote: {remoteShort}"); + + if (localHash != null && !string.Equals(localHash, remoteShort, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(); + Console.WriteLine(" UPDATE AVAILABLE"); + Console.WriteLine(" Run: webstatic update"); + } + else if (localHash != null) + { + Console.WriteLine(" up to date"); + } +} + +static string? ExtractReadmeVersion(string baseDir) +{ + // Search upwards for README.md (it lives at the repo root, not in bin/). + var dir = new DirectoryInfo(Path.GetFullPath(baseDir)); + string? readmePath = null; + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, "README.md"); + if (File.Exists(candidate)) { readmePath = candidate; break; } + dir = dir.Parent; + } + if (readmePath == null) return null; + + try + { + var lines = File.ReadAllLines(readmePath); + // Look for "> **Version: X.Y.Z**" within the first 10 lines + foreach (var line in lines.Take(10)) + { + var m = System.Text.RegularExpressions.Regex.Match( + line, @"\*\*Version:\s*(\d+\.\d+(?:\.\d+)?)\*\*", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (m.Success) return m.Groups[1].Value; + } + } + catch { /* best-effort */ } + return null; +} + +static string? CaptureRemoteHeadShort(string repoDir) +{ + try + { + var psi = new ProcessStartInfo + { + FileName = "git", + Arguments = "ls-remote origin HEAD", + WorkingDirectory = repoDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + using var p = Process.Start(psi); + if (p == null) return null; + var outp = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(5000); // 5s timeout + if (p.ExitCode != 0 || string.IsNullOrEmpty(outp)) return null; + // Format: "\tHEAD" + var parts = outp.Split('\t', ' '); + return parts.Length > 0 && parts[0].Length >= 7 ? parts[0][..7] : null; + } + catch + { + return null; + } +}