DEV: Add `skip_migrations` param when importing remote theme (#25218)

Why this change?

Importing theme with the `bundle` params is used mainly by
`discourse_theme` CLI in the development environment. However, we do not
want migrations to automatically run in the development environment
and instead want the developer to be intentional about running theme
migrations. As such, this commit adds support for a
`skip_migrations` param when importing a theme with the `bundle` params.

This commit also adds a `migrated` attribute for migrations theme fields
to indicate whether a migrations theme field has been migrated or not.
This commit is contained in:
Alan Guo Xiang Tan 2024-01-11 14:04:02 +08:00 committed by GitHub
parent 30bea5c7c2
commit 59839e428f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 80 additions and 20 deletions

View File

@ -142,15 +142,19 @@ class Admin::ThemesController < Admin::AdminController
bundle = params[:bundle] || params[:theme] bundle = params[:bundle] || params[:theme]
theme_id = params[:theme_id] theme_id = params[:theme_id]
update_components = params[:components] update_components = params[:components]
run_migrations = !params[:skip_migrations]
begin begin
@theme = @theme =
RemoteTheme.update_zipped_theme( RemoteTheme.update_zipped_theme(
bundle.path, bundle.path,
bundle.original_filename, bundle.original_filename,
user: theme_user, user: theme_user,
theme_id: theme_id, theme_id:,
update_components: update_components, update_components:,
run_migrations:,
) )
log_theme_change(nil, @theme) log_theme_change(nil, @theme)
render json: @theme, status: :created render json: @theme, status: :created
rescue RemoteTheme::ImportError => e rescue RemoteTheme::ImportError => e

View File

@ -70,13 +70,15 @@ class RemoteTheme < ActiveRecord::Base
original_filename, original_filename,
user: Discourse.system_user, user: Discourse.system_user,
theme_id: nil, theme_id: nil,
update_components: nil update_components: nil,
run_migrations: true
) )
update_theme( update_theme(
ThemeStore::ZipImporter.new(filename, original_filename), ThemeStore::ZipImporter.new(filename, original_filename),
user:, user:,
theme_id:, theme_id:,
update_components:, update_components:,
run_migrations:,
) )
end end
@ -91,7 +93,8 @@ class RemoteTheme < ActiveRecord::Base
importer, importer,
user: Discourse.system_user, user: Discourse.system_user,
theme_id: nil, theme_id: nil,
update_components: nil update_components: nil,
run_migrations: true
) )
importer.import! importer.import!
@ -112,8 +115,14 @@ class RemoteTheme < ActiveRecord::Base
remote_theme.remote_url = "" remote_theme.remote_url = ""
do_update_child_components = false do_update_child_components = false
theme.transaction do theme.transaction do
remote_theme.update_from_remote(importer, skip_update: true, already_in_transaction: true) remote_theme.update_from_remote(
importer,
skip_update: true,
already_in_transaction: true,
run_migrations:,
)
if existing && update_components.present? && update_components != "none" if existing && update_components.present? && update_components != "none"
child_components = child_components.map { |url| ThemeStore::GitImporter.new(url.strip).url } child_components = child_components.map { |url| ThemeStore::GitImporter.new(url.strip).url }
@ -216,7 +225,8 @@ class RemoteTheme < ActiveRecord::Base
importer = nil, importer = nil,
skip_update: false, skip_update: false,
raise_if_theme_save_fails: true, raise_if_theme_save_fails: true,
already_in_transaction: false already_in_transaction: false,
run_migrations: true
) )
cleanup = false cleanup = false
@ -356,12 +366,14 @@ class RemoteTheme < ActiveRecord::Base
update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component
self.save! self.save!
if raise_if_theme_save_fails if raise_if_theme_save_fails
theme.save! theme.save!
else else
raise ActiveRecord::Rollback if !theme.save raise ActiveRecord::Rollback if !theme.save
end end
theme.migrate_settings(start_transaction: false)
theme.migrate_settings(start_transaction: false) if run_migrations
end end
if already_in_transaction if already_in_transaction

View File

@ -86,7 +86,7 @@ class Theme < ActiveRecord::Base
:user, :user,
:color_scheme, :color_scheme,
:theme_translation_overrides, :theme_translation_overrides,
theme_fields: :upload, theme_fields: %i[upload theme_settings_migration],
) )
end end
@ -130,6 +130,7 @@ class Theme < ActiveRecord::Base
remove_from_cache! remove_from_cache!
ColorScheme.hex_cache.clear ColorScheme.hex_cache.clear
notify_theme_change(with_scheme: notify_with_scheme) notify_theme_change(with_scheme: notify_with_scheme)
if theme_setting_requests_refresh if theme_setting_requests_refresh
@ -647,13 +648,14 @@ class Theme < ActiveRecord::Base
def settings def settings
field = settings_field field = settings_field
return [] unless field && field.error.nil? return [] unless field && field.error.nil?
settings = [] settings = []
ThemeSettingsParser ThemeSettingsParser
.new(field) .new(field)
.load do |name, default, type, opts| .load do |name, default, type, opts|
settings << ThemeSettingsManager.create(name, default, type, self, opts) settings << ThemeSettingsManager.create(name, default, type, self, opts)
end end
settings settings
end end
@ -847,6 +849,7 @@ class Theme < ActiveRecord::Base
self.theme_settings.destroy_all self.theme_settings.destroy_all
final_result = results.last final_result = results.last
final_result[:settings_after].each do |key, val| final_result[:settings_after].each do |key, val|
self.update_setting(key.to_sym, val) self.update_setting(key.to_sym, val)
rescue Discourse::NotFound rescue Discourse::NotFound
@ -874,7 +877,8 @@ class Theme < ActiveRecord::Base
record.calculate_diff(res[:settings_before], res[:settings_after]) record.calculate_diff(res[:settings_before], res[:settings_after])
record.save! record.save!
end end
self.save!
self.reload
end end
if start_transaction if start_transaction

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ThemeFieldSerializer < ApplicationSerializer class ThemeFieldSerializer < ApplicationSerializer
attributes :name, :target, :value, :error, :type_id, :upload_id, :url, :filename attributes :name, :target, :value, :error, :type_id, :upload_id, :url, :filename, :migrated
def include_url? def include_url?
object.upload object.upload
@ -34,4 +34,12 @@ class ThemeFieldSerializer < ApplicationSerializer
def include_error? def include_error?
object.error.present? object.error.present?
end end
def migrated
!!object.theme_settings_migration
end
def include_migrated?
target == "migrations"
end
end end

Binary file not shown.

View File

@ -0,0 +1,3 @@
export default function migrate(settings) {
return settings;
}

View File

@ -494,7 +494,7 @@ RSpec.describe RemoteTheme do
theme = RemoteTheme.import_theme_from_directory(theme_dir) theme = RemoteTheme.import_theme_from_directory(theme_dir)
expect(theme.name).to eq("Header Icons") expect(theme.name).to eq("Header Icons")
expect(theme.theme_fields.count).to eq(5) expect(theme.theme_fields.count).to eq(6)
end end
end end
end end

View File

