fix(archreview): CI pipeline + codegen freshness guards (IPC-01/09/19/20, TST-03)

- IPC-01: regenerate the stale client descriptor set and add ClientProtoInputTests
  (semantic, protoc-free) so a missing contract symbol fails the build.
- IPC-20: make publish-client-proto-inputs.ps1 -Check source_code_info-normalized
  so it is protoc-version tolerant.
- IPC-19: document + guard the "Generated/ must be committed for net48" rule
  (docs/Contracts.md, csproj comment, check-codegen.ps1).
- IPC-09: harden the per-client generate-proto scripts (PATH resolution + pinned
  grpcio/protobuf/java version asserts) so regeneration is reproducible.
- TST-03: add .gitea/workflows/ci.yml (portable/java/windows/live jobs) running the
  build, tests, client checks, and the codegen guards.
- Also: check-codegen.ps1 Check 3 guards the CLI-02 vendored Rust protos against drift.

archreview: IPC-01/09/19/20 Done, TST-03 In review (pipeline authored + validated,
not yet run on a Gitea runner). Verified on macOS: NonWindows build clean,
ClientProtoInputTests 5/5, -Check exit 0.
This commit is contained in:
Joseph Doherty
2026-07-09 06:17:56 -04:00
parent 219fa6ddb4
commit d5248f61a2
13 changed files with 645 additions and 47 deletions
@@ -23,6 +23,13 @@
</PropertyGroup>
<ItemGroup>
<!-- Generated\**\*.cs is Compile-Removed and regenerated by Grpc.Tools into the TRACKED Generated
folder. It MUST be committed after any .proto change: Grpc.Tools skips regeneration when the
committed output looks up to date, so on net10 the freshly regenerated code compiles (drift is
invisible) while the net48 worker, which consumes the COMMITTED Generated\*.cs, fails with
CS0246 on new types. Regenerate + commit after editing a .proto; if a build does not pick up
the change, delete Generated\*.cs to force a full regen. scripts/check-codegen.ps1 enforces
this in CI. See docs/Contracts.md. -->
<Compile Remove="Generated\**\*.cs" />
<Protobuf Include="Protos\mxaccess_gateway.proto" ProtoRoot="Protos" OutputDir="Generated" GrpcOutputDir="Generated" GrpcServices="Both" />
<Protobuf Include="Protos\mxaccess_worker.proto" ProtoRoot="Protos" OutputDir="Generated" GrpcServices="None" />
@@ -1,5 +1,6 @@
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
@@ -7,6 +8,102 @@ namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
public sealed class ClientProtoInputTests
{
/// <summary>
/// Guards the published client descriptor set against silent staleness (IPC-01). Every message
/// and field compiled into the in-process contract (which the build regenerates from the current
/// <c>.proto</c> sources) must appear in the committed protoset. A missing symbol means the
/// descriptor was not regenerated after a proto change; run
/// <c>scripts/publish-client-proto-inputs.ps1</c> and commit the refreshed protoset.
/// The check is semantic (symbol presence) rather than byte-wise, so it is independent of protoc
/// version and does not require protoc on the test runner.
/// </summary>
[Fact]
public void Descriptor_ContainsEveryContractMessageAndField()
{
DirectoryInfo repositoryRoot = FindRepositoryRoot();
string descriptorPath = Path.Combine(
repositoryRoot.FullName,
"clients",
"proto",
"descriptors",
"mxaccessgw-client-v1.protoset");
Assert.True(File.Exists(descriptorPath), $"Expected descriptor set '{descriptorPath}' to exist.");
FileDescriptorSet descriptorSet = FileDescriptorSet.Parser.ParseFrom(File.ReadAllBytes(descriptorPath));
HashSet<string> publishedMessages = new(StringComparer.Ordinal);
HashSet<string> publishedFields = new(StringComparer.Ordinal);
foreach (FileDescriptorProto file in descriptorSet.File)
{
foreach (DescriptorProto message in file.MessageType)
{
CollectPublishedSymbols(file.Package, message, publishedMessages, publishedFields);
}
}
List<string> missing = [];
foreach (FileDescriptor file in new[] { MxaccessGatewayReflection.Descriptor, MxaccessWorkerReflection.Descriptor })
{
foreach (MessageDescriptor message in file.MessageTypes)
{
CollectMissingContractSymbols(message, publishedMessages, publishedFields, missing);
}
}
Assert.True(
missing.Count == 0,
"Published client descriptor is stale; regenerate with scripts/publish-client-proto-inputs.ps1 and commit. "
+ "Missing symbols: "
+ string.Join(", ", missing));
}
private static void CollectPublishedSymbols(
string package,
DescriptorProto message,
HashSet<string> messages,
HashSet<string> fields)
{
string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name;
messages.Add(fullName);
foreach (FieldDescriptorProto field in message.Field)
{
fields.Add(fullName + "/" + field.Name);
}
foreach (DescriptorProto nested in message.NestedType)
{
CollectPublishedSymbols(fullName, nested, messages, fields);
}
}
private static void CollectMissingContractSymbols(
MessageDescriptor message,
HashSet<string> publishedMessages,
HashSet<string> publishedFields,
List<string> missing)
{
if (!publishedMessages.Contains(message.FullName))
{
missing.Add(message.FullName);
}
foreach (FieldDescriptor field in message.Fields.InDeclarationOrder())
{
string key = message.FullName + "/" + field.Name;
if (!publishedFields.Contains(key))
{
missing.Add(key);
}
}
foreach (MessageDescriptor nested in message.NestedTypes)
{
CollectMissingContractSymbols(nested, publishedMessages, publishedFields, missing);
}
}
/// <summary>Verifies that the proto inputs manifest declares current protocol versions and existing source files.</summary>
[Fact]
public void Manifest_DeclaresCurrentProtocolVersionsAndExistingInputs()