Release v0.62.0
Date: 2026-04-11
Theme
SDK coverage initiative — Stage 1: the .NET SDK ships.
v0.61.0 closed Developer Blockers #1, 3–10. The last unfinished item was #2 — ”.NET / Java / Go SDKs” — which shipped as a self-serve openapi-generator recipe in docs/sdk-generation-guide.md without packaged clients. That’s a worse experience than no SDK at all, because it looks superficially supported but leaves every consumer to install a Java tool, figure out package metadata, and host stubs themselves.
v0.62.0 is the first language to ship via the new generator-driven SDK track: PulpEngine.Sdk, a fully generated C# client, committed to the repo, version-locked with the rest of the monorepo, and published to NuGet on tag push. The TypeScript and Python SDKs predate the OpenAPI surface being first-class and stay hand-written; new languages join the generator track so the maintenance cost is bounded.
Stages 2 (Go) and 3 (Java) follow in their own releases. See docs/initiatives/sdk-coverage-stage1.md for the condensed execution checklist and the running deviation log.
Highlights
@pulp-engine/sdk-dotnet — committed, buildable, smoke-tested
- Package:
packages/sdk-dotnet/. NuGet package idPulpEngine.Sdk. Targetsnet8.0. Uses thehttpclientlibrary template (async-only API surface). Newtonsoft.Json for serialization via the generator’s default (System.Text.Json migration is a follow-up). - Generated from:
openapi.jsonvia openapi-generatorcsharpgenerator, JAR version7.10.0pinned in openapitools.json. - All generated source is committed. Same pattern as
packages/sdk-typescript/openapi.d.ts— debuggable, reviewable, reproducible, and greppable. - Hand-written files (README, RELEASING, generator config, tests) are protected from regeneration by
.openapi-generator-ignore. Verified by regenerating twice with a sentinel edit between runs. - Round-trip smoke test
tests/HealthSmokeTest.csexercises the generatedHealthApi.HealthGetAsync()against a local API, asserts theHealthGet200Responsedeserializes correctly, and verifies the SDK’s<Version>matches the API’s runtimeversionfield as a lockstep-integrity check. Passed end-to-end on this release commit.
pnpm sdk:generate orchestrator
New workspace-root script at scripts/generate-sdks.ts:
- Shells out to
openapi-generator-cli generateonce per language with the workspace root version injected via--additional-properties packageVersion=$ROOT_VERSION. - Auto-detects missing Java and falls back to Docker (
openapitools/openapi-generator-cli:v7.10.0). HonorsPULP_ENGINE_SDK_GEN=dockeras an override. Removes the “install a JDK first” friction for contributors on Windows or any box that already has Docker Desktop. --checkmode generates to a fresh tmpdir and diffs against committed source, exiting 1 on drift. Pre-seeds the tmpdir with.openapi-generator-ignoreso the check path is a true apples-to-apples comparison (otherwise the ignored files get emitted into the tmpdir but skipped in the committed tree, producing spurious drift).- Stage 1:
--dotnetis the only wired target.--javaand--goare recognized but emit “not yet wired” messages until their respective stages.
CI freshness gates
Two new steps added to .github/workflows/ci.yml immediately after pnpm build, in this exact order:
- name: Verify openapi.json is fresh
run: pnpm extract-openapi --check
- name: Verify generated SDKs are fresh
run: pnpm sdk:generate --check
Closes two gaps at once:
- Pre-existing openapi.json drift hole. Before v0.62.0, there was no CI step verifying
openapi.jsonwas up to date with the API routes. If a developer added an endpoint without runningpnpm extract-openapi, the spec would silently fall behind reality. This gate catches that now. - New SDK drift hole. Any edit to
openapi.jsonthat would regenerate differently into the committed.NETSDK tree fails CI, forcing the developer to commit the regenerated diff.
Order is load-bearing: if the spec is stale, running the SDK check second produces noisy output that’s really just “the spec changed” rehashed across the SDK — failing on the spec check first points at the real problem.
scripts/check-version.mjs now covers the .NET SDK
New readCsprojVersion() reader parses the <Version> element from packages/sdk-dotnet/src/PulpEngine.Sdk/PulpEngine.Sdk.csproj via a targeted regex (no XML parser dependency). The lockstep version check now spans seven package files: root + api + editor + preview + sdk-typescript + sdk-python + sdk-dotnet. Java and Go readers will be added in their respective stages.
Lenient mode: returns { missing: true } when the .csproj doesn’t exist yet (e.g. before pnpm sdk:generate has run for the first time on a fresh clone). The lockstep loop treats missing: true as a SKIP warning rather than a crash, so developers can commit infra changes before they have Java/Docker available.
Windows-friendly line-ending enforcement
New .gitattributes entries force LF on all generated SDK source and openapi.json:
openapi.json text eol=lf
packages/sdk-dotnet/** text eol=lf
packages/sdk-java/** text eol=lf
packages/sdk-go/** text eol=lf
The openapi-generator JAR emits LF regardless of host OS. Without this, Git’s autocrlf produces CRLF in the working tree on Windows checkouts, breaking pnpm sdk:generate --check locally with a diff that’s pure line-ending noise while CI passes on Linux runners. Mandatory for the cross-platform freshness gate to work.
Schema fix — PreviewStatusSchema no longer uses Type.Null()
While generating the .NET SDK, the csharp template hit a build error on RenderPreviewStatusGet200ResponseAnyOf.cs: the generator mishandles "type": "null" in the success variant of the /render/preview/status response and emits Null as a C# type name that doesn’t exist.
Root cause: apps/api/src/schemas/shared.ts used Type.Null() on the reason field of the success variant. The editor’s PreviewCapabilityStatus type at apps/editor/src/lib/api.ts already treats the success shape as { available: true } without a reason field, and fetchPreviewStatusOnce() never copies a server-side reason: null into its typed result.
Fix: dropped the reason field from the success variant entirely. The runtime output becomes { "available": true } instead of { "available": true, "reason": null }. This is a zero-impact wire-contract change — no existing consumers (editor, TS SDK, Python SDK) read the field on the success path. Comment in shared.ts explains the Stage 1 context for future readers.
.openapi-generator-ignore + packageGuid stability
Two determinism pitfalls caught and fixed during Stage 1 execution, documented so Stages 2 and 3 can inherit the solutions:
.slnfiles carry randomly-generated GUIDs per run. Pinned viapackageGuidin the generator config. The chosen GUID is arbitrary but fixed..openapi-generator/FILESmanifest differs between first-run and regeneration. The generator writes the list of files it intended to emit, which includes files that the committed-dir’s.openapi-generator-ignorerules would have skipped. Fixed by pre-seeding the tmpdir with the ignore file before running the generator in--checkmode.- Build artifact directories (
bin/,obj/,target/,dist/,.vs/) trip the walker. These are gitignored but exist locally after adotnet build. The walker’sBUILD_ARTIFACT_DIRSset excludes them unconditionally.
What operators see
| Before | After |
|---|---|
”.NET SDK? Here’s a dotnet add recipe — you host the stubs.” | dotnet add package PulpEngine.Sdk |
Java/Go recipes in docs/sdk-generation-guide.md | Same (deferred to Stages 2 and 3) |
No CI verification that openapi.json matches the Fastify routes | pnpm extract-openapi --check gate on every PR |
No CI verification that generated SDKs match openapi.json | pnpm sdk:generate --check gate on every PR |
Known limitations and deliberate deferrals
-
Three
CS0618 HttpRequestMessage.Properties is obsoletewarnings in the generatedApiClient.cs. The csharp generator template uses.Propertieswhich is deprecated in favor of.Optionsin newer .NET versions. Not configurable viaadditionalProperties. Acceptable for Stage 1; will resolve when we bump the JAR to a version that fixes the template. -
Newtonsoft.Json instead of
System.Text.Json. The csharp generator’s default serializer stack uses Newtonsoft. Switching toSystem.Text.Jsonis a follow-up config change that requires another regeneration pass and compatibility testing. Not blocking v0.62.0. -
No per-endpoint tests. Stage 1 ships exactly one
/healthround-trip smoke test per SDK. The 80/20 rationale is documented in the plan and Stage 1 checklist. Per-endpoint coverage, multipart upload tests, and authenticated-path tests are all deferred until a regression in the smoke test signals they’re worth the investment. -
Hand-written ergonomic layer on top of the generated client (typed error hierarchy matching the TS/Python SDKs, async helpers, etc.) is not in v0.62.0. Generated quality is 80-90% of hand-written; the remaining 10-20% is a per-SDK follow-up based on user feedback, not speculative work.
-
publish-sdk-dotnet.ymlis wired but the first publish is gated on operator action. The workflow’s [check-ci→build→smoke-test→publish-nuget] chain is in place, but the first production publish requires:- Creating a NuGet API key with
Push new packages and package versionsscope onPulpEngine.*glob. - Saving it as
NUGET_API_KEYin GitHub Actions repo secrets. - (Optional) Verifying
int.nugettest.orgis accepting uploads for the dry-run path, or falling back toPulpEngine.Sdk.Previewon real NuGet per RELEASING.md. - Running the workflow manually via
workflow_dispatchwithtarget=int.nugettest.orgfirst as a smoke test, then pushing thev0.62.0tag to trigger the production publish.
The workflow will not run on this tag push because the CI gate depends on the GitHub Actions billing being resolved from v0.61.0’s blocked PyPI publish. See the v0.61.0 release notes for that unresolved blocker.
- Creating a NuGet API key with
Verification done on this commit
✅ pnpm sdk:generate (Docker fallback, 37 .cs files written)
✅ pnpm sdk:generate --check (determinism gate)
✅ dotnet build (Release) (0 errors, 3 known generator warnings)
✅ dotnet pack (PulpEngine.Sdk.0.62.0.nupkg in bin/Release/)
✅ dotnet test --filter HealthSmokeTest (1 passed, 68ms, against local API)
✅ .openapi-generator-ignore (sentinel README edit survived regen)
✅ node scripts/check-version.mjs (7 packages at 0.62.0; tag mismatch is expected pre-commit)
Residual risk
- v0.61.0’s PyPI publish is still blocked by the GitHub Actions billing issue. v0.62.0’s
.NETpublish will hit the same block when the tag pushes. Both are operator decisions unrelated to the code. - No operator has published this package to NuGet yet. The wiring is in place but the first-publish dry run hasn’t run. Recommend running
workflow_dispatchagainstint.nugettest.orgbefore the production tag push — see RELEASING.md. - Pre-existing flaky tests in the
apps/apisuite under full-suite load (documented inproject_flaky_tests_v1.md—render-dry-run,render-preview,render-batch, etc.). All pass in isolation. Not a v0.62.0 regression.