Skip to content

feat(environments): clone & promote workflows end-to-end [CMS-153]#129

Merged
iipanda merged 2 commits into
mainfrom
feat/cms-153-clone-promote-end-to-end
May 1, 2026
Merged

feat(environments): clone & promote workflows end-to-end [CMS-153]#129
iipanda merged 2 commits into
mainfrom
feat/cms-153-clone-promote-end-to-end

Conversation

@iipanda
Copy link
Copy Markdown
Collaborator

@iipanda iipanda commented May 1, 2026

Closes the CMS-153 epic ("Clone & Promote End-to-End", Phase-6 advanced operations).

Tickets in scope:

  • CMS-94 clone workflow payload options (content/settings/includeDrafts/preservePaths)
  • CMS-95 promote workflow with dryRun, overwrite confirmation, includeUnpublished
  • CMS-96 atomic reference remap by translation_group_id + locale
  • CMS-97 integration suite for locale/env/project/remap behaviors
  • CMS-98 Studio clone + promote UI

Spec delta

None — SPEC-009 already specifies both endpoints, defaults, error codes, and atomic-remap rule. Implementation follows the spec verbatim. Implementation plan: .ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md.

Summary

Backend (CMS-94 / CMS-95 / CMS-96 / CMS-97)

  • POST /api/v1/environments/:id/clone — bulk-insert source documents into the target with new document_ids and preserved translation_group_id. Copies the latest published snapshot (as version 1) when one exists. Supports include.content, include.settings, includeDrafts (default true), preservePaths. Media inclusion is rejected as deferred per spec.
  • POST /api/v1/environments/:id/promote — per-document overwrite-or-create, matched by (translation_group_id, locale). Always auto-publishes (writes a document_versions row at the next version). Supports dryRun (no writes; returns the deterministic plan with pre-allocated target ids) and includeUnpublished (drafts skipped by default with status: skipped_unpublished).
  • Atomic reference remap (environments-reference-remap.ts) walks frontmatter through the source environment's schema registry. Every field.reference UUID is rewritten by (translation_group_id, locale) to the matching target id. Unresolved references throw REFERENCE_REMAP_FAILED (409); the whole operation runs in a Drizzle transaction so any throw rolls back atomically (no partial writes).
  • Auth: session_or_api_key mode with explicit environments:clone / environments:promote scopes; CSRF required on both writes; project routing required.

Studio (CMS-98)

  • Environment management page gains a per-row "Clone into ..." dropdown action with a payload-options dialog (toggles for content/settings/drafts/paths, source picker, media-deferred messaging).
  • New /admin/promote page implements the workflow: source + target pickers, multi-select content list scoped to the source env, includeUnpublished toggle, dry-run preview showing create/overwrite/skipped counts with remap-failure surfacing (which field on which document failed), and a confirmation dialog that lists the exact target documents that will be overwritten before executing the real run. No-merge messaging is rendered explicitly.
  • Sidebar gets a "Promote" item gated to admin surfaces.
  • createStudioEnvironmentApi extended with clone() and promote() methods that validate payloads before the network call.

Tests

  • Server unitenvironments-reference-remap.test.ts (6 tests) and extended environments-api.route.test.ts (7 tests) covering payload validation, scope auth, CSRF, project routing.
  • Server integration — new environments-clone-promote.integration.test.ts (9 tests) running against real Postgres. Skips when no DB.
  • Studio — extended environment-api.test.ts (12 tests including clone/promote happy paths and validation), updated environments-page.test.tsx, new promote-page.test.tsx.

Notes

  • apps/studio-review has no source under version control on origin/main (only .next/.generated build artifacts), so there are no review-app fixtures to sync in this PR.
  • Two pre-existing unrelated test failures observed during validation (CLI login auth styled HTML pages, demo:seed project mismatch, studio portal-border-theme reading dist) — not introduced by this change.

