From 97609a67bec0846e08f398eb7ad849c0068c9741 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 11 Nov 2026 22:06:59 +0000 Subject: [PATCH 1/3] test(blazor): sample the pending state inside the dispatcher The async field validation suspends at its first await and queues its continuation on the renderer's dispatcher. Sampling IsValidationPending after the InvokeAsync call returned raced that continuation: on the Release Linux runner it resolved first, so the test asserted False. Read the state inside the same dispatcher work item, where the continuation cannot run yet. --- src/tests/Blazor.UnitTests/GeneratedSchemaTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tests/Blazor.UnitTests/GeneratedSchemaTests.cs b/src/tests/Blazor.UnitTests/GeneratedSchemaTests.cs index 59e5136..7da8898 100644 --- a/src/tests/Blazor.UnitTests/GeneratedSchemaTests.cs +++ b/src/tests/Blazor.UnitTests/GeneratedSchemaTests.cs @@ -236,9 +236,14 @@ public async Task DataAnnotationsValidator_GivenAsyncOnlyMemberFieldChange_IsPen var form = context.Render(editContext, FormHost.DataAnnotations()); FieldIdentifier field = new(cat, nameof(GeneratedCat.Name)); - // Act - await form.ChangeAsync(editContext, field); - var pending = editContext.IsValidationPending(field); + // Act - read the pending state inside the renderer's dispatch operation. The validation suspends at its + // first await, and its continuation is queued on that same dispatcher, so sampling after the call returns + // races the completion (a Release run on a busy Linux runner resolves before the test thread resumes). + var pending = await form.InvokeAsync(() => + { + editContext.NotifyFieldChanged(field); + return editContext.IsValidationPending(field); + }); await FormHost.WaitUntilAsync(() => !editContext.IsValidationPending(field), "resolved"); // Assert From ba937a67ef7249f04a1ebe91aff0f22fb8c6b99b Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 11 Nov 2026 22:07:01 +0000 Subject: [PATCH 2/3] ci: run every test in Debug and pin Purview.Build The shared pipeline runs a single configuration (Release), so Debug - the configuration just test uses locally - was never exercised in CI. Add a Debug job that builds the solution and runs every test, keep run-tests explicit on the Release gate, and guard both with ReleaseGateTests. Pin build-version to the 0.9.0 in .config/dotnet-tools.json so CI runs what local runs and stops warning about an unpinned tool. --- .github/workflows/pr.yml | 38 +++++++++++++++++++ .github/workflows/release.yml | 3 ++ docs/wiki/development/Release-Flow.md | 4 +- .../Repository.UnitTests/ReleaseGateTests.cs | 28 ++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 76e1703..c287e7f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,6 +13,9 @@ jobs: name: Build and test uses: purview-dev/build/.github/workflows/purview-build.yml@main with: + # Pinned to the version in .config/dotnet-tools.json, so CI runs exactly what `just` runs locally + # instead of whichever Purview.Build was latest when the job started (which warns on every run). + build-version: "0.9.0" # The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins # the .NET 11 SDK in global.json. Without the matching SDK the runner cannot resolve global.json # ("A compatible .NET SDK was not found") and every step fails, starting with `dotnet tool @@ -20,10 +23,45 @@ jobs: # NETSDK1045 on the net11.0 target, because the 10.0.x SDK cannot build it. # Keep in sync with `sdk.version` in global.json. dotnet-version: "11.0.100" + # Explicit, so PrWorkflow_GivenTheTestGates_RunsEveryTestInBothConfigurations can prove the + # Release gate still runs tests; the workflow default is true, but a default is invisible here. + run-tests: true run-pack: true validate-pack: true secrets: inherit + # The shared pipeline runs a single configuration (Release, the tool's default). A failure that only + # reproduces in Debug would therefore reach a developer's `just test` (which defaults to Debug) and + # never CI. The cross-platform job below also runs Debug, but filters to the parity producers; this + # job builds the whole solution and runs every test in Debug. + debug: + name: Build and test (Debug) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # Same reason as the cross-platform job: the SDK comes from global.json, not a floating + # version plus a feed quality. + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + + # Repository.UnitTests drives the vitest suites from the .NET test run, so the JavaScript + # dependencies have to be installed first. + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + + - run: bun install --frozen-lockfile + + - name: Build the solution (Debug) + run: dotnet build src/ZodSharp.slnx --configuration Debug + + - name: Run every test (Debug) + run: >- + dotnet test --solution src/ZodSharp.slnx --configuration Debug + --no-restore --no-build --treenode-filter '/*/*/*/*' + # The shared pipeline runs the C# suite only, so the TypeScript/Zod side of the cross-platform # parity guarantee (docs/wiki/quality/Guarantees-and-Limitations.md) would otherwise never run in CI. # The vitest suite validates the JSON the C# cross-platform tests write, so the C# tests must run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01283a5..781a38a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,9 @@ jobs: # file name the policy is registered against. - uses: purview-dev/build/actions/purview-release@main with: + # Pinned to the version in .config/dotnet-tools.json, so the release runs exactly what the + # pull request validated instead of whichever Purview.Build was latest when the job started. + build-version: "0.9.0" release-mode: NuGet trusted-publishing: true # The nuget.org profile name, not the sign-in email. diff --git a/docs/wiki/development/Release-Flow.md b/docs/wiki/development/Release-Flow.md index 35e021d..00ec031 100644 --- a/docs/wiki/development/Release-Flow.md +++ b/docs/wiki/development/Release-Flow.md @@ -27,7 +27,7 @@ rather than passing. | Gate | What it proves | Local | CI | |---|---|---|---| -| Build, lint and tests | The solution builds warning-free, CSharpier reports no change, and every test project passes (including the cross-platform vitest suites, through `Repository.UnitTests`). | `just build`, `just lint-check`, `just test` | `pr.yml` `build` | +| Build, lint and tests | The solution builds warning-free, CSharpier reports no change, and every test project passes (including the cross-platform vitest suites, through `Repository.UnitTests`). | `just build`, `just lint-check`, `just test` | `pr.yml` `build` (Release, with lint and pack) and `pr.yml` `debug` (the whole solution and every test in Debug) | | Pack and pack validation | Every package packs; package validation holds the public API to the 2.4.2 baseline; `.config/purview-build.json` `PackValidation` finds exactly the declared content (`RequiredContent`, `RequireExplicitContent`) and none of the `ForbiddenContent`. | `just pipeline-pack-validate` | `pr.yml` `build` (`run-pack`, `validate-pack`) | | Forbidden-content rule proof | Every `ForbiddenContent` rule rejects a package: a matching file is injected into scratch copies of each package and the real `purview-build` validation must fail, per package and per rule. | `just verify-pack-rules` | `pr.yml` `packed-consumers` | | Packed generator | The IL-merged generator in the `.nupkg` loads and generates in a net10.0 consumer (it has regressed twice while the in-repo tests, which use the unmerged generator, stayed green). | `just smoke-packed-generator` | `pr.yml` `packed-generator` | @@ -74,7 +74,7 @@ just publish-aot-sample # Native AOT minimal API sample an | Workflow | Trigger | Pipeline mode | |---|---|---| -| `pr.yml` | pull requests against `main` | `purview-build.yml` with `run-pack: true`, `validate-pack: true` | +| `pr.yml` | pull requests against `main` | `purview-build.yml` with `run-pack: true`, `validate-pack: true` (Release), plus a Debug build and full test run | | `release.yml` | pushes to `main` | `purview-release.yml` with `release-mode: NuGet` | Both consume `.config/purview-build.json`: diff --git a/src/tests/Repository.UnitTests/ReleaseGateTests.cs b/src/tests/Repository.UnitTests/ReleaseGateTests.cs index cbabdf7..8e49680 100644 --- a/src/tests/Repository.UnitTests/ReleaseGateTests.cs +++ b/src/tests/Repository.UnitTests/ReleaseGateTests.cs @@ -265,6 +265,34 @@ CancellationToken cancellationToken await Assert.That(job).Contains("dotnet workload install wasm-tools"); } + /// + /// The shared pipeline runs a single configuration, so without the Debug job CI would only ever exercise + /// Release while `just test` runs Debug locally. A test that fails only in Debug would reach a developer and + /// never the pull request. Both configurations must build the solution and run every test. + /// + [Test] + public async Task PrWorkflow_GivenTheTestGates_RunsEveryTestInBothConfigurations( + CancellationToken cancellationToken + ) + { + // Arrange + var text = await File.ReadAllTextAsync( + Path.Combine(RepositoryRoot.Value, ".github", "workflows", "pr.yml"), + cancellationToken + ); + + // Act + var jobs = Jobs(text).ToArray(); + var release = jobs.SingleOrDefault(static j => j.Name == "build").Body ?? string.Empty; + var debug = jobs.SingleOrDefault(static j => j.Name == "debug").Body ?? string.Empty; + + // Assert + await Assert.That(release).Contains("run-tests: true"); + await Assert.That(debug).Contains("dotnet build src/ZodSharp.slnx --configuration Debug"); + await Assert.That(debug).Contains("dotnet test --solution src/ZodSharp.slnx --configuration Debug"); + await Assert.That(debug).Contains("--treenode-filter '/*/*/*/*'"); + } + /// /// Compiler-host assemblies must stay forbidden everywhere NuGet hands files to an application, and the generator /// and code fixes everywhere an application loads assemblies from. scripts/test-pack-validation-rules.ps1 From 84d9d64bd144a09fb952180c0e260200e220104b Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 11 Nov 2026 22:09:10 +0000 Subject: [PATCH 3/3] chore: bumped version numbers --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index bc9bc27..1978504 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "purview.build": { - "version": "0.9.0", + "version": "0.9.1", "commands": [ "purview-build" ],