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
@@ -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()