Test plan

  • bun run format:check
  • bun run check
  • bun test --cwd apps/server src/lib/environments-reference-remap.test.ts src/lib/environments-api.route.test.ts
  • bun test --cwd apps/server src/lib/environments-clone-promote.integration.test.ts (with Postgres up)
  • bun test --cwd packages/studio src/lib/environment-api.test.ts src/lib/runtime-ui/app/admin/environments-page.test.tsx src/lib/runtime-ui/app/admin/promote-page.test.tsx
  • Manual: clone production into a freshly created env via Studio with content + settings; promote a single localized doc with dry-run preview, then confirm overwrite.
  • Manual: trigger a remap failure by promoting a BlogPost without its Author; confirm UI surfaces the field path + locale.

Summary by CodeRabbit

  • New Features

    • Environment cloning UI and API with options for content, settings, drafts, and path preservation.
    • Document promotion UI and API with dry‑run preview, deterministic replay, and atomic publish behavior.
    • Automatic frontmatter reference remapping with actionable preview/error details and overwrite/create summaries.
    • "Promote" admin nav item and per-environment "Clone into…" actions.
  • Bug Fixes / Validation

    • Stronger input validation, CSRF and scoped authorization enforcement, and clearer error responses.
  • Tests

    • Extensive unit and DB integration tests covering cloning, promotion, atomicity, remap failures, permissions, and CSRF.
  • Documentation

    • Added an end-to-end implementation plan for clone & promote flows.

Implements SPEC-009 environment clone and promote with atomic reference
remap, MVP payload options, dryRun preview, and a Studio UI for both
flows. Closes the Phase-6 advanced-operations epic.

Backend (CMS-94, CMS-95, CMS-96, CMS-97):
- POST /api/v1/environments/:id/clone — `include.content/settings`,
  `includeDrafts` (default true), `preservePaths`. Copies head rows
  (and latest published snapshot when present) into the target with
  new `document_id`s and preserved `translation_group_id`. Optional
  schema sync + registry copy when `include.settings: true`. Media
  inclusion is rejected as deferred.
- POST /api/v1/environments/:id/promote — per-document overwrite or
  create matched by `(translation_group_id, locale)`, auto-publishes,
  supports `dryRun` (no writes, returns deterministic plan with
  pre-allocated target ids) and `includeUnpublished` (drafts skipped
  by default with `status: skipped_unpublished`).
- Atomic reference remap (`environments-reference-remap.ts`) walks
  frontmatter through the schema registry and either rewrites every
  reference value to the target environment id or aborts the whole
  operation with `REFERENCE_REMAP_FAILED` (409). Tests cover top-level,
  nested-object, and array-of-references paths.
- Integration suite (`environments-clone-promote.integration.test.ts`)
  covers clone happy path, settings copy, `includeDrafts: false`,
  atomic abort on broken ref, promote dryRun, real promote with
  overwrite + auto-publish v2, unpublished gating, atomic remap abort,
  and same-env validation.

Studio (CMS-98):
- Environment management page gets a per-row "Clone into <env>" action
  with payload-options dialog (content / settings / drafts / paths
  toggles, source picker, media-deferred copy).
- New `/admin/promote` page implements the per-document workflow:
  source/target picker, multi-select content list, includeUnpublished
  toggle, dry-run preview with create/overwrite/skipped breakdown and
  remap-failure surfacing, confirmation dialog listing exact targets
  before execute, no-merge messaging. Sidebar gets a "Promote" item
  gated to admin surfaces.
- `createStudioEnvironmentApi` extended with `clone()` and `promote()`
  client methods; payload validation runs before the network call.

Spec delta: none — SPEC-009 already specifies the contracts. Plan in
`.ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md`.

Note: `apps/studio-review` has no source under version control on
`origin/main` (only `.next` / `.generated` build artifacts), so there
are no review-app fixtures to sync in this change.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8341f0b5-fc1f-4134-bfc2-45f924445440

📥 Commits

