Skip to content

Sync with upstream Ghost v6.23.0#59

Open
madewithlove-machine-user wants to merge 275 commits intomainfrom
chore/sync-v6.23.0
Open

Sync with upstream Ghost v6.23.0#59
madewithlove-machine-user wants to merge 275 commits intomainfrom
chore/sync-v6.23.0

Conversation

@madewithlove-machine-user
Copy link
Copy Markdown

Syncing fork to upstream release v6.23.0.

sagzy and others added 30 commits March 4, 2026 11:16
…ryGhost#26672)

closes https://linear.app/ghost/issue/BER-3406

- previously, we expected the /tiers endpoint to always have
tier-related data to render retention offer popups
- however, archived tiers are not returned as part of the `/tiers`
endpoint
- with this fix, we directly get necessary data from the member
subscription object
ref TryGhost#26416

Fixed `SharedStorage` deprecation warning
Added label picker with inline editing to members filter

ref https://linear.app/tryghost/issue/BER-3342/

The members list label filter now uses a custom label picker built with Popover + Command (cmdk) from Shade. Labels can be created, renamed, and deleted directly from both the filter dropdown and the bulk-action modals (add/remove label), without leaving the members page.
Fixed DST drift in domain warming integration test clock

no ref
The test used setDate(), which diverged from elapsed-24h day math around DST and caused flaky mysql8 CI failures.
ref TryGhost#26655

Addresses review feedback from TryGhost#26655 and fixes additional issues found
during follow-up testing.

**Review comment fixes:**
- Added `aria-selected="false"` to loading option in
`power-select-options-with-scroll.hbs` to fix a11y lint violation
instead of suppressing it
- Fixed `gh-member-single-label-input` to not overwrite a preselected
label with the first available option when the selected label isn't in
the first loaded page — only calls `onChange` for default (no
pre-selection) case
- Changed `options.unshift` to `options.push` in
`gh-members-recipient-select` so the Labels group appears after Tiers,
keeping infinite scroll predictable
- Added `@tracked` to `_meta` in `labels-manager` so
`hasLoaded`/`hasLoadedAll` getters reactively update
- Added `parseInt` to pagination comparison in `loadMoreTask` for
consistency with `hasLoadedAll`

**Additional fixes:**
- Used public `labelsManager.labels` getter instead of private
`labelsManager._labels` in `gh-member-label-input`
- Switched `addSearchedLabels` deduplication to ID-based `Set` lookup
(reference equality can fail with Ember Data models)
- Made `searchLabelsTask` restartable to prevent stacking concurrent
search requests
- Removed redundant `|| []` after `.filter(Boolean)` in `selectedLabels`
- Added explicit `waitFor` calls in E2E label selection helpers to
prevent flaky tests with paginated dropdown loading
ref https://linear.app/ghost/issue/ONC-1525/

- Improved field matching in `rejectPrivateFieldsTransformer` on public content API endpoints
- Extracted the transformer into a shared utility
no issue

