Pulp Engine Document Rendering
Get started
Release v0.75.0

Release v0.75.0 — Post-audit hygiene + operational follow-through

Date: 2026-04-21 Tag: v0.75.0

Summary

v0.74.0 closed the 2026-04-18 audit’s verification-gap asks and removed the “do before new features” blockers. v0.75.0 lands the maintainability + operational follow-through that was explicitly deferred at planning time. Twelve commits since v0.74.0 across five cohesive workstreams: WIP-suite drain, HA Run 2 enablement, GHCR republish enablement, editor build polish, and a render-route preview-routes split.

No public-API surface changes. No breaking changes. The only YAML edit to release.yml corrects a workflow_dispatch tag-checkout bug; routes, schemas, status codes, and CLI shapes are untouched.

This release ships without CI signal. CI and Release workflows remain in disabled_manually from the prior billing block. The release commit carries [skip ci] so no Actions fire on the push or the tag. See docs/runbooks/ghcr-republish.md for the post-billing publish runbook — that runbook’s revision-label verification is also the end-to-end proof of the workflow_dispatch fix below.

What shipped

1. WIP-suite drain (2 of 3 entries promoted)

The file-mode WIP suite dropped from 3 entries to 1. The two fixes that landed were both the same root cause: tests reading lowercased response-header keys with a dash (x-pulp-engine-*) when Node’s HTTP layer lowercases the emitted X-PulpEngine-* headers to x-pulpengine-* (no dash). Test-only fixes — the shipped public header names are unchanged.

  • apps/api/src/__tests__/batch-async.test.ts HMAC signature read fixed; promoted to gated test:file suite (commit 6b673e5).
  • apps/api/src/__tests__/template-labels.route.test.ts four B.3 variant-resolution tests fixed; promoted (commit 07d3fe4).
  • apps/api/src/__tests__/plugin-integration/plugin-storage-activation.test.ts re-parked in WIP with an accurate diagnosis (commit 36dfa35). Initial promotion was over-eager: the test passes in isolation and as part of the plugin-integration/ subset, but fails the first test with /health/ready = 503 in the full 74-file singleFork gated run. The original AUD-001 boot-ordering bug is fixed and verified — the seam that remains is a narrower cross-file singleFork state-pollution issue tracked in docs/initiatives/file-mode-wip-followups.md.

vitest.file.wip.config.ts now sets passWithNoTests: true so the empty-list case stays robust.

2. HA Run 2 enablement (scripts + playbook + override scaffold)

Run 1 (recorded in v0.74.0) closed 3 of 6 checks PASS, 2 deferred, 1 N/A. v0.75.0 lands reproducible drivers for the two deferred checks so Run 2 is operator-executable without re-deriving setup steps.

  • scripts/ha/check-2-schedule-fires-once.mjs + the pnpm ha:check-2 wrapper (commit 984cc5a). API-only against a live docker-compose.ha.yml stack: seeds a temp template + per-minute schedule, polls GET /schedules/:id/executions for ~3 ticks, asserts each fireTime appears exactly once, tears its own fixtures down in a finally block. No DB client / pg dep added. Bash + PowerShell invocation documented in the script header.
  • Check-6 API-key-rotation playbook in docs/ha-validation-report.md (commit be300c6). End-to-end sequence matching the *_PREVIOUS contract: previous keys are verify-only for editor session tokens, cannot mint, cannot be used as X-Api-Key. Restart-window log evidence is captured BEFORE the four explicit 200/200/401/401 probes so background traffic isn’t conflated with deliberate negatives. PowerShell variants inline.
  • docker-compose.ha.override.yml.example with two commented sections: port override (!override) for the lb service when host port 3000 is held, and the rotation cutover env block.
  • Run 2 evidence template appended to docs/ha-validation-report.md for operators to fill in post-run.

Operational execution remains operator-gated.

3. GHCR republish enablement (workflow fix + runbook)

Recon during planning uncovered a real bug in release.yml: while check-ci correctly resolved the tag SHA on workflow_dispatch, every downstream job (docker, scan, windows-installer, eval-bundle, release) called actions/checkout@v4 with no ref: and used ${{ github.sha }} in Docker build args + OCI revision labels. On workflow_dispatch, github.sha is the dispatch ref’s HEAD — so gh workflow run Release -f tag=vX.Y.Z from a main past the tag would have built main HEAD and labeled the image as vX.Y.Z.

  • Fix in commit 19247ef: check-ci exports sha/tag/owner job outputs; every downstream job declares needs: [check-ci] and checks out with ref: ${{ needs.check-ci.outputs.sha }}; every Docker GIT_SHA build arg + org.opencontainers.image.revision label uses the resolved SHA.
  • docs/runbooks/ghcr-republish.md (commit 9afdf5d) — operator-gated runbook for republishing the three release images (pulp-engine{,-worker,-controller}) under their post-rename names. Includes a critical org.opencontainers.image.revision == git rev-parse vX.Y.Z^{} verification that doubles as the end-to-end proof of the workflow_dispatch fix. Appendix A gates the dangerous tag-delete-and-repush fallback behind explicit maintainer approval; Appendix B recommends leaving the stale pre-rename docuforge* GHCR packages in place.

The first end-to-end proof of both lands when an operator runs the runbook post-billing-restoration.

4. Editor build polish (manualChunks + spawn modernization)

