Use when modifying, debugging, or extending the upcoming changes framework code and system itself.
This skill is for working on the upcoming changes framework itself — the internal machinery that powers feature flag rollout in Discourse. For adding a new feature flag using the framework, see the discourse-upcoming-changes skill instead.
The upcoming changes system has three layers: a Ruby core that manages state and business logic, a services layer that orchestrates tracking/notifications/toggling, and an Ember frontend that renders the admin UI and applies per-user overrides.
lib/upcoming_changes.rb — The central module. All business logic for resolving values, checking user eligibility, caching, and image handling lives here.
Key methods to understand:
resolved_value(setting_name) — Determines the effective value of a setting. This is where auto-promotion logic lives: if a setting's status meets/exceeds , the resolved value is even if the DB default is . Permanent settings always resolve to (admins can't disable them).promote_upcoming_changes_on_statustruefalsetrueenabled_for_user?(setting_name, user) — The primary access check. Considers: resolved value, group restrictions, anonymous users (only get access if no group restrictions).stats_for_user(user:, acting_guardian:) — Returns per-change status for a user including why they have/don't have access (the user_enabled_reasons enum).current_statuses / permanent_upcoming_changes — Cached lookups keyed by git version (one-time cost per deploy). Cleared by clear_caches! and automatically when TrackNotifyStatusChanges detects changes.app/models/upcoming_change_event.rb — Audit trail. Every lifecycle event (added, removed, status change, manual toggle, admin notification) is recorded here. Has unique indexes to prevent duplicate events of specific types per change.
lib/site_setting_extension.rb — Where upcoming_change: metadata in site_settings.yml gets parsed. When a setting is registered with this metadata, it stores the parsed result in @upcoming_change_metadata and defines a {name}_groups_map method. The impact string is split into impact_type and impact_role. Also handles upcoming_change_default_override: metadata — see Default Overrides below.
lib/site_settings/defaults_provider.rb — Manages the default values for all settings, including upcoming change default overrides. Tracks which overrides are active via @active_upcoming_change_overrides and applies them when resolving defaults. Provides upcoming_change_override_metadata for the frontend to display warnings about changed defaults.
app/models/site_setting_group.rb — Stores group restrictions for settings. Group IDs are pipe-separated strings ("1|2|3"). The setting_group_ids class method returns a hash used for in-memory caching.
All services use Service::Base. They're organized under app/services/upcoming_changes/:
| Service | Purpose |
|---|---|
List | Admin-only, fetches all changes with metadata, group data, and images |
Toggle | Admin enable/disable — updates SiteSetting, clears groups if disallow_enabled_for_groups, logs staff action, fires DiscourseEvent |
Track | Orchestrator called by the scheduled job — delegates to three action sub-services |
TrackNotifyAddedChanges | Compares current settings against event history, creates added events |
TrackRemovedChanges | Creates removed events for settings no longer present |
TrackNotifyStatusChanges | Detects status changes in metadata, creates events, clears caches |
NotifyPromotions | Iterates all changes and calls NotifyPromotion for each |
NotifyPromotion | Handles one promotion — checks policies, merges notifications, fires events |
NotifyAdminsOfAvailableChange | Notifies admins when a change reaches one status below promotion threshold |
NotificationDataMerger | Consolidates multiple change notifications into one to avoid spam |
SiteSetting::UpsertGroups — Manages group assignments for settings (upserts SiteSettingGroup, refreshes caches, notifies clients).
app/jobs/scheduled/check_upcoming_changes.rb — Runs every 20 minutes inside a DistributedMutex. Calls Track then NotifyPromotions. Supports verbose logging via the upcoming_change_verbose_logging setting.
Admin page — admin/templates/admin-config/upcoming-changes.gjs renders the page header, admin/components/admin-config-areas/upcoming-changes.gjs is the container with filtering, and admin/components/admin-config-areas/upcoming-change-item.gjs renders each row.
Key frontend patterns:
AdminFilterControlsSite settings service (app/services/site-settings.js) — Loads upcoming changes from PreloadStore, applies them as overrides to site settings, and stores them in settings.currentUserUpcomingChanges.
Body CSS classes — app/controllers/application.js generates uc-{dasherized-key} classes on <body> for each enabled upcoming change, allowing CSS-based feature gating.
Notifications — Two notification types (upcoming-change-available, upcoming-change-automatically-promoted) handle singular/dual/many change descriptions and link to the admin page with filter params.
Sidebar — Badge notification dot appears on the upcoming changes link when currentUser.hasNewUpcomingChanges is true.
MessageBus — Subscribes to /client_settings and updates both siteSettings and currentUserUpcomingChanges in real time.
admin/config/upcoming_changes_controller.rb — Three endpoints:
GET index — List changes (with filter_statuses param)PUT update_groups — Set group restrictions for a settingPUT toggle_change — Enable/disable a settingapp/services/problem_check/upcoming_change_stable_opted_out.rb — Warns admins hourly if they've opted out of a stable/permanent change.
Upcoming changes can override the default value of a different site setting when enabled. This allows feature rollouts to change related setting defaults without breaking admin customizations.
A setting declares a default override with the upcoming_change_default_override key in config/site_settings.yml:
# The upcoming change setting (the "trigger")
increase_suggested_topics_max_days_old_default:
default: false
type: bool
upcoming_change:
status: experimental
impact: "site_setting_default,all_members"
# The setting whose default changes (the "target")
suggested_topics_max_days_old:
default: 365
type: integer
upcoming_change_default_override:
upcoming_change: increase_suggested_topics_max_days_old_default
new_default: 1000
When increase_suggested_topics_max_days_old_default is enabled (either manually by admin or via auto-promotion), the default value of suggested_topics_max_days_old changes from 365 to 1000. The impact field on the trigger setting should include site_setting_default as its impact_type.
Registration — lib/site_setting_extension.rb parses upcoming_change_default_override during setting registration and stores it in upcoming_change_default_overrides (a hash keyed by setting name).
Activation — During SiteSetting.refresh!, each override is checked: if UpcomingChanges.enabled?(override[:upcoming_change]) returns true, the override is activated via defaults.activate_upcoming_change_override. The setting's current value is updated to new_default only if the admin has not manually modified it.
Default resolution — DefaultsProvider#all applies active overrides when resolving defaults, so code reading SiteSetting.defaults[:setting_name] gets the overridden value.
Frontend display — DefaultsProvider#upcoming_change_override_metadata returns { old_default:, new_default:, change_setting_name: } for active overrides. The site settings UI (admin/components/site-setting.gjs) shows a warning linking to the upcoming changes page.
UpcomingChanges.find_related_default_override_for_change finds the target setting for a given upcoming change, used to show cross-links in the UI.The current_statuses and permanent_upcoming_changes caches are keyed by git version (Discourse.git_version). This means they're naturally invalidated on every deploy — no TTL needed. Within a deploy, TrackNotifyStatusChanges calls clear_caches! when it detects metadata changes. Always call clear_caches! in tests after modifying metadata.
The resolved_value method is the single source of truth for whether a setting is "on." Auto-promotion happens implicitly: when a setting's status meets the threshold, resolved_value returns true regardless of the DB value. The DB value only changes when an admin explicitly toggles. This separation means promotion is reversible by the admin without losing the original opt-in/opt-out state.
When multiple changes need notifications, NotificationDataMerger consolidates them into a single notification per admin. It finds existing unread notifications and merges the change names array. The frontend notification types handle singular ("Feature X"), dual ("Feature X and Feature Y"), and many ("Feature X and 2 others") display.
Notifications for added and promoted changes are skipped on new sites (determined by Migration::Helpers.new_site? in lib/migration/helpers.rb — a site is "new" if its first schema migration was less than 1 hour ago). This prevents freshly provisioned sites from being flooded with notifications for every existing upcoming change on their first run. The tracking/detection steps still execute — only the notification delivery is suppressed.
Group restrictions use a separate SiteSettingGroup model rather than storing groups on the setting itself. This allows the caching layer (site_setting_group_ids) to work independently. When disallow_enabled_for_groups is set in metadata, the UI only shows Everyone/No One options. Group IDs are pipe-separated in the DB for efficient single-row storage.
UpcomingChangeEvent has unique indexes on specific event type + change name combinations. This prevents duplicate added, removed, or notification events even if the job runs multiple times. Always check for existing events before creating new ones in service code.
UpcomingChanges.statuses in lib/upcoming_changes.rbmeets_or_exceeds_status? uses these valuesprevious_status mapping if the new status fits in the progressionapp/assets/stylesheets/admin/upcoming-changes.scss (.upcoming-change__badge.--status-{name})UpcomingChangeEvent (app/models/upcoming_change_event.rb)The three main components to know:
upcoming-changes.gjs) — Filtering logic and data managementupcoming-change-item.gjs) — Individual change row rendering and interactionsadmin-user-upcoming-changes.gjs) — Read-only per-user viewState is managed via trackedObject for reactivity. API calls go through ajax() directly in the item component.
To make an upcoming change control the default of another setting:
upcoming_change_default_override metadata to the target setting in config/site_settings.yml:
target_setting:
default: original_value
upcoming_change_default_override:
upcoming_change: trigger_setting_name
new_default: new_value
impact: "site_setting_default,..." in its upcoming_change: metadatamock_upcoming_change_default_overrides — see Mocking Default OverridesAll value resolution goes through resolved_value in lib/upcoming_changes.rb. If you need to change how settings are evaluated (e.g., adding a new override condition), this is the single place to modify. The method checks in order: permanent status, admin manual override, auto-promotion threshold.
Use the test helper to mock upcoming change metadata — never modify site_settings.yml in tests:
mock_upcoming_change_metadata(
{
enable_some_feature: {
impact: "feature,all_members",
status: :experimental,
impact_type: "feature",
impact_role: "all_members",
},
},
)
Use mock_upcoming_change_default_overrides to set up override metadata in tests — never modify site_settings.yml:
mock_upcoming_change_default_overrides(
{
suggested_topics_max_days_old: {
upcoming_change: :increase_suggested_topics_max_days_old_default,
new_default: 1000,
},
},
)
# Enable the trigger setting and refresh to activate the override
SiteSetting.increase_suggested_topics_max_days_old_default = true
SiteSetting.refresh!
# Now SiteSetting.suggested_topics_max_days_old returns 1000 (the overridden default)
To test that the override does NOT apply when the admin has customized the target setting:
# Admin sets a custom value before the override activates
SiteSetting.suggested_topics_max_days_old = 730
SiteSetting.increase_suggested_topics_max_days_old_default = true
SiteSetting.refresh!
# Override is not applied — admin's explicit choice is preserved
expect(SiteSetting.suggested_topics_max_days_old).to eq(730)
After modifying metadata or settings, call UpcomingChanges.clear_caches! to ensure tests see fresh data. The caches are keyed by git version, so they persist across test examples unless explicitly cleared.
Services follow standard Service::Base test patterns — see the discourse-service-authoring skill. Use run_successfully, fail_a_policy, etc.
Page objects live at:
spec/system/page_objects/pages/admin_upcoming_changes.rb — Main pagespec/system/page_objects/pages/admin_upcoming_change_item.rb — Item componentKey page object methods:
change_item(setting_name) — Get an item component by setting namehas_change? / has_no_change? — Visibility assertionsselect_enabled_for(option) — Toggle the enabled dropdownadd_group / remove_group / save_groups — Group managementhas_enabled_for_success_toast? — Verify success feedbackSystem tests use mock_upcoming_change_metadata in before blocks. When revisiting pages to verify persistence, be aware of rate limiting on API calls.
Use track_log_messages to verify job output:
track_log_messages do |logger|
described_class.new.execute({})
expect(logger.infos.join("\n")).to include("Expected message")
end
Set up event history with UpcomingChangeEvent.create! and clean up with delete_all as needed.
Cache isolation tests live in spec/multisite/upcoming_changes_spec.rb. Use test_multisite_connection("default") / test_multisite_connection("second") blocks and clean up cache keys explicitly per site.
Notification type tests create notifications with Notification.create() and verify director.description, director.linkHref, and director.icon. Test singular, dual, and many-change scenarios plus backward compatibility with old data formats.
| Area | Key Files |
|---|---|
| Core module | lib/upcoming_changes.rb |
| Event model | app/models/upcoming_change_event.rb |
| Group model | app/models/site_setting_group.rb |
| Settings integration | lib/site_setting_extension.rb (search for upcoming_change) |
| Defaults provider | lib/site_settings/defaults_provider.rb (default override activation/resolution) |
| Services | app/services/upcoming_changes/*.rb |
| Group upsert | app/services/site_setting/upsert_groups.rb |
| Controller | admin/config/upcoming_changes_controller.rb |
| Scheduled job | app/jobs/scheduled/check_upcoming_changes.rb |
| Problem check | app/services/problem_check/upcoming_change_stable_opted_out.rb |
| Initializer | config/initializers/015-track-upcoming-change-toggle.rb |
| Admin page | admin/templates/admin-config/upcoming-changes.gjs |
| Admin container | admin/components/admin-config-areas/upcoming-changes.gjs |
| Admin item | admin/components/admin-config-areas/upcoming-change-item.gjs |
| User view | admin/components/admin-user-upcoming-changes.gjs |
| Site settings svc | frontend/discourse/app/services/site-settings.js |
| App controller | frontend/discourse/app/controllers/application.js |
| Notifications | frontend/discourse/app/lib/notification-types/upcoming-change-*.js |
| Sidebar | frontend/discourse/app/lib/sidebar/admin-sidebar.js |
| Constants | frontend/discourse/app/lib/constants.js |
| Styles | app/assets/stylesheets/admin/upcoming-changes.scss |
| Core spec | spec/lib/upcoming_changes_spec.rb |
| Request spec | spec/requests/admin/config/upcoming_changes_controller_spec.rb |
| Admin system spec | spec/system/admin_upcoming_changes_spec.rb |
| Member system spec | spec/system/member_upcoming_changes_spec.rb |
| Job spec | spec/jobs/scheduled/check_upcoming_changes_spec.rb |
| Multisite spec | spec/multisite/upcoming_changes_spec.rb |
| Page objects | spec/system/page_objects/pages/admin_upcoming_changes.rb, admin_upcoming_change_item.rb |
| Test helpers | spec/support/helpers.rb (search for mock_upcoming_change_metadata) |
PyTorch深度学习模式与最佳实践,用于构建稳健、高效且可复现的训练流程、模型架构和数据加载。