Skip to content

🐛 Fixed theme settings reset when editing Source or Casper - #29839

Merged
kevinansfield merged 4 commits into
mainfrom
theme-editor-custom-duplicate-718d49
Aug 10, 2026
Merged

kevinansfield merged 4 commits into
mainfrom
theme-editor-custom-duplicate-718d49

Conversation

@kevinansfield

@kevinansfield kevinansfield commented Aug 10, 2026

Copy link
Copy Markdown
Member

fixes https://linear.app/ghost/issue/ONC-1936/

Editing a default theme (Source/Casper) in the built-in theme editor forces a "save as new theme" flow because default themes can't be overwritten. Custom theme settings are stored against the theme name, so the newly-named copy starts with the theme's default settings — the moment it was activated, all customised design settings (fonts, colours, header styles, etc.) silently reverted to defaults and unexpectedly broke the site's appearance.

Fixed it so the editor asks the API to carry the settings over:

  • POST /themes/upload now accepts an optional copy_settings_from query option. After the theme is stored, the custom theme settings service duplicates the source theme's stored settings under the new theme name, so activating the copy keeps the site's design.
  • Copied values are reconciled by the existing activation sync (unknown keys pruned, invalid select values reset), which handles the case where the edit also changed the theme's settings definition.
  • Copying no-ops when the destination theme already has stored settings, so saving over an existing theme never clobbers its customisations.
  • The theme editor sends copy_settings_from whenever a save results in a new theme name — the forced save-as for default themes, and any rename of a custom theme.

…copy

fixes https://linear.app/ghost/issue/ONC-1936/theme-editor-saving-an-edited-default-theme-as-a-copy-resets-all

- custom theme settings are stored against the theme name, so the copy the
  theme editor creates when saving an edited default theme started from
  defaults and activating it unexpectedly reverted the site's design
- the editor now asks the API to seed the copy's settings from the theme it
  was edited from (`?copy_settings_from`), and the existing activation sync
  reconciles the copied values against the new theme's settings definition
- copying no-ops when the destination theme already has stored settings so
  saving over an existing theme never clobbers its customisations
@kevinansfield kevinansfield changed the title 🐛 Fixed theme settings resetting when saving an edited default theme copy 🐛 Fixed theme settings reset when editing Source or Casper Aug 10, 2026
@nx-cloud

nx-cloud Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 7a5043c

Command Status Duration Result
nx run ghost:test:ci:integration ✅ Succeeded 2m 58s View ↗
nx run @tryghost/admin:test:acceptance ✅ Succeeded 6m 31s View ↗
nx run ghost:test:integration ✅ Succeeded 3m 5s View ↗
nx run ghost:test:legacy ✅ Succeeded 2m 47s View ↗
nx run ghost:test:e2e ✅ Succeeded 2m 40s View ↗
nx run ghost-monorepo:lint:boundaries ✅ Succeeded 23s View ↗
nx run-many -t test:unit -p @tryghost/admin,ghost ✅ Succeeded 31s View ↗
nx run-many -t lint -p @tryghost/admin,ghost,gh... ✅ Succeeded 22s View ↗
Additional runs (2) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-10 09:42:11 UTC

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca8c36be-097e-4451-a853-eeefa4d729fa

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5043c and 9e7c1d7.

📒 Files selected for processing (2)
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js

Walkthrough

Theme save-as uploads now include the original theme name. The upload endpoint passes this value to theme installation. Theme installation validates the source and copies persisted custom settings to the renamed theme. Existing destination settings remain unchanged. Tests cover successful copying, validation failures, and rollback behavior.