- Added `contents: read` permission to `job_setup` so checkout works in private repos (the implicit public repo read access doesn't apply here)
- Replaced hardcoded `TryGhost/Ghost` repo reference with `github.repository` in the PR metadata API call
- Added `github.repository == 'TryGhost/Ghost'` guard to all deploy/publish jobs so they skip cleanly on other repos: canary, publish_packages, deploy_tinybird_staging, deploy_tinybird_production,
deploy_admin, trigger_cd, and create-release-branch
- Consolidated Docker image push strategy: non-main repos use artifact-based image transfer (same path as fork PRs) so E2E tests still work without GHCR access
- Renamed `is-fork-pr`/`is-fork` to `use-artifact` throughout ci.yml and load-docker-image action to accurately reflect the broadened semantics
ref https://linear.app/ghost/issue/NY-1127/
- made urls full instead of relative, which won't work in email
…ost#26702)

closes https://linear.app/ghost/issue/BER-3402/

- Removed `ignorePunctuation: true` from label client-side sort to match server ordering
- `ignorePunctuation` caused `#`-prefixed labels to sort as if the `#` didn't exist, but the server's `name asc` puts `#` before alphabetical characters
- This mismatch meant each new page of labels loaded via infinite scroll appeared in the wrong position instead of appending to the list
TryGhost#26697)

no ref

Node can natively run TypeScript files, but only if they have "erasable
syntax". This means it can't handle things like enums and namespaces.

This enforces that in the `@tryghost/parse-email-address` package.
closes https://linear.app/ghost/issue/NY-1128
closes https://linear.app/ghost/issue/NY-1121
closes https://linear.app/ghost/issue/NY-1120

- The editor didn't have enough contrast in dark mode
- Font sizes and spacings were overridden in the editor toolbar
- Scrolling was attached to the editor area instead of the whole inner
container
- Missed some padding at the bottom
- Safari had clipping issue

Co-authored-by: Evan Hahn <evan@ghost.org>
towards https://linear.app/ghost/issue/NY-1125

Test-only change.

Improves the reliability of these tests somewhat.
…ost#26691)

no ref

_I recommend [reviewing this with whitespace changes
disabled](https://github.com/TryGhost/Ghost/pull/26691/changes?w=1)._

I wrote this package so I should review changes to it.
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"`
9larsons and others added 28 commits March 25, 2026 13:59
ref https://linear.app/ghost/issue/PLA-9/
- adds fake Stripe coupon support for offer-backed checkout sessions
- records checkout discounts and carries them into fake subscription
state
- adds a minimal admin offers helper for top-level e2e setup
- adds a thin offer checkout smoke test to prove the capability end to
end
DuckDuckGo Tracker Radar scores navigator.vendor, navigator.platform,
and navigator.maxTouchPoints as high-entropy fingerprinting APIs. Since
comments-ui loads from cdn.jsdelivr.net (which has a domain-level
fingerprinting score of 3), Safari's Advanced Fingerprinting Protection
— on by default for all users since Safari 26 — restricts API access
and storage for our script, breaking the comment editor.

This Vite build plugin replaces those API accesses in ProseMirror and
tiptap dependencies with navigator.userAgent-only equivalents at build
time. The patched detection produces identical results for all major
browser/OS combinations, with the only difference being iPadOS 13+
(which already sends a desktop Mac UA and works fine with desktop
handling). Includes equivalence tests covering 13 browser fingerprints
and a bundle scan test that verifies the built UMD output contains none
of the blocked APIs.
Changed the condition from `github.repository_owner == 'TryGhost'` to
`github.repository == 'TryGhost/Ghost'` so that forks owned by the
TryGhost org cannot inadvertently trigger the Ghost(Pro) CD pipeline.
ref https://linear.app/ghost/issue/PLA-9/
- migrates the remaining Portal offer redemption coverage into top-level
e2e
- adds the Portal offer page object and redemption flow support
- deletes the legacy browser offers spec
towards https://linear.app/ghost/issue/NY-1163

This change should have no user impact. Just fixes a type error when
this test function was used.
no ref

Before this change, `foo #ff9900` was considered a valid hex color.
After this change, it is not.

I believe it's difficult to have bad data in the database, but if that
happens, we want to check it properly.
…ryGhost#26960)

## Problem

`@site.admin_url` was using `getAdminUrl()` which returns the base admin
domain (e.g. `https://admin.example.com/`) **without** `/ghost/`. This
means the "Site owner login" link on the private page pointed to the
domain root instead of the admin panel for any site with a separate
admin URL configured (including all Ghost(Pro) sites).

## Fix

Changed `update-local-template-options.js` to use `urlFor('admin',
true)` which correctly appends `/ghost/` to the admin URL.

## Test

Added a test that stubs `getAdminUrl()` with a separate domain
(`https://admin.example.com/`) and asserts the resulting `admin_url`
ends with `/ghost/`. This test fails on main and passes with the fix.
no ref
- adds stable Portal offer-page test ids for title, discount label,
message, and updated price
- simplifies the Portal offer page object to expose plain locators
instead of text-specific helper methods
- hides paid-tier Stripe readiness behind createPaidPortalTier(...,
{stripe})
- reduces repeated assertion blocks in portal-offers.test.ts with local
expectation helpers
- teaches the e2e Playwright lint rule to recognize local expect...
assertion helpers
closes https://linear.app/ghost/issue/NY-1178
towards https://linear.app/ghost/issue/NY-1163

This was written almost entirely by Claude Opus 4.6 against the
following prompt:

> I want to create a snapshot test for member welcome emails. We
currently have _some_ tests for this, like in
`ghost/core/test/e2e-api/admin/automated-emails.test.js` and
`ghost/core/test/integration/services/member-welcome-emails.test.js`,
but these don't assert on the final rendered output.
>
> Build a (test-only) change that renders the full HTML of a member
welcome email, and asserts on the snapshot. We have
`assertMatchSnapshot` for this purpose, which I reckon you should use.

I reviewed the code thoroughly and made a couple of small tweaks.
ref https://linear.app/ghost/issue/FEA-480

- adds the `show_share_button` column to newsletters to prep for future
share button functionality
- includes the bare minimum for setting up the migration and for tests
to pass. In this case it includes updating snapshots related to the
newsletter
- also bumps to `6.23.0-rc.0`
no ref

Released new transistor.fm integration, enabling customizable Portal
account settings re: podcast RSS feeds and new embed/editor card support
in the editor.
closes https://linear.app/ghost/issue/NY-1105

This change should have no user impact. All it does is remove the flag,
enabling the feature for everyone.

In addition to automated tests, I manually verified that the new welcome
email editor shows up.
https://linear.app/ghost/issue/BER-3466

- Stripe does not return a discount end date for once offers
- As a result, the next payment object was returning a null discount end date for once offers
- As a result, clients (Portal, themes) had to differentiate between types of offers (once offers expire at the end of the current billing period, repeating offers at discount end, forever offers never)
- We want to abstract away this business logic, so that Portal and themes don’t have to know nor worry about the differences between types of offers. Instead, if the next payment object exposes an end date, then it’s a limited-time offer with an end date (once, repeating). If not, it’s an offer without time limit (forever offer)

Code sample from themes before:
```
 {{#if next_payment.discount}}
    <!-- {{next_payment.discount.duration}} -->
    <s>{{price plan}}/{{plan.interval}}</s>
    <p>
    {{price next_payment}}/{{next_payment.interval}}
    {{#match next_payment.discount.duration "=" "forever"}}
        — Forever
    {{/match}}
    {{#match next_payment.discount.duration "=" "repeating"}}
        — Ends {{date next_payment.discount.end format="D MMM YYYY"}}
    {{/match}}
    {{#match next_payment.discount.duration "=" "once"}}
        — Ends {{date current_period_end format="D MMM YYYY"}}
    {{/match}}
    </p>
{{else}}
    <p>{{price plan}}/{{plan.interval}}</p>
{{/if}}
```

Code sample from themes after:
```
{{#if next_payment.discount}}
      <s>{{price plan}}/{{plan.interval}}</s>
      <p>
          {{price next_payment}}/{{next_payment.interval}}
          {{#if (next_payment.discount.end)}}
              — Ends {{date next_payment.discount.end format="D MMM YYYY"}}
          {{else}}
              — Forever
          {{/if}}
      </p>
  {{else}}
      <p>{{price plan}}/{{plan.interval}}</p>
  {{/if}}
```
ref https://linear.app/ghost/issue/BER-3466

- The next_payment object now exposes a discount end date for limited-time offers (once, repeating) and null for unlimited offers (forever)
- This simplifies the logic for displaying the offer duration in Portal: display "Ends - {date}" if discount end is present, "Forever" otherwise
ref https://linear.app/ghost/issue/BER-3498/dropdown-improvements

- Search field for finding options was missing in the filter dropdown.
This is especially important for great UX when paid-memberships are
turned on.
- The height of the filter dropdown was too small. Making it larger
helps scanning options fast.
)

ref https://linear.app/ghost/issue/PLA-10/
- Migrated the staff invite acceptance test from
`ghost/core/test/e2e-browser/portal/invites.spec.js` to
`e2e/tests/admin/settings/staff-invites.test.ts`
- The old file had two identical tests — the "2FA invite" variant never
enabled 2FA and was a duplicate. Replaced both with a single test.
- Uses Mailpit to extract the invite URL from the email instead of
direct database access via `models.Invite.findOne()`
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
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.