Skip to content

[pull] main from TryGhost:main - #1527

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

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

Conversation

@pull

@pull pull Bot commented Sep 29, 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 : )

9larsons and others added 16 commits September 29, 2026 12:49
no ref

Two Core tests failed outside UTC: the cron assertion ignored Bree's active scheduler timezone, and an email fixture used local midnight despite mocking a UTC publication. Match the scheduler's calendar and give the fixture an explicit UTC timestamp.
no ref

The deep-queue regression test could time out under load while waiting through 50,000 real event-loop turns. Use controlled `setImmediate` timers, keeping the full queue depth and verifying that other queued work runs before the drain completes.
fixes https://linear.app/ghost/issue/BER-3974/an-integration-reads-the-custom-fields-a-site-defines

An integration such as Zapier has to learn which custom fields a site collects before it can write them to a member, and every integration key was refused the definitions with a 403. The Admin API decides which endpoints an integration key may reach by naming the resource from the first segment of the request path, and the definitions are served by a router mounted under /members/, which strips its mount from the path before that check runs, so the check saw a resource called "custom" and refused it. Authenticating as a route alongside the mount, where the path is still whole, restores the access integrations had before the definitions moved behind a mounted router, reads and writes alike. The check itself is left alone: it is due to be replaced as authentication moves to Better Auth, and this keeps the fix to where the routes are declared rather than reshaping something that is about to change.
no ref

Flaky test patch. A new post's create acknowledgement could render before React Router published a blocked exit, replacing the intended Posts destination with the new editor URL. Added checking the synchronous navigation guard before replacing that URL, keeping cancellation from reviving stale blocked state.
no ref

Admin reloads to /ghost/ when an API request says the session has expired. That also happened on page loads where nobody was signed in yet, and the reload could beat Ember to saving the route the visitor asked for, so they landed on the dashboard after signing in instead of where they were going. The user's path should now be preserved through auth.
no ref

Sites created before 2048-bit key generation still sign member and staff
tokens with 1024-bit RSA keys, and jsonwebtoken 9 won't sign with those.
These rows let a later change rotate each keypair without breaking
verifiers: the next key is published before it's used to sign, and the
previous public key stays published briefly after the switch. The next
public key is derived from its private key, and row `updated_at` records
when each was written, so neither needs its own row. The active key
stays in the existing settings rows, so downgrading keeps working.
Improves the React editor’s title and feature image layout. Titles match
the Ember editor’s typography and resize to fit their content, and
feature images keep their original aspect ratio instead of being clipped
at 480px.

Fixes [PLA-447 — Editor header
refinements](https://linear.app/ghost/issue/PLA-447/editor-header-refinements).

## Changes

- Match title typography across desktop and mobile, and remove inherited
textarea constraints that prevented titles from growing and shrinking
correctly.
- Place feature image upload and Unsplash controls side by side with a
13px label and 20px gap. Keep the Unsplash icon at 14px inside a
circular 32 × 32px button.
- Use consistent 13px sans-serif typography for feature image captions,
placeholders, and alt text, with a black-and-white selected Alt toggle.
- Display full feature images at their original proportions, including
tall portrait images.
- Reset virtual-list row windows before layout effects so opening
Members from the sidebar reliably starts at the top, while Back and
breadcrumb navigation retain their saved position.

## Validation

- [CI
passed](https://github.com/TryGhost/Ghost/actions/runs/36559439696) on
`58ec72537c`: all 12 E2E shards, both Admin acceptance shards, both
unit-test jobs, lint, and build checks passed. CodeRabbit reported no
actionable findings.

- Fixed the CI failure in the members virtual-window E2E test. Both Back
and breadcrumb flows passed three repeated runs each; all 20 focused
virtual-window and scroll-restoration unit tests passed, including a new
regression for reset timing.

- Post editor, feature image, and X card acceptance suites passed, with
regressions covering title resizing and portrait/landscape image
clipping. All 12 feature-image tests passed after the final button
adjustment.
- Admin TypeScript, focused ESLint, formatting, and pre-commit checks
passed.
- Visually checked the editor at 1280px, 600px, and 390px widths, and
verified the Unsplash button’s 32 × 32px dimensions and circular shape.
- Full `pnpm check` passed formatting and lint, but the Admin unit suite
encountered a Koenig module-resolution failure in
`engine/__fixtures__/after-load.test.tsx` and a timeout in the members
custom-field country filter test. Neither failing file is changed by
this PR.
…31078)

no ref

Flaky test fix. After returning to a scrolled list, clicking its sidebar link could leave the fresh list at the old position instead of the top (flaky `e2e/tests/admin/members/virtual-window.test.ts`).

The reset wrote `scrollTop` directly, but the virtualizer only picks up the new offset from the next frame's `scroll` event. If the shorter fresh list rendered first, it corrected row sizes against the old offset and scrolled back. Scroll writes now dispatch the `scroll` event themselves, and the departing entry's listener ignores it so Back still restores that entry.
no ref

Adds React versions of Admin's sign in, 2FA verification, password reset, staff invite signup, setup and sign out. They are served when the new private Labs flag `authReact` is on, and by Ember otherwise. Behaviour, copy and validation match the Ember screens, though there's a visual change that'll need to be reviewed.

The purpose is to make these screens Better Auth 'shaped' so that when we're able to turn to implementing that, that there's less churn needed.
no ref

Opening Post history on a post with no saved revisions (e.g. created via
the API or an import and never saved in the editor) crashed the editor
with `Cannot read properties of undefined (reading
'feature_image_caption')`. Since 6.54.1 the `selectedRevision` getter
sanitizes the caption without checking that a revision exists; it now
returns early, so the preview falls back to the post's own title as
before.

Covered by a new Ember acceptance test that fails without the fix.
…31079)