Reviewing files that changed from the base of the PR and between 02354a9 and 3d4552d.

📒 Files selected for processing (8)
  • .ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md
  • apps/server/src/lib/environments-clone-promote.integration.test.ts
  • apps/server/src/lib/environments-clone-promote.ts
  • apps/server/src/lib/environments-reference-remap.test.ts
  • apps/server/src/lib/environments-reference-remap.ts
  • packages/shared/src/lib/contracts/environments.ts
  • packages/studio/src/lib/runtime-ui/app/admin/promote-page.test.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/server/src/lib/environments-clone-promote.integration.test.ts

📝 Walkthrough

Walkthrough

Adds end-to-end environment cloning and document promotion: API routes with scoped authorization and CSRF, transactional DB implementations for clone/promote with reference remapping and deterministic preallocation, shared request/response contracts and validators, comprehensive server & UI tests, and Studio UI pages for cloning and promoting.

Changes

Cohort / File(s) Summary
Plan & Spec
\.ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md
New implementation plan describing endpoint contracts, atomic reference-remap semantics, dry-run/confirm control flow, authorization/CSRF, UI workflows, and integration test matrix.
API Surface & Routing
apps/server/src/lib/environments-api.ts, apps/server/src/lib/runtime-with-modules.ts
Adds EnvironmentScopedAuthorizer, expands EnvironmentStore with clone/promote, mounts POST /api/v1/environments/:id/clone and /.../:id/promote, enforces CSRF and per-operation scoped auth, and validates request bodies.
Server Implementation
apps/server/src/lib/environments-clone-promote.ts, apps/server/src/lib/environments-reference-remap.ts
New transactional implementations: cloneEnvironment and promoteDocuments with deterministic target ID preallocation, schema-aware remapFrontmatterReferences, version management, atomic rollback on remap/conflicts, and exported types for inputs/results.
Server Tests & Integration
apps/server/src/lib/environments-clone-promote.integration.test.ts, apps/server/src/lib/environments-reference-remap.test.ts, apps/server/src/lib/environments-api.route.test.ts
Adds comprehensive integration and unit tests covering remap semantics, clone/promote dry-run vs execute, atomicity guarantees, permission/CSRF enforcement, input validation, and route mounting helpers.
Studio Client API & Tests
packages/shared/src/lib/contracts/environments.ts, packages/studio/src/lib/environment-api.ts, packages/studio/src/lib/environment-api.test.ts
Adds new contract types and runtime assertion validators for clone/promote payloads/results; extends StudioEnvironmentApi with clone and promote methods and tests input validation, CSRF handling, request/response parsing.
Studio UI — Promote
packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx, .../promote-page.test.tsx, packages/studio/src/lib/remote-studio-app.tsx, packages/studio/src/lib/runtime-ui/components/layout/app-sidebar.tsx
New /admin/promote page and route registration, sidebar nav entry, preview (dry-run) and confirm flows, replay using preallocated IDs, error surfacing for remap failures and forbidden/missing routes, plus server-render tests.
Studio UI — Environments (Clone)
packages/studio/src/lib/runtime-ui/app/admin/environments-page.tsx, .../environments-page.test.tsx
Adds clone dialog, form state, per-target "Clone into …" action, input validation, calls to environmentApi.clone, success/error messaging and environment list refresh.
Misc runtime wiring
apps/server/src/lib/runtime-with-modules.ts
Wires new authorizeScoped callback that derives project/environment routing and forwards required scope to auth service.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Studio Admin
    participant UI as Promote UI
    participant API as Environment API
    participant Auth as Authorization Service
    participant DB as Database

    Client->>UI: select documents, click "Preview promote"
    UI->>Auth: ensure session authenticated
    UI->>API: POST /environments/:targetId/promote (dryRun: true, documentIds)
    API->>Auth: authorizeScoped("environments:promote")
    Auth-->>API: authorized
    API->>DB: load source docs, schemas, preallocate IDs
    DB-->>API: docs + schema + preallocations
    API->>API: remapFrontmatterReferences (source→target)
    API-->>UI: PromotionResult[] (planned created/overwrote/skipped)
    Client->>UI: confirm promotion
    UI->>API: POST /environments/:targetId/promote (dryRun: false, snapshot, preallocatedTargetIds)
    API->>Auth: authorizeScoped("environments:promote")
    Auth-->>API: authorized
    API->>DB: begin transaction, create/update drafts, insert versions, update publishedVersion
    DB-->>API: commit or rollback on remap/conflict
    API-->>UI: final PromotionResult[]
