Skip to content

[pull] main from TryGhost:main - #1425

Merged
pull[bot] merged 22 commits into
code:mainfrom
TryGhost:main
Aug 18, 2026
Merged

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

Conversation

@pull

@pull pull Bot commented Aug 18, 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 : )

ErisDS and others added 22 commits August 18, 2026 11:26
Add a guide for testing Ghost on physical devices and with non-default
development URLs: HTTPS, subdirectory installs, and a separate Admin hostname.
…0024)

ref https://linear.app/ghost/issue/MIG-1437/

- adds a testid attribute to the existing file upload dropbox
description
- adds test to ensure existing behaviour is not changed when the
`csvContentImporter` flag is enabled
- adds tests for when the `csvContentImporter` is enabled
- adds a conditional for the file drop placeholder text
Ghost Admin composes a filter from its own field catalogue and sends NQL,
and nothing on either side can tell whether the server honours what was
written: admin cannot see the SQL, and the query layer cannot see what admin
meant. The only place the two are observable together is the browse
endpoint, so these run the NQL admin emits against a real database and
assert exactly which members come back.

Derived fields are the reason this matters more than it looks. A column
filter is its own evidence, but `subscribed` is rewritten to a newsletter
relation before it reaches the model, `email_disabled` combines with it to
mean something neither column says alone, and a label is a join. Those
shapes are agreed between two codebases and pinned nowhere.

This lands ahead of the filter engine refactor so the safety net exists
before anything moves.
…reen (#29962)

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

Automation workflows could be panned completely out of view, while graph or zoom changes could leave the viewport outside its updated bounds.

Centralized viewport measurement, zoom configuration and constraint handling so workflows remain recoverable across canvas, graph and zoom changes. Updated the controls to match the supported 50–100% zoom range and disable actions at their limits.
…30061)

closes #30040

The file upload allowlist introduced with the file type validation was
derived from observed usage and did not include Linux package formats,
so sites distributing software could no longer attach `.rpm` or `.deb`
files while sibling archive formats like `.tar.gz` continued to work.
no ref

- `free`: kostenlos → kostenloses
- `complimentary`: kostenlos → kostenloses (kept identical to `free`,
mirroring `de` which uses one word for both)
- `Subscription details`: Abo Details → Abo-Details (German orthography
requires hyphenated compounds)
- `This email address will not be used.`: E-Mail Adresse →
E-Mail-Adresse (same hyphenation rule)
no ref

The code injection, webhook, labs YAML editor, feature-toggle
confirmation and universal import dialogs were the last
`NiceModal.create` dialogs under `settings/advanced`. They now take an
`onClose` prop and are rendered by the component that opens them, using
`SettingsModal`'s controlled `onClose` path that the routed settings
dialogs already use. This removes five of the remaining NiceModal call
sites in settings so the provider and dependency can be torn down once
the theme and membership dialogs follow.
no ref

The invalid-theme, theme-installed, theme-editor confirm and
theme-editor input dialogs were the remaining `NiceModal.create` dialogs
in the theme settings area. They now render in-tree from the component
that opens them: the two `ConfirmationModalContent` dialogs take an
`onClose` prop, and the two promise-style editor dialogs take an
`onResolve` prop, with the code editor keeping its await-based
`requestConfirmation`/`requestInput` helpers via a small pending-request
state instead of `NiceModal.show`.

The theme code editor's dialogs are rendered as fragment siblings of its
dark, backdrop-blurred root so they keep the same theme and stacking
they had when NiceModal rendered them at the provider.
ref https://linear.app/ghost/issue/HKG-1914

Ghost's background jobs are moving onto a class-based interface whose transport
is swappable, and this package defines the seam every backend implements. A
backend extends JobsBackendBase - a frozen start/enqueue/scheduleRecurring/
shutdown contract - and only ever sees the serialised {type, payload} envelope
and a single delivery-processor callback, never a live job instance. Keeping the
backend blind to job instances is what makes jobs serialisable end to end and
lets a durable backend drop in for the in-memory one without touching a call
site. The package also exports a backend-agnostic contract test suite, so the
in-memory reference backend and any future durable backend are held to the same
acceptance, delivery, drain and bounded-shutdown behaviour. It is a private
workspace package, matching the other adapter-base-* packages.
ref https://linear.app/ghost/issue/HKG-1914

This is the first working layer on top of the backend contract, and the point at
which jobs can be registered and dispatched at all. The class-based jobs service
owns everything the transport does not: the handler registry, the JSON envelope
boundary, rehydration, cron validation and error reporting. InMemoryJobsBackend
is the reference implementation - delivery runs through a bounded fastq queue
whose concurrency comes from adapter config, shutdown drains in-flight work
within a bounded time, and a recurring schedule is first-write-wins so a
re-registration never disturbs one that is already running. A handler failure is
reported and rethrown so the backend sees a rejected delivery as a failed one;
the in-memory backend defensively logs and drops it, matching the legacy
in-process queue with no redelivery yet. Because the backend is selected through
adapter config, a durable backend is a drop-in replacement. Boot constructs the
service, runs an initially-empty central registration step so there is a single
place to wire handlers, then starts delivery.
To quote [ESLint's docs][0]:

> If a variable is never reassigned, using the `const` declaration is
better.

This enables the `prefer-const` ESLint rule and fixes all violations.

[0]: https://eslint.org/docs/latest/rules/prefer-const
no ref

We had a bunch of code like this in a test:

```js
assertExists(result);
assert.equal(result, 123);
```

That `assertExists` can be removed.

(This also fixes the fact that `assertExists` was imported strangely.)
closes https://linear.app/ghost/issue/NY-1529
ref ce08414

This adds `stats.last_run_created_at` to each automation, which we'll
display in the UI soon.

This was built by Claude Opus 5 with the following prompt:

> Commit `ce0841435c44a24bd444277cc587e3a18adc052d` adds a "last run
created at" stat to the automation browse endpoint.
>
> I want a new key, `total_run_count`, which is a count of all the runs
for that automation.
>
> * Update `AutomationBrowseResult`.
> * Update `AutomationBrowseRow`.
> * Update `loadAutomations`.
> * Update `buildAutomationBrowseResult`.
> * Update `ghost/core/test/e2e-api/admin/automations.test.js`.
> * Update
`ghost/core/test/unit/server/services/automations/automations-repository.test.ts`.
> * Re-generate the snapshots with `UPDATE_SNAPSHOTS=1` and running the
necessary tests.
>
> Use red/green TDD.
>
> This should be a fairly straightforward change.

In addition to unit and E2E tests, I also verified that the data
appeared by checking the Network tab of the Firefox devtools:

![devtools
screenshot](https://github.com/user-attachments/assets/de9bb167-ac6d-487c-9196-37d7528ba6bd)

Co-authored-by: Claude <noreply@anthropic.com>
ref #23361

This PR completes the Serbian (`sr`) locale. I'm a native Serbian
speaker, and I've filled in all the previously-empty strings across
`comments.json`, `ghost.json`, and `portal.json` — including the Serbian
plural forms (`{count} month_*` / `{count} year_*`, which need the
`_few` paucal form).
no ref

This change should have no user impact.

[`@ts-ignore` should never be used.][0] This removes all usages. Most of
the time, we could simply delete it. Sometimes, we needed to replace it
with something.

All of the functionality changes were (1) only in tests (2) minor.

[0]: https://evanhahn.com/ts-ignore-is-almost-always-the-worst-option/
no ref

[`@ts-ignore` should never be used.][0] ESLint now errors if you do.
`@ts-nocheck` is also forbidden. `@ts-expect-error` is allowed with a
description.

No "real" code changes were necessary after this.

[0]: https://evanhahn.com/ts-ignore-is-almost-always-the-worst-option/
towards https://linear.app/ghost/issue/PLA-137

This installs Oxfmt in the Ghost repo. It doesn't add any scripts or
enforcement, but it enables a workflow where we can run `pnpm exec oxfmt
my-file.ts`, which will ease the future transition.

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
ref https://linear.app/ghost/issue/BER-3876/refine-the-field-picker-in-the-members-import-mapping

The trigger named the kind of every field it showed, putting "Membership field"
under nearly every row to answer a question almost none of them raise. The kind
is named now only where a custom field's name is one a native field already has,
and with the second line gone the icon leaves its tile, so the control stands at
the height every other one does.

Building that turned up a bug. The list identified its items by their label, so a
custom field named "Name" and the native "Name" were one item to cmdk: both drew
as selected, and since the active item is resolved by querying the first match in
the DOM, Enter took the native one whichever had been arrowed to. Identity moves
to the targets, already namespaced apart as `name` and `custom_fields.name`, and
the label moves to keywords — which needs a filter of its own, since cmdk scores
an item's value and its keywords as one joined string and a match is otherwise
free to run from one into the other.

A composite's parts differ only in their last few words, so truncating the label
as one string took away the only thing telling those rows apart. The name gives
way first now and the part stays whole.
no ref

Uses the settings `DialogPortal` from #30014.

The custom field, welcome email and welcome email customize dialogs were
the last `NiceModal.create` dialogs in the membership and email settings
areas. They now take an `onClose` prop and are rendered by the component
that opens them: the two `SettingsModal` dialogs through the settings
dialog portal so they paint above the settings chrome, and the customize
dialog directly, since its `EmailDesignModal` is already a portalled
Shade `Dialog`.

With these converted (plus #30014 and #30015), no settings dialog is
created or shown through NiceModal any more, which clears the way to
remove the provider, the dependency and the compatibility bridges.
@pull pull Bot locked and limited conversation to collaborators Aug 18, 2026
@pull pull Bot added the ⤵️ pull label Aug 18, 2026
@pull
pull Bot merged commit d3915e3 into code:main Aug 18, 2026
4 checks 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.

10 participants