diff --git a/.claude/commands b/.claude/commands index a3970d47..c506bb75 160000 --- a/.claude/commands +++ b/.claude/commands @@ -1 +1 @@ -Subproject commit a3970d474c65afd4d2709a88704e6cc14f84f3d7 +Subproject commit c506bb75e02f076aad45deca3c9d9e45c36b51c9 diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 61f941eb..8669e353 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -25,10 +25,10 @@ jobs: run: dotnet restore src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj - name: Build - run: dotnet build src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj --configuration Release --no-restore + run: dotnet build src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj --configuration Release --no-restore -p:SourceRevisionId=${{ github.sha }} - name: Publish - run: dotnet publish src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj --configuration Release --output ./publish --no-build + run: dotnet publish src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj --configuration Release --output ./publish --no-build -p:SourceRevisionId=${{ github.sha }} - name: Deploy to Azure Functions uses: Azure/functions-action@v1 diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt index 58c9bdf9..9b252fd0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt @@ -1 +1 @@ -111 +113 diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 19d50492..41495a48 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -1078 +1079 diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/BackendBuildInfoTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/BackendBuildInfoTests.cs new file mode 100644 index 00000000..3f5c408d --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions.Tests/BackendBuildInfoTests.cs @@ -0,0 +1,64 @@ +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.Tests; + +public class BackendBuildInfoTests +{ + [Fact] + public void Parse_VersionWithLongSha_TrimsTo7Chars() + { + var (version, commit) = BackendBuildInfo.ParseInformationalVersion( + "1.0.0+2e82dd2bdeadbeefcafe1234567890abcdef1234"); + + Assert.Equal("1.0.0", version); + Assert.Equal("2e82dd2", commit); + } + + [Fact] + public void Parse_VersionWithoutSha_ReturnsEmptyCommit() + { + var (version, commit) = BackendBuildInfo.ParseInformationalVersion("1.0.0"); + + Assert.Equal("1.0.0", version); + Assert.Equal(string.Empty, commit); + } + + [Fact] + public void Parse_ShortShaBelowSeven_PreservedAsIs() + { + var (version, commit) = BackendBuildInfo.ParseInformationalVersion("1.0.0+abc12"); + + Assert.Equal("1.0.0", version); + Assert.Equal("abc12", commit); + } + + [Fact] + public void Parse_Empty_ReturnsFallback() + { + var (version, commit) = BackendBuildInfo.ParseInformationalVersion(""); + + Assert.Equal("0.0.0", version); + Assert.Equal(string.Empty, commit); + } + + [Fact] + public void Parse_NullSafe() + { + var (version, commit) = BackendBuildInfo.ParseInformationalVersion(null!); + + Assert.Equal("0.0.0", version); + Assert.Equal(string.Empty, commit); + } + + [Fact] + public void Constructor_ReadsCurrentAssembly_ProducesNonEmptyVersion() + { + var info = new BackendBuildInfo(); + + // Version comes from in .csproj via generated AssemblyInformationalVersion. + // Test asserts only that the parser wired to the assembly produces something — we + // don't hardcode the version string here to avoid coupling tests to every bump. + Assert.False(string.IsNullOrWhiteSpace(info.Version)); + Assert.NotEqual(default, info.BuildUtc); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj index 6da0673c..74efbf9d 100644 --- a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj +++ b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj @@ -6,6 +6,12 @@ Exe enable enable + + + 1.0.0 + true + true diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs index 8f393753..85199df1 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs @@ -12,6 +12,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure private readonly ILogger _logger; private readonly HealthCheckService _healthCheckService; private readonly IMemoryCache _cache; + private readonly BackendBuildInfo _buildInfo; private const int MaxRequestsPerMinute = 30; private static readonly TimeSpan RateWindow = TimeSpan.FromMinutes(1); @@ -19,11 +20,13 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure public HealthCheckFunction( ILogger logger, HealthCheckService healthCheckService, - IMemoryCache cache) + IMemoryCache cache, + BackendBuildInfo buildInfo) { _logger = logger; _healthCheckService = healthCheckService; _cache = cache; + _buildInfo = buildInfo; } /// @@ -71,7 +74,10 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure { status = "healthy", service = "Autopilot Monitor API", - timestamp = DateTime.UtcNow + timestamp = DateTime.UtcNow, + version = _buildInfo.Version, + commitHash = _buildInfo.CommitHash, + buildUtc = _buildInfo.BuildUtc }); return response; @@ -101,7 +107,10 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure service = "Autopilot Monitor API", timestamp = healthCheckResult.Timestamp, overallStatus = healthCheckResult.OverallStatus, - checks = healthCheckResult.Checks + checks = healthCheckResult.Checks, + version = _buildInfo.Version, + commitHash = _buildInfo.CommitHash, + buildUtc = _buildInfo.BuildUtc }); return response; diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs index e4066287..4f1f886b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Program.cs +++ b/src/Backend/AutopilotMonitor.Functions/Program.cs @@ -146,6 +146,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/BackendBuildInfo.cs b/src/Backend/AutopilotMonitor.Functions/Services/BackendBuildInfo.cs new file mode 100644 index 00000000..af39a7c5 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/BackendBuildInfo.cs @@ -0,0 +1,61 @@ +using System.Reflection; + +namespace AutopilotMonitor.Functions.Services +{ + /// + /// Exposes the backend build identity (version, commit hash, build time) surfaced + /// via /api/health and /api/health/detailed. Values come from the assembly's + /// InformationalVersion attribute which MSBuild populates from <Version> and + /// -p:SourceRevisionId (the latter is set by the deploy workflow to github.sha). + /// + public sealed class BackendBuildInfo + { + public string Version { get; } + public string CommitHash { get; } + public DateTime BuildUtc { get; } + + public BackendBuildInfo() + { + var asm = typeof(BackendBuildInfo).Assembly; + var info = asm.GetCustomAttribute()?.InformationalVersion + ?? "0.0.0"; + + (Version, CommitHash) = ParseInformationalVersion(info); + + try + { + var path = asm.Location; + BuildUtc = !string.IsNullOrEmpty(path) && File.Exists(path) + ? File.GetLastWriteTimeUtc(path) + : DateTime.UtcNow; + } + catch + { + BuildUtc = DateTime.UtcNow; + } + } + + /// + /// Splits an InformationalVersion ("1.0.0+abc1234567...") into a semver part + /// and a 7-char short commit hash. No '+' → empty commit hash. + /// + internal static (string Version, string CommitHash) ParseInformationalVersion(string informationalVersion) + { + if (string.IsNullOrWhiteSpace(informationalVersion)) + { + return ("0.0.0", string.Empty); + } + + var plus = informationalVersion.IndexOf('+'); + if (plus < 0) + { + return (informationalVersion, string.Empty); + } + + var version = informationalVersion.Substring(0, plus); + var sha = informationalVersion.Substring(plus + 1); + var shortSha = sha.Length > 7 ? sha.Substring(0, 7) : sha; + return (version, shortSha); + } + } +} diff --git a/src/Web/autopilot-monitor-web/app/health-check/page.tsx b/src/Web/autopilot-monitor-web/app/health-check/page.tsx index 39a2a9fb..1fcfd1a3 100644 --- a/src/Web/autopilot-monitor-web/app/health-check/page.tsx +++ b/src/Web/autopilot-monitor-web/app/health-check/page.tsx @@ -22,6 +22,9 @@ interface HealthCheckResult { timestamp: string; overallStatus: string; checks: HealthCheck[]; + version?: string; + commitHash?: string; + buildUtc?: string; } export default function HealthCheckPage() { @@ -182,6 +185,44 @@ export default function HealthCheckPage() { )} + {/* Backend Build (Global Admin only) */} + {user?.isGlobalAdmin && healthResult?.version && ( +
+

Backend Build

+
+
+
+
Version
+
{healthResult.version}
+
+
+
Commit
+
+ {healthResult.commitHash ? ( + + {healthResult.commitHash} + + ) : ( + unknown + )} +
+
+
+
Built
+
+ {healthResult.buildUtc ? new Date(healthResult.buildUtc).toLocaleString() : '—'} +
+
+
+
+
+ )} + {/* Real-Time Connection Status */}

Real-Time Connection