Pulp Engine Document Rendering
Get started
Release v0.79.0

Release v0.79.0 — Audit remediation consolidation + contract evolution

Date: 2026-05-30 Tag: v0.79.0

Summary

Minor release consolidating the full v0.78.4 post-hardening quality-audit remediation (eight phases) plus a test & evidence-hardening pass, cut from the development line. It closes the audit’s adoption gates — funnel integrity, evidence-vs-claim CI gating, licence observability, and supply-chain/legal — and ships the user-approved pre-1.0 API contract evolution with backward-compatible deprecation aliases. One critical security fix (SEC-1), a broadened credential-collision check (SEC-3), and a DNS-rebinding TOCTOU fix (XC-3) are included.

This is a minor bump (not a patch): a new canonical route, a new consumer-visible batch error-envelope shape, and the deprecation of an existing route are contract evolution under SemVer even with backward-compatible aliases.

Version-line note

The previous tags v0.78.5 and v0.78.6 were cut from the release/v0.78 maintenance branch and were never merged back into the development line, so this release advances from v0.78.4. The only development-relevant content unique to those patch tags — #50’s evaluator/auth documentation corrections (including a wrong templateKeytemplate field in a runbook curl example) — is forward-ported into this release. The container-image license label and the public-pull guardrail introduced during the audit make this line strictly ahead of v0.78.6 on those surfaces.

API contract evolution (pre-1.0, backward-compatible)

These are the only consumer-visible contract changes. Both are additive with one-version deprecation windows; existing callers keep working.

  • Canonical POST /render/pdf. PDF rendering now has a dedicated path matching every other format (/render/docx, /render/pptx, …). The bare POST /render is retained as a deprecated alias (operationId renderPdfLegacy, deprecated: true in openapi.json) for one version. Migrate to POST /render/pdf.
  • Unified batch sub-item error envelope. A failing item in any batch surface — sync PDF/DOCX/PPTX results, NDJSON streaming lines, async job results, and the webhook payload — now carries a single shape: { success: false, error: { code?, message, details? } } (the shared BatchItemErrorSchema), where code, when present, is a RenderErrorCodeSchema value (it is optional because not every failure maps to a render-error code). Previously these were bare scalars that differed per surface. Both the TypeScript and Python SDK batch result types were updated to match.

OpenAPI request/response schemas also gained example payloads (previously none), improving generated-client and evaluator ergonomics.

Security

  • SEC-1 (critical) — per-template rate-limit fail-open is honored. The Redis-failure branch in the per-template limiter read object truthiness instead of the RATE_LIMIT_FAIL_OPEN flag, so an operator who explicitly opted into fail-open still got fail-closed 429s during a Redis outage. Corrected to read the flag, with a mocked-Redis-failure regression test asserting requests are allowed when fail-open is set.
  • SEC-3 — credential-collision detection broadened. The startup “named-user key must be distinct from all API keys” check omitted several real verification secrets. It now covers API_KEY_SUPER_ADMIN, both API_KEYS_JSON and API_KEYS_JSON_FILE forms, the verify-only rollover secrets (API_KEY_EDITOR_PREVIOUS / API_KEY_ADMIN_PREVIOUS), and — when OIDC is configured (OIDC_DISCOVERY_URL + OIDC_COOKIE_SECRET) — the secret derived from OIDC_COOKIE_SECRET. The shared loader/parser was relocated to a neutral module to avoid an import cycle; getActiveApiKeys() keeps its signature.
  • XC-3 — DNS-rebinding TOCTOU closed on outbound fetch. Webhook delivery and scheduled URL data sources validated the target’s resolved IPs at submission time, then later re-resolved the hostname inside a retry loop, reopening the rebinding window. Both now route through a shared safeFetch that resolves the hostname once, rejects any private/internal resolved IP, and pins the connection to the validated IP (via a custom dispatcher) while preserving the original Host header and TLS SNI.

What landed (by audit gate)

