Skip to content

fix(core): enforce revision checks for content lifecycle mutations - #2783

Merged
ascorbic merged 4 commits into
emdash-cms:mainfrom
yumam0815:fix/publish-expected-revision-cas
Sep 2, 2026
Merged

fix(core): enforce revision checks for content lifecycle mutations#2783
ascorbic merged 4 commits into
emdash-cms:mainfrom
yumam0815:fix/publish-expected-revision-cas

Conversation

@yumam0815

@yumam0815 yumam0815 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds an optional _rev precondition to publish, unpublish, and discard-draft mutations across the REST and MCP APIs. The repository includes the expected version and timestamp in the final conditional update, so a writer save or autosave after approval causes a CONFLICT response instead of publishing unapproved content.

Successful lifecycle mutations return the item and its next _rev. Existing calls that omit _rev keep their current behavior.

Closes #2782

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (not applicable: no admin UI strings)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: not applicable, this is a bug fix

The changed files pass type-aware lint and pnpm lint:quick. The repository-wide pnpm lint:json currently reports 115 diagnostics, all outside this change.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

Non-visual change.

  • PR head: 958b29a259d90c1edd37c5a65dbf93fbd68d1c0a
  • Core TypeScript typecheck: pass
  • Focused type-aware lint: pass
  • Focused format check: pass
  • MCP M01–M09 conditional lifecycle suite: 9 tests passed
  • Related content suites: 114 tests passed across 6 files

@changeset-bot

changeset-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8a8e276

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/release-service Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The approach is sound: adding an optional _rev precondition to the publish/unpublish/discard-draft lifecycle mutations is the right fix for the concurrent-edit race described in #2782, and it follows the same optimistic-concurrency shape already used by handleContentUpdate. The implementation is consistent through the repository, handlers, runtime, Astro route wrappers, and ApiResult/ContentResponse types, and the new tests exercise the critical paths (happy path, stale _rev, concurrent publishes, malformed token, backward compatibility, unpublish/discard parity).

I didn't find any logic bugs, regressions, security issues, or AGENTS.md violations in the changed code: the conditional UPDATE is properly parameterized, _rev is optional so legacy callers keep working, ContentMutationConflictError is mapped to 409 CONFLICT, and the changeset is a reasonable patch entry.

The only notable gap is that the MCP content_publish, content_unpublish, and content_discard_draft tools were not updated to accept _rev, even though the lower-level handlers now support it and the content_update MCP tool already exposes _rev. That's a surface inconsistency for MCP clients and leaves them without the same race-condition guard. I left a line-anchored suggestion on the publish tool; the same _rev input and pass-through should be added to the unpublish and discard tools for parity.


Findings

  • [suggestion] packages/core/src/mcp/server.ts:1260

    The REST route and runtime handler now accept an optional _rev precondition for publish, but the MCP content_publish tool input schema does not. The sibling content_update tool already exposes _rev, so these lifecycle tools should match so MCP clients can guard against concurrent edits too. Consider adding _rev to the input schema and passing it through to handleContentPublish. The same applies to content_unpublish and content_discard_draft.

    			inputSchema: z.object({
    				collection: z.string().describe("Collection slug"),
    				id: z.string().describe("Content item ID or slug"),
    				publishedAt: z.iso
    					.datetime({ offset: true, message: "must be an ISO 8601 datetime" })
    					.optional()
    					.describe(
    						"Override publication timestamp (ISO 8601). Requires content:publish_any permission. Useful when importing content with original publish dates.",
    					),
    				_rev: z
    					.string()
    					.optional()
    					.describe("Revision token from content_get for conflict detection"),
    			}),
    

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review size/XL overlap and removed review/needs-review No maintainer or bot review yet size/L labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@yumam0815

Copy link
Copy Markdown
Contributor Author

Addressed the EmDashBot MCP parity finding in 958b29a2. content_publish, content_unpublish, and content_discard_draft now accept the optional _rev precondition and pass it to the existing lifecycle handlers. MCP M01–M09 pass 9/9, the related suites pass 114/114, and core typecheck, focused type-aware lint, and format checks pass.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks

@ascorbic ascorbic changed the title Fix/publish expected revision cas fix(core): enforce revision checks for content lifecycle mutations Sep 2, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2783

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2783

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2783

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2783

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2783

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2783

emdash

npm i https://pkg.pr.new/emdash@2783

create-emdash

npm i https://pkg.pr.new/create-emdash@2783

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2783

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2783

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2783

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2783

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2783

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@2783

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2783

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2783

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2783

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2783

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2783

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2783

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2783

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2783

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2783

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2783

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2783

commit: 8a8e276

@ascorbic
ascorbic enabled auto-merge (squash) September 2, 2026 12:27
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 2, 2026
@ascorbic
ascorbic merged commit cd294dc into emdash-cms:main Sep 2, 2026
52 checks passed
@emdashbot emdashbot Bot mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish API cannot atomically require the approved content revision

2 participants