mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-21 17:30:58 +08:00
Implements date-based versioning, based on the RFC at https://meta.discourse.org/t/383536 Manual version-bump tasks are removed, and replaced with new `release:*` rake tasks. These are run in GitHub actions. Release process will work something like: 1. Trigger `release-prepare-latest-bump` via workflow_dispatch. This will create a PR which bumps the in-development version to the next `-latest` 2. Merge that PR 3. `release-handler` will be triggered automatically, tag the commit with `v0000.00.0-latest`, and cut a `release/xxxx.xx` branch from the previous commit 4. `release-prepare-bump` will be triggered automatically, and create a PR which bumps the version on `release/xxxx.xx` branch to remove the `-latest` suffix 5. Merge that PR 6. `release-handler` will be triggered automatically, and will tag the commit with `v0000.00.0` In future, we will add handling for ESR, and new workflows for security fixes.
237 lines
6.6 KiB
Ruby
Vendored
237 lines
6.6 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
def dry_run?
|
|
!!ENV["DRY_RUN"]
|
|
end
|
|
|
|
def test_mode?
|
|
ENV["RUNNING_VERSION_BUMP_IN_RSPEC_TESTS"] == "1"
|
|
end
|
|
|
|
class PlannedTag
|
|
attr_reader :name, :message
|
|
|
|
def initialize(name:, message:)
|
|
@name = name
|
|
@message = message
|
|
end
|
|
end
|
|
|
|
class PlannedCommit
|
|
attr_reader :version, :tags, :ref
|
|
|
|
def initialize(version:, tags: [])
|
|
@version = version
|
|
@tags = tags
|
|
end
|
|
|
|
def perform!
|
|
write_version(@version)
|
|
git "add", "lib/version.rb"
|
|
git "commit", "-m", "Bump version to v#{@version}"
|
|
@ref = git("rev-parse", "HEAD").strip
|
|
end
|
|
end
|
|
|
|
def read_version_rb
|
|
File.read("lib/version.rb")
|
|
end
|
|
|
|
def parse_current_version
|
|
version = read_version_rb[/STRING = "(.*)"/, 1]
|
|
raise "Unable to parse current version" if version.nil?
|
|
puts "Parsed current version: #{version.inspect}"
|
|
version
|
|
end
|
|
|
|
def write_version(version)
|
|
File.write("lib/version.rb", read_version_rb.sub(/STRING = ".*"/, "STRING = \"#{version}\""))
|
|
end
|
|
|
|
def git(*args, allow_failure: false, silent: false)
|
|
puts "> git #{args.inspect}" unless silent
|
|
args.prepend(*%w[-c commit.gpgsign=false]) if test_mode?
|
|
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 ref_exists?(ref)
|
|
git "rev-parse", "--verify", ref
|
|
true
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
def confirm(msg)
|
|
loop do
|
|
print "#{msg} (yes/no)..."
|
|
break if test_mode?
|
|
|
|
response = $stdin.gets.strip
|
|
|
|
case response.downcase
|
|
when "no"
|
|
raise "Aborted"
|
|
when "yes"
|
|
break
|
|
else
|
|
puts "unknown response: #{response}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def make_commits(commits:, branch:, base:)
|
|
raise "You have other staged changes. Aborting." if !git("diff", "--cached").empty?
|
|
|
|
git "branch", "-D", branch if ref_exists?(branch)
|
|
git "checkout", "-b", branch
|
|
|
|
commits.each(&:perform!)
|
|
|
|
git("push", "-f", "--set-upstream", "origin", branch)
|
|
|
|
make_pr(
|
|
base: base,
|
|
branch: branch,
|
|
title:
|
|
"Version bump#{"s" if commits.length > 1} for #{base}: #{commits.map { |c| "v#{c.version}" }.join(", ")}",
|
|
)
|
|
end
|
|
|
|
def make_pr(base:, branch:, title:)
|
|
params = { expand: 1, title: title, body: <<~MD }
|
|
> :warning: This PR should not be merged via the GitHub web interface
|
|
>
|
|
> It should only be merged (via fast-forward) using the associated `bin/rake version_bump:*` task.
|
|
MD
|
|
|
|
if !test_mode?
|
|
open_command =
|
|
case RbConfig::CONFIG["host_os"]
|
|
when /darwin|mac os/
|
|
"open"
|
|
when /linux|solaris|bsd/
|
|
"xdg-open"
|
|
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
"start"
|
|
else
|
|
raise "Unsupported OS"
|
|
end
|
|
|
|
system(
|
|
open_command,
|
|
"https://github.com/discourse/discourse/compare/#{base}...#{branch}?#{params.to_query}",
|
|
exception: true,
|
|
)
|
|
end
|
|
|
|
puts "Do not merge the PR via the GitHub web interface. Get it approved, then come back here to continue."
|
|
end
|
|
|
|
def fastforward(base:, branch:)
|
|
if dry_run?
|
|
puts "[DRY RUN] Skipping fastforward of #{base}"
|
|
return
|
|
end
|
|
|
|
confirm "Ready to merge? This will fast-forward #{base} to #{branch}"
|
|
begin
|
|
git "push", "origin", "#{branch}:#{base}"
|
|
rescue => e
|
|
raise <<~MSG
|
|
#{e.message}
|
|
Error occured during fastforward. Maybe another commit was added to `#{base}` since the PR was created, or maybe the PR wasn't approved.
|
|
Don't worry, this is not unusual. To update the branch and try again, rerun this script again. The existing PR and approval will be reused.
|
|
MSG
|
|
end
|
|
puts "Merge successful"
|
|
end
|
|
|
|
def stage_tags(commits)
|
|
puts "Staging tags locally..."
|
|
commits.each do |commit|
|
|
commit.tags.each { |tag| git "tag", "-f", "-a", tag.name, "-m", tag.message, commit.ref }
|
|
end
|
|
end
|
|
|
|
def push_tags(commits)
|
|
tag_names = commits.flat_map { |commit| commit.tags.map(&:name) }
|
|
|
|
if dry_run?
|
|
puts "[DRY RUN] Skipping pushing tags to origin (#{tag_names.join(", ")})"
|
|
return
|
|
end
|
|
|
|
confirm "Ready to push tags #{tag_names.join(", ")} to origin?"
|
|
tag_names.each { |tag_name| git "push", "-f", "origin", "refs/tags/#{tag_name}" }
|
|
end
|
|
|
|
def with_clean_worktree(origin_branch)
|
|
origin_url = git("remote", "get-url", "origin").strip
|
|
|
|
if !test_mode? && !origin_url.include?("discourse/discourse")
|
|
raise "Expected 'origin' remote to point to discourse/discourse (got #{origin_url})"
|
|
end
|
|
|
|
git "fetch", "origin", origin_branch
|
|
path = "#{Rails.root}/tmp/version-bump-worktree-#{SecureRandom.hex}"
|
|
begin
|
|
FileUtils.mkdir_p(path)
|
|
git "worktree", "add", 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
|
|
|
|
desc <<~DESC
|
|
squash-merge many security fixes into a single branch for review/merge.
|
|
Pass a list of comma-separated branches in the SECURITY_FIX_REFS env var, including the remote name.
|
|
Pass the name of the destination branch as the argument of the rake task.
|
|
e.g.
|
|
SECURITY_FIX_REFS='privatemirror/mybranch1,privatemirror/mybranch2' bin/rake "version_bump:stage_security_fixes[main]"
|
|
DESC
|
|
task "version_bump:stage_security_fixes", [:base] do |t, args|
|
|
base = args[:base]
|
|
raise "Unknown base: #{base.inspect}" if %w[stable main].exclude?(base)
|
|
|
|
fix_refs = ENV["SECURITY_FIX_REFS"]&.split(",")&.map(&:strip)
|
|
raise "No branches specified in SECURITY_FIX_REFS env" if fix_refs.nil? || fix_refs.empty?
|
|
|
|
fix_refs.each do |ref|
|
|
if !ref.include?("/")
|
|
raise "Ref #{ref} did not specify an origin. Please specify the origin, e.g. privatemirror/mybranch"
|
|
end
|
|
end
|
|
|
|
puts "Staging security fixes for #{base} branch: #{fix_refs.inspect}"
|
|
|
|
branch = "security/#{base}-security-fixes"
|
|
|
|
with_clean_worktree(base) do
|
|
git "branch", "-D", branch if ref_exists?(branch)
|
|
git "checkout", "-b", branch
|
|
|
|
fix_refs.each do |ref|
|
|
origin, origin_branch = ref.split("/", 2)
|
|
git "fetch", origin, origin_branch
|
|
|
|
first_commit_on_branch = git("log", "--format=%H", "origin/#{base}..#{ref}").lines.last.strip
|
|
git "cherry-pick", "#{first_commit_on_branch}^..#{ref}"
|
|
end
|
|
|
|
puts "Finished merging commits into a locally-staged #{branch} branch. Git log is:"
|
|
puts git("log", "origin/#{base}..#{branch}")
|
|
|
|
confirm "Check the log above. Ready to push this branch to the origin and create a PR?"
|
|
git("push", "-f", "--set-upstream", "origin", branch)
|
|
|
|
make_pr(base: base, branch: branch, title: "Security fixes for #{base}")
|
|
fastforward(base: base, branch: branch)
|
|
end
|
|
end
|