Loading
sequenceDiagram
    participant Client as Studio Admin
    participant UI as Clone UI
    participant API as Environment API
    participant Auth as Authorization Service
    participant DB as Database

    Client->>UI: open clone dialog, choose source and options
    UI->>Auth: ensure session authenticated
    UI->>API: POST /environments/:targetId/clone (sourceEnvironmentId, include, includeDrafts)
    API->>Auth: authorizeScoped("environments:clone")
    Auth-->>API: authorized
    API->>DB: load source docs, schema snapshot
    DB-->>API: source data
    API->>API: preallocate target IDs, remapFrontmatterReferences
    API->>DB: begin transaction, insert documents/versions, copy settings if requested
    DB-->>API: commit or rollback on errors
    API-->>UI: CloneResult { targetEnvironmentId, documentsCloned }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 Hopping through branches, I map every reference,

Dry-run then commit with transactional deference,
From staging to prod I preallocate IDs,
Remap frontmatter, then publish with ease—
A rabbit-approved clone, snug as new seedlings!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: implementing clone and promote workflows for environments. It is specific, clear, and directly corresponds to the core functionality added across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cms-153-clone-promote-end-to-end

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
packages/shared/src/lib/contracts/environments.ts (1)

220-282: 🏗️ Heavy lift

Avoid mutating caller-owned payloads in these shared validators.

Both helpers normalize by Object.assign-ing back into the original object. In @mdcms/shared, that is a surprising side effect for a validator API, and it will blow up on frozen/readonly payloads. A safer shape is to return a normalized payload from a parse/helper function and keep the assertion helpers side-effect free.

Based on learnings: Treat the public API of mdcms/shared as a contract - changes here affect every other package.

Also applies to: 284-317

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md:
- Line 17: Update the plan text to use the actual route-option name used in the
server wiring: replace the mention of authorizeWriteScope with authorizeScoped
so the plan matches the implementation (referencing the route-option name
authorizeScoped and the server module runtime-with-modules.ts where the
authorization is wired).

In `@apps/server/src/lib/environments-clone-promote.ts`:
- Around line 152-155: The current loadSourceContext/clone flow uses fields
row.body, row.frontmatter, and row.path from the documents table even when the
source row has a published revision, causing unpublished draft content to be
used; change loadSourceContext (and the clone/includeDrafts:false path) to
explicitly load the published payload from documentVersions when a
published_revision_id (or equivalent) exists: query documentVersions for that
revision and use its body/frontmatter/path as the "published" payload while
keeping the documents row as the live/draft payload so callers can select
published vs draft; update any code branches that copy version-1 snapshots or
call clone() to choose the published payload when includeDrafts is false
(references: loadSourceContext, clone, documentVersions, documents,
published_revision_id, row.body/frontmatter/path).
- Around line 349-377: The code always writes row.path into the new document and
only echoes input.preservePaths in the error payload, so preservePaths does
nothing; fix by computing a targetPath variable before the txDb.insert that
respects input.preservePaths (e.g. if input.preservePaths is true use row.path,
otherwise compute a new path — e.g. use remap.path if available or generate a
unique path based on documentId/translationGroupId or a suffix/prefix), then use
that targetPath in the insert call (replace row.path) and in the
buildConflictError payload so the conflict message reflects the actual attempted
path; also update any other references in this function that still read row.path
to use targetPath (look for txDb.insert(... path: ...) and the conflict error
block that includes path: row.path).
- Around line 723-745: The update path in overwriteTargetDraft() currently lets
a Postgres unique-violation (SQLSTATE '23505') bubble out as a 500; catch
database errors around the db.update(...) call inside overwriteTargetDraft(),
detect error.code === '23505' (or error.constraint if used elsewhere), and
convert/throw the same structured conflict response used by createTargetDraft()
(HTTP 409 / Conflict) so path-collision uniqueness violations are normalized to
409s; reference overwriteTargetDraft(), createTargetDraft(), and the
db.update(documents).set(...) block when applying the fix.
- Around line 535-548: The current promoteDocuments logic uses randomUUID() to
generate preallocatedTargetIds (see preallocatedTargetIds, targetMap, and
randomUUID usage inside the loop over sourceContext.rows), causing
non-deterministic targetDocumentIds across calls; update promoteDocuments to
accept an optional preallocated IDs map (e.g., input.preallocatedTargetIds) and
use that value for a given targetMapKey before falling back to generating a new
UUID, and ensure dry-run returns the supplied/preallocated IDs so a subsequent
real run can reuse them for deterministic replay.

