Versions-Definition: --version mit Remote-Check + README-Mismatch-Warnung

- 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).
This commit is contained in:
2026-07-11 17:34:45 +02:00
parent 6ca81679d9
commit ca34a5f866
3 changed files with 174 additions and 5 deletions
+113 -4
View File
@@ -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 <Version> 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: "<full-sha>\tHEAD"
var parts = outp.Split('\t', ' ');
return parts.Length > 0 && parts[0].Length >= 7 ? parts[0][..7] : null;
}
catch
{
return null;
}
}