@@ -154,6 +154,10 @@ def initialize(version)
154154 "v#{ @stable_branch } .0"
155155 end
156156
157+ # The most recent release on this line. For patch releases it bounds the
158+ # search for backport PRs merged straight onto the stable branch since then.
159+ @last_release_tag = @level == :patch ? "v#{ @stable_branch } .#{ segments [ 2 ] - 1 } " : @previous_release_tag
160+
157161 rubygems_version = segments . join ( "." ) . gsub ( /([a-z])\. (\d )/i , '\1\2' )
158162 @rubygems = Rubygems . new ( rubygems_version , @stable_branch )
159163
@@ -174,19 +178,33 @@ def set_rubygems_as_current_library
174178 def prepare!
175179 initial_branch = `git rev-parse --abbrev-ref HEAD` . strip
176180
181+ # Refresh the upstream refs first so the release is cut from the latest
182+ # origin state. A stale local `master` or stable branch would otherwise
183+ # silently drop PRs merged after the last local fetch.
184+ system ( "git" , "fetch" , "--prune" , "origin" , exception : true )
185+
177186 check_git_state!
178187
179188 unless @prerelease
180- create_if_not_exist_and_switch_to ( @stable_branch , from : "master" )
189+ create_if_not_exist_and_switch_to ( @stable_branch , from : "origin/ master" )
181190 system ( "git" , "push" , "origin" , @stable_branch , exception : true ) if @level == :minor_or_major && !ENV [ "DRYRUN" ]
182191 end
183192
184- from_branch = if @level == :minor_or_major && @prerelease
193+ base_branch = if @level == :minor_or_major && @prerelease
185194 "master"
186195 else
187196 @stable_branch
188197 end
189- create_if_not_exist_and_switch_to ( @release_branch , from : from_branch )
198+
199+ # The ref the release branch is cut from. Patch releases and prereleases
200+ # branch straight off the upstream ref so a stale local copy can't leave
201+ # commits behind; a new stable branch was just created locally above.
202+ release_base = if @level == :minor_or_major
203+ @prerelease ? "origin/master" : @stable_branch
204+ else
205+ "origin/#{ @stable_branch } "
206+ end
207+ create_if_not_exist_and_switch_to ( @release_branch , from : release_base )
190208
191209 begin
192210 @bundler . set_relevant_pull_requests_from ( unreleased_pull_requests )
@@ -200,14 +218,14 @@ def prepare!
200218
201219 gh_client . create_pull_request (
202220 "ruby/rubygems" ,
203- from_branch ,
221+ base_branch ,
204222 @release_branch ,
205223 "Prepare RubyGems #{ @rubygems . version } and Bundler #{ @bundler . version } " ,
206224 release_pull_request_body
207225 ) unless ENV [ "DRYRUN" ]
208226
209227 unless @prerelease
210- create_if_not_exist_and_switch_to ( "cherry_pick_changelogs" , from : "master" )
228+ create_if_not_exist_and_switch_to ( "cherry_pick_changelogs" , from : "origin/ master" )
211229
212230 begin
213231 system ( "git" , "cherry-pick" , bundler_changelog , rubygems_changelog , exception : true )
@@ -271,6 +289,11 @@ def cherry_pick_pull_requests
271289 prs = relevant_unreleased_pull_requests
272290 raise "No unreleased PRs were found. Make sure to tag them with appropriate labels so that they are selected for backport." unless prs . any?
273291
292+ # Dedicated backport PRs target the stable branch directly, so they are
293+ # already on the release branch and only need a changelog entry, not
294+ # another cherry-pick.
295+ prs = prs . reject { |pr | already_on_stable_branch? ( pr ) }
296+
274297 puts "The following unreleased prs were found:\n #{ prs . map { |pr | "* #{ pr . url } " } . join ( "\n " ) } "
275298
276299 prs . each do |pr |
@@ -375,6 +398,22 @@ def unreleased_pull_requests
375398 @unreleased_pull_requests ||= scan_unreleased_pull_requests ( unreleased_pr_ids )
376399 end
377400
401+ # True when the PR's merged commit is already reachable from the release
402+ # branch, e.g. a backport PR merged straight onto the stable branch rather
403+ # than cherry-picked from master.
404+ def already_on_stable_branch? ( pr )
405+ system ( "git" , "merge-base" , "--is-ancestor" , pr . merge_commit_sha , "HEAD" , out : IO ::NULL , err : IO ::NULL )
406+ end
407+
408+ # Commits merged directly onto the stable branch since the last release, such
409+ # as dedicated backport PRs that target the stable branch instead of being
410+ # cherry-picked from master. They never land on master, so the master scan in
411+ # `unreleased_pr_ids` cannot see them and their changelog entries would
412+ # otherwise be dropped from the release.
413+ def stable_branch_backport_commits
414+ `git log --format=%H #{ @last_release_tag } ..origin/#{ @stable_branch } ` . split ( "\n " ) . reject ( &:empty? )
415+ end
416+
378417 # Source SHAs already cherry-picked onto the stable branch, derived from the
379418 # `(cherry picked from commit X)` footer that `git cherry-pick -x` records.
380419 # When the footer references a merge commit (PRs merged with "Create a merge
@@ -383,7 +422,7 @@ def unreleased_pull_requests
383422 # through those commits left on master.
384423 def released_commit_shas
385424 @released_commit_shas ||= begin
386- log = `git log --format=%B #{ @previous_release_tag } ..#{ @stable_branch } `
425+ log = `git log --format=%B #{ @previous_release_tag } ..origin/ #{ @stable_branch } `
387426 shas = Set . new
388427 log . scan ( /cherry picked from commit ([0-9a-f]+)/ ) . flatten . each do |sha |
389428 shas << sha
@@ -410,9 +449,10 @@ def scan_unreleased_pull_requests(ids)
410449 end
411450
412451 def unreleased_pr_ids
413- head = @level == :minor_or_major ? "HEAD" : "master"
452+ head = @level == :minor_or_major ? "HEAD" : "origin/ master"
414453 commits = `git log --format=%H #{ @previous_release_tag } ..#{ head } ` . split ( "\n " )
415454 commits . reject! { |sha | released_commit_shas . include? ( sha ) } if @level == :patch
455+ commits . concat ( stable_branch_backport_commits ) if @level == :patch
416456
417457 # GitHub search API has a rate limit of 30 requests per minute for authenticated users
418458 rate_limit = 28
0 commit comments