Two commits, both shipped under verification (full editor build green on Windows; pnpm --filter @pulp-engine/editor typecheck clean; 1313/1313 Vitest suite green).

  • apps/editor/scripts/build-all.mjs modernization (commit 720f9bb). Drops shell: true and the pnpm-as-parent-process pattern. Resolves vite/bin/vite.js and typescript/bin/tsc via createRequire + <pkg>/package.json (Vite 8’s exports field blocks the direct vite/bin/vite.js path) and invokes them via spawn(process.execPath, [script, ...args]). Pure Node — no shell, no cmd.exe, no .cmd indirection. Manual new Promise + dual child.on(close|error) handlers replaced with events.once(child, 'exit'). The empirical “just use pnpm.cmd on Windows” alternative was rejected: spawn('pnpm.cmd', ...) with shell: false fails with spawn EINVAL on Windows. MSYS_NO_PATHCONV: '1' env shim removed. windowsHide: true added.
  • apps/editor/vite.config.ts manualChunks (commit 3857220). First-pass vendor grouping: react-vendor (react, react-dom, scheduler), radix (@radix-ui/), tiptap (@tiptap/), dnd-kit (@dnd-kit/*), generic vendor catch-all. id normalized to forward slashes up front so Windows dev builds group identically to CI. Lib-mode wrapper configs (vite.config.embed.ts, vite.config.form.ts) intentionally untouched — embedders consume them as a single <script>. Measured effect: chunk count 96 → 87 (−9 dedupe wins; lazy boundaries no longer carry duplicate vendor copies); total size flat at 3.7 MB; vendor chunks stay cache-stable across releases that only touch app code. Named chunk sizes: react-vendor 178 KB, radix 115 KB, tiptap 311 KB, dnd-kit 60 KB, vendor 2 MB.

5. Render-route preview split (highest-ROI hotspot decomposition)

The user’s triage of three large apps/api/src/ files identified render.ts (3,200 lines) as the only one with a clean seam — the renderPreviewRoutes plugin export was already structurally separate from renderRoutes, registered separately in server.ts behind the PREVIEW_ROUTES_ENABLED gate. The split moves code, not logic.

  • apps/api/src/routes/render/render-preview.ts new sibling (commit b734889) holds renderPreviewRoutes + PreviewBodySchema + PreviewOptionsSchema. Imports its 8 shared symbols from ./render.js. Matches the batch-shared.tsbatch-async.ts sibling pattern.
  • render.ts shrinks 3,200 → 2,302 lines (−898). Two helpers (makePptxAssetResolver, logPptxWarnings) gained export so the sibling can consume them. RenderConfigSchema import dropped (was preview-only). Top-of-file JSDoc added documenting the sibling layout.
  • server.ts splits one import line into two; registration logic unchanged.
  • Public HTTP surface — every route’s path, schema, status codes, streaming semantics — unchanged byte-for-byte.

Verified: typecheck clean, lint clean, file-mode gated suite at the post-WIP-drain baseline (73 files / 1226 passed / 96 skipped), 28/28 preview tests pass.

6. Deferred (with rationale)

Two of the three named hotspot files stay untouched, by design:

  • apps/api/src/config.ts (1,575 lines) — organized around a single envSchema Zod object that parses into a frozen config singleton. No clean seam to extract preview-config or any other domain into a sub-schema without forking validation atomicity. Splitting would add fragility without functional benefit.
  • apps/api/src/server.ts (1,230 lines) — bootstrap orchestrator with order-sensitive plugin registration. The 62eba04 AUD-001 reorder is a recent reminder that the ordering is load-bearing. Splitting along any plausible seam (e.g. health routes into health.ts) fights the order constraint. Right move is a plugin-architecture rethink, not a line-count split.

Verification

The release commit ships under these signals (executed locally during the work; CI signal blocked by billing):

  • pnpm --filter @pulp-engine/api typecheck — clean.
  • pnpm --filter @pulp-engine/api lint — clean.
  • pnpm --filter @pulp-engine/api test:file — 73 passed | 8 skipped (81 files), 1226 passed | 96 skipped (1322 tests). Matches the post-WIP-drain baseline.
  • pnpm --filter @pulp-engine/api exec dotenv -e ../../.env -- vitest run src/__tests__/render-preview.test.ts — 28/28 green.
  • pnpm --filter @pulp-engine/editor typecheck — clean.
  • pnpm --filter @pulp-engine/editor build — full three-stage build green on Windows; dist contains index.html, embed.html, form.html, assets/*, pulp-engine-embed.{js,iife.js}, pulp-engine-form.{js,iife.js}.
  • pnpm --filter @pulp-engine/editor test — 95 files, 1313/1313 tests pass.

Operational status (carried forward)

Release and CI workflows remain disabled_manually per memory project_actions_billing_block.md. The first end-to-end proof of the release.yml workflow_dispatch fix happens when an operator follows docs/runbooks/ghcr-republish.md post-billing — the runbook’s org.opencontainers.image.revision verification is the proof point.

HA Run 2 scripts and playbook are ready; actual run-and-capture is operator-gated against a live docker-compose.ha.yml stack.

Two-axis status framing

Per the framing locked in this session (memory project_post_audit_status_framing.md):

  • Codebase gating work is complete. The audit’s verification-gap asks closed in v0.74.0; v0.75.0 adds maintainability + scripts/runbooks on top.
  • Operational publish/HA evidence work is prepared but still pending external prerequisites. The runbook + harness exist; their actual execution awaits billing restoration and a live HA-stack run.