mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 03:05:45 +08:00
Introduces a [scheduled workflow](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule) that runs weekly to find upcoming changes which have not changed status in the 14+ days. If found, the following happens: * A branch for the bump is made, which is used as a unique key to avoid duplicate PRs * The status change is made in the relevant settings.yml file * A commit & PR is made for the bump, and it is assigned to the person who originally introduced the upcoming change The status report & script used by the workflow can be called in a dry run, which will give an output of the actions that will be taken by the workflow. Here is how to run the report: ```bash SKIP_DB_AND_REDIS=1 RAILS_DB=nonexistent bin/rails runner script/upcoming_changes_status_report -- --stale-after-days 14 --pretty > /tmp/upcoming_changes_status_report.json ``` An example of the report output: ```json { "name": "ai_bot_enable_docked_composer", "settings_path": "plugins/discourse-ai/config/settings.yml", "current_status": "alpha", "next_status": "beta", "eligible": true, "eligibility_reason": "status_unchanged_for_14_days", "days_since_status_change": 22, "last_status_change_commit": "b3b561e5fa", "last_status_change_date": "2026-05-04T09:26:32-07:00", "original_commit": "b3b561e5fa", "original_commit_date": "2026-05-04T09:26:32-07:00", "original_author_name": "Keegan George", "original_author_email": "kgeorge13@gmail.com", "original_pr_number": "39708" }, ``` And here is the result of a dry run of the script (it prints out this set of commands for every upcoming change that will be bumped). The dry run command: ```bash REPORT_FILE=/tmp/upcoming_changes_status_report.json BASE_BRANCH=main DRY_RUN=true STALE_AFTER_DAYS=14 script/create_upcoming_change_status_prs ``` And the result: ```bash git checkout -B dev/upcoming-change-status-bump/simple_email_subject origin/main SKIP_DB_AND_REDIS=1 RAILS_DB=nonexistent bin/rails runner script/upcoming_changes_status_report -- --stale-after-days 14 --apply simple_email_subject git add config/site_settings.yml git commit -m "DEV: Bump simple_email_subject upcoming change to beta" git push -f origin dev/upcoming-change-status-bump/simple_email_subject gh pr create --base main --head dev/upcoming-change-status-bump/simple_email_subject --title "DEV: Bump simple_email_subject upcoming change to beta" --body-file /tmp/upcoming-change-simple_email_subject-body.md --label upcoming-change --assignee dbattersby ``` And here is an example of the body of the PR: ```markdown <!-- upcoming-change-status-pr:simple_email_subject --> This automated PR moves `simple_email_subject` from `alpha` to `beta` after 14+ days without a status change. - Last status change commit: [`695b393dac0ee829f12d54ba741f847c0621980d`](695b393dac) - Last status change date: `2026-05-07T12:58:02+04:00` - Settings file: `config/site_settings.yml` - Original author: David Battersby (<info@davidbattersby.com>) - Original PR: #36040 ```
233 lines
7.8 KiB
Ruby
Vendored
233 lines
7.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "fileutils"
|
|
require "open3"
|
|
require "tmpdir"
|
|
|
|
RSpec.describe UpcomingChanges::StatusReport do
|
|
subject(:report) do
|
|
described_class.new(repo_path:, stale_after_days: 14, now: Time.iso8601("2026-05-26T00:00:00Z"))
|
|
end
|
|
|
|
let(:repo_path) { Dir.mktmpdir("upcoming-changes-status-report") }
|
|
let(:commit_shas) { {} }
|
|
|
|
before do
|
|
git("init")
|
|
git("config", "user.name", "Discourse CI")
|
|
git("config", "user.email", "ci@ci.invalid")
|
|
|
|
write_settings(
|
|
"experimental_change" => "experimental",
|
|
"alpha_change" => "alpha",
|
|
"beta_change" => "beta",
|
|
"recent_change" => "alpha",
|
|
"stable_change" => "stable",
|
|
"conceptual_change" => "conceptual",
|
|
"permanent_change" => "permanent",
|
|
"never_change" => "never",
|
|
)
|
|
write_plugin_settings("plugin_alpha_change" => "alpha")
|
|
commit_shas[:original] = commit(
|
|
"FEATURE: Add upcoming changes (#123)",
|
|
date: "2026-04-01T12:00:00Z",
|
|
author_name: "Alice Example",
|
|
author_email: "alice@example.com",
|
|
)
|
|
|
|
write_settings(
|
|
"experimental_change" => "experimental",
|
|
"alpha_change" => "alpha",
|
|
"beta_change" => "beta",
|
|
"recent_change" => "beta",
|
|
"stable_change" => "stable",
|
|
"conceptual_change" => "conceptual",
|
|
"permanent_change" => "permanent",
|
|
"never_change" => "never",
|
|
)
|
|
write_plugin_settings("plugin_alpha_change" => "alpha")
|
|
commit_shas[:recent] = commit(
|
|
"DEV: Bump recent upcoming change (#456)",
|
|
date: "2026-05-20T12:00:00Z",
|
|
author_name: "Bob Example",
|
|
author_email: "bob@example.com",
|
|
)
|
|
end
|
|
|
|
after { FileUtils.remove_entry(repo_path) }
|
|
|
|
describe "#report" do
|
|
it "reports eligibility and git metadata", :aggregate_failures do
|
|
records = report.report.index_by { |record| record.fetch(:name) }
|
|
|
|
expect(records.fetch("experimental_change")).to include(
|
|
settings_path: "config/site_settings.yml",
|
|
current_status: "experimental",
|
|
next_status: "alpha",
|
|
eligible: true,
|
|
eligibility_reason: "status_unchanged_for_14_days",
|
|
last_status_change_commit: commit_shas[:original],
|
|
original_commit: commit_shas[:original],
|
|
original_author_name: "Alice Example",
|
|
original_author_email: "alice@example.com",
|
|
original_pr_number: "123",
|
|
branch: "dev/upcoming-change-status-bump/experimental_change",
|
|
title: "FEATURE: Bump experimental_change upcoming change to alpha",
|
|
pr_label: "upcoming-change",
|
|
)
|
|
expect(records.fetch("experimental_change").fetch(:pr_body)).to include(
|
|
"<!-- upcoming-change-status-pr:experimental_change -->",
|
|
"This automated PR moves `experimental_change` from `experimental` to `alpha`",
|
|
"Original PR: #123",
|
|
)
|
|
expect(records.fetch("alpha_change")).to include(
|
|
current_status: "alpha",
|
|
next_status: "beta",
|
|
eligible: true,
|
|
)
|
|
expect(records.fetch("beta_change")).to include(
|
|
current_status: "beta",
|
|
next_status: "stable",
|
|
eligible: true,
|
|
)
|
|
recent_change = records.fetch("recent_change")
|
|
expect(recent_change).to include(
|
|
current_status: "beta",
|
|
next_status: "stable",
|
|
eligible: false,
|
|
eligibility_reason: "status_changed_recently",
|
|
last_status_change_commit: commit_shas[:recent],
|
|
days_since_status_change: 5,
|
|
)
|
|
expect(Time.iso8601(recent_change.fetch(:last_status_change_date))).to eq(
|
|
Time.iso8601("2026-05-20T12:00:00Z"),
|
|
)
|
|
expect(records.fetch("stable_change")).to include(
|
|
current_status: "stable",
|
|
next_status: nil,
|
|
eligible: false,
|
|
eligibility_reason: "terminal_status",
|
|
)
|
|
expect(records.fetch("conceptual_change")).to include(
|
|
current_status: "conceptual",
|
|
eligible: false,
|
|
eligibility_reason: "terminal_status",
|
|
)
|
|
expect(records.fetch("permanent_change")).to include(
|
|
current_status: "permanent",
|
|
eligible: false,
|
|
eligibility_reason: "terminal_status",
|
|
)
|
|
expect(records.fetch("never_change")).to include(
|
|
current_status: "never",
|
|
eligible: false,
|
|
eligibility_reason: "terminal_status",
|
|
)
|
|
expect(records.fetch("plugin_alpha_change")).to include(
|
|
settings_path: "plugins/chat/config/settings.yml",
|
|
current_status: "alpha",
|
|
next_status: "beta",
|
|
eligible: true,
|
|
original_commit: commit_shas[:original],
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "#apply" do
|
|
it "updates only the target setting file" do
|
|
report.apply("plugin_alpha_change")
|
|
|
|
metadata =
|
|
described_class::MetadataLoader.from_file(
|
|
File.join(repo_path, "plugins/chat/config/settings.yml"),
|
|
strict: true,
|
|
)
|
|
|
|
expect(metadata.transform_values { |value| value[:status].to_s }).to include(
|
|
plugin_alpha_change: "beta",
|
|
)
|
|
expect(File.read(File.join(repo_path, "config/site_settings.yml"))).to include(
|
|
" alpha_change:\n default: false\n client: true\n hidden: true\n upcoming_change:\n status: alpha\n",
|
|
)
|
|
end
|
|
end
|
|
|
|
describe described_class::SourceStatusUpdater do
|
|
it "raises when the status line cannot be edited" do
|
|
settings_file = File.join(repo_path, "config/site_settings.yml")
|
|
File.write(settings_file, <<~YAML)
|
|
experimental:
|
|
malformed_change:
|
|
upcoming_change:
|
|
status: "alpha beta"
|
|
YAML
|
|
|
|
updater = described_class.new(settings_file:)
|
|
|
|
expect {
|
|
updater.update!(change_name: "malformed_change", next_status: "beta")
|
|
}.to raise_error(RuntimeError, /Could not parse status line/)
|
|
end
|
|
end
|
|
|
|
def git(*args, env: {})
|
|
stdout, stderr, status = Open3.capture3(env, "git", "-C", repo_path, *args)
|
|
raise "git #{args.join(" ")} failed: #{stderr}" if !status.success?
|
|
|
|
stdout
|
|
end
|
|
|
|
def commit(message, date:, author_name:, author_email:)
|
|
git("add", ".")
|
|
git(
|
|
"commit",
|
|
"-m",
|
|
message,
|
|
env: {
|
|
"GIT_AUTHOR_DATE" => date,
|
|
"GIT_COMMITTER_DATE" => date,
|
|
"GIT_AUTHOR_NAME" => author_name,
|
|
"GIT_AUTHOR_EMAIL" => author_email,
|
|
"GIT_COMMITTER_NAME" => "Discourse CI",
|
|
"GIT_COMMITTER_EMAIL" => "ci@ci.invalid",
|
|
},
|
|
)
|
|
git("rev-parse", "HEAD").strip
|
|
end
|
|
|
|
def write_settings(statuses)
|
|
FileUtils.mkdir_p(File.join(repo_path, "config"))
|
|
File.write(File.join(repo_path, "config/site_settings.yml"), "experimental:\n")
|
|
|
|
File.open(File.join(repo_path, "config/site_settings.yml"), "a") do |file|
|
|
statuses.each do |name, status|
|
|
file.write(" #{name}:\n")
|
|
file.write(" default: false\n")
|
|
file.write(" client: true\n")
|
|
file.write(" hidden: true\n")
|
|
file.write(" upcoming_change:\n")
|
|
file.write(" status: #{status}\n")
|
|
file.write(" impact: \"feature,all_members\"\n")
|
|
file.write(" learn_more_url: \"https://meta.discourse.org/t/-/123\"\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
def write_plugin_settings(statuses)
|
|
FileUtils.mkdir_p(File.join(repo_path, "plugins/chat/config"))
|
|
File.write(File.join(repo_path, "plugins/chat/config/settings.yml"), "chat:\n")
|
|
|
|
File.open(File.join(repo_path, "plugins/chat/config/settings.yml"), "a") do |file|
|
|
statuses.each do |name, status|
|
|
file.write(" #{name}:\n")
|
|
file.write(" default: false\n")
|
|
file.write(" client: true\n")
|
|
file.write(" hidden: true\n")
|
|
file.write(" upcoming_change:\n")
|
|
file.write(" status: #{status}\n")
|
|
file.write(" impact: \"feature,all_members\"\n")
|
|
file.write(" learn_more_url: \"https://meta.discourse.org/t/-/123\"\n")
|
|
end
|
|
end
|
|
end
|
|
end
|