Skip to content

[pull] main from TryGhost:main#1120

Merged
pull[bot] merged 8 commits intocode:mainfrom
TryGhost:main
May 6, 2026
Merged

[pull] main from TryGhost:main#1120
pull[bot] merged 8 commits intocode:mainfrom
TryGhost:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 6, 2026

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 : )

sagzy and others added 8 commits May 6, 2026 05:31
closes https://linear.app/ghost/issue/BER-3541

- Existing free members who redeem a gift subscription now receive the
paid welcome email — previously only new gift signups got it.
- Centralised the paid welcome email enqueue inside
`gift-service.redeem()` so new and existing-member redemptions share one
code path.
- Refactored existing welcome email paths to use the new shared
`enqueueWelcomeEmailRun()` method


---------

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

- the allowlist introduced in #26869 protects against executable
  content on the shared CDN domain, but rejects many legitimate
  non-executable formats that worked before (camera RAW files, common
  archive formats, modern image/video containers, 3D/CAD formats,
  etc.)
- the additions are safe because the storage adapter's
  `getStorageContentType()` already falls back to
  `application/octet-stream` for any extension whose MIME type is not
  on the browser-renderable allowlist, forcing a download rather than
  rendering or executing the file
- new extensions cover photography (.np3, .nef, .cr2, .cr3, .arw,
  .raf, .orf, .rw2, .dng, .tif, .tiff, .bmp, .heic, .heif, .avif),
  audio (.flac, .aac, .aif, .aiff, .opus, .mid, .midi),
  video (.webm, .mkv, .avi, .m4v),
  archives (.7z, .rar, .gz, .tgz, .tar, .bz2),
  3D/design (.stl, .obj, .glb, .gltf, .fbx, .blend, .ai, .eps, .xcf),
  fonts (.ttf, .eot),
  data/docs (.toml, .tsv, .geojson, .vcf, .numbers, .odp, .ppt, .fb2)
- sorted the array alphabetically for easier future maintenance
- updated the importer glob test to match the expanded list
ref https://linear.app/ghost/issue/BER-3587/

The onboarding flow was lost during the Ember->React migration of the
Admin shell. This recreates it in the React admin app on a dedicated
route, while keeping Analytics as the return destination until the
checklist is skipped or completed.

Because onboarding has been missing for several months, existing owners
may have stale onboarding preferences. This PR records
`onboarding.startedAt` when `/setup/done` starts the checklist and only
shows `started` checklists whose timestamp is on or after the fixed 30
April 2026 cutoff. Missing or older timestamps are dismissed so
long-time product users are not suddenly forced into onboarding.

## Summary
- adds a dedicated React admin onboarding route at `/setup/onboarding`
- stores checklist state through the user preferences hooks while
keeping the legacy accessibility storage detail hidden
- redirects active onboarding users away from Analytics until they skip
or complete the checklist
- updates Ember `/setup/done` so new owner setup is the only path that
starts onboarding
- records `onboarding.startedAt` and dismisses missing/legacy
`startedAt` values before the fixed 30 April 2026 cutoff so existing
long-time owners are not forced into onboarding
- shares the Shade `ShareModal` base between the onboarding publication
share dialog and `PostShareModal`
- aligns publication and post share options, including X, Threads,
Facebook, LinkedIn, and copy link
- adds Shade Storybook coverage for the shared share modal and fixes
Storybook dark-mode handling for portaled content
- adds onboarding E2E coverage, including pending existing owners and
legacy started owners reaching Analytics normally
closes https://linear.app/ghost/issue/BER-3596

- When a gift subscription's validity period ends and the member drops
back to free, the admin member-activity feed now renders an "ended paid
subscription" entry
- Backend reads from the existing `members_status_events` rows
(`from_status: gift, to_status: free`), so no new write path or schema
change is needed
- Copy and icon match the paid-subscription `expired` event so the
lifecycle reads consistently to publishers
ref https://linear.app/ghost/issue/BER-3587/

Follow-up to #27625 — the apps/admin build was failing with a TS2353
error because Radix's `DialogContentProps` type does not include an
index signature for `data-*` attributes, so passing `data-testid` (or
any `data-*` attr) as a literal in `contentProps` failed strict
excess-property checks.

- widened `ShareModal`'s `contentProps` type to additionally accept
arbitrary `data-*` keys, matching how the attribute is consumed on the
rendered DOM element
- removed the vestigial `data-test-modal` attribute from the onboarding
share dialog; nothing references it (the e2e helper uses the
`data-testid` selector)
#27681)

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

Fixes the member activity feed entry for gift members who continue into
a paid subscription.

When a gift member upgraded to a paid subscription, the subscription
event initially had enough context to show `continued paid subscription
after gift`. However, `getSubscriptionEvents()` deleted the eager-loaded
`paidStatusEvent` relation from the shared `SubscriptionCreatedEvent`
Bookshelf model while serializing the first event row.

Because multiple `MemberPaidSubscriptionEvent` rows can share the same
`SubscriptionCreatedEvent` instance, later rows could lose access to
`paidStatusEvent`. That caused `previous_status` to become `null`, and
the activity feed could revert to the generic `started paid
subscription` wording after the next payment/update event.

This change:

- keeps `paidStatusEvent` available on the shared relation while
deriving `previous_status`
- removes `subscriptionCreatedEvent.paidStatusEvent` only from the
serialized event payload
- adds unit coverage for shared subscription-created-event relations so
the regression is caught
ref https://linear.app/tryghost/issue/ONC-1673

Several callers pass either absolute filesystem paths (built via path.join with getContentPath) or paths that already include the storagePath prefix (built via getTargetDir) when calling exists(), save() or delete(). path.posix.join concatenated those onto storagePath verbatim, producing malformed bucket keys that embedded the local filesystem prefix or duplicated the storagePath segment. The exists() probe then targeted a different key than the eventual write, defeating uniqueness checks; on stricter bucket policies the malformed HEAD threw non-NotFound errors, surfacing as the bookmark favicon fallback users have been reporting. Routed all keys through toCanonicalRelativePath, a chain of named handlers (fromAbsoluteFilesystemPath, fromStoragePathPrefixed, fromLeadingSlashPath) that each handle one input shape and return null when their shape doesn't apply, mirroring how LocalStorageBase absorbs the same shapes via _resolveAndValidateStoragePath. Existing path-traversal protection still fires for `..` segments and the change is forward-only — no existing S3 objects move or rename.
@pull pull Bot locked and limited conversation to collaborators May 6, 2026
@pull pull Bot added the ⤵️ pull label May 6, 2026
@pull pull Bot merged commit 7c3ca72 into code:main May 6, 2026
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