Skip to content

[pull] main from TryGhost:main - #1446

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

[pull] main from TryGhost:main#1446
pull[bot] merged 11 commits into
code:mainfrom
TryGhost:main

Conversation

@pull

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

PaulAdamDavis and others added 11 commits August 26, 2026 11:38
ref https://linear.app/ghost/project/4b2edbd66469/ - Milestone 5

Each commit references an individual Linear issue

- Skips duplicate posts and updates existing posts when imported data is
newer
- Matches posts by source ID or slug
- Reconciles and creates authors and tags, with safe Owner fallbacks and
warnings
- Adds full coverage across all changed CSV importer modules
`db-date` held the one codec that turns a database value into something the
domain can use, and a second was about to join it for booleans, where SQLite
answers 0 or 1 and MySQL answers true or false. Two of them loose in lib is how
lib became a drawer of unrelated things, so they sit in a folder that names what
they are and gives the next one somewhere obvious to go. The boolean codec
arrived without tests, and the interesting part of it is not that 0 and 1 become
false and true but what it does with the rest of the range: a boolean column is
a tinyint underneath, and both engines read every non-zero value in it as true,
so the codec does too rather than failing a read over a value the database
itself is happy with.

ref https://linear.app/ghost/issue/BER-3872

Claude-Session: https://claude.ai/code/session_018jPEJMYLoUzNvYbdxWb3dY
ref https://linear.app/ghost/issue/BER-3872

Storage, validation, CSV columns, the import mapping, the member form and filters
all read the parts a composite type declares, and the part labels fail the build
until they name a new one. The one-line summary did not: it was written out per
type, so a part left out of it was collected, stored, exported and filtered on
while being invisible in every list cell and detail row.

It now comes from declaration order, and the only thing stated by hand is which
parts read as one run, so that a state and a postal code still read "NY 00001".
That is typed against the parts each type declares, which makes a part added
upstream appear on its own and a part renamed or removed upstream fail the build
here rather than going quietly missing from what a publisher sees.
Stripe Checkout can be asked to collect a shipping address, a phone number or a
tax number alongside the payment, and the fake server the end-to-end tests run
against knew none of it. Anything built on top would have been tested against a
server that accepted whatever it was sent, which is the opposite of what a fake
is for. The rules it now enforces were measured against the live API at the
version Ghost pins rather than read from the reference, which disagreed with the
API in three of five probes, so a request the real Stripe would refuse now fails
a test here first, including the one refusal that is easy to miss: a tax number
cannot be collected for a customer Stripe may not rename.

Modelling those rules turned up one of its own. Stripe's SDK form-encodes its
request bodies, so a checkout that switched tax collection off arrives carrying
the string "false" rather than the boolean, and reading that flag for truthiness
refused a checkout that had asked for no tax number at all. The existing tests
missed it because they post JSON, where the flag is a real boolean, so the
refusal only appeared under the encoding Ghost actually uses. A captured fixture
also carried the real street and postcode of a payment made while capturing it,
and carries invented ones instead now that nothing asserts on their contents.

ref https://linear.app/ghost/issue/BER-3872

Claude-Session: https://claude.ai/code/session_018jPEJMYLoUzNvYbdxWb3dY
ref https://linear.app/ghost/issue/BER-3890

The personalised gift purchase form has grown out of bounds. Splitting it into smaller components and porting to TS in preparation for reworking the state management.
Every value carries the writer that put it there: a type and an id, held per
leaf path so the parts of a composite can name different writers, and merged
with the value so a leaf names whoever wrote what it holds rather than whoever
wrote its first. The type is the namespace its id resolves in — a user, an
integration, or the binding that routes a value in from a checkout — which
leaves an import as the one writer with no id to give, until import runs are
tracked.

The values service takes the writer as a required argument with no default, so
no call site can omit one and inherit the identity of whichever wrote first, and
a request carrying neither an authenticated user nor an integration is refused
rather than credited to a writer nobody can resolve. The migration clears the
values table rather than invent a writer for rows that carry none, which the
feature flag makes safe.

ref https://linear.app/ghost/issue/BER-3872

Claude-Session: https://claude.ai/code/session_018jPEJMYLoUzNvYbdxWb3dY
no ref

This change should have no user impact.

Four options were never used: `errorMessage`, `errorContext`,
`errorHelp`, and `async`. We can remove them and simplify the function a
bit.
no ref
- Scope tier option lookup to the open command listbox.
- Wait for each tier option to become visible before clicking it.
- Match tier names exactly to avoid unrelated option matches.

This removes the race between the asynchronous mocked tiers request and the React Query result rendering in the combobox.
no ref

The shell (`routes.tsx`, `layout/`) deep-imported domain internals, and only analytics had a declared public surface. Every domain now exposes an `api.ts` mirroring `analytics/api.ts`, and dependency-cruiser keeps it that way.
closes https://linear.app/ghost/issue/NY-1515/find-a-way-to-catch-stripped-query-parameter-issue-in-development

Local development and tests previously bypassed Ghost(Pro)'s query
parameter filtering, allowing new parameters to work locally but fail
after deployment. This makes Ghost the canonical source for the policy
and applies it before requests reach Ghost's route handlers, so missing
parameters are caught earlier. The production allowlist must still be
updated separately until infrastructure consumes Ghost's exported
policy.

## Summary

- add a canonical, machine-readable policy containing documented public
and Content API query parameters
- validate the policy with Zod, including its schema version, required
fields, valid names, and duplicate entries
- filter frontend and Content API requests against their respective
allowlists while preserving API and internal-route exemptions
- enable filtering in the root development environment, Ghost's
server-side test configurations, and Playwright test containers while
leaving it disabled by default
- preserve Express 4 and Express 5 query handling, repeated allowed
parameters, and warning logs for stripped parameters
- add a TypeScript exporter that validates and writes the policy
deterministically for future infrastructure integration
- cover policy validation, exporting, filtering, exemptions, and query
parsing behavior with tests

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
@pull pull Bot locked and limited conversation to collaborators Aug 26, 2026
@pull pull Bot added the ⤵️ pull label Aug 26, 2026
@pull
pull Bot merged commit 1a1ac3e into code:main Aug 26, 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.

6 participants