Skip to content

Sync with upstream Ghost v6.24.0#60

Open
madewithlove-machine-user wants to merge 291 commits intomainfrom
chore/sync-v6.24.0
Open

Sync with upstream Ghost v6.24.0#60
madewithlove-machine-user wants to merge 291 commits intomainfrom
chore/sync-v6.24.0

Conversation

@madewithlove-machine-user
Copy link
Copy Markdown

Syncing fork to upstream release v6.24.0.

troyciesco and others added 30 commits March 4, 2026 21:31
no ref

- a test for labs ensures that config values take precedence over GA
keys. However, the test referenced the `announcementBar` flag was
removed in TryGhost#26196. That means this
test was looking at a state that wasn't really true
- instead, we can check for the first item in the `GA_KEYS` list. If
there's nothing in the list, the test can be skipped.
- a few other tests had this same issue, so this PR updates them as well
- there was a test for alpha flags that wasn't actually doing anything,
so it's deleted
no ref

I wrote this package so I should review changes to it.
closes https://linear.app/ghost/issue/NY-1125

- added new `usePinturaConfig` hook to admin

Co-authored-by: Evan Hahn <evan@ghost.org>
closes https://linear.app/ghost/issue/BER-3393

Aligned the copy for once offers with the repeating / free months
offers:
- Before "$4.00 - Next payment"
- After: "$4.00/month - Ends {date}"
ref https://linear.app/ghost/issue/BER-3388/design-bugsimprovements

- Fixed empty price gap in retention offer screen for free month discounts — the price div was rendering even when not applicable, causing a large visual gap
- Fixed title overlapping with back/close buttons in non-full-size retention offer
- Improved responsive layout for the offer list table
- Added `aria-label` to the icon-only filter button in retention offers
ref
https://linear.app/ghost/issue/BER-3348/portal-paid-section-doesnt-indicate-subscription-is-canceled

When a member cancels their subscription, the Portal account page looked
nearly identical to an active subscription — the only signal was a small
plain-text line that was easy to miss.
                  