Refines the React editor settings sidebar so its fields, navigation and
metadata tools match the intended editor layout.

Fixes [PLA-449 — Sidebar
refinements](https://linear.app/ghost/issue/PLA-449/sidebar-refinements).

- Keeps every settings pane at the sidebar width, with larger headings,
circular back buttons with the same hover background and icon stroke
weight as the sidebar toggle, a separator above history and consistent
navigation rows with medium-weight labels.
- Adds the URL icon and a published-post link, and gives publish
date/time equal space with icons and an inline timezone. Adds a shared
Shade TimePicker, also used by publishing schedules.
- Adds validated canonical URLs (rejecting incomplete schemes and
malformed hosts while preserving root-relative paths), restores the
Google-style search preview, and gives code injection fields a white
background.
- Shares token-field sizing between tags, authors and member labels;
tag/author chevrons stay at the top right as pills wrap.
- Gives X and Facebook image uploaders a white background, subtle dashed
border and centered upload icon above the label, matching tag details.
- Places the primary header action last before the sidebar toggle;
Unpublish and Unschedule precede Update and use ghost styling.
- Fixes keyboard-shortcut labels clipped by legacy definition-list CSS.
Uses individual Shade keycaps, compact hover rows and larger underlined
group headings. Adds a reusable `Kbd variant="contrast"` with a slightly
darker background and applies it in the sidebar.
- Confirms the existing inline-excerpt flag already hides the sidebar
excerpt.

Also updates Shade’s disabled primary buttons globally: an opaque light
grey surface with softened grey text, with semantic dark-mode colors.
This applies in both host modes without a feature flag. The Button
stories compare enabled and disabled controls in light/dark and
current/legacy modes.

Validation: focused editor acceptance suites (including scheduling, all
32 header tests and 9 shortcut tests with Mac/Windows legacy-host CSS
regression coverage), 135 focused Admin unit tests, all 284 Shade tests,
Admin typecheck, repository lint/boundary checks and commit hooks
passed. The canonical URL follow-up passed 36 focused unit tests and 19
metadata acceptance tests, including invalid-URL rejection and recovery.
Visually checked desktop and 390px mobile layouts, time-picker states
and wrapping tokens in Storybook.

Full `pnpm check` encountered Ghost Core test timeouts and
temporary-file errors. Two Admin tests also timed out under the full
run; both passed on an isolated rerun (44 tests). Koenig browser tests
then loaded a different local app occupying port 5174, so the remaining
full run was stopped. These failures are outside the sidebar changes.

- [x] I've read and followed the Contributor Guide
- [x] I've explained my change
- [x] I've written automated tests for changed behavior
no ref

`generate-data --quantities redirects:0` (or `comments:0`, etc.)
silently generated the importer's *default* quantity instead of nothing.
The CLI, `DataGenerator` and `TableImporter` already treat an explicit 0
as zero rows, but most importers that override `import()` tested the
argument for truthiness:

```js
this.quantity = quantity ? quantity / posts.length : 10;
```

These fan out per model, so on a large dataset asking for none of a
table gave you the most of it — e.g. ~1.4M redirect rows on a 300k-post
dataset, all buffered in `importForEach` before insert.

- 21 importers now check `quantity !== undefined`. Defaults (including
the function defaults in the recommendation importers) are unchanged, so
any invocation without an explicit 0 behaves exactly as before.
- `posts-tags-importer` had the same bug split across lines;
`comment-reports-importer` used `if (quantity)` and its `Math.max(1, …)`
would have produced reports even for 0 — it now returns early.
- `offer-redemptions-importer` is intentionally untouched: its
`defaultQuantity` is 0, so falsy already means nothing.
- Added `DataGenerator` tests covering explicit 0 for
`importForEach`-style importers (`posts_authors`, `redirects`) and the
default when omitted. The zero test fails without the fix.
- README notes the contract for importer authors.
closes https://linear.app/ghost/issue/NY-1631

## What

Members can no longer enter the same automation twice.

For example, let's say a publisher has a "paid subscription started"
automation. If a member signs up for a paid plan, then downgrades, then
signs up *again*, they won't get the automation's emails twice.

## Why

In the near future, members will be able to trigger the same automation
multiple times more often. For example, we might add an automation like
"member has label", which we might poll for every 5 minutes. (This is
just an idea.)

This change prevents these cases.

It also simplifies the current state. Now there's no way a member could
go from free → paid → free → paid and get the same automation emails.

(We may want to make this configurable in the future.)
towards https://linear.app/ghost/issue/NY-1617

It should be impossible to change an automation's slug, which could
cause all sorts of problems. This prevents that.

Currently, the frontend deploys before the backend. That's safe because
`slug` isn't required by the server, so nothing breaks if we stop
sending it.

In addition to automated tests, I manually tested this by changing a
welcome email and verifying that things worked fine.

I think this is useful on its own, but it also makes [some future
work](https://linear.app/ghost/issue/NY-1617) easier.
@pull pull Bot locked and limited conversation to collaborators Sep 29, 2026
@pull pull Bot added the ⤵️ pull label Sep 29, 2026
@pull
pull Bot merged commit 06ca9de into code:main Sep 29, 2026
1 check 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.

5 participants