mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 04:03: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 ```
150 lines
5.5 KiB
Ruby
Vendored
150 lines
5.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "fileutils"
|
|
require "json"
|
|
require "open3"
|
|
require "tmpdir"
|
|
|
|
RSpec.describe "script/create_upcoming_change_status_prs" do # rubocop:disable RSpec/DescribeClass
|
|
let(:tmpdir) { Dir.mktmpdir("upcoming-change-status-prs") }
|
|
let(:report_file) { File.join(tmpdir, "report.json") }
|
|
let(:summary_file) { File.join(tmpdir, "summary.md") }
|
|
let(:command_log) { File.join(tmpdir, "commands.log") }
|
|
let(:fake_bin) { File.join(tmpdir, "bin") }
|
|
let(:fake_rails) { File.join(fake_bin, "rails") }
|
|
|
|
before do
|
|
FileUtils.mkdir_p(fake_bin)
|
|
write_fake_executable("git", <<~BASH)
|
|
#!/usr/bin/env bash
|
|
echo "git $*" >> "${COMMAND_LOG}"
|
|
BASH
|
|
write_fake_executable("gh", <<~BASH)
|
|
#!/usr/bin/env bash
|
|
echo "gh $*" >> "${COMMAND_LOG}"
|
|
|
|
if [ "$1" = "pr" ] && [ "$2" = "view" ]; then
|
|
echo original-author
|
|
elif [ "$1" = "pr" ] && [ "$2" = "list" ]; then
|
|
echo "${EXISTING_PR_COUNT:-0}"
|
|
fi
|
|
BASH
|
|
write_fake_executable("rails", <<~BASH)
|
|
#!/usr/bin/env bash
|
|
echo "rails $*" >> "${COMMAND_LOG}"
|
|
BASH
|
|
write_fake_executable("jq", <<~'RUBY')
|
|
#!/usr/bin/env ruby
|
|
require "json"
|
|
|
|
args = ARGV.dup
|
|
args.delete("-r")
|
|
args.delete("-c")
|
|
filter = args.shift
|
|
input = args.empty? ? STDIN.read : File.read(args.shift)
|
|
data = JSON.parse(input)
|
|
|
|
if filter == ".[] | select(.eligible)"
|
|
data.select { |record| record["eligible"] }.each { |record| puts JSON.generate(record) }
|
|
elsif filter.match?(/\A\.[a-z_]+( \/\/ empty)?\z/)
|
|
field = filter.delete_prefix(".").delete_suffix(" // empty")
|
|
print(data[field] || "")
|
|
else
|
|
warn "Unsupported jq filter: #{filter}"
|
|
exit 1
|
|
end
|
|
RUBY
|
|
File.write(report_file, JSON.generate(report_records))
|
|
end
|
|
|
|
after { FileUtils.remove_entry(tmpdir) }
|
|
|
|
it "prints dry-run PR commands for eligible changes" do
|
|
stdout, stderr, status = run_script("DRY_RUN" => "true")
|
|
|
|
expect(status).to be_success, stderr.presence || stdout
|
|
expect(File.read(summary_file)).to include(
|
|
"SKIP_DB_AND_REDIS=1 RAILS_DB=nonexistent bin/rails runner script/upcoming_changes_status_report -- --stale-after-days 14 --apply alpha_change",
|
|
"git add plugins/chat/config/settings.yml",
|
|
"gh pr create --base main --head dev/upcoming-change-status-bump/alpha_change",
|
|
"--label upcoming-change",
|
|
"--assignee original-author",
|
|
)
|
|
expect(File.read(summary_file)).not_to include("stable_change")
|
|
end
|
|
|
|
it "creates a pull request for each eligible change" do
|
|
stdout, stderr, status = run_script("DRY_RUN" => "false")
|
|
|
|
expect(status).to be_success, stderr.presence || stdout
|
|
expect(File.read(command_log)).to include(
|
|
"git fetch origin main",
|
|
"gh pr list --head dev/upcoming-change-status-bump/alpha_change --state open --json number --jq length",
|
|
"git checkout -B dev/upcoming-change-status-bump/alpha_change origin/main",
|
|
"rails runner script/upcoming_changes_status_report -- --stale-after-days 14 --apply alpha_change",
|
|
"git add plugins/chat/config/settings.yml",
|
|
"git commit -m FEATURE: Bump alpha_change upcoming change to beta",
|
|
"git push -f origin dev/upcoming-change-status-bump/alpha_change",
|
|
"gh pr create --base main --head dev/upcoming-change-status-bump/alpha_change --title FEATURE: Bump alpha_change upcoming change to beta --body-file /tmp/upcoming-change-alpha_change-body.md --label upcoming-change --assignee original-author",
|
|
)
|
|
expect(File.read("/tmp/upcoming-change-alpha_change-body.md")).to include(
|
|
"<!-- upcoming-change-status-pr:alpha_change -->",
|
|
"This automated PR moves `alpha_change` from `alpha` to `beta`",
|
|
)
|
|
end
|
|
|
|
it "skips branches that already have open pull requests" do
|
|
stdout, stderr, status = run_script("DRY_RUN" => "false", "EXISTING_PR_COUNT" => "1")
|
|
|
|
expect(status).to be_success, stderr.presence || stdout
|
|
expect(File.read(command_log)).to include(
|
|
"gh pr list --head dev/upcoming-change-status-bump/alpha_change --state open --json number --jq length",
|
|
)
|
|
expect(File.read(command_log)).not_to include(
|
|
"git checkout -B dev/upcoming-change-status-bump/alpha_change origin/main",
|
|
)
|
|
end
|
|
|
|
def report_records
|
|
[
|
|
{
|
|
name: "alpha_change",
|
|
settings_path: "plugins/chat/config/settings.yml",
|
|
current_status: "alpha",
|
|
next_status: "beta",
|
|
eligible: true,
|
|
original_pr_number: "123",
|
|
branch: "dev/upcoming-change-status-bump/alpha_change",
|
|
title: "FEATURE: Bump alpha_change upcoming change to beta",
|
|
pr_label: "upcoming-change",
|
|
pr_body:
|
|
"<!-- upcoming-change-status-pr:alpha_change -->\n\nThis automated PR moves `alpha_change` from `alpha` to `beta`.",
|
|
},
|
|
{ name: "stable_change", current_status: "stable", next_status: nil, eligible: false },
|
|
]
|
|
end
|
|
|
|
def run_script(env)
|
|
Open3.capture3(
|
|
{
|
|
"PATH" => "#{fake_bin}:#{ENV["PATH"]}",
|
|
"COMMAND_LOG" => command_log,
|
|
"REPORT_FILE" => report_file,
|
|
"BASE_BRANCH" => "main",
|
|
"STALE_AFTER_DAYS" => "14",
|
|
"GITHUB_REPOSITORY" => "discourse/discourse",
|
|
"GITHUB_STEP_SUMMARY" => summary_file,
|
|
"RAILS_COMMAND" => fake_rails,
|
|
**env,
|
|
},
|
|
"script/create_upcoming_change_status_prs",
|
|
chdir: Rails.root.to_s,
|
|
)
|
|
end
|
|
|
|
def write_fake_executable(name, content)
|
|
path = File.join(fake_bin, name)
|
|
File.write(path, content)
|
|
FileUtils.chmod("+x", path)
|
|
end
|
|
end
|