Suggested reviewers: johnonolan, erisds

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix for theme settings resetting when editing the Source or Casper themes.
Description check ✅ Passed The description directly explains the theme settings copy flow, API changes, and safeguards implemented by the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch theme-editor-custom-duplicate-718d49

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/services/themes/storage.js`:
- Around line 104-109: Make theme replacement recoverable across both affected
sites: in ghost/core/core/server/services/themes/storage.js lines 104-109,
update the settings-copy failure path around copySettingsBetweenThemes to remove
the newly saved theme, restore backupName when present, and rethrow; in
ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js
lines 179-197, make the copied setting writes atomic or clean up all destination
rows created by a failed copy so partial settings cannot remain.

In `@ghost/core/test/e2e-api/admin/themes.test.js`:
- Around line 430-483: Update the “Can copy custom theme settings when uploading
a theme under a new name” test to declare tmpDir and perform its setup,
assertions, and cleanup inside try/finally. In finally, restore the source
theme, remove source-edited when it exists, and delete tmpDir recursively,
ensuring cleanup runs after any failure.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa91cced-cbba-4c1c-90c0-27942f770914

📥 Commits

Reviewing files that changed from the base of the PR and between f318505 and 89b12b0.

📒 Files selected for processing (7)
  • apps/admin/src/settings/app/components/settings/site/theme/theme-code-editor-modal.tsx
  • apps/admin/src/settings/site/theme.acceptance.test.tsx
  • ghost/core/core/server/api/endpoints/themes.js
  • ghost/core/core/server/services/themes/storage.js
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js
  • ghost/core/test/e2e-api/admin/themes.test.js
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js

Comment thread ghost/core/core/server/services/themes/storage.js Outdated
Comment thread ghost/core/test/e2e-api/admin/themes.test.js
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.41%. Comparing base (784f22c) to head (9e7c1d7).
⚠️ Report is 45 commits behind head on main.

Files with missing lines Patch % Lines
...me-settings-cache/custom-theme-settings-service.js 89.74% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29839      +/-   ##
==========================================
- Coverage   75.58%   75.41%   -0.17%     
==========================================
  Files        1613     1606       -7     
  Lines      142620   140427    -2193     
  Branches    17644    17398     -246     
==========================================
- Hits       107805   105909    -1896     
+ Misses      33764    33442     -322     
- Partials     1051     1076      +25     
Flag Coverage Δ
e2e-tests 77.56% <94.11%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ref https://linear.app/ghost/issue/ONC-1936/theme-editor-saving-an-edited-default-theme-as-a-copy-resets-all

- a failure part-way through copying settings rows could leave a partial
  copy behind that a retry would then treat as complete
- the copy now removes any rows it created before rethrowing, and runs
  before the theme's files are touched so a failed copy leaves the
  installed themes exactly as they were
- the settings-copy e2e test now cleans up its temp dir and restores the
  active theme in a finally block so a failure can't leak state into
  other tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js`:
- Around line 200-206: Update the settings-copy operation in the relevant
service method to execute all destination inserts through a single database
transaction, using the repository’s existing transaction API and passing its
transaction context to each insert. Roll back the transaction when any insert
fails and rethrow the original error; avoid relying on the catch-block cleanup
loop so retries cannot encounter partial destination records.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b96d24a8-cc62-425a-bc87-c251e63e6b76

📥 Commits

Reviewing files that changed from the base of the PR and between 89b12b0 and a1c665b.

📒 Files selected for processing (4)
  • ghost/core/core/server/services/themes/storage.js
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js
  • ghost/core/test/e2e-api/admin/themes.test.js
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js
  • ghost/core/test/e2e-api/admin/themes.test.js
  • ghost/core/core/server/services/themes/storage.js

ref https://linear.app/ghost/issue/ONC-1936/theme-editor-saving-an-edited-default-theme-as-a-copy-resets-all

- the previous delete-on-failure cleanup could itself fail part-way,
  leaving partial rows that make later copy attempts skip the copy
- running all inserts in one transaction means a failed copy can never
  leave anything behind, regardless of which write fails

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js (1)

68-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that copied rows use the transaction context.

The stub rolls back all writes even when add() does not receive {transacting}. The test can pass if the production code stops forwarding the transaction context, but production writes would then not participate in the transaction.

  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js#L68-L77: expose a transaction sentinel that represents the active transaction.
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js#L544-L557: assert that each add() call receives the active transaction sentinel in options.transacting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js`
around lines 68 - 77, Update
ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js:68-77 so
transaction() exposes a distinct active transaction sentinel through its
callback context, then update
ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js:544-557
to assert every add() call receives that sentinel as options.transacting. Use
the existing transaction test setup and preserve rollback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js`:
- Around line 191-201: The custom theme settings copy transaction does not
atomically protect the destination from concurrent inserts. Update the flow
around the repository transaction and destination-existence check to enforce
uniqueness for each (theme, key) pair using a database constraint or appropriate
locking, and add a test that runs concurrent copies and verifies no duplicate
destination rows are created.

---

Nitpick comments:
In `@ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js`:
- Around line 68-77: Update
ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js:68-77 so
transaction() exposes a distinct active transaction sentinel through its
callback context, then update
ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js:544-557
to assert every add() call receives that sentinel as options.transacting. Use
the existing transaction test setup and preserve rollback behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d804c07-96be-4ff8-a85f-81b6199046fe

📥 Commits

Reviewing files that changed from the base of the PR and between a1c665b and 7a5043c.

📒 Files selected for processing (3)
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-bread-service.js
  • ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js
  • ghost/core/test/unit/shared/custom-theme-settings-cache/service.test.js

ref https://linear.app/ghost/issue/ONC-1936/theme-editor-saving-an-edited-default-theme-as-a-copy-resets-all

- the destination-empty check ran outside the copy transaction, so two
  concurrent copies could both see an empty destination and insert
  duplicate (theme, key) rows
- the check now runs inside the transaction with a forUpdate lock, which
  serialises concurrent copies on MySQL; the second one then sees the
  copied rows and no-ops
- a schema-level unique constraint was considered but needs a migration
  with dedupe handling and matches the existing check-then-insert shape
  of the activation sync, so it's left out of scope here

@JohnONolan JohnONolan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 👍

@kevinansfield
kevinansfield merged commit 6f6f195 into main Aug 10, 2026
52 checks passed
@kevinansfield
kevinansfield deleted the theme-editor-custom-duplicate-718d49 branch August 10, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants