Pulp Engine Document Rendering
Get started
Release v0.62.0

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 id PulpEngine.Sdk. Targets net8.0. Uses the httpclient library 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.json via openapi-generator csharp generator, JAR version 7.10.0 pinned 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.cs exercises the generated HealthApi.HealthGetAsync() against a local API, asserts the HealthGet200Response deserializes correctly, and verifies the SDK’s <Version> matches the API’s runtime version field 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 generate once 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). Honors PULP_ENGINE_SDK_GEN=docker as an override. Removes the “install a JDK first” friction for contributors on Windows or any box that already has Docker Desktop.
  • --check mode generates to a fresh tmpdir and diffs against committed source, exiting 1 on drift. Pre-seeds the tmpdir with .openapi-generator-ignore so 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: --dotnet is the only wired target. --java and --go are 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:

  1. Pre-existing openapi.json drift hole. Before v0.62.0, there was no CI step verifying openapi.json was up to date with the API routes. If a developer added an endpoint without running pnpm extract-openapi, the spec would silently fall behind reality. This gate catches that now.
  2. New SDK drift hole. Any edit to openapi.json that would regenerate differently into the committed .NET SDK 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:

  1. .sln files carry randomly-generated GUIDs per run. Pinned via packageGuid in the generator config. The chosen GUID is arbitrary but fixed.
  2. .openapi-generator/FILES manifest 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-ignore rules would have skipped. Fixed by pre-seeding the tmpdir with the ignore file before running the generator in --check mode.
  3. Build artifact directories (bin/, obj/, target/, dist/, .vs/) trip the walker. These are gitignored but exist locally after a dotnet build. The walker’s BUILD_ARTIFACT_DIRS set excludes them unconditionally.

What operators see

BeforeAfter
”.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.mdSame (deferred to Stages 2 and 3)
No CI verification that openapi.json matches the Fastify routespnpm extract-openapi --check gate on every PR
No CI verification that generated SDKs match openapi.jsonpnpm sdk:generate --check gate on every PR

Known limitations and deliberate deferrals

  • Three CS0618 HttpRequestMessage.Properties is obsolete warnings in the generated ApiClient.cs. The csharp generator template uses .Properties which is deprecated in favor of .Options in newer .NET versions. Not configurable via additionalProperties. 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 to System.Text.Json is 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 /health round-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.yml is wired but the first publish is gated on operator action. The workflow’s [check-cibuildsmoke-testpublish-nuget] chain is in place, but the first production publish requires:

    1. Creating a NuGet API key with Push new packages and package versions scope on PulpEngine.* glob.
    2. Saving it as NUGET_API_KEY in GitHub Actions repo secrets.
    3. (Optional) Verifying int.nugettest.org is accepting uploads for the dry-run path, or falling back to PulpEngine.Sdk.Preview on real NuGet per RELEASING.md.
    4. Running the workflow manually via workflow_dispatch with target=int.nugettest.org first as a smoke test, then pushing the v0.62.0 tag 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.

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 .NET publish 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_dispatch against int.nugettest.org before the production tag push — see RELEASING.md.
  • Pre-existing flaky tests in the apps/api suite under full-suite load (documented in project_flaky_tests_v1.mdrender-dry-run, render-preview, render-batch, etc.). All pass in isolation. Not a v0.62.0 regression.