From 55be316fa883469278d5591f83cb80e718ef88f9 Mon Sep 17 00:00:00 2001 From: "Mars.P" Date: Fri, 14 Aug 2026 17:07:09 +0800 Subject: [PATCH] Stamp the commit sha into the published image A deployed pod reported "Running build 1.0.0" -- a version that cannot tell two deployments apart, which is the only thing a build identity is for. The publish never passed SourceRevisionId, so the SDK had nothing to append. Both Dockerfiles now pass it, from a build arg when the pipeline knows the sha and from the copied .git otherwise, falling back to "unknown" rather than to a version that looks meaningful and is not. BuildIdentity also stops reading Assembly.GetEntryAssembly(). The entry assembly is whatever launched the process: under dotnet test that is the VSTest host, which reported 17.12.0 -- a Microsoft tooling version presented as ours. It now reads the assembly it is defined in, which every host ships and which is built from this repository. --- backend/Dockerfile.api | 12 +++++- backend/Dockerfile.worker | 12 +++++- .../Settings/Logging/BuildIdentity.cs | 6 ++- .../Settings/BuildIdentityTests.cs | 39 +++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 backend/tests/CodeSpace.UnitTests/Settings/BuildIdentityTests.cs diff --git a/backend/Dockerfile.api b/backend/Dockerfile.api index 07ed360b2..0d1baceb7 100644 --- a/backend/Dockerfile.api +++ b/backend/Dockerfile.api @@ -27,7 +27,17 @@ COPY . . # (A csproj-only restore-cache split was attempted but reverted — the API's build-only ProjectReference to # CodeSpace.Mcp breaks a manifest-only restore; not worth the build-speed micro-optimization.) RUN dotnet restore CodeSpace.sln -RUN dotnet publish src/CodeSpace.Api/CodeSpace.Api.csproj -c Release -o /app --no-restore +# The commit this image is built from, stamped into AssemblyInformationalVersion so BuildIdentity can +# report it on every log line and on the boot banner. A deployment incident is not diagnosable until +# a pod can say which code it is running; a bare "1.0.0" cannot. +# +# The arg is for pipelines that know the sha (CI passes it); the git fallback covers every other build, +# because the whole source tree including .git is already in the context. "unknown" is the honest last +# resort rather than a version that looks meaningful and is not. +ARG SOURCE_REVISION_ID="" +RUN SHA="${SOURCE_REVISION_ID:-$(git rev-parse HEAD 2>/dev/null || echo unknown)}" \ + && echo "Building from revision $SHA" \ + && dotnet publish src/CodeSpace.Api/CodeSpace.Api.csproj -c Release -o /app --no-restore -p:SourceRevisionId="$SHA" FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime diff --git a/backend/Dockerfile.worker b/backend/Dockerfile.worker index 6477c5288..74616eee8 100644 --- a/backend/Dockerfile.worker +++ b/backend/Dockerfile.worker @@ -46,7 +46,17 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY . . RUN dotnet restore CodeSpace.sln -RUN dotnet publish src/CodeSpace.Api/CodeSpace.Api.csproj -c Release -o /app --no-restore +# The commit this image is built from, stamped into AssemblyInformationalVersion so BuildIdentity can +# report it on every log line and on the boot banner. A deployment incident is not diagnosable until +# a pod can say which code it is running; a bare "1.0.0" cannot. +# +# The arg is for pipelines that know the sha (CI passes it); the git fallback covers every other build, +# because the whole source tree including .git is already in the context. "unknown" is the honest last +# resort rather than a version that looks meaningful and is not. +ARG SOURCE_REVISION_ID="" +RUN SHA="${SOURCE_REVISION_ID:-$(git rev-parse HEAD 2>/dev/null || echo unknown)}" \ + && echo "Building from revision $SHA" \ + && dotnet publish src/CodeSpace.Api/CodeSpace.Api.csproj -c Release -o /app --no-restore -p:SourceRevisionId="$SHA" # ── 2. The Node-based harness CLIs, version-pinned (override at build time with --build-arg) ── # These two ARG defaults are the SINGLE SOURCE OF TRUTH for the pinned CLI versions. A test diff --git a/backend/src/CodeSpace.Core/Settings/Logging/BuildIdentity.cs b/backend/src/CodeSpace.Core/Settings/Logging/BuildIdentity.cs index c13ee3a2a..f0a906522 100644 --- a/backend/src/CodeSpace.Core/Settings/Logging/BuildIdentity.cs +++ b/backend/src/CodeSpace.Core/Settings/Logging/BuildIdentity.cs @@ -25,7 +25,11 @@ public static class BuildIdentity private static string Resolve() { - var assembly = Assembly.GetEntryAssembly() ?? typeof(BuildIdentity).Assembly; + // This assembly, deliberately, not Assembly.GetEntryAssembly(). The entry assembly is whatever + // launched the process, which under `dotnet test` is the VSTest host — it reported "17.12.0", + // a version belonging to Microsoft's tooling. Every host that matters ships this assembly, and + // it is the one built from this repository. + var assembly = typeof(BuildIdentity).Assembly; var informational = assembly.GetCustomAttribute()?.InformationalVersion; diff --git a/backend/tests/CodeSpace.UnitTests/Settings/BuildIdentityTests.cs b/backend/tests/CodeSpace.UnitTests/Settings/BuildIdentityTests.cs new file mode 100644 index 000000000..55e6e0f44 --- /dev/null +++ b/backend/tests/CodeSpace.UnitTests/Settings/BuildIdentityTests.cs @@ -0,0 +1,39 @@ +using CodeSpace.Core.Settings.Logging; +using Shouldly; + +namespace CodeSpace.UnitTests.Settings; + +/// +/// The value every log line carries as Build, and the one the boot banner prints. +/// +/// It exists to answer the first question of any deployment incident — is this pod running the +/// code I think it is — so the only failure that matters is it being unable to distinguish two +/// builds. A bare 1.0.0 does exactly that, and is what a deployed image reported until the +/// Dockerfiles began passing SourceRevisionId. +/// +[Trait("Category", "Unit")] +public class BuildIdentityTests +{ + [Fact] + public void It_reports_something() + { + BuildIdentity.Value.ShouldNotBeNullOrWhiteSpace(); + } + + /// + /// Under dotnet test the SDK stamps the revision from the local checkout, so the sha is + /// present here exactly as it must be in a published image. This fails if the SDK ever stops + /// appending it — the silent version of the deployed 1.0.0. + /// + [Fact] + public void It_carries_the_source_revision() + { + BuildIdentity.Value.ShouldContain("+", + customMessage: $"Build identity is '{BuildIdentity.Value}' — a version with no commit sha cannot tell two " + + "deployments apart, which is the only thing it is for. Publishes must pass " + + "-p:SourceRevisionId (see backend/Dockerfile.api)."); + + BuildIdentity.Value.Split('+')[^1].Length.ShouldBeGreaterThanOrEqualTo(7, + customMessage: "The suffix after '+' must be a commit sha long enough to identify a commit."); + } +}