Sync with upstream Ghost v6.59.0 - #22
Open
andreascreten wants to merge 246 commits into
Open
Conversation
…t#29873) no ref - makes the core build consistent with admin/e2e builds
no ref First slice of the settings NiceModal burn-down (follows the router swap TryGhost#29780 and un-portal TryGhost#29789). `ConfirmationModal` and `LimitModal` were NiceModal-shown from **54 call sites across 27 files** — the bulk of settings' remaining NiceModal usage. They now go through a settings-local `ConfirmationProvider`:
ref https://linear.app/ghost/issue/BER-3816 A member import too large to run inside the request is accepted straight away and reported by email once the background job finishes, and when that job failed no email was ever sent. The publisher was told to expect one and nothing ever came, leaving a failed import indistinguishable from one still running. Nobody else found out either, since a queued import runs in-process where a thrown error never reaches the error tracker. The import now runs in three phases and each says what it may do. Gathering what the rows are written against is the only phase allowed to throw, so a throw means nothing was written; writing reports rather than throws, since once a row has committed an import that failed halfway is not one that never ran; and settling up afterwards guards each step on its own, so a failed lookup cannot skip archiving a Stripe price that nothing else will ever clean up. The job then always tells the publisher something, sending a distinct email for an import that could not be completed at all. Whether a row failed for a reason the publisher can act on is not asked. A row that failed is a row that failed, and it goes into the report they receive with the reason beside it. Nothing inspects those errors, which is also why a row's values -- which a database driver inlines into the message of the statement that failed -- cannot reach a log or an error tracker from here. What does reach them is the run itself failing, a cleanup that did not finish, or a notification that could not be sent, none of which carry a row.
no ref - Added an additive Shade `CodeEditor` with lazy CodeMirror loading. - Adopted it only for the header and footer fields on the flagged React tag-details screen. - Added Storybook states for default, focused, error, and disabled behavior. - Portalled Shade autocomplete into a transparent body-level host so suggestions escape clipped editor and accordion containers. - Connected hint and error copy to the editable element and reset shared focus state when a focused editor unmounts. - Added real-browser coverage for editing, saving, autocomplete visibility, positioning stability, and portal paint safety.
ref BER-3863 An investigation into whether a member could reach custom fields through the members API found that they cannot, and never could. Every member-facing surface serialises from a field whitelist that predates the feature, and the member update path drops unknown keys twice before it reaches any custom-field code. Nothing asserted that, so these two tests pin it: a member's own response carries no custom fields, and custom fields a member sends are dropped while the rest of the update still applies, the same way email already behaves on that endpoint.
ref https://linear.app/ghost/issue/BER-3862 A key is typed by hand into member filters, CSV columns, email replacement strings and, before long, themes and editor cards. The hyphen a slug separates with is the one character those readers disagree about: NQL will not parse a hyphen in a property path, and a replacement string matches word characters only, so a hyphenated field reaches the reader as literal text with nothing to say it was never substituted. The convention contradicted itself as well, since the parts of a composite field were already underscored and a name typed with underscores kept them, leaving a single export column carrying both. Underneath that the format had no definition of its own. It was whatever slugify produced, so a rule a growing number of surfaces depend on lived in a helper that answers to URLs and could widen on a version bump. The characters a key may contain are now stated where its readers are known, as an allowlist rather than a list of things to strip out, and the libraries keep only the transliteration and invisible-character stripping they own. Trimming the ends falls out of that rule and makes __proto__ unmintable under every spelling, so it no longer needs reserving alongside constructor. A key is minted once and never changes, so a definition created earlier keeps its old key for good, and rewriting one in place would leave its stored values and every reference to it pointing somewhere else. Those definitions are discarded with their values instead: custom fields sit behind a private flag and have never been released, so only a site that deliberately opted in can hold one, and re-creating the field re-mints the key. The migration measures a key against the shape this release mints rather than looking for a hyphen, because the previous minting passed underscores through untouched and the definitions endpoint shipped four days before the reserved-key guard did.
no ref - Clear populated CodeMirror fields through their own `ControlOrMeta+A` and Backspace key handling. - Wait for the empty document before filling each replacement, then verify the rendered value before saving. - Reuse the expected replacement values in the exact request-body assertions.
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@tanstack/react-query](https://tanstack.com/query) ([source](https://redirect.github.com/TanStack/query/tree/HEAD/packages/react-query)) | [`5.101.2` → `5.101.4`](https://renovatebot.com/diffs/npm/@tanstack%2freact-query/5.101.2/5.101.4) |  |  | --- ### Release Notes <details> <summary>TanStack/query (@&TryGhost#8203;tanstack/react-query)</summary> ### [`v5.101.4`](https://redirect.github.com/TanStack/query/blob/HEAD/packages/react-query/CHANGELOG.md#51014) [Compare Source](https://redirect.github.com/TanStack/query/compare/@tanstack/react-query@5.101.3...@tanstack/react-query@5.101.4) ##### Patch Changes - Updated dependencies \[]: - [@&TryGhost#8203;tanstack/query-core](https://redirect.github.com/tanstack/query-core)@​5.101.4 ### [`v5.101.3`](https://redirect.github.com/TanStack/query/blob/HEAD/packages/react-query/CHANGELOG.md#51013) [Compare Source](https://redirect.github.com/TanStack/query/compare/@tanstack/react-query@5.101.2...@tanstack/react-query@5.101.3) ##### Patch Changes - Updated dependencies \[[`7e3c822`](https://redirect.github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]: - [@&TryGhost#8203;tanstack/query-core](https://redirect.github.com/tanstack/query-core)@​5.101.3 </details> --- ### Configuration 📅 **Schedule**: (in timezone Etc/UTC) - Branch creation - Only on Sunday and Saturday (`* * * * 0,6`) - Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`) - Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * * 1-6`) - Automerge - Only on Sunday and Saturday (`* * * * 0,6`) - Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`) - Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * * 1-6`) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMyIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119--> Co-authored-by: tryghost-renovate[bot] <269725441+tryghost-renovate[bot]@users.noreply.github.com>
ref https://linear.app/ghost/issue/BER-3861 A value is addressed by key everywhere it matters: the write names the field by key, a segment filter names it by key, and the key is immutable. Carrying the key directly on the values row lets the read and filter paths drop the id-to-key join, so the foreign key now references the field's key column rather than its id.
Two migrations read custom_field_id in their up: the 6.57 leaf-rows migration and the 6.58 underscore-keys reset. The value re-key drops that column, and the idempotency check re-runs every current-major up against the final schema, so each guards its custom_field_id work on the column still being present. A forward install always sees the column, so this changes nothing an install actually applies.
…e holes (TryGhost#29817) ref https://linear.app/ghost/project/moving-routesyaml-off-of-disk-9887873946a1/ - removed comments describing code we already deleted: they point at an eager URL service and a macrotask queue that no longer exist, and one names the wrong place for where `{field}` placeholders get rewritten - removed `unmountRoute` - it had no callers and didn't work anyway, matching `item.path` when a mounted route's path lives at `layer.route.path` - removed the `resourceAlias` fallback and the `filter` key on static pages, neither of which is set anywhere, so both were already fixed values - dropped the query defaults in `fetch-data`, since both callers now pass a complete query and nothing was being defaulted - built the default post query from the API adapter instead of repeating `postsPublic` by hand, so renaming it in `config.ts` can't quietly point the frontend at the Admin endpoint - fixed `Omit<Route, 'path'>` dropping `filter`, `order`, `limit`, `rss` and `contentType` - `Omit` over a union keeps only the shared keys, so the parser could leave one out and still compile - added a `satisfies` check so a resource added without a `QUERY` entry fails the build instead of erroring on every request through that route - fixed three fixtures stubbing `api.posts` where production passes `postsPublic`, and ran the FileStore against the store contract it was the only adapter missing - no behaviour change: every path removed was already unreachable
Hardens the internal-package migration workflow based on its first cold-start use. Requires isolated, freshly based worktrees and fail-fast checks around subtree splitting and importing. Verifies the source checkout is clean, the destination is a linked worktree, and the resulting merge topology preserves and reaches the recorded source history. Adds guidance for assessing public consumers, mapping workspace dependencies under Ghost's strict catalog policy, and minimally integrating legacy CommonJS packages without mixing migration with modernization. Expands verification to cover package resolution, Nx tests, consumer tests, the full build, and release-archive contents.
Removes the permanently enabled `llmsTxt` labs flag now that llms.txt support is generally available. LLMs.txt is still off by default, and can be enabled in settings.
Removes the permanently enabled `explore` Labs flag from Explore Ping now that the feature is generally available. Explore is still configurable in settings.
Updates the settings API response header snapshot after the merged Labs flag removals. Minimal change required to get main green.
…ryGhost#29884) no issue - the config API snapshot embedded the exact labs flag list, and the settings API Stripe test pinned an exact content-length that varies with the size of the labs setting — both churned every time a feature flag was added or removed, causing avoidable snapshot conflicts between PRs - labs is now matched as any object, with an explicit assertion keeping the meaningful coverage: the flags must be a non-empty plain object of booleans; the settings header match now uses anyContentLength like the other settings tests
no issue - the flag has been permanently enabled via GA_FEATURES for a while, so the conditional it gated was dead in practice - the Featurebase widget remains gated by the `featurebase` config, which was already the real on/off switch everywhere else (token endpoint, config serializer)
Removes the permanently enabled `dangerZoneResetAuth` labs flag now that resetting all authentication is generally available. The Danger Zone action is always shown, and the tests no longer need to opt in to the flag.
ref https://linear.app/ghost/issue/BER-3850 Definitions came back in whatever order they happened to be created in, and every surface reads that one list: the settings list, a member's details, the import mapping's targets, the export's columns. A sort_order column now carries the order, added without a backfill because the read tie-breaks on created_at, so a site that never reorders keeps the order it has. The order is set by PUTting the whole collection in the order it should have rather than by editing one field, because where a field sits is a fact about the list and not about the field, so no rank is exposed on a definition anywhere in the API or the domain. That list has to name every definition exactly once, and the rows are locked in key order so two publishers dragging at the same moment cannot deadlock.
ref https://linear.app/ghost/issue/BER-3850 The read that assembles a member's values sorted its rows by field, on the hottest query in the domain: every member of a page, joined against their values. It could never deliver that order to anyone, because the rows become an object keyed by field and an object carries no order, since JSON gives member order no meaning and JavaScript enumerates any key that looks like an array index first. Path ordering stays, so the parts of one composite field still assemble the same way every time.
ref https://linear.app/ghost/issue/BER-3850 The API could take an order but nothing could give it one. The active tab in Settings is now a sortable list, following the newsletters list it borrows its component from. A drag is applied to the whole list rather than the tab it was made in, because a reorder names every definition and the archived ones keep their places around the move. The order just drawn is held on the screen that drew it and rendered from immediately, so letting go is the end of the interaction rather than the start of a round-trip; the response then settles the cached lists directly, since a reorder only succeeds when it named exactly the fields the site has and a refetch would learn nothing. Each cached list is put into the new order rather than replaced, because several live under one key and they do not hold the same fields.
…#29845) no ref *I recommend [reviewing this with whitespace changes disabled](https://github.com/TryGhost/Ghost/pull/29845/changes?w=1).* What ---- Before this change, there was a class called `EmailAnalyticsProviderMailgun`, which basically just exposed a single function that fetched Mailgun events. After this change, there's a function called `fetchMailgunEvents` that does this. Why --- There was no reason for this to be a stateful class. The function is simpler. Test plan --------- - Automated tests - Made sure newsletter opened events still worked with Mailgun. Sent a newsletter, opened it in Gmail, and verified that it appeared in the UI
no issue - removed the `commentsThreads` and `commentsPinning` labs flags now that both features are GA, along with all flag conditionals - deleted the unreachable non-threaded comments UI: the flat reply layout, "Replied to:" snippets, and top-level reply collapsing - made pinned-first ordering and pin moderation unconditional, removing the comments service's labs dependency entirely - bumped comments-ui to 1.6 so already-deployed bundles that still read the flags stay paired with backends that serve them
…ost#29846) ref TryGhost#29845 This change should have no user impact. --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
towards https://linear.app/ghost/issue/NY-1473 This change should have no user impact. I think this is a useful change on its own, but it'll make [an upcoming change][0] easier. [0]: https://linear.app/ghost/issue/NY-1473 --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
towards https://linear.app/ghost/issue/NY-1473 This change should have no user impact.
values-service hardcoded the members_custom_fields.key length as 191 while its sibling definitions-service already reads it from the canonical schema. This reads it from the same source so the two services cannot disagree, and neither can drift from the column if the width ever changes.
The static schema restates the field-type and status lists because it cannot import them, guarded only by a keep-in-sync comment. This asserts the schema's isIn lists equal FIELD_TYPE_IDS and FIELD_STATUS, so a divergence fails the build rather than shipping a schema that accepts or rejects the wrong values.
…schema The presentation catalog listed an address's sub-field labels as a loose Record of string to string, so a mistyped or omitted part compiled and silently lost its label. Typing it against the shared Address value schema's keys makes a part added, removed, or renamed upstream a compile error here instead of a missing label at runtime.
no ref Fourth chunk out of `settings/app/`: `email/*` and the shared `email-design/*` now live at `settings/email` and `settings/email-design`, beside the email acceptance tests. Moving them enables the type-aware lint rules the rest of admin uses, so that debt is cleared for these files: - async handlers passed to void callbacks are wrapped; fire-and-forget promises get `void` - `searchKeywords` → `email/search-keywords.ts` (email-settings) and `email/emails-search-keywords.ts` (emails) - `EmailDesignContext` split the way admin's other contexts are: context + `useEmailDesign` in `email-design-context.ts`, `EmailDesignProvider` in `email-design-provider.tsx` - two `act(async …)` callbacks with no awaits made sync in the customize-modal unit test
…yGhost#30112) ref TryGhost/renovate-config#31 Removes the local `Require human review for CSS preprocessor updates` rule from `.github/renovate.json5`. It existed only to mirror the shared preset's CSS/style toolchain exclusion so the pnpm-catalog automerge rule wouldn't override it; the shared exclusion is being removed. ## Why Minor/patch updates to PostCSS, Sass, Stylelint, Tailwind, etc. don't cause visual regressions in practice, and the exclusion was backing up PRs. Majors for these packages still require dashboard approval via the existing `Require dashboard approval for major updates` rule.
no ref Fifth area out of `settings/app/`: site components now live beside their acceptance tests in `settings/site`. Moving them enables the type-aware lint rules the rest of admin uses, so that debt is cleared for these files: - async handlers passed to void callbacks are wrapped; fire-and-forget promises get `void`; async functions that never await become sync - `searchKeywords` → `site/search-keywords.ts`; `FatalErrors` + `getIssuesFromFatalErrors`/`getIssuesFromInstalledTheme` → `theme/theme-validation-issues.ts`, so component files only export components - the three identical icon/logo/cover upload handlers in `global-settings.tsx` collapse into one `uploadSettingImage` - `getThemeLabel` builds the default/legacy label as a string instead of `+=` onto a `ReactNode` - `design-modal.tsx`'s `useForm` gets an explicit type parameter instead of the `as Dirtyable<…>` cast (the lint autofix strips that cast and breaks `dirty` typing — same trap as TryGhost#30082's portal modal)
…ost#30107) ref https://linear.app/ghost/issue/BER-3851 Gift delivery needs the same UTC timestamp conversion as automations and seeders. Keeping separate implementations risks differences in timezone, precision and validation behaviour when handling raw Knex values. A single Luxon-backed conversion path beside the DbDate codec keeps MySQL and SQLite behaviour consistent and rejects invalid inputs explicitly.
ref https://linear.app/ghost/issue/BER-3851 Transactional messages carrying private redemption links must be able to override publication tracking settings. Mailgun transports drop boolean false values, so the adapters use Mailgun's explicit string form and keep the transport-specific detail behind their existing interfaces.
…0113) no ref The settings shell and the shared `components/`, `hooks/`, `utils/`, `data/`, `assets/` and `providers/` now live directly under `settings/`, leaving only `advanced/` in `settings/app/` until the PRs touching it land (TryGhost#30018, TryGhost#30019, TryGhost#29916, TryGhost#30054, TryGhost#30099, TryGhost#30110, TryGhost#28368). Big diff, but ~300 of the 325 files are import-line rewrites; the substantive changes are the lint-debt fixes the move surfaces: - shell files grouped in `settings/layout/`: `app.tsx` (provider tree), `main-content.tsx`, `sidebar.tsx`, `settings-sections.tsx` (was `components/settings.tsx`); root `settings.tsx` route entry unchanged - contexts split the way admin's other contexts are — hooks + context in `*-context.ts`, provider component in its own file — for global data, settings app, confirmations, scroll sections; `withErrorBoundary` → `with-error-boundary.tsx` - `loadKoenig` → `components/koenig-loader.ts`; the untyped `@tryghost/koenig-lexical` bundle gets a minimal `KoenigLexicalModule` type instead of `any` - `@tryghost/limit-service` and `@tryghost/nql` get typed ambient declarations in `vite-env.d.ts` (clears ~25 `no-unsafe-*` in `use-limiter`) - `iframe-buffering` reuses `utils/debounce`; `TopLevelGroup.saveState` uses the framework's `SaveState`; the usual `void`-wrapping of async handlers
Fixes TryGhost#30102 - Maps the exact source path `css/header_v2.css` to the documented public `header` card when building the card asset manifest. - Keeps the versioned source files separate while combining both stylesheets under one logical manifest key. - Preserves the existing source order and separator so the default all-card bundle remains byte-for-byte identical. - Makes the manifest builder importable without executing it and adds regression coverage for mapping, inclusion, exclusion, and asset-type isolation.
no ref Last area out of `settings/app/`: advanced components now live beside their acceptance tests in `settings/advanced`. With it gone `settings/app/` is empty and **the ESLint quarantine override is deleted** — every settings file now lints under the same type-aware rules as the rest of admin.
ref https://linear.app/ghost/issue/BER-3851 Delayed payment methods can complete a Checkout Session before Stripe marks its payment as paid. Routing completed and asynchronous payment events through one guarded path prevents legacy gift purchases from being finalized early while leaving donation behavior unchanged.
) no ref Tail of the settings integration: - `admin-x-settings-{content,sidebar,sidebar-scroller,scroller}` element ids → `settings-*`; the `admin-x-base admin-x-settings` wrapper class → `settings-app` (and the heading line-height rule in `index.css` follows). Nothing outside `apps/admin/src/settings` referenced any of them (checked Ember, e2e, shade, test-utils); no id collisions with Ember's DOM. - `SettingsAppProvider` typed every context field as an optional prop but overwrote `sortingState`/`setSortingState`/`offersShowArchived`/`setOffersShowArchived` after spreading them (the CodeRabbit note on TryGhost#30113 — pre-existing). It now takes only `upgradeStatus`, the one prop its caller passes.
The list read a column's value by switching on the column key in a function far from where that column was declared, so a field naming a column and the code filling it had to be kept in step by hand. A column now carries its reader, attached where the column is built, and a cell asks the column for a member's value instead of working back from its key. What the list must ask the API to include is declared on the field alongside the column rather than derived from the columns that resolve, because whether a value is needed follows from the filter alone while naming a column can wait on data still in flight, and a column can be dropped by the display budget while its values are still wanted. Custom fields are addressed under one custom_fields namespace named in a single place, so a field reads the same as a filter key and as a column key. No behaviour change.
A filter told a publisher who matched but not what they said, so reading the data they had just filtered on meant opening members one at a time. Labels, tiers and the subscription filters already append a column for that reason; custom fields are the case where it matters most, since a field exists precisely because its value differs member to member. Every custom field filtered on now brings its own column, whatever the operator, the way Label does. The column layer already lets a field declare a column and how to read it. What is new is that a field whose key is a pattern stands for many columns rather than one, so `custom_fields.:key` resolves its column per instance from the matched key, hydrated from the fields the API loaded -- the same hydration the filter list already does for newsletters, tiers and offers. A name it cannot resolve is a column it does not show, which is also what the flag being off looks like from there. Asking the API for the values does not wait on those names, so the list does not fetch them twice, and the fetch that names them waits for a filter rather than riding every visit to the list. Rendering a value as one line was the member detail screen's, and is now the framework's user-type catalog, next to the part labels and CSV column names it already owns. A scalar reads as it stands and a composite joins its parts the way that type reads; the detail screen renders through the same function, so the two cannot drift, and the catalog is total over the field types so a new composite cannot arrive without a line to read as. Values a publisher collected have no length a column can be sized to, so a dynamic cell carries its full text as a native title. That is a fallback for the edge, not a tooltip component per row across a virtualised list.
closes https://linear.app/ghost/issue/NY-1525/check-how-the-caching-works-for-the-automations-endpoint-and-make-sure Automation run counts change independently of Admin mutations. Refetching the browse query whenever the list mounts prevents the five-minute client freshness window from leaving summary stats behind the detail view while retaining cached data for rendering.
TryGhost#30119) no ref Closing a dirty settings dialog through Escape, its buttons, the exit button or a sidebar link already confirms before discarding changes; the browser **back/forward buttons did not** — the router swap lost the route-transition guard Ember settings had, and tags/members already guard this with `useBlocker`. - `layout/dirty-navigation-guard.tsx`: `useBlocker` on the global dirty state, wired to the shared `DirtyConfirmDialog` (Leave → `proceed`, Stay → `reset`). Only `POP` navigations are blocked — every in-app exit already runs its own dirty confirmation, so blocking PUSH/REPLACE would double-prompt. Also registers `useConfirmUnload` while dirty, like the other guarded screens. Mounted beside `<Outlet />` in `layout/app.tsx` so contributors' profile dialog is covered too. - Blocks only when the entry being left was **router-created** (`history.state.key`). React Router can only undo a POP from an entry it created; from a native hash-navigation entry it miscounts the delta and can call `history.go(0)` (reload) — found during review, so the guard deliberately stays out of that case. - Compares matched dialog routes, not pathnames: sibling routes rendered by one dialog instance declare `handle: {dialogGroup}` in `settings/routes.tsx` (currently the staff-profile tabs) so back between tabs doesn't prompt; every other route change out of a dialog does. Offers and design/theme containers swap child components per route, so they are intentionally *not* grouped.
ref https://linear.app/ghost/project/4b2edbd66469/ Each commit references the issue individually This PR covers various issues, all linked in the individual commits. It adds basic processing of post content CSV files, limits imports to 100 posts (while in development, this will be removed later), and lays the foundation for future milestones. - **CSV import engine** — Uploading a title/html/published_at CSV to `POST /posts/upload/` now creates real posts instead of a no-op 202. Rows are parsed in-request, then written by an in-process background job via `models.Post.add` under `{importing: true}`; html is converted to lexical, slugs are set as a slugified title, and a file that can't be parsed as CSV at all is rejected with a 422. - **Temporary 100-post cap** — Files with more than 100 rows are rejected with a clear 422 before anything is written. A hard-coded constant with a single check, deliberately with no config surface — the whole limit goes away when the durable job system lands. - **Dates come from the CSV** — The one date column sets `published_at`, `created_at` and `updated_at`, so imported archives look written when they were written, not created at import time. - **In-memory outcome tracking** — Each import registers a run in an in-memory store recording one outcome per row (status, source line number, title, post id/URL); the 202 body returns the run's `import_id` and row total. This is what the completion-report milestone will render from; nothing is persisted until the durable job system. - **Bad rows don't kill the run** — A malformed row (missing/overlong title, invalid date, unconvertible html) is skipped on its own with an actionable reason; the rest of the file still imports. Skipped (fix the file) is distinguished from failed (write attempted and lost). - **Defaults for absent fields** — Imported posts land published, public, type `post`, authored by the site owner, with two internal batch tags: a date stamp (matching the JSON importer's format) and a unique `#Import Run <id>` tag the report milestones key on. - **Zero side-effects, pinned** — A regression suite proves a bulk import sends no newsletter emails and fires no per-post webhooks, with a positive control so the assertion can't pass vacuously.
…Ghost#29967) ref https://linear.app/ghost/issue/BER-3851 - Added labs-gated recipient details, buyer names and personal messages to gift checkout. - Pre-created payment-pending gifts and optional delivery records before Stripe Checkout, keeping recipient data inside Ghost. - Sent only the internal gift ID to Stripe and preserved the locally calculated amount and currency, because Stripe totals may include tax. - Completed immediate and delayed payments idempotently while retaining support for legacy link-gift checkout sessions. - Delivered recipient emails through atomically claimed jobs with Mailgun tracking disabled, recording success only after provider acceptance. - Recovered pending and interrupted deliveries during boot and scheduled cleanup, and cancelled unsent delivery when a gift became unusable. - Deleted abandoned payment-pending gifts and recipient data after 30 days. - Added the persistence schema, service boundaries, email templates and translations required for the new delivery flow.
no ref Ghost shuts down slowly on some sites and the cleanup tasks were anonymous closures, so there was no way to tell which task (or the HTTP server stop itself) was hanging. This labels each cleanup task and logs how long it and _stopServer take, so a single SIGTERM on the affected environment names the culprit instead of requiring a bisect.
…outer (TryGhost#30122) no ref Follow-up from the review of TryGhost#30119. The admin sidebar rendered every destination as a plain `<a href="#/…">`. Those navigations bypass the React router, so the history entries they create carry no router state and React Router refuses to block a browser back onto them (`delta == null`) — the `useBlocker` guards on tags, members and settings never fire when the screen was entered from the sidebar, and from some entry shapes the router miscounts the delta and calls `history.go(0)` (reload) instead of stepping back. - `NavMenuLink` renders a router `Link` for React-owned routes and keeps a native anchor for Ember-owned ones. Ember's `HashLocation` only follows `hashchange`, which the router's `pushState` doesn't fire, so Ember targets must stay anchors. - Ownership comes from the route table at runtime (`isEmberOwnedRoute(path)` in `routes.tsx`: leaf route served by `EmberFallback` / `EmberListWithGiftLinks` / `TagDetailGate`), so flag-gated routes follow the same rule without a hand-maintained list. Query strings (`posts?type=…`, member views) are preserved either way. - External (`target="_blank"`) links unchanged.
…st#30124) no issue The custom fields import dialog should ask before closing when the mapping has been changed. It could miss that check if Escape was pressed immediately after an edit, and close without showing "Leave without importing?". `hasEdits` was React state read from the `handleOpenChange` render closure. Radix keeps `onOpenChange` in a callback ref that it updates in a passive effect, while its Escape listener runs on `document` outside React event dispatch. If Escape arrived before React committed and flushed the edit, Radix could call the old closure and see `hasEdits` as false. Nothing renders from this flag, so it uses a ref now. The edit updates the ref immediately, and `handleOpenChange` reads the current value.
no refs ## History and intent Portal introduced `contrast-color.js` in July 2020 to calculate complementary text and icon colours for accent-coloured controls: TryGhost@fb6a2c9 Portal deliberately removed that automatic colour inversion in October 2020: TryGhost@b037d98 The second commit provides direct evidence that making this utility unreachable was intentional rather than accidental: its message is `Removed auto-invert color from primary buttons`, and its atomic diff removes every import of the utility while replacing each computed foreground colour with `#fff` across trigger icons, trigger text, and primary action buttons. The historical record does **not** explain the deeper UX rationale for preferring white. The commit body contains only `no refs`, GitHub has no associated pull request or commit discussion, and adjacent styling work provides no written rationale. Therefore, the supported conclusion is limited to this: automatic inversion was intentionally retired in favour of a fixed-white treatment. Design consistency is suggested by the diff, but is an inference rather than documented intent. ## Why deletion is safe The utility remained in the tree after its final consumers were deliberately removed and has been unreachable for nearly six years. A repository-wide usage search finds no remaining consumer, and Knip independently reports the file as unused. Portal has a single `src/index.jsx` build entry, and the published package includes only built UMD output rather than internal source paths, so the utility cannot be reached through a secondary entry point or public export. Deleting the orphan preserves the intentional fixed-white behavior established by the October 2020 change. ## Validation - `pnpm --dir apps/portal lint` - `pnpm --dir apps/portal test` (592 passed, 1 skipped) - `pnpm --dir apps/portal build` - `pnpm check` (lint and all other project tests passed; the aggregate run reported `ghost:test` as flaky) - `pnpm nx test ghost --skip-nx-cache` (604 files and 7,852 tests passed on retry; Nx classified the prior failure as flaky)
…t#30127) - Cache the extracted `.ttf` files with `actions/cache` (key `msttcorefonts-ttf-v1`; the fonts never change). - Cache hit → copy the fonts into place, no apt, no network. - Cache miss → install from apt with the existing bounded retry/timeout, then stash the `.ttf`s for the cache to save. - `fc-cache` after placing the fonts; then install the firefox media codecs the video-card tests need. Only a cache miss (first run, or after ~7-day cache eviction) touches SourceForge, and that path still fails fast instead of hanging. Ghost's CI runs frequently enough to keep the cache warm.
…ryGhost#30129) no ref TryGhost#28368 added icons and visibility controls to navigation behind the `navigationIcons` lab flag, but one part of it shipped ungated: the built-in navigation partial started wrapping every label in `<span class="nav-label">`. That affects every theme that uses `{{navigation}}` without shipping its own `partials/navigation.hbs`, whether or not the flag is on. CSS and JS that target the anchor's text directly — `.nav a` flex/gap layouts, `::before` tricks, truncation on the anchor, `a.textContent` reads — see different markup than before. The helper now passes the flag to the template as `@navigationIcons`, and the template renders the icon plus the label wrapper only when it is set. With the flag off, output is byte-identical to the pre-TryGhost#28368 markup. The flag-off tests now assert the bare label, plus a new test that asserts the full rendered string matches the old markup exactly.
fixes TryGhost#30100 The 6.58 rename of `email_batches.provider_id` asked for `ALGORITHM=INSTANT`, which MySQL only accepts for RENAME COLUMN from 8.0.28 onwards. Anyone on an older 8.0 server — still supported per our docs — hit ER_ALTER_OPERATION_NOT_SUPPORTED and could not upgrade past 6.58. That migration now asks for `auto`, which is what every other algorithm-bearing migration uses and costs nothing here: a plain RENAME COLUMN is metadata-only on INPLACE too, and 8.0.28+ servers still pick INSTANT themselves. Editing a shipped migration is safe because knex-migrator tracks migrations by name with no checksum, so sites that already ran it skip it and stuck sites run the fixed version. To stop this recurring, the schema commands now retry an ALTER without the algorithm clause when the server rejects it. INSTANT support is per-operation (ADD COLUMN 8.0.12, RENAME COLUMN 8.0.28, DROP COLUMN 8.0.29), so a version matrix in the migration utils would be a maintenance trap; reacting to the server's own error is version- and vendor-agnostic and costs nothing on the happy path. Verified against real MySQL 8.0.21 and 8.4 containers.
closes https://linear.app/ghost/issue/NY-1526 Added Last entry, Total entries, and In progress columns using stats from the automation browse API. Kept the mobile list focused on name and status, and gated the new stats behind automationRunAnalytics while run analytics remains experimental. <img width="1560" height="1043" alt="image" src="https://github.com/user-attachments/assets/c60cfc64-480e-4e6f-a0c9-490391d1099e" />
…0131) no ref Follow-up to TryGhost#30129. The navigation settings validator accepts an item with an icon and no label regardless of the `navigationIcons` flag. So enabling the flag, adding an icon-only item, then disabling the flag leaves the front end rendering `<li class="nav-"><a href="/foo"></a></li>` — an empty, unlabelled link, since the icon is dropped when the flag is off. Before TryGhost#28368 that data threw an `IncorrectUsageError`, which is the worse option: it 500s the whole front end for data Admin allowed to be saved. So items with nothing to render are now dropped instead, and a nav where every item drops out renders nothing, matching the existing empty-nav behaviour. Note this also drops an item with a null label and no icon, which previously rendered the same empty link. That shape is only reachable by writing to the database directly — Admin has always required a label (now label-or-icon). Tests: an icon-only item is dropped while a sibling label item still renders, and an all-icon-only nav renders an empty string. Flag-on icon-only rendering is unchanged and still covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncing fork to upstream release
v6.59.0.