Skip to content

publish silently reverts content on collections without revisions support #2095

Description

@wunderloopDE

Summary

On a collection whose supports does not include revisions, content update writes data straight into the content table columns — correct and immediately live. A subsequent content publish then promotes the stored liveRevisionId, which may be arbitrarily old, and syncs every field of it back over the fresh columns.

The update is destroyed, and publish reports {"success": true} with exit code 0.

The damage scales with the age of liveRevisionId. In our case the live revision was six weeks old.

Affected: verified in 0.1.0 (our deployment) and 0.29.0 (latest) — the logic is unchanged between them.

Reproduction

Verified end-to-end against a live deployment on 0.1.0:

  1. A collection with supports = NULL (or any value without revisions), on an item that has a live_revision_id older than its current column data.
  2. content update <collection> <slug> --rev <rev> --file payload.json — change one field, e.g. title.
    Works. Columns updated, marker present in content get, in the single-item API and in ?status=published. version bumps, no revision row is created, draft_revision_id stays NULL.
  3. content publish <collection> <slug>
    {"success": true}, ✔ Published, exit 0.
  4. Re-read any channel: the change is gone. live_revision_id unchanged, still zero revision rows for the entry.

Measured state around step 3 (title had marker before, clean after; live_revision_id never moved):

before publish after publish
column title … W842TEST (six-week-old value)
live_revision_id 01KT90EE87RDH41K5096A1MC2E unchanged
revision rows 1 (six weeks old) 1

Root cause

Update correctly skips the revision machinery when the collection lacks revisions:

// 0.29.0 src/emdash-runtime.ts:2743-2811  (0.1.0: 1457-1521)
let usesDraftRevisions = false;
if (processedData) {
  try {
    const collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
    if (collectionInfo?.supports?.includes("revisions")) {
      usesDraftRevisions = true;
      // ... create draft revision, set draft_revision_id
    }
  } catch { /* Don't fail the update if revision creation fails */ }
}
const result = await handleContentUpdate(this.db, collection, resolvedId, {
  data: usesDraftRevisions ? undefined : processedData,   // -> columns, correctly
});

Publish then has no draft to promote and falls back to the live revision:

// 0.29.0 src/database/repositories/content.ts:1283
let revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;
// ...
// :1299
await this.syncDataColumns(type, id, revision.data);   // writes EVERY field back

For a revisionless collection draftRevisionId is always NULL, so publish always restores whatever snapshot liveRevisionId happens to point at — overwriting every update made since.

Suggested fix

When the collection does not support revisions, publish should change status only and leave the data columns alone — the columns are the source of truth for that collection. Concretely: skip the syncDataColumns call (and the _slug sync) unless the promoted revision came from draftRevisionId, or gate the whole promotion on supports.includes("revisions").

An alternative that also closes it: refresh liveRevisionId from the current columns on each update for revisionless collections — though that reintroduces the revision writes the design deliberately avoids there.

Notes

  • The silent catch {} in the update path is a separate hazard: if the revision block throws after usesDraftRevisions = true, the edit is written nowhere while the call still reports success. We did not hit this, but the shape is the same — a write that vanishes with a green exit code.
  • We're aware supports = NULL may be unintended configuration on our side. The report stands regardless: the code tolerates it explicitly (?.includes), and the resulting behaviour is silent data loss rather than an error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions