Skip to content

[pull] main from TryGhost:main - #1530

Merged
pull[bot] merged 18 commits into
code:mainfrom
TryGhost:main
Sep 30, 2026
Merged

pull[bot] merged 18 commits into
code:mainfrom
TryGhost:main

Conversation

@pull

@pull pull Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

jonatansberg and others added 18 commits September 30, 2026 13:22
closes https://linear.app/ghost/issue/BER-3932/make-email-analytics-event-result-merging-linear-time

Merging each event's result rebuilt the accumulated email and member ID lists from scratch, so ingestion slowed quadratically as a batch grew towards the 5000-member mid-fetch aggregation threshold. Persistent membership sets now let merge() check only the incoming IDs while keeping the same lists in the same first-seen order.

The service's running result now tracks event counts only. It exists because each mid-fetch aggregation resets the processing result, and the job still needs totals for the whole run. It used to copy every page's new IDs as well, which meant diffing against all IDs seen so far on each page and holding them until the job finished, even though only the counts are ever reported.
Refines the React editor’s publish flow to match the preview controls
and make recipient selection, scheduling, and final confirmation easier
to use. Addresses all 11 items in
[PLA-461](https://linear.app/ghost/issue/PLA-461/publish-flow-refinements),
each in its own commit.

- Aligns fullscreen actions, updates spacing and typography, and removes
animation when switching between Preview and Publish while retaining
editor entry transitions. Closing Preview dismisses the publish flow too
and returns to the editor; switching back with Publish preserves its
choices.
- Adds animated, indented options; reuses the searchable member label
picker without editing controls, grouped into active tiers, archived
tiers, and labels; places compact scheduling inputs in a bottom-aligned
second column so the timing radios stay in place.
- Keeps empty and single-row token fields at the standard input height
with equal padding, and restores the label-picker chevron in publish
recipients and member details.
- Places the green publish action on the right with white text,
alongside an outlined Back button.
- Applies the same content width, typography, spacing, Close
positioning, and action alignment to the unschedule and unpublish
modals.
- Restores Ember’s completion navigation: pages return to the page list,
scheduled posts and posts without email return to the post list, and
immediate posts with email open post analytics. Includes previously
emailed posts and email-only sends, and hands navigation to Ember when
it owns the destination list.
- Matches completion-screen headings, spacing, card typography, and
actions to the other publish steps.
- Addresses review findings around keyboard access, heading line height,
mobile overflow, transition state reset, and recipient row height while
newsletter counts refresh.

Validation: 158 focused browser tests passed across publish, preview,
editor header, scheduled dates, member labels, and bulk actions; 271
focused unit tests and Admin typecheck passed. Repository-wide
formatting, lint, dependency boundaries, package checks, and
documentation checks passed. Full `pnpm check` failed in untouched test
files: 30 Ghost Core test timeouts, three Admin test/hook timeouts, and
one Admin fixture suite unable to resolve the Koenig build output
(`koenig-lexical/dist/koenig-lexical.js`). Visually checked desktop and
390px mobile layouts; the mobile dialog has no horizontal overflow. The
latest layout follow-ups passed all 56 publish-flow browser tests,
including regressions for timing radio movement, recipient section
height during newsletter count loading, and single half-turn chevrons
alongside legacy Admin CSS. The Preview dismissal follow-up passed all
87 editor-header and preview browser tests, including Close, Escape, the
preview shortcut, focus restoration, and reopening. The
unschedule/unpublish styling passed all 12 update-flow browser tests and
was visually checked at desktop and 390px mobile widths. The
completion-navigation follow-up passed all 92 editor-header and preview
browser tests plus Admin typecheck, with regression coverage for posts,
pages, schedules, email-only sends, and previously emailed posts. The
recipient-grouping follow-up passed 68 publish/member-label browser
tests and Admin typecheck, with the dropdown visually checked locally.
The no-flash follow-up passed 107 publish-flow/editor-header browser
tests and Admin typecheck. Completion CI follow-ups passed 51
editor-header and 9 newsletter-retry acceptance tests, Admin typecheck,
and E2E typecheck. All 12 journeys across the three affected E2E files
passed against the local test backend across the main run and a focused
rerun; one unrelated settings-sidebar timeout passed on rerun. A
subsequent CI scheduling failure exposed a race with the destination
Ember success modal; the test now explicitly waits for and closes that
modal before reopening the post. The focused scheduling journey and E2E
workspace lint/type checks passed after this correction. These journeys
cover scheduling and unscheduling through the returned Ember list,
publish-only navigation, newsletter delivery and analytics navigation,
access rules, and post settings. Component and acceptance tests use the
fake Admin API; E2E tests use isolated backend fixtures.

The token-field and chevron follow-up passed all 284 Shade tests, Shade
lint/build, 65 publish/member-label browser tests, and Admin typecheck.
Visually verified standard/empty/selected fields at 32px in Storybook,
plus the chevron on publish and member forms. A subsequent local Admin
asset-copy step hit an ActivityPub dist symlink collision after
compilation; the clean Admin build passed in CI on commit `9ec27614c8`.

- [x] Read and followed the Contributor Guide
- [x] Explained the change
- [x] Added automated regression coverage
closes https://linear.app/ghost/issue/NY-1582
closes https://linear.app/ghost/issue/NY-1584

Introduces a combined performance-stats endpoint for all-time entry
history
and in-progress, completed, and exited-early counts. Derives chart and
status
totals from one Tinybird query so they reconcile.

Updates Tinybird sorting keys to support scoped queries while preserving
existing data and latest-version deduplication. Retains the automation
list's MySQL fallback.

Commits progress through storage, query, history formatting, response
validation, and API wiring.

Tests cover:
- Status precedence, deduplication, and site/automation isolation.
- Complete history, date boundaries, empty data, and matching totals.
- Staff authentication, permissions, invalid responses, and failure
paths.

_Best to review this commit-by-commit._
…31026)

No ref 

Replaced the post analytics sending banner with a single status line
under the post title, and gave posts list rows the same line. All
wording is unchanged from main.

<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/3b2829bf-0740-436b-96ca-8022811f290d"
/>

<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/22c2b0c6-3b1d-44d7-9f06-cca9e4c737f1"
/>

---------

Co-authored-by: Kevin Ansfield <kevin@lookingsideways.co.uk>
no ref

Embed previews render in a separate-origin renderer, and when it fails to
answer the editor only showed a placeholder. Nothing was reported, so there
was no way to tell a blocked frame from a wrong document or a version
mismatch. The card now passes each failure to Koenig's `onError`, which the
post editors already send to Sentry.

`onError` was typed on the composer context but never provided, so it is now
passed through. Card render crashes caught by Koenig's error boundary reach
the host's handler as a result.
…31110)

no ref

The admin acceptance suite flaked on CI in [this
run](https://github.com/TryGhost/Ghost/actions/runs/36593525443/job/109493906527)
on #31093: `Posts list rows › marks a featured post`
failed with a 418 for `POST /stats/posts-member-counts/`, even though
that endpoint was faked for the test. The request had been issued just
before the test ended, but the harness only learned of it once the
service worker relayed it. By then the drain had finished and the fakes
had been reset.

Slowing the MSW service-worker relay (a random 0–150ms delay before it
hands each request to the page) turned that one flake into about 80
failures across the suite. It also exposed specs that only passed
because the in-browser fake API answers almost instantly. Working
through those showed three kinds of problem: the hole in the harness
behind the CI flake, specs that assumed an ordering the app doesn't
promise, and two real UI races.
closes https://linear.app/ghost/issue/NY-1621

This change should have no impact on functionality.

Now that the server always sends an automation description, let's remove
the fallback.

In addition to automated tests, I also manually verified that this works
as before.
refs https://linear.app/ghost/issue/NY-1626/

## Problem

MailgunClient rejects failed sends with `{ error, messageData }`.
Automation polling logs that wrapper as `err`, but Ghost's logger
expects the error itself. The resulting
`automations.poll.step_execution_failed` event ends in `undefined` and
loses the original message and stack, making failed sends difficult to
diagnose.

## Solution

Unwrap a nested Error in the step failure handler before logging it.
Preserve the original error object, event name, and step ID without
adding the email payload to the log.

Co-authored-by: Evan Hahn <evan@ghost.org>
…UI (#30856)

ref https://linear.app/ghost/issue/GVA-1001

When the selfServeArchives flag is on, the export tab only showed the
"Export data" button, so the quick post analytics CSV download was only
reachable by opening the export dialog and downloading a full zip. Post
analytics serve a bit of a different purpose, with publishers often
exporting these on a regular basis (compared to exporting for the
purpose of a backup or change of hosting provider).

The button is shared between the flagged and unflagged layouts so the
two can't drift apart while the flag is still in place.
…hanges (#31094)

no ref

A background read of the post could hand the React editor another writer's version token without their content. This tab's next save then passed Core's collision check and silently overwrote their changes.
closes https://linear.app/ghost/issue/NY-1618

*I recommend reviewing with whitespace changes disabled.*

What
----

This change should have no user impact.

Before this change, automations were run off their `slug`.

Now, they're run off their `trigger_tier_scope`.

(This will get more complicated when we add new trigger types, but this
is good enough for now and much more easily extensible.)

Why
---

In the near future, publishers will be able to create their own
automations. We don't want to rely on hard-coded slugs for this.

Test plan
---------

In addition to manual tests, I also manually ensured automations still
ran (e.g., received an email) after this change.
no ref

Embed cards kept the oembed provider's `thumbnail_url`, so newsletter
video previews hotlinked third-party images and broke whenever the
provider stopped serving them. Imported posts already had their embed
thumbnails self-hosted, but newly inserted embeds did not.

Also switched YouTube thumbnail fetching to their larger, higher-quality
images without borders.

- The oEmbed endpoint now stores the thumbnail via image storage and
returns the stored URL as `thumbnail_url`, keeping the provider's URL in
a new `thumbnail_url_original` property
- YouTube's `hqdefault.jpg` letterboxes 16:9 videos with black borders,
so the 1280x720 `maxresdefault.jpg` is stored instead and the thumbnail
dimensions are updated to keep email previews at the right aspect ratio.
Videos without a max resolution thumbnail (YouTube 404s for older
low-res uploads and some Shorts) fall back to the provider's thumbnail
- If the image can't be stored, the provider's URL is kept so the card
still has a thumbnail
- Mentions are skipped because they aren't stored in content and can be
triggered by third parties sending webmentions
- The embed node now URL-transforms `metadata.thumbnail_url`, so
self-hosted thumbnails (relative or absolute site URLs) are stored with
the `__GHOST_URL__` placeholder. Otherwise the relative URLs returned by
local storage would reach emails unresolved
SSO adapters currently receive only the owner's ID and email, so an
owner-mapping adapter cannot distinguish an active seeded owner from
Ghost's inactive first-run placeholder. Expose the existing user status
through both SSO repository lookups and an optional field on the base
interface. Existing production adapters keep their current behavior.

This supports Ghost-Moya's preview-only IAP adapter: authorized
reviewers share the active preview owner, without changing its email or
reseeding the database. Empty previews do not activate that adapter and
retain owner setup and password login.

Validation:

- SSO base package tests and type checks passed.
- Ghost session-from-token unit test passed.
- Database-backed SSO API tests passed for verified login,
inactive-owner rejection/setup availability, and SSO-disabled behavior.
- Changed-file lint, format, dependency-boundary, Markdown, and secret
checks passed; Ghost TypeScript build passed.
- `pnpm check` passed formatting/lint but failed on unrelated
gift-preview image timeouts and Koenig browser tests requiring an
unavailable Firefox binary.

Deployment: merge before enabling seeded preview SSO in Moya, then
deploy a Ghost PR image containing this commit or a descendant. No
migration or database rewrite is needed. A patch changeset covers the
SSO base package.
closes https://linear.app/ghost/issue/NY-1468

This is a test-only change that should help keep migrations and
`schema.js` in sync.
no ref

The embed renderer works out an iframe embed's height from its own width.
Where scrollbars take up space, a frame a few pixels too short shows one,
which narrows the renderer and shrinks the height, which removes the
scrollbar and grows it again. Each change triggered another report, so the
renderer never yielded and flooded the editor with resize messages.

The editor sizes the frame to fit, so the renderer never needs to scroll.
Hiding its overflow keeps the width, and so the height, stable. The message
protocol is unchanged, so this ships as the same renderer version.
@pull pull Bot locked and limited conversation to collaborators Sep 30, 2026
@pull pull Bot added the ⤵️ pull label Sep 30, 2026
@pull
pull Bot merged commit b89dfe5 into code:main Sep 30, 2026
6 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants