0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/lib/discourse_updates.rb
David Taylor 9527868295
DEV: Replace JS build system with Rolldown (#35963)
This replaces the old ember-cli build with a modern Rolldown build. In
local testing, this provides an 80% improvement in build times, while
remaining 100% backwards compatible for themes and plugins.

As part of this move, we have decided to stop using a proxy in front of
Discourse for development. Development should now be done directly
against the Rails server.

`bin/ember-cli -u` has been replaced with `bin/dev`. This will launch
Rails on `:3000`, and will run the rolldown build in the background. Log
output from both processes will be shown with an appropriate prefix. You
should visit `:3000` in your browser. `:4200` will no longer serve
anything.

To help with migration, `bin/ember-cli` is now a backwards-compatible
shim. It will print help information, and will launch a lightweight
server on `:4200` with instructions to move to `:3000`.

If you prefer to launch Rails and the JS build as separate commands, you
can still do that. Rails boot commands are unchanged, and the rolldown
development builder can be run using `bin/dev --only ember`.

https://meta.discourse.org/t/403908

---------

Co-authored-by: Jarek Radosz <jarek@cvx.dev>
Co-authored-by: Chris Manson <chris@manson.ie>
2026-05-29 11:11:55 +01:00

388 lines
12 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseUpdates
class << self
def check_version
attrs = {
installed_version: Discourse::VERSION::STRING,
installed_sha: (Discourse.git_version == "unknown" ? nil : Discourse.git_version),
installed_describe: Discourse.full_version,
git_branch: Discourse.git_branch,
updated_at: updated_at,
}
unless updated_at.nil?
attrs.merge!(
latest_version: latest_version,
critical_updates: critical_updates_available?,
missing_versions_count: missing_versions_count,
)
end
version_info = DiscourseVersionCheck.new(attrs)
# replace -commit_count with +commit_count
if version_info.installed_describe =~ /-(\d+)-/
version_info.installed_describe =
version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}")
end
if SiteSetting.version_checks?
is_stale_data =
(
version_info.missing_versions_count == 0 &&
version_info.latest_version != version_info.installed_version
) ||
(
version_info.missing_versions_count != 0 &&
version_info.latest_version == version_info.installed_version
)
# Handle cases when version check data is old so we report something that makes sense
if version_info.updated_at.nil? || last_installed_version != Discourse::VERSION::STRING || # never performed a version check # updated since the last version check
is_stale_data
Jobs.enqueue(:call_discourse_hub, all_sites: true)
version_info.version_check_pending = true
unless version_info.updated_at.nil?
version_info.missing_versions_count = 0
version_info.critical_updates = false
end
end
version_info.stale_data =
version_info.version_check_pending || (updated_at && updated_at < 48.hours.ago) ||
is_stale_data
end
version_info
end
# last_installed_version is the installed version at the time of the last version check
def last_installed_version
Discourse.redis.get last_installed_version_key
end
def last_installed_version=(arg)
Discourse.redis.set(last_installed_version_key, arg)
end
def latest_version
Discourse.redis.get latest_version_key
end
def latest_version=(arg)
Discourse.redis.set(latest_version_key, arg)
end
def missing_versions_count
Discourse.redis.get(missing_versions_count_key).try(:to_i)
end
def missing_versions_count=(arg)
Discourse.redis.set(missing_versions_count_key, arg)
end
def critical_updates_available?
(Discourse.redis.get(critical_updates_available_key) || false) == "true"
end
def critical_updates_available=(arg)
Discourse.redis.set(critical_updates_available_key, arg)
end
def updated_at
t = Discourse.redis.get(updated_at_key)
t ? Time.zone.parse(t) : nil
end
def updated_at=(time_with_zone)
Discourse.redis.set updated_at_key, time_with_zone.as_json
end
def missing_versions=(versions)
# delete previous list from redis
prev_keys = Discourse.redis.lrange(missing_versions_list_key, 0, 4)
if prev_keys
Discourse.redis.del prev_keys
Discourse.redis.del(missing_versions_list_key)
end
if versions.present?
# store the list in redis
version_keys = []
versions[0, 5].each do |v|
key = "#{missing_versions_key_prefix}:#{v["version"]}"
Discourse.redis.mapped_hmset key, v.slice("version", "notes")
version_keys << key
end
Discourse.redis.rpush missing_versions_list_key, version_keys
end
versions || []
end
def missing_versions
keys = Discourse.redis.lrange(missing_versions_list_key, 0, 4) # max of 5 versions
keys.present? ? keys.map { |k| Discourse.redis.hgetall(k) } : []
end
def current_version
last_installed_version || Discourse::VERSION::STRING
end
def new_features(force_refresh: false)
update_new_features if force_refresh
entries =
begin
JSON.parse(Discourse.redis.get(new_features_key))
rescue StandardError
nil
end
return [] if entries.blank?
entries.select! do |item|
valid_version =
item["discourse_version"].nil? ||
Discourse.has_needed_version?(current_version, item["discourse_version"]) ||
GitUtils.has_commit?(item["discourse_version"])
valid_plugin_name =
item["plugin_name"].blank? || Discourse.plugins_by_name[item["plugin_name"]].present?
valid_version && valid_plugin_name
rescue StandardError
false
end
entries.sort_by { |item| Time.zone.parse(item["created_at"]).to_i }.reverse
end
def update_new_features(response_json = nil)
response_json ||= new_features_response_json
Discourse.redis.set(new_features_key, response_json)
Discourse.redis.del(latest_new_feature_created_at_key)
end
def new_features_response_json
response =
Excon.new(new_features_full_endpoint_url).request(
expects: [200],
method: :Get,
read_timeout: 5,
)
response.body
end
def merge_new_features_with_upcoming_changes(new_features)
# Any new features that have an upcoming_change_setting_name that is not
# in the permanent upcoming changes list are excluded, since sites can
# have different versions of Discourse deployed and may not have the
# upcoming change or the same status.
permanent_upcoming_change_names =
UpcomingChanges.permanent_upcoming_changes.map { |uc| uc[:setting].to_s }
new_features.reject! do |feature|
if feature[:upcoming_change_setting_name].present?
!permanent_upcoming_change_names.include?(feature[:upcoming_change_setting_name])
else
false
end
end
return new_features if permanent_upcoming_change_names.blank?
# Any permanent upcoming changes that have not been overridden/attached
# in the new features feed have to be injected into the array.
upcoming_changes_to_inject =
UpcomingChanges.permanent_upcoming_changes.reject do |permanent_uc|
new_features.any? do |feature|
feature[:upcoming_change_setting_name] == permanent_uc[:setting].to_s
end
end
upcoming_changes_to_inject.each do |permanent_uc|
# We track whenever an upcoming change moves from one status to another,
# the most logical datetime to use for created/released at in the UI is
# the datetime that the upcoming change became permanent on this site.
became_permanent_at =
UpcomingChanges.current_statuses.dig(permanent_uc[:setting].to_s, :changed_at) ||
Time.zone.now
new_features << {
title: permanent_uc[:humanized_name],
description: permanent_uc[:description],
link: permanent_uc[:upcoming_change][:learn_more_url],
created_at: became_permanent_at,
updated_at: became_permanent_at,
released_at: became_permanent_at,
screenshot_url: permanent_uc.dig(:upcoming_change, :image, :url),
upcoming_change_setting_name: permanent_uc[:setting],
}
end
new_features
.sort_by do |item|
if item[:created_at].is_a?(String)
Time.zone.parse(item[:created_at]).to_i
else
item[:created_at].to_i
end
end
.reverse
end
def has_unseen_features?(user_id)
latest_ts = latest_new_feature_created_at
return false if latest_ts.nil?
last_seen = new_features_last_seen(user_id)
return true if last_seen.nil?
latest_ts.to_i > last_seen.to_i
end
def latest_new_feature_created_at
cached = Discourse.redis.get(latest_new_feature_created_at_key)
return Time.zone.parse(cached) if cached.present?
entries =
merge_new_features_with_upcoming_changes(
new_features&.map { |item| item.symbolize_keys } || [],
)
return nil if entries.blank?
max_entry =
entries.max_by do |item|
val = item[:created_at]
val.is_a?(String) ? Time.zone.parse(val).to_i : val.to_i
end
max_created_at =
if max_entry[:created_at].is_a?(String)
Time.zone.parse(max_entry[:created_at])
else
max_entry[:created_at]
end
Discourse.redis.set(latest_new_feature_created_at_key, max_created_at.iso8601)
max_created_at
end
def clear_latest_new_feature_created_at_cache
Discourse.redis.del(latest_new_feature_created_at_key)
rescue Redis::CannotConnectError
end
def new_features_last_seen(user_id)
last_seen = Discourse.redis.get new_features_last_seen_key(user_id)
return nil if last_seen.blank?
Time.zone.parse(last_seen)
end
def mark_new_features_as_seen(user_id)
entries =
merge_new_features_with_upcoming_changes(
new_features&.map { |item| item.symbolize_keys } || [],
)
return nil if entries.blank?
last_seen =
entries.max_by do |item|
val = item.with_indifferent_access[:created_at]
val.is_a?(String) ? Time.zone.parse(val).to_i : val.to_i
end
Discourse.redis.set(
new_features_last_seen_key(user_id),
last_seen.with_indifferent_access[:created_at],
)
end
def get_last_viewed_feature_date(user_id)
date = Discourse.redis.hget(last_viewed_feature_dates_for_users_key, user_id.to_s)
return if date.blank?
Time.zone.parse(date)
end
def bump_last_viewed_feature_date(user_id, feature_date)
Discourse.redis.hset(last_viewed_feature_dates_for_users_key, user_id.to_s, feature_date.to_s)
end
def clean_state
Discourse.redis.del(
last_installed_version_key,
latest_version_key,
critical_updates_available_key,
missing_versions_count_key,
updated_at_key,
missing_versions_list_key,
new_features_key,
last_viewed_feature_dates_for_users_key,
latest_new_feature_created_at_key,
*Discourse.redis.keys("#{missing_versions_key_prefix}*"),
*Discourse.redis.keys(new_features_last_seen_key("*")),
)
end
def new_features_full_endpoint_url
"#{new_features_endpoint}#{new_features_endpoint_query_params}"
end
def new_features_endpoint
return "https://meta.discourse.org/new-features.json" if Rails.env.production?
ENV["DISCOURSE_NEW_FEATURES_ENDPOINT"] || "http://localhost:3000/new-features.json"
end
# We no longer delete new features on Meta, so we need to filter out old ones.
# Maybe in future we can show even older ones in a separate Archived tab.
def new_features_endpoint_query_params
"?released_after=#{5.months.ago.end_of_month.to_date}"
end
private
def last_installed_version_key
"last_installed_version"
end
def latest_version_key
"discourse_latest_version"
end
def critical_updates_available_key
"critical_updates_available"
end
def missing_versions_count_key
"missing_versions_count"
end
def updated_at_key
"last_version_check_at"
end
def missing_versions_list_key
"missing_versions"
end
def missing_versions_key_prefix
"missing_version"
end
def new_features_key
"new_features"
end
def new_features_last_seen_key(user_id)
"new_features_last_seen_user_#{user_id}"
end
def latest_new_feature_created_at_key
"latest_new_feature_created_at"
end
def last_viewed_feature_dates_for_users_key
"last_viewed_feature_dates_for_users_hash"
end
end
end