mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +08:00
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>
388 lines
12 KiB
Ruby
Vendored
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
|