Skip to content

Merge post_platforms into posts - #397

Open
paulocastellano wants to merge 8 commits into
mainfrom
feat/merge-post-platforms-into-posts
Open

paulocastellano wants to merge 8 commits into
mainfrom
feat/merge-post-platforms-into-posts

Conversation

@paulocastellano

@paulocastellano paulocastellano commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Why

TryPost 2.0 made every post independent per channel, but the data still lived in two tables: posts and post_platforms, with exactly one destination per post. This PR moves the destination onto the post row and drops post_platforms, so there is one place to read and write a post's channel and publication result.

What changes

Schema (4 migrations, 2026_10_08_2229*)

  1. Prepare. Runs in one transaction on both engines. Clears what the 2.0 split left behind: disabled destinations that never published, unpublished posts without a destination (turned into drafts), published posts without a destination (deleted with their media, quietly, as a disconnect does), and partially_published posts with one destination. It stops with an explanation (pointing to v2.0.0 + release:trypost-2) when a post still has more than one destination, a disabled destination published, a published post has no destination, or a destination's account belongs to another workspace.
  2. Add columns. The destination columns land on posts: social_account_id, publish_status, platform, content_type, meta, the channel snapshot, the publication fields, publication_updated_at, legacy_target_id. Indexes and analytics_publications.post_id are added too.
  3. Backfill. Chunked and idempotent. It copies every destination, links analytics publications by post, and replaces post.partially_published webhook subscriptions with post.published + post.failed. It finishes with a full parity check (counts, scalar columns, JSON, tenancy).
  4. Drop. Removes analytics_publications.post_platform_id and post_platforms, after a guard that every row was copied.

Model

  • posts.status stays the lifecycle. posts.publish_status (App\Enums\Post\PublishStatus) is the network side.
  • partially_published is removed.
  • Publication writes go through Post::markPublication*() / writePublication(). These move publication_updated_at and leave updated_at alone.
  • Channel-less legacy drafts are kept and stay editable. Scheduling or publishing one asks for a channel (posts.errors.choose_channel).

Removed

  • PostPlatform, its factory, observer and resource.
  • The postPlatform morph alias.
  • The one-off 2.0 release commands: release:trypost-2, posts:split-legacy-active, posts:audit-legacy, posts:purge-orphaned, posts:bake-aspect-ratio-crops, analytics:backfill-existing.

⚠️ Breaking: API, MCP and webhooks (no compatibility shape)

  • Post object: no platforms[]. The top level carries publish_status, social_account, platform, content_type, meta, platform_url, error_message and display_*.
  • Create / update input: top-level social_account_id / content_type / meta. Sending platforms is a 422.
  • Validation errors: single-post writes report meta.* / content_type. Batches keep destinations.{i}.*.
  • Preview and post metrics: one object each. Analytics returns post_id instead of post_platform_id. Each repurpose item post carries platform + publish_status.
  • Webhooks: channel fields at the top of the post.* payload, with no error_context. post.partially_published is removed, and stored subscriptions are migrated.

DEPLOYMENT.md §4 lists the docs.trypost.it pages to update.

Before merging

Deploy (see DEPLOYMENT.md)

  • Maintenance + full queue drain. Includes delayed jobs, so no old job carrying a destination id survives.
  • Build before migrate. On the production copy of 2026-10-09 (23,120 posts, 68,022 destinations) the four migrations took 12 s: 45,520 disabled destinations dropped, 1 failed post turned into a draft, 139 published posts without a channel deleted (236 media), every other destination copied with parity.
  • Forge script: add artisan horizon:terminate after $RESTART_QUEUES().
  • Rehearse migrate on a production copy first. The first migration tells you if anything still needs posts:split-legacy-active.
  • Self-hosted on v1.1.0: must upgrade through v2.0.0 and run release:trypost-2 first.
  • Forward-fix only after up.

Tests

  • Full suite on PostgreSQL: 9,484 passed, 0 failed.
  • MySQL: Migrations, Posts, Commands, Analytics, Jobs, Api, Parity, Mcp, Services and Unit, about 6,250 passed, 0 failed.
  • Browser suite: 704 passed.
  • New: merge migration tests (guard, cleanup, backfill of every column, publish time, analytics links, webhook events, parity corruption, re-runs, drop guard), Post publication model tests, a web edit without content_type.
  • Deleted, because the behaviour no longer exists:
    • tests of posts with several destinations, disabled destinations and partially_published;
    • tests of the removed release commands;
    • old migration tests that need post_platforms at runtime (ClearGiphySource, CollapseLinkedInContentTypes, RemoveAspectRatioMeta, RemoveChannelPause, PublishLegacyTargetMedia).

Paulo Castellano added 6 commits October 8, 2026 20:00
Every post now carries its single destination (social account, content
type, meta) and its publication result (publish_status, platform post id,
URL, error, checkpoints) on its own row. The migrations resolve the
legacy rows TryPost 2.0's split left behind, refuse to run while a post
still has several destinations, copy each destination onto its post,
link analytics publications by post, and drop post_platforms.

Code that only existed for posts with several destinations is removed
instead of ported, together with the one-off TryPost 2.0 release scripts.
Post cards, the calendar, post details and the failure popover read the
channel and publication from the post itself. Edits report meta errors
under meta.*, the partially published status and webhook event are gone,
and saving a channel-less draft asks for a channel first.
Covers the guard, the legacy cleanup, the backfill (every column, publish
time, analytics links, webhook events, parity, re-runs) and the drop. The
deploy guide and the project rules now describe posts that carry their
own channel.
Every test now builds posts that carry their own channel. Tests of
multi-destination posts, disabled destinations, partially published
posts and the removed 2.0 release commands are deleted.

Fixes found while converting:
- single-post writes (API and MCP create, every update) report errors
  under the post's own keys (meta.*, content_type), not destinations.0.*
- the edit composer shows thread reply, Pinterest, Discord and TikTok
  errors keyed meta.*
- a web edit without content_type keeps the stored type again
- importing media for an imported post no longer reads an undefined
  variable
- the analytics publication factory writes post_id
MySQL does not wrap migrations in a transaction, so the legacy cleanup
was committed before the guard threw. The preparation now runs in one
transaction on both engines.
Paulo Castellano added 2 commits October 9, 2026 09:30
The production copy has 139 published posts without any destination.
They are history whose channel no longer exists, so the preparation
deletes them with their media (files removed after commit), quietly,
the way disconnecting a channel does.
The backfill took the destination's publish time, which is a second or
so before the time the post shows, so 57 cards on the production copy
moved. A published post now keeps its own time and falls back to the
destination's only when it has none.

This branch has not been deployed

No deployments
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