Skip to content

Commit 992790f

Browse files
committed
github: add rake task to backport PR
you can simulate on manually: GITHUB_TOKEN=... rake backport:v1_19 Signed-off-by: Kentaro Hayashi <[email protected]>
1 parent d3adf6f commit 992790f

File tree

4 files changed

+246
-0
lines changed

4 files changed

+246
-0
lines changed

.github/workflows/backport.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Backport
2+
3+
on:
4+
schedule:
5+
# Sun 10:00 (JST)
6+
- cron: '0 1 * * 0'
7+
workflow_dispatch:
8+
9+
permissions: read-all
10+
11+
concurrency:
12+
group: ${{ github.head_ref || github.sha }}-${{ github.workflow }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
continue-on-error: false
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
ruby-version: ['3.4']
23+
task: ['backport:v1_16', 'backport:v1_19']
24+
25+
name: Backport PR on ${{ matrix.os }}
26+
steps:
27+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
28+
- name: Set up Ruby
29+
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
30+
with:
31+
ruby-version: ${{ matrix.ruby-version }}
32+
- name: Install dependencies
33+
run: bundle install
34+
- name: Run Benchmark
35+
shell: bash
36+
run: |
37+
bundle exec rake ${{ matrix.task }}

Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require 'rake/testtask'
66
require 'rake/clean'
77

88
require_relative 'tasks/benchmark'
9+
require_relative 'tasks/backport'
910

1011
task test: [:base_test]
1112

tasks/backport.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require_relative 'backport/backporter'
2+
3+
=begin
4+
5+
When you want to manually execute backporting, set the following
6+
environment variables:
7+
8+
* GITHUB_REPOSITORY: fluent/fluentd
9+
* GITHUB_TOKEN: ${PERSONAL_ACCESS_TOKEN}
10+
11+
Optional:
12+
13+
* REPOSITORY_REMOTE: origin
14+
If you execute in forked repository, it might be 'upstream'
15+
16+
=end
17+
18+
def append_additional_arguments(commands)
19+
if ENV['DRY_RUN']
20+
commands << '--dry-run'
21+
end
22+
if ENV['GITHUB_REPOSITORY']
23+
commands << '--upstream'
24+
commands << ENV['GITHUB_REPOSITORY']
25+
end
26+
if ENV['REPOSITORY_REMOTE']
27+
commands << '--remote'
28+
commands << ENV['REPOSITORY_REMOTE']
29+
end
30+
commands
31+
end
32+
33+
namespace :backport do
34+
35+
desc "Backport PR to v1.16 branch"
36+
task :v1_16 do
37+
backporter = PullRequestBackporter.new
38+
commands = ['--branch', 'v1.16', '--log-level', 'debug']
39+
commands = append_additional_arguments(commands)
40+
backporter.run(commands)
41+
end
42+
43+
desc "Backport PR to v1.19 branch"
44+
task :v1_19 do
45+
commands = ['--branch', 'v1.19', '--log-level', 'debug']
46+
commands = append_additional_arguments(commands)
47+
backporter = PullRequestBackporter.new
48+
backporter.run(commands)
49+
end
50+
end

tasks/backport/backporter.rb

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
require 'open-uri'
2+
require 'json'
3+
require 'optparse'
4+
require 'logger'
5+
6+
class PullRequestBackporter
7+
8+
def initialize
9+
@logger = Logger.new(STDOUT)
10+
@options = {
11+
upstream: "fluent/fluentd",
12+
branch: "v1.16",
13+
dry_run: false,
14+
log_level: Logger::Severity::INFO,
15+
remote: 'origin'
16+
}
17+
end
18+
19+
def current_branch
20+
branch = IO.popen(["git", "branch", "--contains"]) do |io|
21+
io.read
22+
end
23+
branch.split.last
24+
end
25+
26+
def parse_command_line(argv)
27+
opt = OptionParser.new
28+
opt.on('--upstream REPOSITORY',
29+
'Specify upstream repository (e.g. fluent/fluentd)') {|v| @options[:upstream] = v }
30+
opt.on('--branch BRANCH') {|v| @options[:branch] = v }
31+
opt.on('--dry-run') {|v| @options[:dry_run] = true }
32+
opt.on('--log-level LOG_LEVEL (e.g. debug,info)') {|v|
33+
@options[:log_level] = case v
34+
when "error"
35+
Logger::Severity::ERROR
36+
when "warn"
37+
Logger::Severity::WARN
38+
when "debug"
39+
Logger::Severity::DEBUG
40+
when "info"
41+
Logger::Severity::INFO
42+
else
43+
puts "unknown log level: <#{v}>"
44+
exit 1
45+
end
46+
}
47+
opt.on('--remote REMOTE') {|v| @options[:remote] = v }
48+
opt.parse!(argv)
49+
end
50+
51+
def collect_backports
52+
backports = []
53+
pages = 5
54+
pages.times.each do |page|
55+
@logger.debug "Collecting backport information (#{page + 1}/#{pages})"
56+
URI.open("https://api.github.com/repos/fluent/fluentd/pulls?state=closed&per_page=100&page=#{page+1}",
57+
"Accept" => "application/vnd.github+json",
58+
"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}",
59+
"X-GitHub-Api-Version" => "2022-11-28") do |request|
60+
JSON.parse(request.read).each do |pull_request|
61+
unless pull_request["labels"].empty?
62+
labels = pull_request["labels"].collect { |label| label["name"] }
63+
unless labels.include?("backport to #{@options[:branch]}")
64+
next
65+
end
66+
if labels.include?("backported")
67+
@logger.info "[DONE] \##{pull_request['number']} #{pull_request['title']} LABELS: #{pull_request['labels'].collect { |label| label['name'] }}"
68+
next
69+
end
70+
@logger.info "* \##{pull_request['number']} #{pull_request['title']} LABELS: #{pull_request['labels'].collect { |label| label['name'] }}"
71+
# merged into this commit
72+
@logger.debug "MERGE_COMMIT_SHA: #{pull_request['merge_commit_sha']}"
73+
body = pull_request["body"].gsub(/\*\*Which issue\(s\) this PR fixes\*\*: \r\n/,
74+
"**Which issue(s) this PR fixes**: \r\nBackport \##{pull_request['number']}\r\n")
75+
backports << {
76+
number: pull_request["number"],
77+
merge_commit_sha: pull_request["merge_commit_sha"],
78+
title: "Backport(#{@options[:branch]}): #{pull_request['title']} (\##{pull_request['number']})",
79+
body: body
80+
}
81+
end
82+
end
83+
end
84+
end
85+
backports
86+
end
87+
88+
def create_pull_requests
89+
backports = collect_backports
90+
if backports.empty?
91+
@logger.info "No need to backport pull requests"
92+
return
93+
end
94+
95+
failed = []
96+
original_branch = current_branch
97+
backports.each do |backport|
98+
@logger.info "Backport #{backport[:number]} #{backport[:title]}"
99+
if @options[:dry_run]
100+
@logger.info "DRY_RUN: PR was created: \##{backport[:number]} #{backport[:title]}"
101+
next
102+
end
103+
begin
104+
branch = "backport-to-#{@options[:branch]}/pr#{backport[:number]}"
105+
@logger.debug "git switch --create #{branch} --track #{@options[:remote]}/#{@options[:branch]}"
106+
IO.popen(["git", "switch", "--create", branch, "--track", "#{@options[:remote]}/#{@options[:branch]}"]) do |io|
107+
@logger.debug io.read
108+
end
109+
@logger.info `git branch`
110+
@logger.info "cherry-pick for #{backport[:number]}"
111+
@logger.debug "git cherry-pick --signoff #{backport[:merge_commit_sha]}"
112+
IO.popen(["git", "cherry-pick", "--signoff", backport[:merge_commit_sha]]) do |io|
113+
@logger.debug io.read
114+
end
115+
if $? != 0
116+
@logger.warn "Give up cherry-pick for #{backport[:number]}"
117+
@logger.debug `git cherry-pick --abort`
118+
failed << backport
119+
next
120+
else
121+
@logger.info "Push branch: #{branch}"
122+
@logger.debug `git push origin #{branch}`
123+
end
124+
125+
upstream_repo = "/repos/#{@options[:upstream]}/pulls"
126+
owner = @options[:upstream].split('/').first
127+
head = "#{owner}:#{branch}"
128+
@logger.debug "Create pull request repo: #{upstream_repo} head: #{head} base: #{@options[:branch]}"
129+
IO.popen(["gh", "api", "--method", "POST",
130+
"-H", "Accept: application/vnd.github+json",
131+
"-H", "X-GitHub-Api-Version: 2022-11-28",
132+
upstream_repo,
133+
"-f", "title=#{backport[:title]}",
134+
"-f", "body=#{backport[:body]}",
135+
"-f", "head=#{head}",
136+
"-f", "base=#{@options[:branch]}"]) do |io|
137+
json = JSON.parse(io.read)
138+
@logger.info "PR was created: #{json['url']}"
139+
end
140+
rescue => e
141+
@logger.error "ERROR: #{backport[:number]} #{e.message}"
142+
ensure
143+
IO.popen(["git", "checkout", original_branch]) do |io|
144+
@logger.debug io.read
145+
end
146+
end
147+
end
148+
failed.each do |backport|
149+
@logger.error "FAILED: #{backport[:number]} #{backport[:title]}"
150+
end
151+
end
152+
153+
def run(argv)
154+
parse_command_line(argv)
155+
@logger.info("Target upstream: #{@options[:upstream]} target branch: #{@options[:branch]}")
156+
create_pull_requests
157+
end
158+
end

0 commit comments

Comments
 (0)