@ -243,6 +243,7 @@ RSpec.describe Admin::ThemesController do
} }
JS JS
) )
repo_url = MockGitImporter.register("https://example.com/initial_repo.git", repo_path) repo_url = MockGitImporter.register("https://example.com/initial_repo.git", repo_path)
post "/admin/themes/import.json", params: { remote: repo_url } post "/admin/themes/import.json", params: { remote: repo_url }
@ -336,7 +337,7 @@ RSpec.describe Admin::ThemesController do
json = response.parsed_body json = response.parsed_body
expect(json["theme"]["name"]).to eq("Header Icons") expect(json["theme"]["name"]).to eq("Header Icons")
expect(json["theme"]["theme_fields"].length).to eq(5) expect(json["theme"]["theme_fields"].length).to eq(6)
expect(json["theme"]["auto_update"]).to eq(false) expect(json["theme"]["auto_update"]).to eq(false)
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
end end
@ -347,30 +348,45 @@ RSpec.describe Admin::ThemesController do
other_existing_theme = Fabricate(:theme, name: "Some other name") other_existing_theme = Fabricate(:theme, name: "Some other name")
messages = messages =
MessageBus.track_publish do MessageBus.track_publish("/file-change") do
expect do expect do
post "/admin/themes/import.json", post "/admin/themes/import.json",
params: { params: {
bundle: theme_archive, bundle: theme_archive,
theme_id: other_existing_theme.id, theme_id: other_existing_theme.id,
} }
expect(response.status).to eq(201)
end.not_to change { Theme.count } end.not_to change { Theme.count }
end end
expect(response.status).to eq(201)
json = response.parsed_body json = response.parsed_body
# Ensure only one refresh message is sent. # Ensure only one refresh message is sent.
# More than 1 is wasteful, and can trigger unusual race conditions in the client # More than 1 is wasteful, and can trigger unusual race conditions in the client
# If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations # If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations
file_change_messages = messages.filter { |m| m[:channel] == "/file-change" } expect(messages.count).to eq(1)
expect(file_change_messages.count).to eq(1)
expect(json["theme"]["name"]).to eq("Some other name") expect(json["theme"]["name"]).to eq("Some other name")
expect(json["theme"]["id"]).to eq(other_existing_theme.id) expect(json["theme"]["id"]).to eq(other_existing_theme.id)
expect(json["theme"]["theme_fields"].length).to eq(5) expect(json["theme"]["theme_fields"].length).to eq(6)
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
end end
it "does not run migrations when importing a theme from an archive and `skip_settings_migrations` params is present" do
other_existing_theme = Fabricate(:theme, name: "Some other name")
post "/admin/themes/import.json",
params: {
bundle: theme_archive,
theme_id: other_existing_theme.id,
skip_migrations: true,
}
expect(response.status).to eq(201)
expect(other_existing_theme.theme_settings_migrations.exists?).to eq(false)
end
it "creates a new theme when id specified as nil" do it "creates a new theme when id specified as nil" do
# Used by theme CLI # Used by theme CLI
existing_theme = Fabricate(:theme, name: "Header Icons") existing_theme = Fabricate(:theme, name: "Header Icons")
@ -383,7 +399,7 @@ RSpec.describe Admin::ThemesController do
expect(json["theme"]["name"]).to eq("Header Icons") expect(json["theme"]["name"]).to eq("Header Icons")
expect(json["theme"]["id"]).not_to eq(existing_theme.id) expect(json["theme"]["id"]).not_to eq(existing_theme.id)
expect(json["theme"]["theme_fields"].length).to eq(5) expect(json["theme"]["theme_fields"].length).to eq(6)
expect(json["theme"]["auto_update"]).to eq(false) expect(json["theme"]["auto_update"]).to eq(false)
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
end end
@ -423,6 +439,12 @@ RSpec.describe Admin::ThemesController do
theme.set_field(target: :common, name: :scss, value: ".body{color: black;}") theme.set_field(target: :common, name: :scss, value: ".body{color: black;}")
theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>") theme.set_field(target: :desktop, name: :after_header, value: "<b>test</b>")
theme.set_field(
target: :migrations,
name: "0001-some-migration",
value: "export default function migrate(settings) { return settings; }",
)
theme.remote_theme = theme.remote_theme =
RemoteTheme.new( RemoteTheme.new(
remote_url: "awesome.git", remote_url: "awesome.git",
@ -444,7 +466,14 @@ RSpec.describe Admin::ThemesController do
expect(json["extras"]["color_schemes"].length).to eq(1) expect(json["extras"]["color_schemes"].length).to eq(1)
theme_json = json["themes"].find { |t| t["id"] == theme.id } theme_json = json["themes"].find { |t| t["id"] == theme.id }
expect(theme_json["theme_fields"].length).to eq(2) expect(theme_json["theme_fields"].length).to eq(3)
expect(
theme_json["theme_fields"].find { |theme_field| theme_field["target"] == "migrations" }[
"migrated"
],
).to eq(false)
expect(theme_json["remote_theme"]["remote_version"]).to eq("7") expect(theme_json["remote_theme"]["remote_version"]).to eq("7")
end end
end end