Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
41def3e
Added internal package golden path checks (#29877)
ErisDS Aug 13, 2026
f146bfa
Added translation and internationalization guides (#29931)
ErisDS Aug 13, 2026
002deb8
✨ Added support for Docker secrets to config loading (#29924)
acburdine Aug 13, 2026
7481efc
Updated public app development commands (#29932)
ErisDS Aug 13, 2026
a4e84d3
🐛 Fixed the members import mapping step on short screens
renatoworks Aug 5, 2026
0a4c422
🐛 Fixed the label dropdown running off the bottom of the screen
renatoworks Aug 11, 2026
cd1110c
Changed an empty members import mapping target to leave the column out
renatoworks Aug 11, 2026
5c7e28b
🐛 Fixed a members import silently dropping columns a short first row …
rob-ghost Aug 12, 2026
79ab775
🎨 Improved Admin error messages to say what actually went wrong
rob-ghost Aug 13, 2026
b40c2db
Added creating a custom field while mapping a members import
renatoworks Aug 11, 2026
e0521f9
Removed the legacy Explore integration (#29880)
ErisDS Aug 13, 2026
75fc5fc
Added 1-click export mockup behind a labs flag (#29915)
sagzy Aug 13, 2026
484acd1
Added configuration guide (#29937)
ErisDS Aug 13, 2026
c3b5e9b
Fixed pack.mjs script not including license file (#29939)
acburdine Aug 13, 2026
d7a31b2
Updated Translating Ghost documentation links (#29942)
ErisDS Aug 13, 2026
884f4a8
Added error handling guide (#29940)
ErisDS Aug 13, 2026
00464ec
Added agent payments for premium markdown URLs (#29840)
louisghost Aug 13, 2026
cd17f77
Added Ghost development setup for Amp orbs (#29941)
louisghost Aug 13, 2026
f4d4043
Removed duplicate package license files (#29936)
ErisDS Aug 13, 2026
0de24e9
Added authentication guide (#29943)
ErisDS Aug 13, 2026
3d87059
Changed gifts service entry point to TypeScript (#29935)
kevinansfield Aug 13, 2026
ec53d73
✨ Released the React member details screen
rob-ghost Aug 4, 2026
cac9f33
Cleaned up the memberDetailsReact flag and deleted the Ember member s…
rob-ghost Aug 4, 2026
c0b55b1
Fixed flaky integration modal acceptance test (#29946)
acburdine Aug 13, 2026
1a7fc83
Fixed incorrect unit in targetDeliveryWindow JSDoc (#29901)
muratcorlu Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/bin/docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
exec sudo --preserve-env=PUBLIC_URL /usr/bin/docker "$@"
6 changes: 6 additions & 0 deletions .agents/resume
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail

if ! systemctl is-active --quiet amp-svc-docker.service || ! systemctl is-active --quiet amp-svc-ghost.service; then
amp orb services ensure
fi
40 changes: 40 additions & 0 deletions .agents/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail

node_version="$(cat .node-version)"
node_dir="$HOME/.local/node-v${node_version}-linux-x64"

if [[ ! -x "$node_dir/bin/node" ]]; then
archive="/tmp/node-v${node_version}-linux-x64.tar.xz"
curl -fsSLo "$archive" "https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.xz"
mkdir -p "$HOME/.local"
tar -xJf "$archive" -C "$HOME/.local"
rm -f "$archive"
fi

export PATH="$node_dir/bin:$PATH"
corepack enable --install-directory "$node_dir/bin"
pnpm setup

profile_marker="# Ghost orb Node.js"
profile_export="export PATH=\"$node_dir/bin:\$PATH\""
if grep -Fqx "$profile_marker" "$HOME/.bash_profile" 2>/dev/null; then
sed -i "/^${profile_marker}$/ { n; c\\
$profile_export
}" "$HOME/.bash_profile"
else
cat >> "$HOME/.bash_profile" <<EOF

$profile_marker
$profile_export
EOF
fi

if ! /usr/bin/docker compose version >/dev/null 2>&1; then
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ Use the subject `Changed <package> file extensions to TypeScript`.
Apply the package contract from `packages/README.md`: shared config packages,
minimal package-local config, ESM metadata and exports, standard scripts, and a
single compiled output unless a verified consumer requires an exception.
Replace the package's `migration` status with
`ghostPackage.goldenPath: compliant` only after every mechanical golden-path
check passes.

Use the subject `Converted <package> to TypeScript`.

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/migrate-internal-package/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ commits manually.
After the subtree commit, add focused integration commits that:

- make the package private with an internal placeholder version;
- set `ghostPackage.goldenPath` to `migration` and `ghostPackage.reason` to a
concise explanation of the remaining modernization work;
- switch Ghost consumers to `workspace:*`;
- update the lockfile with `pnpm`;
- minimally adapt configuration and tests to work in Ghost;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ modernize the implementation.
Change only what Ghost needs to consume and verify the package:

- set `"private": true` and the internal placeholder version;
- set `ghostPackage.goldenPath` to `migration` and describe the remaining
modernization work in `ghostPackage.reason`;
- point repository metadata at Ghost;
- remove public publishing configuration;
- switch Ghost consumers to `workspace:*`;
Expand Down
24 changes: 24 additions & 0 deletions .amp/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
services:
docker:
command: sudo dockerd --group root

ghost:
command: |
export PATH="$PWD/.agents/bin:$HOME/.local/node-v$(cat .node-version)-linux-x64/bin:$PATH"
attempts=0
until docker info >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 60 ]; then
echo "Docker did not become ready after 60 seconds" >&2
exit 1
fi
sleep 1
done
export DEV_COMPOSE_FILES="-f compose.dev.orb.yaml"
export GHOST_URL="$PUBLIC_URL"
unset PORT
exec pnpm dev
port: 2370
portal:
title: Ghost development
description: Full source environment with Admin HMR, backend reloads, MySQL, Redis, and Mailpit.
20 changes: 20 additions & 0 deletions .changeset/salty-cities-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@tryghost/adapter-base-cache": none
"@tryghost/adapter-base-scheduling": none
"@tryghost/adapter-base-sso": none
"@tryghost/kg-card-factory": none
"@tryghost/kg-clean-basic-html": none
"@tryghost/kg-converters": none
"@tryghost/kg-default-cards": none
"@tryghost/kg-default-nodes": none
"@tryghost/kg-default-transforms": none
"@tryghost/kg-html-to-lexical": none
"@tryghost/kg-lexical-html-renderer": none
"@tryghost/kg-markdown-html-renderer": none
"@tryghost/kg-unsplash-selector": none
"@tryghost/kg-utils": none
"@tryghost/koenig-lexical": none
"ghost-storage-base": none
---

Removed duplicate package license files because pnpm includes the repository root license automatically.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ If your change is **user-facing** please prepend the first line of your commit w
- ✨ Feature
- 🎨 Improvement / change
- 🐛 Bug Fix
- 🌐 i18n (translation) submissions [[See Translating Ghost docs for more detail](https://www.notion.so/5af2858289b44f9194f73f8a1e17af59?pvs=25#bef8c9988e294a4b9a6dd624136de36f)]
- 🌐 i18n (translation) submissions [[See Translating Ghost docs for more detail](../docs/contributing/translating-ghost.md)]
- 💡 Anything else flagged to users or whoever is writing release notes

Good commit message examples: [new feature](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f), [bug fix](https://github.com/TryGhost/Ghost/commit/6ef835bb5879421ae9133541ebf8c4e560a4a90e) and [translation](https://github.com/TryGhost/Ghost/commit/83904c1611ae7ab3257b3b7d55f03e50cead62d7).
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ jobs:
- 'package.json'
- 'scripts/check-agent-skill-links.js'
- 'scripts/test/check-agent-skill-links.test.js'
package-standards:
- 'packages/**'
- 'package.json'
- 'pnpm-workspace.yaml'
- 'scripts/check-internal-packages.js'
- 'scripts/create-package.js'
- 'scripts/lib/constants.js'
- 'scripts/lib/package-template.js'
- 'scripts/test/check-internal-packages.test.js'
- '.github/workflows/ci.yml'
core:
- *shared
# Repository documentation and ownership metadata do not affect
Expand Down Expand Up @@ -279,6 +289,7 @@ jobs:
changed_core: ${{ steps.changed.outputs.core }}
changed_any_code: ${{ steps.changed.outputs.any-code }}
changed_docs: ${{ steps.changed.outputs.docs }}
changed_package_standards: ${{ steps.changed.outputs.package-standards }}
changed_tb_cli: ${{ steps.changed.outputs.tb-cli }}
# Single gate for the build + browser-E2E lane. True for tags, or when a
# changed file could affect a running Ghost instance (see the `e2e` path
Expand Down Expand Up @@ -421,6 +432,26 @@ jobs:
- name: Lint agent skills
run: node scripts/check-agent-skill-links.js

job_lint_packages:
name: Lint packages
runs-on: ubuntu-slim
needs: [job_setup]
if: needs.job_setup.outputs.changed_package_standards == 'true'
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9

- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: ${{ env.NODE_VERSION }}

- name: Check internal package golden path
run: node scripts/check-internal-packages.js

- name: Test internal package checker
run: node --test scripts/test/check-internal-packages.test.js

job_i18n:
runs-on: ubuntu-latest
needs: [job_setup]
Expand Down Expand Up @@ -2051,6 +2082,7 @@ jobs:
job_migration_integrity_check,
job_lint,
job_lint_docs,
job_lint_packages,
job_i18n,
job_build_admin,
job_pack,
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ yalc.lock
# useful for keeping local plans etc
/ai

# Amp generates thread-specific portal state at runtime
/.amp/portals/

# direnv environment loader files
.envrc

Expand Down
21 changes: 0 additions & 21 deletions apps/admin-toolbar/LICENSE

This file was deleted.

16 changes: 11 additions & 5 deletions apps/admin-toolbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ scripts where bundle size matters more than ecosystem compatibility.

## Development

Run `pnpm dev:public` from the monorepo root to start the standard development
environment and the Admin Toolbar watcher. To work on this package by itself,
run these commands from this directory:

```bash
pnpm build # one-off build
pnpm dev # build + preview with watch (started automatically by pnpm dev from root)
pnpm test # build + run tests against UMD bundle
pnpm dev # watch and rebuild umd/admin-toolbar.min.js
pnpm test # build + run tests against the built bundle
```

## How it's served

In production, the script is loaded from jsDelivr via the `adminToolbar` config
in `defaults.json`, following the same CDN pattern as portal, comments-ui, and
the other public apps. In development, the Docker Dockerfile overrides the URL
to proxy through Caddy to the local vite preview server on port 4176.
the other public apps. In development, `docker/ghost-dev/Dockerfile` overrides
that URL to `/ghost/assets/admin-toolbar/admin-toolbar.min.js`, which the dev
gateway serves straight off disk from this package's `umd/` directory — so the
watcher's output is picked up on the next request.

# Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE).
Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](https://github.com/TryGhost/Ghost/blob/main/LICENSE).
33 changes: 30 additions & 3 deletions apps/admin-x-framework/src/api/member-custom-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ export const userTypeForField = (field: MemberCustomField): MemberCustomFieldUse

// A custom field CSV column offered as an import mapping target: the column name the
// backend reads (`value`) and a human label for the picker.
export type MemberCustomFieldCsvColumn = {label: string; value: string};
// The field's type rides along so a picker can show what kind of thing the column holds
// without being handed the fields as well. Every column of a composite carries the
// composite's own type, which is what its icon is drawn from.
export type MemberCustomFieldCsvColumn = {label: string; value: string; type: FieldType};

/**
* The CSV import mapping targets for a set of custom fields: one per column the export
Expand All @@ -118,7 +121,8 @@ export const memberCustomFieldCsvColumns = (fields: MemberCustomField[]): Member
const labels = partLabelsFor(field.type);
return csvColumnsForField({key: field.key, type: field.type}).map(({column, subField}) => ({
label: subField === null ? field.name : `${field.name} (${labels[subField]})`,
value: column
value: column,
type: field.type
}));
});
};
Expand Down Expand Up @@ -173,7 +177,30 @@ export const useCreateMemberCustomField = createMutation<MemberCustomFieldsRespo
method: 'POST',
path: () => '/members/custom_fields/',
body: field => ({members_custom_fields: [field]}),
invalidateQueries: {dataType}
invalidateQueries: {dataType},
// The created field is put into the cached lists as well as refetched, so a screen that
// has just made one can use it in the same breath instead of waiting for a round trip or
// keeping its own copy until one arrives. The refetch above remains the truth; this only
// decides what is on screen until it lands.
//
// Appended, because the API assigns a new field the last position. Keyed de-dup because
// the refetch may already have landed, and a list that does not hold the created field is
// left alone: a browse filtered to active fields should not be handed an archived one, and
// by the same token no list here is asked to take a field it did not ask for.
updateQueries: {
dataType,
emberUpdateType: 'skip',
update: (newData, currentData) => {
const current = currentData as MemberCustomFieldsResponseType | undefined;
if (!current?.members_custom_fields) {
return currentData;
}
const created = newData.members_custom_fields.filter(
field => !current.members_custom_fields.some(existing => existing.key === field.key)
);
return {...current, members_custom_fields: [...current.members_custom_fields, ...created]};
}
}
});

// Keys are immutable after creation (the API rejects changes); `name` and
Expand Down
5 changes: 4 additions & 1 deletion apps/admin-x-framework/src/api/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,11 @@ function buildImportMembersFormData({file, labels = [], mapping = {}}: ImportMem
formData.append('labels', label);
}

// An empty target is the caller naming a column to leave out of the import, and the
// importer only honours a column it is told about — so it has to survive to the wire.
// Null and undefined stay dropped: they are a column with nothing said about it.
for (const [key, val] of Object.entries(mapping)) {
if (val) {
if (typeof val === 'string') {
formData.append(`mapping[${key}]`, val);
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/admin-x-framework/src/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Config} from './config';

// Types

export type SettingValue = string | boolean | null;
export type SettingValue = string | boolean | number | null;

export type Setting = {
key: string;
Expand Down
4 changes: 4 additions & 0 deletions apps/admin-x-framework/src/test/msw-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const fixtures = {
navigation: [],
secondary_navigation: [],
llms_enabled: true,
machine_payments_enabled: false,
machine_payments_currency: null,
machine_payments_amount: 100,
machine_payments_stripe_profile_id: null,
meta_title: null,
meta_description: null
},
Expand Down
17 changes: 11 additions & 6 deletions apps/admin-x-framework/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,19 @@ export class ValidationError extends JSONError {

export const errorsWithMessage = [ValidationError, ThemeValidationError, HostLimitError, EmailError];

// The API error serializer puts the human-readable text in `context`;
// `message` is only a generic summary
/**
* What the server said went wrong, for showing to a person.
*
* The API serializer rewrites `message` to a generic summary and leaves the sentence that
* explains the failure — and usually what to do about it — in `context`, so that is read
* first. Any error carrying an API body is read the same way: a caller should not have to
* know which class it got back to find the text, and reading only one class left every
* other one showing "Could not save…" while the reason sat unread in the payload.
*/
export function getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof ValidationError && error.data?.errors[0]) {
return error.data.errors[0].context || error.data.errors[0].message;
}
const apiError = error instanceof JSONError ? error.data?.errors?.[0] : undefined;

return fallback;
return apiError?.context || apiError?.message || fallback;
}

// Frontend errors
Expand Down
Loading
Loading