Funnel integrity

The dead releases link was repointed to the public mirror; the broken TypeScript-SDK snippet was fixed (a typed client.templates.generate(...) now exists); and the advertised-but-unpublished SDKs carry honest “ships in-repo only — not yet on npm/PyPI” caveats (see the standing residual below).

Evidence-vs-claim CI gating

The published Python SDK is now gated in CI (pytest + ruff + mypy, run with the package as the working directory) instead of only syntax-checked; the DOCX/PPTX/chart renderers gained test:coverage scripts with explicit thresholds wired into the coverage gate; and a tenant-propagation CI check now runs the check-tenant-propagation script the docs already advertised.

Licence observability

Two Prometheus gauges — pulp_engine_licence_valid and pulp_engine_licence_days_until_expiry — are refreshed at scrape time (not only on readiness), with valid=1 only for a genuine unexpired commercial licence and a sentinel -1 for evaluation/invalid so an expired or removed licence can never latch a stale positive. PromQL for an expiry alert (gated on validity so evaluation deployments never page) is documented in docs/licence-key-format.md.

A root LICENSE carrying the commercial/evaluation terms; a generated THIRD-PARTY-NOTICES.md (Permissive / Weak-copyleft / Copyleft / Unknown buckets); and a CI license-compliance gate that fails the build on copyleft (GPL/AGPL/SSPL) or any unclassified third-party dependency, scoped to external deps only. Container images now stamp org.opencontainers.image.licenses=LicenseRef-PulpEngine-Commercial.

Test & evidence hardening (Phase 8)

  • Branch-aware coverage of the editor’s tree-utils immutable tree ops (40 cases), including the transplantNode ancestor guard.
  • A SQL Server ↔ Prisma schema-parity guard (25 cases) that builds the effective SQL schema across migrations (CREATE + ALTER in order) and asserts column/NOT-NULL/unique/FK-onDelete parity against the canonical Prisma schema — drift surfaces at build time instead of at runtime for SQL Server customers.
  • The previously-parked plugin-storage-activation test un-parked and made deterministic.
  • The SQL Server v8-coverage exclusion formalized as by-design (gated behaviorally by the real-mssql CI job + the new parity test); the API coverage floor held at 73/63, which the CI-measured 73.66/64.01 confirms with safe margin.

Verification

Local pre-tag verification on the release commit:

  • check-version.mjs (in the pre-tag-release-head mode CI uses, PULPENGINE_ALLOW_UNTAGGED_RELEASE_HEAD=1) — all seven manifests aligned at 0.79.0, with the [0.79.0] CHANGELOG section + link and this release note present. The plain off-tag invocation intentionally requires an [Unreleased] top section; the strict tagged-path check runs at tag time.
  • pnpm typecheck — 46/46.
  • pnpm lint — 22/22.
  • Full CI on the constituent merges was green on main, including the Linux ci/Test, Linux test-file-mode, CI — Windows, Docker build + smoke, test-sqlserver, and both e2e jobs. Two CI flakes surfaced post-merge and were fixed: the PreviewPanel async-capability render race (synchronous getByRole → async findByRole) and the plugin-storage /health/ready renderer-warmup coupling (assert the plugin-owned check, not the aggregate 200).

Known residuals

  • SDK trusted-publisher registration is still pending on npm and PyPI. No @pulp-engine SDK has yet published to a public registry; the publish workflows now fail loudly at the registry-auth step (the earlier silent-masking behavior was fixed). This is a one-time operator-side configuration on npmjs.org and pypi.org — code is fine, and publication can be dispatched retroactively against an existing tag once the trusted publishers are registered. The in-repo SDK caveats remain in place until a registry lookup confirms the packages are live.
  • HA nightly Check 7 (single-replica outage failover) measures availability below its 99% threshold during the replica’s own restart — a mis-calibrated bar for a single-replica topology rather than an API/failover regression. Tracked separately from the per-commit CI gate.