Add admin notifications for new Mastodon versions (#26582)
This commit is contained in:
parent
be991f1d18
commit
16681e0f20
39 changed files with 892 additions and 8 deletions
18
app/controllers/admin/software_updates_controller.rb
Normal file
18
app/controllers/admin/software_updates_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class SoftwareUpdatesController < BaseController
|
||||||
|
before_action :check_enabled!
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :software_update, :index?
|
||||||
|
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_enabled!
|
||||||
|
not_found unless SoftwareUpdate.check_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export const CriticalUpdateBanner = () => (
|
||||||
|
<div className='warning-banner'>
|
||||||
|
<div className='warning-banner__message'>
|
||||||
|
<h1>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.title'
|
||||||
|
defaultMessage='Critical security update available!'
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.body'
|
||||||
|
defaultMessage='Please update your Mastodon server as soon as possible!'
|
||||||
|
/>{' '}
|
||||||
|
<a href='/admin/software_updates'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.link'
|
||||||
|
defaultMessage='See updates'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me, criticalUpdatesPending } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
import { ColumnSettings } from './components/column_settings';
|
import { ColumnSettings } from './components/column_settings';
|
||||||
|
import { CriticalUpdateBanner } from './components/critical_update_banner';
|
||||||
import { ExplorePrompt } from './components/explore_prompt';
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent {
|
||||||
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
const banners = [];
|
||||||
|
|
||||||
let announcementsButton, banner;
|
let announcementsButton;
|
||||||
|
|
||||||
if (hasAnnouncements) {
|
if (hasAnnouncements) {
|
||||||
announcementsButton = (
|
announcementsButton = (
|
||||||
|
@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (criticalUpdatesPending) {
|
||||||
|
banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
|
||||||
|
}
|
||||||
|
|
||||||
if (tooSlow) {
|
if (tooSlow) {
|
||||||
banner = <ExplorePrompt />;
|
banners.push(<ExplorePrompt key='explore-prompt' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
prepend={banner}
|
prepend={banners}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
* @typedef InitialState
|
* @typedef InitialState
|
||||||
* @property {Record<string, Account>} accounts
|
* @property {Record<string, Account>} accounts
|
||||||
* @property {InitialStateLanguage[]} languages
|
* @property {InitialStateLanguage[]} languages
|
||||||
|
* @property {boolean=} critical_updates_pending
|
||||||
* @property {InitialStateMeta} meta
|
* @property {InitialStateMeta} meta
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ export const useBlurhash = getMeta('use_blurhash');
|
||||||
export const usePendingItems = getMeta('use_pending_items');
|
export const usePendingItems = getMeta('use_pending_items');
|
||||||
export const version = getMeta('version');
|
export const version = getMeta('version');
|
||||||
export const languages = initialState?.languages;
|
export const languages = initialState?.languages;
|
||||||
|
export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const sso_redirect = getMeta('sso_redirect');
|
export const sso_redirect = getMeta('sso_redirect');
|
||||||
|
|
|
@ -310,6 +310,9 @@
|
||||||
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
|
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
|
||||||
"home.explore_prompt.title": "This is your home base within Mastodon.",
|
"home.explore_prompt.title": "This is your home base within Mastodon.",
|
||||||
"home.hide_announcements": "Hide announcements",
|
"home.hide_announcements": "Hide announcements",
|
||||||
|
"home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
|
||||||
|
"home.pending_critical_update.link": "See updates",
|
||||||
|
"home.pending_critical_update.title": "Critical security update available!",
|
||||||
"home.show_announcements": "Show announcements",
|
"home.show_announcements": "Show announcements",
|
||||||
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
|
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
|
||||||
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
||||||
|
|
|
@ -143,6 +143,11 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning a {
|
||||||
|
color: $gold-star;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.simple-navigation-active-leaf a {
|
.simple-navigation-active-leaf a {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-highlight-color;
|
||||||
|
|
|
@ -8860,7 +8860,8 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismissable-banner {
|
.dismissable-banner,
|
||||||
|
.warning-banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
@ -8938,6 +8939,21 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning-banner {
|
||||||
|
border: 1px solid $warning-red;
|
||||||
|
background: rgba($warning-red, 0.15);
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
h1 {
|
||||||
|
color: $warning-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
border-top: 1px solid $ui-base-color;
|
border-top: 1px solid $ui-base-color;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
|
||||||
|
&.critical {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $gold-star;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > thead > tr > th {
|
& > thead > tr > th {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Admin::SystemCheck
|
class Admin::SystemCheck
|
||||||
ACTIVE_CHECKS = [
|
ACTIVE_CHECKS = [
|
||||||
|
Admin::SystemCheck::SoftwareVersionCheck,
|
||||||
Admin::SystemCheck::MediaPrivacyCheck,
|
Admin::SystemCheck::MediaPrivacyCheck,
|
||||||
Admin::SystemCheck::DatabaseSchemaCheck,
|
Admin::SystemCheck::DatabaseSchemaCheck,
|
||||||
Admin::SystemCheck::SidekiqProcessCheck,
|
Admin::SystemCheck::SidekiqProcessCheck,
|
||||||
|
|
27
app/lib/admin/system_check/software_version_check.rb
Normal file
27
app/lib/admin/system_check/software_version_check.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::SystemCheck::SoftwareVersionCheck < Admin::SystemCheck::BaseCheck
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
def skip?
|
||||||
|
!current_user.can?(:view_devops) || !SoftwareUpdate.check_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pass?
|
||||||
|
software_updates.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
if software_updates.any?(&:urgent?)
|
||||||
|
Admin::SystemCheck::Message.new(:software_version_critical_check, nil, admin_software_updates_path, true)
|
||||||
|
else
|
||||||
|
Admin::SystemCheck::Message.new(:software_version_patch_check, nil, admin_software_updates_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def software_updates
|
||||||
|
@software_updates ||= SoftwareUpdate.pending_to_a.filter { |update| update.urgent? || update.patch_type? }
|
||||||
|
end
|
||||||
|
end
|
|
@ -45,6 +45,22 @@ class AdminMailer < ApplicationMailer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_software_updates
|
||||||
|
locale_for_account(@me) do
|
||||||
|
mail subject: default_i18n_subject(instance: @instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_critical_software_updates
|
||||||
|
headers['Priority'] = 'urgent'
|
||||||
|
headers['X-Priority'] = '1'
|
||||||
|
headers['Importance'] = 'high'
|
||||||
|
|
||||||
|
locale_for_account(@me) do
|
||||||
|
mail subject: default_i18n_subject(instance: @instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_params
|
def process_params
|
||||||
|
|
40
app/models/software_update.rb
Normal file
40
app/models/software_update.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: software_updates
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# version :string not null
|
||||||
|
# urgent :boolean default(FALSE), not null
|
||||||
|
# type :integer default("patch"), not null
|
||||||
|
# release_notes :string default(""), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class SoftwareUpdate < ApplicationRecord
|
||||||
|
self.inheritance_column = nil
|
||||||
|
|
||||||
|
enum type: { patch: 0, minor: 1, major: 2 }, _suffix: :type
|
||||||
|
|
||||||
|
def gem_version
|
||||||
|
Gem::Version.new(version)
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def check_enabled?
|
||||||
|
ENV['UPDATE_CHECK_URL'] != ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending_to_a
|
||||||
|
return [] unless check_enabled?
|
||||||
|
|
||||||
|
all.to_a.filter { |update| update.gem_version > Mastodon::Version.gem_version }
|
||||||
|
end
|
||||||
|
|
||||||
|
def urgent_pending?
|
||||||
|
pending_to_a.any?(&:urgent?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -44,6 +44,7 @@ class UserSettings
|
||||||
setting :pending_account, default: true
|
setting :pending_account, default: true
|
||||||
setting :trends, default: true
|
setting :trends, default: true
|
||||||
setting :appeal, default: true
|
setting :appeal, default: true
|
||||||
|
setting :software_updates, default: 'critical', in: %w(none critical patch all)
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :interactions do
|
namespace :interactions do
|
||||||
|
|
7
app/policies/software_update_policy.rb
Normal file
7
app/policies/software_update_policy.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SoftwareUpdatePolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
role.can?(:view_devops)
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,9 +3,13 @@
|
||||||
class InitialStatePresenter < ActiveModelSerializers::Model
|
class InitialStatePresenter < ActiveModelSerializers::Model
|
||||||
attributes :settings, :push_subscription, :token,
|
attributes :settings, :push_subscription, :token,
|
||||||
:current_account, :admin, :owner, :text, :visibility,
|
:current_account, :admin, :owner, :text, :visibility,
|
||||||
:disabled_account, :moved_to_account
|
:disabled_account, :moved_to_account, :critical_updates_pending
|
||||||
|
|
||||||
def role
|
def role
|
||||||
current_account&.user_role
|
current_account&.user_role
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def critical_updates_pending
|
||||||
|
role&.can?(:view_devops) && SoftwareUpdate.urgent_pending?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
:media_attachments, :settings,
|
:media_attachments, :settings,
|
||||||
:languages
|
:languages
|
||||||
|
|
||||||
|
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
|
||||||
|
|
||||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
has_one :role, serializer: REST::RoleSerializer
|
has_one :role, serializer: REST::RoleSerializer
|
||||||
|
|
||||||
|
|
82
app/services/software_update_check_service.rb
Normal file
82
app/services/software_update_check_service.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SoftwareUpdateCheckService < BaseService
|
||||||
|
def call
|
||||||
|
clean_outdated_updates!
|
||||||
|
return unless SoftwareUpdate.check_enabled?
|
||||||
|
|
||||||
|
process_update_notices!(fetch_update_notices)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def clean_outdated_updates!
|
||||||
|
SoftwareUpdate.find_each do |software_update|
|
||||||
|
software_update.delete if Mastodon::Version.gem_version >= software_update.gem_version
|
||||||
|
rescue ArgumentError
|
||||||
|
software_update.delete
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_update_notices
|
||||||
|
Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res|
|
||||||
|
return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
|
||||||
|
end
|
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_url
|
||||||
|
ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')
|
||||||
|
end
|
||||||
|
|
||||||
|
def version
|
||||||
|
@version ||= Mastodon::Version.to_s.split('+')[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_update_notices!(update_notices)
|
||||||
|
return if update_notices.blank? || update_notices['updatesAvailable'].blank?
|
||||||
|
|
||||||
|
# Clear notices that are not listed by the update server anymore
|
||||||
|
SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].pluck('version')).delete_all
|
||||||
|
|
||||||
|
# Check if any of the notices is new, and issue notifications
|
||||||
|
known_versions = SoftwareUpdate.where(version: update_notices['updatesAvailable'].pluck('version')).pluck(:version)
|
||||||
|
new_update_notices = update_notices['updatesAvailable'].filter { |notice| known_versions.exclude?(notice['version']) }
|
||||||
|
return if new_update_notices.blank?
|
||||||
|
|
||||||
|
new_updates = new_update_notices.map do |notice|
|
||||||
|
SoftwareUpdate.create!(version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes'])
|
||||||
|
end
|
||||||
|
|
||||||
|
notify_devops!(new_updates)
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_notify_user?(user, urgent_version, patch_version)
|
||||||
|
case user.settings['notification_emails.software_updates']
|
||||||
|
when 'none'
|
||||||
|
false
|
||||||
|
when 'critical'
|
||||||
|
urgent_version
|
||||||
|
when 'patch'
|
||||||
|
urgent_version || patch_version
|
||||||
|
when 'all'
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_devops!(new_updates)
|
||||||
|
has_new_urgent_version = new_updates.any?(&:urgent?)
|
||||||
|
has_new_patch_version = new_updates.any?(&:patch_type?)
|
||||||
|
|
||||||
|
User.those_who_can(:view_devops).includes(:account).find_each do |user|
|
||||||
|
next unless should_notify_user?(user, has_new_urgent_version, has_new_patch_version)
|
||||||
|
|
||||||
|
if has_new_urgent_version
|
||||||
|
AdminMailer.with(recipient: user.account).new_critical_software_updates.deliver_later
|
||||||
|
else
|
||||||
|
AdminMailer.with(recipient: user.account).new_software_updates.deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
app/views/admin/software_updates/index.html.haml
Normal file
29
app/views/admin/software_updates/index.html.haml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.software_updates.title')
|
||||||
|
|
||||||
|
.simple_form
|
||||||
|
%p.lead
|
||||||
|
= t('admin.software_updates.description')
|
||||||
|
= link_to t('admin.software_updates.documentation_link'), 'https://docs.joinmastodon.org/admin/upgrading/#automated_checks', target: '_new'
|
||||||
|
|
||||||
|
%hr.spacer
|
||||||
|
|
||||||
|
- unless @software_updates.empty?
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('admin.software_updates.version')
|
||||||
|
%th= t('admin.software_updates.type')
|
||||||
|
%th
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @software_updates.each do |update|
|
||||||
|
%tr
|
||||||
|
%td= update.version
|
||||||
|
%td= t("admin.software_updates.types.#{update.type}")
|
||||||
|
- if update.urgent?
|
||||||
|
%td.critical= t("admin.software_updates.critical_update")
|
||||||
|
- else
|
||||||
|
%td
|
||||||
|
%td= table_link_to 'link', t('admin.software_updates.release_notes'), update.release_notes
|
|
@ -0,0 +1,5 @@
|
||||||
|
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
|
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_critical_software_updates.body') %>
|
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
|
5
app/views/admin_mailer/new_software_updates.text.erb
Normal file
5
app/views/admin_mailer/new_software_updates.text.erb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
|
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_software_updates.body') %>
|
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
|
|
@ -22,7 +22,7 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails')
|
= ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails')
|
||||||
|
|
||||||
- if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies)
|
- if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops))
|
||||||
%h4= t 'notifications.administration_emails'
|
%h4= t 'notifications.administration_emails'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
@ -31,6 +31,10 @@
|
||||||
= ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users)
|
= ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users)
|
||||||
= ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies)
|
= ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies)
|
||||||
|
|
||||||
|
- if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
|
||||||
|
.fields-group
|
||||||
|
= ff.input :'notification_emails.software_updates', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false
|
||||||
|
|
||||||
%h4= t 'notifications.other_settings'
|
%h4= t 'notifications.other_settings'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
11
app/workers/scheduler/software_update_check_scheduler.rb
Normal file
11
app/workers/scheduler/software_update_check_scheduler.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Scheduler::SoftwareUpdateCheckScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.hour.to_i
|
||||||
|
|
||||||
|
def perform
|
||||||
|
SoftwareUpdateCheckService.new.call
|
||||||
|
end
|
||||||
|
end
|
|
@ -309,6 +309,7 @@ en:
|
||||||
unpublish: Unpublish
|
unpublish: Unpublish
|
||||||
unpublished_msg: Announcement successfully unpublished!
|
unpublished_msg: Announcement successfully unpublished!
|
||||||
updated_msg: Announcement successfully updated!
|
updated_msg: Announcement successfully updated!
|
||||||
|
critical_update_pending: Critical update pending
|
||||||
custom_emojis:
|
custom_emojis:
|
||||||
assign_category: Assign category
|
assign_category: Assign category
|
||||||
by_domain: Domain
|
by_domain: Domain
|
||||||
|
@ -779,6 +780,18 @@ en:
|
||||||
site_uploads:
|
site_uploads:
|
||||||
delete: Delete uploaded file
|
delete: Delete uploaded file
|
||||||
destroyed_msg: Site upload successfully deleted!
|
destroyed_msg: Site upload successfully deleted!
|
||||||
|
software_updates:
|
||||||
|
critical_update: Critical — please update quickly
|
||||||
|
description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your e-mail notification preferences.
|
||||||
|
documentation_link: Learn more
|
||||||
|
release_notes: Release notes
|
||||||
|
title: Available updates
|
||||||
|
type: Type
|
||||||
|
types:
|
||||||
|
major: Major release
|
||||||
|
minor: Minor release
|
||||||
|
patch: Patch release — bugfixes and easy to apply changes
|
||||||
|
version: Version
|
||||||
statuses:
|
statuses:
|
||||||
account: Author
|
account: Author
|
||||||
application: Application
|
application: Application
|
||||||
|
@ -843,6 +856,12 @@ en:
|
||||||
message_html: You haven't defined any server rules.
|
message_html: You haven't defined any server rules.
|
||||||
sidekiq_process_check:
|
sidekiq_process_check:
|
||||||
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
|
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
|
||||||
|
software_version_critical_check:
|
||||||
|
action: See available updates
|
||||||
|
message_html: A critical Mastodon update is available, please update as quickly as possible.
|
||||||
|
software_version_patch_check:
|
||||||
|
action: See available updates
|
||||||
|
message_html: A bugfix Mastodon update is available.
|
||||||
upload_check_privacy_error:
|
upload_check_privacy_error:
|
||||||
action: Check here for more information
|
action: Check here for more information
|
||||||
message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
|
message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
|
||||||
|
@ -956,6 +975,9 @@ en:
|
||||||
body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
|
body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
|
||||||
next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
|
next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
|
||||||
subject: "%{username} is appealing a moderation decision on %{instance}"
|
subject: "%{username} is appealing a moderation decision on %{instance}"
|
||||||
|
new_critical_software_updates:
|
||||||
|
body: New critical versions of Mastodon have been released, you may want to update as soon as possible!
|
||||||
|
subject: Critical Mastodon updates are available for %{instance}!
|
||||||
new_pending_account:
|
new_pending_account:
|
||||||
body: The details of the new account are below. You can approve or reject this application.
|
body: The details of the new account are below. You can approve or reject this application.
|
||||||
subject: New account up for review on %{instance} (%{username})
|
subject: New account up for review on %{instance} (%{username})
|
||||||
|
@ -963,6 +985,9 @@ en:
|
||||||
body: "%{reporter} has reported %{target}"
|
body: "%{reporter} has reported %{target}"
|
||||||
body_remote: Someone from %{domain} has reported %{target}
|
body_remote: Someone from %{domain} has reported %{target}
|
||||||
subject: New report for %{instance} (#%{id})
|
subject: New report for %{instance} (#%{id})
|
||||||
|
new_software_updates:
|
||||||
|
body: New Mastodon versions have been released, you may want to update!
|
||||||
|
subject: New Mastodon versions are available for %{instance}!
|
||||||
new_trends:
|
new_trends:
|
||||||
body: 'The following items need a review before they can be displayed publicly:'
|
body: 'The following items need a review before they can be displayed publicly:'
|
||||||
new_trending_links:
|
new_trending_links:
|
||||||
|
|
|
@ -291,6 +291,12 @@ en:
|
||||||
pending_account: New account needs review
|
pending_account: New account needs review
|
||||||
reblog: Someone boosted your post
|
reblog: Someone boosted your post
|
||||||
report: New report is submitted
|
report: New report is submitted
|
||||||
|
software_updates:
|
||||||
|
all: Notify on all updates
|
||||||
|
critical: Notify on critical updates only
|
||||||
|
label: A new Mastodon version is available
|
||||||
|
none: Never notify of updates (not recommended)
|
||||||
|
patch: Notify on bugfix updates
|
||||||
trending_tag: New trend requires review
|
trending_tag: New trend requires review
|
||||||
rule:
|
rule:
|
||||||
text: Rule
|
text: Rule
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
SimpleNavigation::Configuration.run do |navigation|
|
SimpleNavigation::Configuration.run do |navigation|
|
||||||
navigation.items do |n|
|
navigation.items do |n|
|
||||||
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
|
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
|
||||||
|
|
||||||
|
n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' }
|
||||||
|
|
||||||
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy}
|
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy}
|
||||||
|
|
||||||
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
|
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
|
||||||
|
|
|
@ -201,4 +201,6 @@ namespace :admin do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :software_updates, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,3 +58,7 @@
|
||||||
interval: 1 minute
|
interval: 1 minute
|
||||||
class: Scheduler::SuspendedUserCleanupScheduler
|
class: Scheduler::SuspendedUserCleanupScheduler
|
||||||
queue: scheduler
|
queue: scheduler
|
||||||
|
software_update_check_scheduler:
|
||||||
|
interval: 30 minutes
|
||||||
|
class: Scheduler::SoftwareUpdateCheckScheduler
|
||||||
|
queue: scheduler
|
||||||
|
|
16
db/migrate/20230822081029_create_software_updates.rb
Normal file
16
db/migrate/20230822081029_create_software_updates.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateSoftwareUpdates < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :software_updates do |t|
|
||||||
|
t.string :version, null: false
|
||||||
|
t.boolean :urgent, default: false, null: false
|
||||||
|
t.integer :type, default: 0, null: false
|
||||||
|
t.string :release_notes, default: '', null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :software_updates, :version, unique: true
|
||||||
|
end
|
||||||
|
end
|
12
db/schema.rb
12
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do
|
ActiveRecord::Schema[7.0].define(version: 2023_08_22_081029) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -903,6 +903,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do
|
||||||
t.index ["var"], name: "index_site_uploads_on_var", unique: true
|
t.index ["var"], name: "index_site_uploads_on_var", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "software_updates", force: :cascade do |t|
|
||||||
|
t.string "version", null: false
|
||||||
|
t.boolean "urgent", default: false, null: false
|
||||||
|
t.integer "type", default: 0, null: false
|
||||||
|
t.string "release_notes", default: "", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["version"], name: "index_software_updates_on_version", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "status_edits", force: :cascade do |t|
|
create_table "status_edits", force: :cascade do |t|
|
||||||
t.bigint "status_id", null: false
|
t.bigint "status_id", null: false
|
||||||
t.bigint "account_id"
|
t.bigint "account_id"
|
||||||
|
|
|
@ -39,6 +39,10 @@ module Mastodon
|
||||||
components.join
|
components.join
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gem_version
|
||||||
|
@gem_version ||= Gem::Version.new(to_s.split('+')[0])
|
||||||
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
|
ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
|
||||||
end
|
end
|
||||||
|
|
|
@ -424,6 +424,10 @@ namespace :mastodon do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
prompt.say "\n"
|
||||||
|
|
||||||
|
env['UPDATE_CHECK_URL'] = '' unless prompt.yes?('Do you want Mastodon to periodically check for important updates and notify you? (Recommended)', default: true)
|
||||||
|
|
||||||
prompt.say "\n"
|
prompt.say "\n"
|
||||||
prompt.say 'This configuration will be written to .env.production'
|
prompt.say 'This configuration will be written to .env.production'
|
||||||
|
|
||||||
|
|
7
spec/fabricators/software_update_fabricator.rb
Normal file
7
spec/fabricators/software_update_fabricator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:software_update) do
|
||||||
|
version '99.99.99'
|
||||||
|
urgent false
|
||||||
|
type 'patch'
|
||||||
|
end
|
23
spec/features/admin/software_updates_spec.rb
Normal file
23
spec/features/admin/software_updates_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'finding software updates through the admin interface' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99')
|
||||||
|
|
||||||
|
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows a link to the software updates page, which links to release notes' do
|
||||||
|
visit settings_profile_path
|
||||||
|
click_on I18n.t('admin.critical_update_pending')
|
||||||
|
|
||||||
|
expect(page).to have_title(I18n.t('admin.software_updates.title'))
|
||||||
|
|
||||||
|
expect(page).to have_content('99.99.99')
|
||||||
|
|
||||||
|
click_on I18n.t('admin.software_updates.release_notes')
|
||||||
|
expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
|
||||||
|
end
|
||||||
|
end
|
133
spec/lib/admin/system_check/software_version_check_spec.rb
Normal file
133
spec/lib/admin/system_check/software_version_check_spec.rb
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::SystemCheck::SoftwareVersionCheck do
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
subject(:check) { described_class.new(user) }
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
describe 'skip?' do
|
||||||
|
context 'when user cannot view devops' do
|
||||||
|
before { allow(user).to receive(:can?).with(:view_devops).and_return(false) }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(check.skip?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user can view devops' do
|
||||||
|
before { allow(user).to receive(:can?).with(:view_devops).and_return(true) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(check.skip?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when checks are disabled' do
|
||||||
|
around do |example|
|
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: '' do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(check.skip?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'pass?' do
|
||||||
|
context 'when there is no known update' do
|
||||||
|
it 'returns true' do
|
||||||
|
expect(check.pass?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a non-urgent major release' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(check.pass?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is an urgent major release' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(check.pass?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is an urgent minor release' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'minor', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(check.pass?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is an urgent patch release' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(check.pass?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a non-urgent patch release' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(check.pass?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'message' do
|
||||||
|
context 'when there is a non-urgent patch release pending' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends class name symbol to message instance' do
|
||||||
|
allow(Admin::SystemCheck::Message).to receive(:new)
|
||||||
|
.with(:software_version_patch_check, anything, anything)
|
||||||
|
|
||||||
|
check.message
|
||||||
|
|
||||||
|
expect(Admin::SystemCheck::Message).to have_received(:new)
|
||||||
|
.with(:software_version_patch_check, nil, admin_software_updates_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is an urgent patch release pending' do
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends class name symbol to message instance' do
|
||||||
|
allow(Admin::SystemCheck::Message).to receive(:new)
|
||||||
|
.with(:software_version_critical_check, anything, anything, anything)
|
||||||
|
|
||||||
|
check.message
|
||||||
|
|
||||||
|
expect(Admin::SystemCheck::Message).to have_received(:new)
|
||||||
|
.with(:software_version_critical_check, nil, admin_software_updates_path, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -85,4 +85,46 @@ RSpec.describe AdminMailer do
|
||||||
expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
|
expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.new_software_updates' do
|
||||||
|
let(:recipient) { Fabricate(:account, username: 'Bob') }
|
||||||
|
let(:mail) { described_class.with(recipient: recipient).new_software_updates }
|
||||||
|
|
||||||
|
before do
|
||||||
|
recipient.user.update(locale: :en)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the headers' do
|
||||||
|
expect(mail.subject).to eq('New Mastodon versions are available for cb6e6126.ngrok.io!')
|
||||||
|
expect(mail.to).to eq [recipient.user_email]
|
||||||
|
expect(mail.from).to eq ['notifications@localhost']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the body' do
|
||||||
|
expect(mail.body.encoded).to match 'New Mastodon versions have been released, you may want to update!'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.new_critical_software_updates' do
|
||||||
|
let(:recipient) { Fabricate(:account, username: 'Bob') }
|
||||||
|
let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates }
|
||||||
|
|
||||||
|
before do
|
||||||
|
recipient.user.update(locale: :en)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the headers', :aggregate_failures do
|
||||||
|
expect(mail.subject).to eq('Critical Mastodon updates are available for cb6e6126.ngrok.io!')
|
||||||
|
expect(mail.to).to eq [recipient.user_email]
|
||||||
|
expect(mail.from).to eq ['notifications@localhost']
|
||||||
|
|
||||||
|
expect(mail['Importance'].value).to eq 'high'
|
||||||
|
expect(mail['Priority'].value).to eq 'urgent'
|
||||||
|
expect(mail['X-Priority'].value).to eq '1'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the body' do
|
||||||
|
expect(mail.body.encoded).to match 'New critical versions of Mastodon have been released, you may want to update as soon as possible!'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
87
spec/models/software_update_spec.rb
Normal file
87
spec/models/software_update_spec.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdate do
|
||||||
|
describe '.pending_to_a' do
|
||||||
|
before do
|
||||||
|
allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version))
|
||||||
|
|
||||||
|
Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true)
|
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false)
|
||||||
|
Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated release' do
|
||||||
|
let(:mastodon_version) { '3.4.0' }
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is more recent than anything last returned by the server' do
|
||||||
|
let(:mastodon_version) { '5.0.0' }
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to eq []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated nightly' do
|
||||||
|
let(:mastodon_version) { '4.3.0-nightly.2023-09-10' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is a very outdated nightly' do
|
||||||
|
let(:mastodon_version) { '4.2.0-nightly.2023-07-10' }
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated dev version' do
|
||||||
|
let(:mastodon_version) { '4.3.0-0.dev.0' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated beta version' do
|
||||||
|
let(:mastodon_version) { '4.3.0-beta1' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated beta version and there is a rc' do
|
||||||
|
let(:mastodon_version) { '4.3.0-beta1' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the expected versions' do
|
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
spec/policies/software_update_policy_spec.rb
Normal file
25
spec/policies/software_update_policy_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'pundit/rspec'
|
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdatePolicy do
|
||||||
|
subject { described_class }
|
||||||
|
|
||||||
|
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account }
|
||||||
|
let(:john) { Fabricate(:account) }
|
||||||
|
|
||||||
|
permissions :index? do
|
||||||
|
context 'when owner' do
|
||||||
|
it 'permits' do
|
||||||
|
expect(subject).to permit(admin, SoftwareUpdate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not owner' do
|
||||||
|
it 'denies' do
|
||||||
|
expect(subject).to_not permit(john, SoftwareUpdate)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
158
spec/services/software_update_check_service_spec.rb
Normal file
158
spec/services/software_update_check_service_spec.rb
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdateCheckService, type: :service do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
shared_examples 'when the feature is enabled' do
|
||||||
|
let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" }
|
||||||
|
|
||||||
|
let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) }
|
||||||
|
let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) }
|
||||||
|
let(:old_devops_user) { Fabricate(:user) }
|
||||||
|
let(:none_user) { Fabricate(:user, role: devops_role) }
|
||||||
|
let(:patch_user) { Fabricate(:user, role: devops_role) }
|
||||||
|
let(:critical_user) { Fabricate(:user, role: devops_role) }
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
queue_adapter = ActiveJob::Base.queue_adapter
|
||||||
|
ActiveJob::Base.queue_adapter = :test
|
||||||
|
|
||||||
|
example.run
|
||||||
|
|
||||||
|
ActiveJob::Base.queue_adapter = queue_adapter
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
|
||||||
|
Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false)
|
||||||
|
|
||||||
|
owner_user.settings.update('notification_emails.software_updates': 'all')
|
||||||
|
owner_user.save!
|
||||||
|
|
||||||
|
old_devops_user.settings.update('notification_emails.software_updates': 'all')
|
||||||
|
old_devops_user.save!
|
||||||
|
|
||||||
|
none_user.settings.update('notification_emails.software_updates': 'none')
|
||||||
|
none_user.save!
|
||||||
|
|
||||||
|
patch_user.settings.update('notification_emails.software_updates': 'patch')
|
||||||
|
patch_user.save!
|
||||||
|
|
||||||
|
critical_user.settings.update('notification_emails.software_updates': 'critical')
|
||||||
|
critical_user.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the update server errors out' do
|
||||||
|
before do
|
||||||
|
stub_request(:get, full_update_check_url).to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes outdated update records but keeps valid update records' do
|
||||||
|
expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the server returns new versions' do
|
||||||
|
let(:server_json) do
|
||||||
|
{
|
||||||
|
updatesAvailable: [
|
||||||
|
{
|
||||||
|
version: '4.2.1',
|
||||||
|
urgent: false,
|
||||||
|
type: 'patch',
|
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '4.3.0',
|
||||||
|
urgent: false,
|
||||||
|
type: 'minor',
|
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '5.0.0',
|
||||||
|
urgent: false,
|
||||||
|
type: 'minor',
|
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the list of known updates' do
|
||||||
|
expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0'])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no update is urgent' do
|
||||||
|
it 'sends e-mail notifications according to settings', :aggregate_failures do
|
||||||
|
expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates)
|
||||||
|
.with(hash_including(params: { recipient: owner_user.account })).once
|
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
|
||||||
|
.and(have_enqueued_mail.at_most(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an update is urgent' do
|
||||||
|
let(:server_json) do
|
||||||
|
{
|
||||||
|
updatesAvailable: [
|
||||||
|
{
|
||||||
|
version: '5.0.0',
|
||||||
|
urgent: true,
|
||||||
|
type: 'minor',
|
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends e-mail notifications according to settings', :aggregate_failures do
|
||||||
|
expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates)
|
||||||
|
.with(hash_including(params: { recipient: owner_user.account })).once
|
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
|
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once)
|
||||||
|
.and(have_enqueued_mail.at_most(3))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when update checking is disabled' do
|
||||||
|
around do |example|
|
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: '' do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes outdated update records' do
|
||||||
|
expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when using the default update checking API' do
|
||||||
|
let(:update_check_url) { 'https://api.joinmastodon.org/update-check' }
|
||||||
|
|
||||||
|
it_behaves_like 'when the feature is enabled'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when using a custom update check URL' do
|
||||||
|
let(:update_check_url) { 'https://api.example.com/update_check' }
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'when the feature is enabled'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Scheduler::SoftwareUpdateCheckScheduler do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
describe 'perform' do
|
||||||
|
let(:service_double) { instance_double(SoftwareUpdateCheckService, call: nil) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(SoftwareUpdateCheckService).to receive(:new).and_return(service_double)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls SoftwareUpdateCheckService' do
|
||||||
|
subject.perform
|
||||||
|
expect(service_double).to have_received(:call)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue