added version info to backend

This commit is contained in:
Oliver Kieselbach
2026-04-22 19:36:19 +02:00
parent 2e82dd2ba6
commit 42ff4284d8
10 changed files with 190 additions and 8 deletions
+2 -2
View File
@@ -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
@@ -1 +1 @@
111
113
@@ -1 +1 @@
1078
1079
@@ -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 <Version> 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);
}
}
@@ -6,6 +6,12 @@
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Backend version surfaced via /api/health. CommitHash is injected at CI time
through -p:SourceRevisionId=<github.sha> and appended to InformationalVersion. -->
<Version>1.0.0</Version>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<IncludeSourceRevisionInInformationalVersion>true</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
<ItemGroup>
@@ -12,6 +12,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
private readonly ILogger<HealthCheckFunction> _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<HealthCheckFunction> logger,
HealthCheckService healthCheckService,
IMemoryCache cache)
IMemoryCache cache,
BackendBuildInfo buildInfo)
{
_logger = logger;
_healthCheckService = healthCheckService;
_cache = cache;
_buildInfo = buildInfo;
}
/// <summary>
@@ -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;
@@ -146,6 +146,7 @@ builder.Services.AddSingleton<McpUserService>();
builder.Services.AddSingleton<PreviewWhitelistService>();
builder.Services.AddSingleton<TenantAdminsService>();
builder.Services.AddSingleton<HealthCheckService>();
builder.Services.AddSingleton<BackendBuildInfo>();
builder.Services.AddSingleton<GatherRuleService>();
builder.Services.AddSingleton<AnalyzeRuleService>();
builder.Services.AddSingleton<ImeLogPatternService>();
@@ -0,0 +1,61 @@
using System.Reflection;
namespace AutopilotMonitor.Functions.Services
{
/// <summary>
/// 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 &lt;Version&gt; and
/// -p:SourceRevisionId (the latter is set by the deploy workflow to github.sha).
/// </summary>
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<AssemblyInformationalVersionAttribute>()?.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;
}
}
/// <summary>
/// Splits an InformationalVersion ("1.0.0+abc1234567...") into a semver part
/// and a 7-char short commit hash. No '+' → empty commit hash.
/// </summary>
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);
}
}
}
@@ -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() {
</div>
)}
{/* Backend Build (Global Admin only) */}
{user?.isGlobalAdmin && healthResult?.version && (
<div className="mb-6">
<h2 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Backend Build</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border-l-4 border-indigo-500">
<div className="p-4 grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm">
<div>
<dt className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Version</dt>
<dd className="mt-1 font-mono text-gray-900 dark:text-gray-100">{healthResult.version}</dd>
</div>
<div>
<dt className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Commit</dt>
<dd className="mt-1 font-mono text-gray-900 dark:text-gray-100">
{healthResult.commitHash ? (
<a
href={`https://github.com/okieselbach/Autopilot-Monitor/commit/${healthResult.commitHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 dark:text-indigo-400 hover:underline"
>
{healthResult.commitHash}
</a>
) : (
<span className="text-gray-400">unknown</span>
)}
</dd>
</div>
<div>
<dt className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Built</dt>
<dd className="mt-1 text-gray-900 dark:text-gray-100">
{healthResult.buildUtc ? new Date(healthResult.buildUtc).toLocaleString() : '—'}
</dd>
</div>
</div>
</div>
</div>
)}
{/* Real-Time Connection Status */}
<div className="mb-6">
<h2 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Real-Time Connection</h2>