- Replaced the plain cancellation text with an accent-colored banner
containing the expiry date and "Continue subscription" button
  - Added a CANCELED badge next to the tier name in the plan row
  - Added i18n keys for the new copy
  - Added unit tests for both the banner and badge

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> UI-only changes to account display plus new tests/i18n strings; no
backend logic or sensitive flows modified.
> 
> **Overview**
> Improves the Portal account UI for *canceled-but-still-active* paid
subscriptions by replacing the easy-to-miss expiry notice with a
prominent brand-accented banner showing the expiry date and a **Continue
subscription** CTA.
> 
> Adds a `Canceled` badge next to the tier/plan name, introduces styling
for the new banner/badge, and includes i18n keys plus new unit tests
covering the banner, expiry date rendering, and badge visibility.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
39b77fe. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ngs (TryGhost#26696)

closes https://linear.app/ghost/issue/BER-3392
closes https://linear.app/ghost/issue/BER-3218

- added max. length display title / description fields
- added inline validation for floating numbers for free-months /
duration-in-months fields


---------

Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
The canary job dispatched to Ghost-Moya's deploy.yml which cloned Ghost
from source and built a Docker image from scratch. trigger_cd now
handles all cases by dispatching to cd.yml which extends the pre-built
GHCR image instead.

trigger_cd supports the deploy-to-staging label on PRs, matching the
old canary behaviour: PRs from org members or renovate[bot] with the
label deploy to staging via cd.yml. Main builds dispatch with
dry_run=false and should_rollout=true. Regular PRs remain dry-run only.

deploy_admin remains enabled and unchanged — it continues to update
production admin-forward via deploy-admin.yml.
ref https://linear.app/tryghost/issue/BER-3380/

When bulk-unsubscribing members, the modal now lets you choose between
unsubscribing from all newsletters or selecting specific ones via an
inline searchable picker. When only one newsletter is active, the modal
shows a simple confirmation instead.
…ts (TryGhost#26699)

refs https://linear.app/tryghost/issue/ONC-1529

- The paginated label loading refactor (TryGhost#26655) missed adding server-side search to `gh-members-recipient-select` and `gh-members-segment-select`
- Users with many labels had to manually scroll through all pages before search could find labels not yet loaded client-side
- Added conditional server-side search (via `labelsManager.searchLabelsTask`) to both components, matching the
pattern already used by `gh-member-label-input`
- Tiers, statuses, and offers are still filtered client-side since they're always fully loaded
- Fixed labels found via search result not being selectable in some cases

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
closes
https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period

- When a member redeems a 1-month free offer, Stripe sets the discount
start date to now and end date to now + 1 month, i.e. anchors them to
the redemption date, not the billing date
- In Portal, to avoid confusion, we want to anchor the `"- Ends {date}"`
to a billing date to avoid confusion
- For example, today is 5 Mar 2026 and my subscription renews on 3 Apr
2026. I hit cancel, redeem a free-month offer that applies to my next
payment. I expect to see `"$0.00/month - Ends 3 Apr 2026"`, not
`"$0.00/month - Ends 5 Apr 2026"`
…6647)

closes https://linear.app/ghost/issue/ONC-1516

- When multiple `DELETE /members/{id}` requests target the same member concurrently, a race condition causes Bookshelf to throw "No Rows Deleted" errors that bubble up as 500s to Sentry
- The `findOne` check passes for both requests (member exists), but by the time the second request's `destroy()` runs, the member is already gone
- Fixed by passing `require: false` to Bookshelf's `destroy()` so it treats a missing row as a no-op — the member is already deleted, which is the desired outcome
ref f9f5f72

*This change should have no user impact.*

The `announcementBar` feature flag has been enabled since 2023. We can
remove the flag and assume it's always on.
closes https://linear.app/ghost/issue/BER-3336

The fallback to `sub.offer` was added as a transitional measure while
core and admin could be out of sync during CD. Both are now fully
deployed with `offer_redemptions` always populated, making the fallback
dead code
Ref https://linear.app/ghost/issue/BER-3408/design-clean-up

- Hid bulk actions for 0 selected members
- Used text-foreground for columns with values for open rate
ref https://linear.app/ghost/issue/ENG-1326/

Ember's dynamic asset loading (lazy-loader, admin-x components, Koenig
editor, user avatars) previously relied on a build-time `cdnUrl` config
to construct absolute CDN URLs. This required piping GHOST_CDN_URL
through environment.js into an encoded meta tag.

Instead, derive the asset base at runtime from the Ember script's own
URL — the same principle ES modules use for relative imports. If the
script loaded from a CDN (via index.html sed rewrite), dynamic loads
inherit the CDN origin. If local, they inherit the local path.

New `assetBase()` utility replaces 6 duplicated cdnUrl conditionals.
Also fixes the vite-ember-assets build to read from ghost/admin/dist.
fixes https://linear.app/ghost/issue/ONC-1512/

The contributor layout used `h-full overflow-auto` on the `<main>`
element, with the EmberRoot child also using `h-full`. This constrained
the Ember content to exactly the scroll container's height, so
`.gh-viewport`'s `overflow: hidden` clipped the posts list with no way
to scroll.

Changed the contributor layout to use the same flex column pattern as
the non-contributor `SidebarInset` layout — a `flex flex-col
overflow-y-auto` container with a `flex-1` wrapper for children. This
allows the content to grow beyond the container via `min-height: auto`,
enabling scroll.
…ryGhost#26716)

ref https://linear.app/ghost/issue/ONC-1512/

- Fixed the "Posts" link in the contributor user menu pointing to `/` instead of `/posts`, causing navigation to the wrong route
ref https://ghost.slack.com/archives/C018EKC56JF/p1771527734858719

The existing JWT is used for identity and only contains the email
address. It has a TTL of 10 minutes which is reasonable for the use case
but little else.

We have another use case where integrations often want to validate the
source, but are left with no option but to hit the members Admin API
endpoint with an Admin API key because the email isn't sufficient to
know if the member has access to a tier, is paid, isn't specific to
email (which could change), etc., all of which are used for
integrations.

This endpoint would still require the members session cookie and has the
following props, e.g.:
```
 sub: darylmayer880479@example.com
 scope: members:entitlements:read
 member_uuid: 629c5190-f068-4f49-b533-00d7477a7aed
 paid: false
 active_tier_ids: []
 jti: present
 iat: 1771555398
 exp: 1771555518
 aud/iss: http://localhost:2368/members/api
 Lifetime: 120s (2 minutes)
```
ref https://linear.app/ghost/issue/ONC-1533/

- replaced regex IP matching with a WHATWG URL parser normalization step and octet matching
ref https://linear.app/ghost/issue/ONC-1533/

The `gotOpts` passed to `metascraper-logo-favicon` only included a
User-Agent header, meaning its internal requests (e.g. favicon probes
via `reachable-url`) did not inherit the request hooks configured on
`externalRequest`. Fixed by using `got.mergeOptions` to clone the full
`externalRequest.defaults.options` (including hooks) and merge our
User-Agent override on top
broccoli-asset-rev rewrites string literals in compiled JS at build time,
prepending the CDN origin and fingerprint hash. The assetBase() utility
introduced in TryGhost#26555 always prepends a non-empty base URL, which caused
double-prefixing when the URL had already been made absolute by
broccoli-asset-rev. Added prefixAssetUrl() helper that skips prefixing
when the URL is already absolute, and updated all call sites.
ref https://linear.app/ghost/issue/ONC-1505/

The editor's settings view had two issues:

- **Missing background color**: The content container had no `bg-white` class, making it transparent over the underlying admin UI and breaking the layout
- **Infinite re-render loop**: `keywords={[]}` created a new array reference every render, causing `TopLevelGroup`'s `useEffect` to continuously re-register the component via `setVisibleComponents`,
making the UI unresponsive
ref https://linear.app/ghost/project/ghost-cicd-e70bca221364

The trigger_cd dispatch was sending deployment policy fields
(dry_run, should_rollout, environment) alongside artifact identity,
which meant Ghost CI was dictating how Moya should deploy. This made
it impossible to change deployment defaults without modifying Ghost.

Stripped the payload to identity fields only (sha, ref, image_tag,
pr_number, admin artifact IDs). Added a `deploy` flag that signals
when the deploy-to-staging label is present — this is a fact about
the PR, not a policy decision. Moya derives dry_run and should_rollout
from pr_number presence and the deploy flag.
rob-ghost and others added 28 commits March 26, 2026 12:22
ref https://linear.app/ghost/issue/BER-3440

Bumps comments-ui to 1.4 which patches out fingerprinting API usage by the comment editor library we're using. This should prevent 1.4 being flagged as fingerprinting and being blocked by fingerprint blocking extensions and browsers.
ref https://linear.app/ghost/issue/BER-3501/improve-avatars

- Avatars in the members list were falling back to the default user icon
instead of the initials with a colored background.
…UID (TryGhost#26745)

no ref

When running Ghost in development with `yarn dev` in a remote VM, the
analytics app crashes when loading over a plaintext http connection
because `crypto.randomUUID` is only available in a secure context (https
or localhost).

`crypto.randomUUID` is also overkill for React keys anyways, so this
changes to using integers instead, to make it easier to develop Ghost in
a remote VM environment.
The workflow has two fundamental problems: (1) admin deploys are global
per-environment, not per-site, so deploying a PR's admin overwrites
admin-forward/ for ALL staging sites, and (2) any merge to main triggers
a full staging rollout that overwrites both server and admin immediately.
The deployment only lasts until the next merge to main, making it
unreliable.
ref https://linear.app/ghost/issue/ONC-1591

The browse endpoint for settings did not correctly filter integration
keys based on user role, causing them to be included in the response
for contributor-level users.
ref https://linear.app/ghost/issue/ONC-1593

The /authentication/setup GET endpoint was returning the owner user's
name and email even after setup had completed. Now it only returns
the setup status boolean once the site is already configured.
ref https://linear.app/ghost/issue/ONC-1592

The invite acceptance flow was not wrapped in a transaction, which
meant the invite token was not atomically consumed alongside user
creation. Wrapped the operation in a transaction so the invite is
properly marked as used.
ref https://linear.app/ghost/issue/ONC-1594

The urlToPath method could return a path outside the expected storage
directory when the input URL contained relative segments. Added path
normalization to ensure the resolved path stays within the configured
storage root. Applied the same fix to the S3 storage adapter for
consistency.
ref https://linear.app/ghost/issue/PLA-10/
- Migrated 3 valuable tier tests (create, update with portal
verification, archive/unarchive) from `ghost/core/test/e2e-browser/` to
`e2e/tests/admin/settings/tiers.test.ts`
- Added `TiersSection` page object with reusable tier CRUD methods
(create, edit, archive, unarchive, enable in portal)
- Dropped 2 redundant tests (default prices smoke check, tier+offer
creation already covered by `portal-offers.test.ts`)
no ref

Hardened GitHub Actions shell usage to remove direct expression interpolation.
ref https://linear.app/ghost/issue/PLA-10/
- Adds a fake Mailgun HTTP server (following the fake Stripe server
pattern) that accepts newsletter email batches from Ghost and forwards
personalized emails to MailPit for test assertions
- Adds `mailgunEnabled` fixture to configure Ghost's
`bulkEmail.mailgun.*` settings to point at the fake server
- Adds `emailClient` fixture providing a shared MailPit client to any
test
- Extends `PublishFlow` page object with `selectPublishType()` and
`confirm()` methods
- Includes a smoke test that publishes a newsletter post and verifies
email delivery end-to-end via MailPit
ref https://linear.app/ghost/issue/BER-3496/table-broken-on-mobile-sizes

- The members list layout was broken on mobile. The table cells were
collapsed because of a incorrect widths.
no ref

This should be a performance-only change. This patch makes the following
changes:

1. Only read `.hbs` files once, and only compile the Handlebars template
once.
2. Read `.hbs` files in parallel, for performance.
3. Small: don't re-convert a string unnecessarily. (We were reading it
as a UTF-8 string, then converting it back to a buffer, then re-reading
that buffer as a UTF-8 string.)
4. Small: don't keep useless references around.
no ref

**What?** This enables [TypeScript's `erasableSyntaxOnly` option][0]
across the codebase. This means we can't use enums, namespaces, or a few
other things. This change should have no user impact.

**Why?** The big reason: [Node supports running TypeScript files
directly][1], but only if all the syntax is erasable. This means we can
remove build steps in many cases! It has a few other advantages, too:
simplifying source maps, restricting some tricky parts of TypeScript,
slightly accelerating TypeScript compilation,

This doesn't enable some of the other options that are sometimes
recommended in tandem, like `verbatimModuleSyntax`. Nor does it actually
run `.ts` files with Node. Those are for another day.

[0]: https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly
[1]: https://nodejs.org/api/typescript.html#type-stripping
no refs

## Summary

- Expanded the `create-database-migration` skill description to improve
triggering accuracy
- Skill now activates for broader set of user requests: referencing
schema.js, knex-migrator, migrations directory, "add a field/column", or
Linear issues requiring schema changes
- Also added a missing step to update the integration tests, which the
agent missed the first time.
- With this change, the agent was able to one-shot the addition of an
add table migration, with all tests passing in CI on the first try.
ref https://linear.app/ghost/issue/PLA-10/
- adds an OfferFactory to the e2e data-factory layer
- moves offer creation and update call sites off the old offer service
- removes the old helpers/services/offers helper
…st#26966)

closes https://linear.app/ghost/issue/NY-1163

_I recommend reviewing this
[commit-by-commit](https://github.com/TryGhost/Ghost/pull/26966/commits)
rather than all at once._

_This change should have no user impact._

This lets the welcome email renderer match the newsletter email
renderer's design settings. For example, it now fetches the background
color the same way.

It's currently hard-coded, but it should be easy to add real values on
top of this.

In addition to automated tests, I manually verified that welcome emails'
HTML was unchanged.
no ref

- Fixes a regression of bookmark cards in emails having unnecessary
underline.
no ref
- removes the thin Stripe checkout-initiation smoke spec from the
top-level e2e suite

This path is now covered more directly through product-facing e2e tests
instead of a separate harness-oriented smoke test.

Overlapping coverage already exists in:
- e2e/tests/admin/settings/portal-settings.test.ts
- e2e/tests/public/portal-tiers.test.ts
- e2e/tests/public/portal-upgrade.test.ts
closes
https://linear.app/ghost/issue/NY-1137/create-email-design-settings-table-migration

## Summary

Adds a new `email_design_settings` table to store email design/styling
settings independently from newsletters. The table columns mirror the
design-related columns from the `newsletters` table (background colors,
fonts, button styles, image corners, etc.) with identical types,
constraints, and defaults.

`title_alignment` and `post_title_color` are intentionally omitted —
this table initially supports automated emails which don't use those
columns. They can be added later if/when we extend templates for
newsletters.

## Changes

- Added `email_design_settings` table definition in `schema.js` with
`isIn` validations for enum fields
- Added `addTable` migration for the new table
- Added `email_design_settings` to the backup tables list in
`table-lists.js`
- Updated exporter test expectations and schema integrity hash

---------

Co-authored-by: Troy Ciesco <tmciesco@gmail.com>
no ref

There are a number of places we can add that are type checking in this
file. This is a types only change that should have no user impact.
ref
https://linear.app/ghost/issue/BER-2416/starter-customers-cannot-obtain-a-content-api-key

- Starter plan customers can't create custom integrations, which means
they have no way to obtain their Content API key
- Added a "Content API" card to the built-in integrations tab that opens
a modal showing the Content API key and API URL
- Follows the same modal pattern as Zapier and other built-in
integrations
ref https://ghost.slack.com/archives/C0A0YQ2S53R/p1774572762467409
- marked transistor integration as locked/disabled when limited by host;
overrode related backend settings
- updated sort behavior of integrations (disabled sorts to bottom)

Host plans may limit custom integrations via blocking webhooks. This
currently happens for Zapier and was not extended to Transistor, which
needs different handling given it has both externally set up webhooks +
native Ghost functionality.

We now disable the Ghost-related functionality based on the host
settings which is consistent with the webhook behavior.
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.