discourse/lib/release_utils.rb

206 lines
5.4 KiB
Ruby

# frozen_string_literal: true
require "open3"
require_relative "release_utils/version"
module ReleaseUtils
PRIMARY_RELEASE_TAG = "release"
RELEASE_TAGS = [PRIMARY_RELEASE_TAG, "beta", "latest-release"].freeze
PRIMARY_ESR_TAG = "esr"
ESR_TAGS = [PRIMARY_ESR_TAG, "stable"].freeze
PR_LABEL = "release"
def self.dry_run?
!!ENV["DRY_RUN"]
end
def self.test_mode?
ENV["RUNNING_RELEASE_IN_RSPEC_TESTS"] == "1"
end
def self.read_versions_json
ReleaseUtils.with_clean_worktree("main") { JSON.parse(File.read("versions.json")) }
end
def self.released_versions
read_versions_json
.select { |_version, info| info["released"] }
.keys
.sort_by { |v| Gem::Version.new(v) }
end
def self.released_esrs
read_versions_json
.select { |_version, info| info["released"] && info["esr"] }
.keys
.sort_by { |v| Gem::Version.new(v) }
end
def self.read_version_rb
File.read("lib/version.rb")
end
def self.write_version(version)
File.write("lib/version.rb", read_version_rb.sub(/STRING = ".*"/, "STRING = \"#{version}\""))
end
def self.commit_version_bump(version, message)
write_version(version)
git "add", "lib/version.rb"
git "commit", "-m", message
end
def self.update_versions_json(new_version)
today_date = DateTime.now.utc.strftime("%Y-%m-%d")
version_year, version_month = new_version.split(".").map(&:to_i)
esr = [1, 7].include?(version_month)
support_period = esr ? 8.months : 2.months
support_end_date = (Date.new(version_year, version_month, 1) + support_period).strftime("%Y-%m")
new_version_info = {
new_version => {
developmentStartDate: today_date,
releaseDate: "#{version_year}-#{version_month.to_s.rjust(2, "0")}",
supportEndDate: support_end_date,
released: false,
esr: esr,
supported: true,
},
}
data = JSON.parse(File.read("versions.json"))
data.transform_values! do |v|
if !v["released"]
v["released"] = true
v["releaseDate"] = today_date
end
if v["supported"] &&
Date.parse(v["supportEndDate"] + "-01") < Date.new(version_year, version_month, 1)
v["supported"] = false
v["supportEndDate"] = today_date
end
v
end
File.write("versions.json", JSON.pretty_generate({ **new_version_info, **data }) + "\n")
end
def self.git(*args, allow_failure: false, silent: false)
puts "> git #{args.inspect}" unless silent
stdout, stderr, status = Open3.capture3({ "LEFTHOOK" => "0" }, "git", *args)
if !status.success? && !allow_failure
raise "Command failed: git #{args.inspect}\n#{stdout.indent(2)}\n#{stderr.indent(2)}"
end
stdout
end
def self.gh(*args, capture: false, input: nil)
puts "> gh #{args.inspect}"
return true if test_mode? && !capture
if capture
stdout_text, stderr_text, status = Open3.capture3("gh", *args, stdin_data: input)
return stdout_text if status.success?
raise <<~MESSAGE
gh failed with status #{status.exitstatus}
Command: gh #{args.join(" ")}
STDOUT:
#{stdout_text}
STDERR:
#{stderr_text}
MESSAGE
else
system "gh", *args
end
end
def self.supported_version_info
read_versions_json.select { |_version, info| info["supported"] }
end
def self.ref_exists?(ref)
git "rev-parse", "--verify", ref
true
rescue StandardError
false
end
def self.confirm(msg)
return true if test_mode?
TTY::Prompt.new.yes?(msg)
end
def self.confirm_or_abort(msg)
return if test_mode?
raise "Aborted" unless confirm(msg)
end
def self.merge_pr(base:, branch:)
if dry_run?
puts "[DRY RUN] Skipping merge of #{branch}"
return
end
loop do
confirm_or_abort "Ready to merge #{branch}?"
if test_mode?
git "push", "origin", "HEAD:#{base}"
break
else
if !gh("pr", "ready", branch) # remove draft status
puts "Failed to mark PR as ready-for-review... trying to merge anyway"
end
success = gh("pr", "merge", branch, "--rebase", "--delete-branch")
break if success
puts "Merge failed. Maybe the PR isn't approved yet, or there's a conflict."
end
end
puts "Merge successful"
end
def self.make_pr(base:, branch:, title: nil, body: nil, draft: false)
title ||= git("log", "-1", branch, "--pretty=%s").strip
body ||= git("log", "-1", branch, "--pretty=%b").strip
title_and_body = ["--title", title, "--body", body]
draft_flag = draft ? ["--draft"] : []
success =
gh(
"pr",
"create",
"--base",
base,
"--head",
branch,
*title_and_body,
*draft_flag,
"--label",
PR_LABEL,
) || gh("pr", "edit", branch, *title_and_body, "--add-label", PR_LABEL)
raise "Failed to create or update PR" unless success
end
def self.with_clean_worktree(origin_branch)
git "fetch", "origin", origin_branch
path = "#{Rails.root}/tmp/version-bump-worktree-#{SecureRandom.hex}"
begin
FileUtils.mkdir_p(path)
git "worktree", "add", "--detach", path, "origin/#{origin_branch}"
Dir.chdir(path) { yield } # rubocop:disable Discourse/NoChdir
ensure
puts "Cleaning up temporary worktree..."
git "worktree", "remove", "--force", path, silent: true, allow_failure: true
FileUtils.rm_rf(path)
end
end
end