In `@apps/server/src/lib/environments-reference-remap.ts`:
- Around line 177-183: The code currently returns the raw value when an object
schema field contains references but the stored value has the wrong container
shape; instead of silently returning input.value, update the branches in the
function handling input.field.kind === "object" (the block using
containsReference(input.field) and isRecord(input.value)) and the analogous
block at the other occurrence to throw a descriptive Error (or otherwise cause
the operation to fail) when containsReference(input.field) is true but
isRecord(input.value) is false, so malformed reference containers fail fast and
prevent unremapped IDs from being committed; reference the containsReference and
isRecord checks and the input.field / input.value variables when making the
change.

In `@packages/shared/src/lib/contracts/environments.ts`:
- Around line 38-43: The exported public input types (start with
EnvironmentCloneInput) declare required fields that validators accept as
omitted; update EnvironmentCloneInput so the fields that have runtime defaults
are optional (make include, includeDrafts, preservePaths optional) and also scan
and make optional any other exported input types in this file that use defaulted
fields (e.g., types that include includeUnpublished and dryRun) so TS callers
can omit them without casting; keep the same property names and types, only add
optionality (?) to those defaulted properties.

In `@packages/studio/src/lib/runtime-ui/app/admin/promote-page.test.tsx`:
- Around line 78-82: The test's non-deterministic assertion allows "Loading
environments" to pass and hide regressions; update the assertion that inspects
the SSR markup (the variable named markup used with assert.match) to only assert
the missing-project message by replacing the current regex (/requires an active
project|Loading environments/i) with a deterministic check for the
missing-project text (e.g., /requires an active project/i) so the test fails if
the missing-route state does not render.

In `@packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx`:
- Around line 303-326: The execution can differ from the preview because
handleConfirmExecute reads live state (selectedIds, includeUnpublished,
sourceEnv, targetEnv) instead of the snapshot used to produce preview; fix by
capturing a snapshot of all inputs that drive the preview when the preview is
generated (e.g., store previewSnapshot = { selectedIds, includeUnpublished,
sourceEnv, targetEnv } alongside preview) and then use that snapshot inside
handleConfirmExecute (replace references to
selectedIds/includeUnpublished/sourceEnv/targetEnv with previewSnapshot.*), or
alternatively clear/invalidate preview whenever any of those inputs change so
the user must re-preview before executing; update createStudioEnvironmentApi and
promote call to use the snapshot values (function handleConfirmExecute, variable
preview, and the promote call) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fdafeb5e-6e16-43fd-bb5a-922bb0487c2f

📥 Commits

Reviewing files that changed from the base of the PR and between f076417 and 02354a9.

📒 Files selected for processing (17)
  • .ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md
  • apps/server/src/lib/environments-api.route.test.ts
  • apps/server/src/lib/environments-api.ts
  • apps/server/src/lib/environments-clone-promote.integration.test.ts
  • apps/server/src/lib/environments-clone-promote.ts
  • apps/server/src/lib/environments-reference-remap.test.ts
  • apps/server/src/lib/environments-reference-remap.ts
  • apps/server/src/lib/runtime-with-modules.ts
  • packages/shared/src/lib/contracts/environments.ts
  • packages/studio/src/lib/environment-api.test.ts
  • packages/studio/src/lib/environment-api.ts
  • packages/studio/src/lib/remote-studio-app.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/environments-page.test.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/environments-page.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/promote-page.test.tsx
  • packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx
  • packages/studio/src/lib/runtime-ui/components/layout/app-sidebar.tsx

Comment thread .ai/plans/2026-05-01-cms-153-clone-promote-end-to-end.md Outdated
Comment thread apps/server/src/lib/environments-clone-promote.ts
Comment thread apps/server/src/lib/environments-clone-promote.ts
Comment thread apps/server/src/lib/environments-clone-promote.ts
Comment thread apps/server/src/lib/environments-clone-promote.ts Outdated
Comment thread apps/server/src/lib/environments-reference-remap.ts
Comment thread packages/shared/src/lib/contracts/environments.ts
Comment thread packages/studio/src/lib/runtime-ui/app/admin/promote-page.test.tsx Outdated
Comment thread packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx
Comment thread packages/studio/src/lib/runtime-ui/app/admin/promote-page.tsx
- Plan doc: rename `authorizeWriteScope` → `authorizeScoped` to match the
  actual route option in environments-api.ts.
- Contracts (packages/shared/.../environments.ts): make defaulted fields
  optional on EnvironmentCloneInput / EnvironmentPromoteInput so TS callers
  can omit them. Add optional `preallocatedTargetIds` on the promote input
  for deterministic dryRun → real-run replay; the validator coerces and
  UUID-validates entries.
- loadSourceContext now also pulls the published payload from
  `documentVersions` for any source row with a non-null `publishedVersion`,
  exposed as `publishedByDocumentId`. Clone with `includeDrafts: false`
  copies that payload (not the draft state on the documents row), and the
  cloned version-1 snapshot uses the published payload always — so source
  draft state cannot leak into a target's published history.
- Clone now respects `preservePaths` (previously a no-op echoed only in
  the conflict error). `preservePaths: false` derives a deterministic
  suffix from the new target documentId so the target doesn't collide on
  the active-path unique index.
- overwriteTargetDraft catches Postgres 23505 around the update so
  uniqueness violations surface as the same 409 conflict shape
  createTargetDraft uses, instead of bubbling as 500.
- promoteDocuments accepts `input.preallocatedTargetIds` (source → target
  map). Used before generating fresh UUIDs so a real run replays the dry
  run's plan exactly. Studio promote page captures the dryRun snapshot
  (source/target/selection/includeUnpublished) and feeds the preallocated
  ids back on confirm; preview is invalidated when any input changes so
  the confirmation dialog never lies.
- Reference remap throws REFERENCE_REMAP_FAILED with `container_shape_mismatch`
  when the schema declares an object/array of references but the stored
  value is the wrong shape, instead of silently passing unremapped ids
  through.
- PromotePage initial state honors missing routing synchronously so SSR
  matches the hydrated state; tighten its missing-route test to require
  the missing-route message and explicitly forbid the loading message.
- Add integration tests for: published-payload selection on
  includeDrafts:false; preservePaths:false suffix derivation; deterministic
  dryRun → real-run replay via preallocatedTargetIds. Add unit tests for
  the new shape-mismatch failure paths in the remap walker.
@iipanda iipanda merged commit 19b0047 into main May 1, 2026
5 checks passed
@iipanda iipanda deleted the feat/cms-153-clone-promote-end-to-end branch May 1, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant