domain: publishing a draft via Micropub update leaves published_at unset, post never goes live #53
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier#53
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during a code review of the
domaincrate.Location:
crates/domain/src/models/micropub.rs:76(MicropubUpdate::apply_to)Severity: Correctness — silent invisibility
Problem
An update that replaces post-status with
publishedsetsstatus=Publishedbut carriespublished_at=Nonewhen the update does not also set apublishedproperty.Post::is_liverequirespublished_at.is_some_and(|p| p <= now), so the post is never viewable, listed, or webmention-dispatched. The create path defaultspublished_attoUtc::now, butapply_tohas no counterpart.Failure scenario
Create a draft via Micropub (published_at=None), then send an update flipping post-status to published with no
publishedproperty. The post is marked Published butis_livestays false forever; only the author sees it.Suggested fix
In
apply_to, when a status transition to Published leavespublished_atunset, default it tonow(matching the create path).Agent Brief
Category: bug
Summary: Publishing a draft via Micropub update must default
published_atto the current instant, so the post actually goes live.Current behavior:
MicropubUpdate::apply_toresolves an update against the existing post. When the update replacespost-statuswithpublishedbut does not include thepublishedproperty, the resultingPostEditcarries the post's existingpublished_at— which isNonefor a draft.Post::is_liverequires a published status and apublished_atat or before now, so the post is marked Published but never becomes viewable, publicly listed, or webmention-dispatched; only the author can see it. Confirmed by a reproduction test during triage.The Micropub create path already handles this: a non-draft create defaults
published_atto the current instant when the client omitspublished. The update path has no counterpart.Desired behavior:
When an update transitions a post's status to Published, does not specify the
publishedproperty, and the post has no existingpublished_at, the resolved edit'spublished_atdefaults to the current instant — mirroring the create path. The post is live immediately after such an update.The trigger is deliberately transition-based, not resulting-state-based:
publishedvalue alongside the status change, the client's value wins (already the case today).published, that client-specified operation must not be counteracted by re-defaulting — the existing clear-unsets-it behavior stays, even in the same request as a status change.publishedkeeps the existingpublished_atunchanged.Spec note (checked during triage):
post-statusis an IndieWeb extension, not part of the core Micropub spec, and the spec's update semantics constrain only the properties the client specified — it is silent on the server defaulting an unmentionedpublished. Defaulting on the draft→publish transition matches the create-path behavior and ecosystem practice (e.g. the WordPress Micropub plugin sets the publish date at the moment of publishing).Key interfaces:
MicropubUpdate::apply_to(&self, post: &Post)— the resolution point. It is currently pure with no clock; take the current instant as a parameter (e.g.now: DateTime<Utc>) supplied by the caller rather than callingUtc::now()inside, so the function stays deterministic in tests. The Micropub update service method that calls it already deals in the current time.PostEdit.published_at— where the defaulted value lands. No shape change needed.published.unwrap_or_else(Utc::now)for non-draft status) is the behavior to mirror.Post::is_live— unchanged; it is the observer that makes the bug visible.Acceptance criteria:
published_at, updated with only apost-statusreplace topublished, resolves to an edit withstatus = Publishedandpublished_at = now; the post is live immediately after the update persists.publishedvalue in the update uses the client's value, not now.publisheddoes not get a defaulted date.publisheduntouched keeps the existingpublished_at.publishedwithout a status change still unsets it (existing test behavior preserved).ciandclippymise tasks pass.Out of scope:
publishedon a live post leaves it Published-but-invisible — separate semantics question (reject vs. honor), separate issue if pursued.publishedin the future) andis_livesemantics — unchanged.Fixed in
d3aaaa7.MicropubUpdate::apply_tonow takes the current instant and, on a draft→publish transition with no client-suppliedpublished, defaultspublished_atto now — mirroring the create path — so the post goes live immediately. Explicitpublished(replace or delete) still wins, and an already-published post keeps its date. Covered by new tests; ci and clippy pass.