From 992790fe1f34a279d6a7955123490c999af1f3b5 Mon Sep 17 00:00:00 2001 From: Kentaro Hayashi Date: Wed, 26 Nov 2025 15:38:07 +0900 Subject: [PATCH 1/2] github: add rake task to backport PR you can simulate on manually: GITHUB_TOKEN=... rake backport:v1_19 Signed-off-by: Kentaro Hayashi --- .github/workflows/backport.yml | 37 ++++++++ Rakefile | 1 + tasks/backport.rb | 50 +++++++++++ tasks/backport/backporter.rb | 158 +++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 .github/workflows/backport.yml create mode 100644 tasks/backport.rb create mode 100644 tasks/backport/backporter.rb diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..2b85a2bca7 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,37 @@ +name: Backport + +on: + schedule: + # Sun 10:00 (JST) + - cron: '0 1 * * 0' + workflow_dispatch: + +permissions: read-all + +concurrency: + group: ${{ github.head_ref || github.sha }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + continue-on-error: false + strategy: + fail-fast: false + matrix: + ruby-version: ['3.4'] + task: ['backport:v1_16', 'backport:v1_19'] + + name: Backport PR on ${{ matrix.os }} + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Set up Ruby + uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 + with: + ruby-version: ${{ matrix.ruby-version }} + - name: Install dependencies + run: bundle install + - name: Run Benchmark + shell: bash + run: | + bundle exec rake ${{ matrix.task }} diff --git a/Rakefile b/Rakefile index 61b85932e7..590657fcf3 100755 --- a/Rakefile +++ b/Rakefile @@ -6,6 +6,7 @@ require 'rake/testtask' require 'rake/clean' require_relative 'tasks/benchmark' +require_relative 'tasks/backport' task test: [:base_test] diff --git a/tasks/backport.rb b/tasks/backport.rb new file mode 100644 index 0000000000..080e8992f8 --- /dev/null +++ b/tasks/backport.rb @@ -0,0 +1,50 @@ +require_relative 'backport/backporter' + +=begin + +When you want to manually execute backporting, set the following +environment variables: + +* GITHUB_REPOSITORY: fluent/fluentd +* GITHUB_TOKEN: ${PERSONAL_ACCESS_TOKEN} + +Optional: + +* REPOSITORY_REMOTE: origin + If you execute in forked repository, it might be 'upstream' + +=end + +def append_additional_arguments(commands) + if ENV['DRY_RUN'] + commands << '--dry-run' + end + if ENV['GITHUB_REPOSITORY'] + commands << '--upstream' + commands << ENV['GITHUB_REPOSITORY'] + end + if ENV['REPOSITORY_REMOTE'] + commands << '--remote' + commands << ENV['REPOSITORY_REMOTE'] + end + commands +end + +namespace :backport do + + desc "Backport PR to v1.16 branch" + task :v1_16 do + backporter = PullRequestBackporter.new + commands = ['--branch', 'v1.16', '--log-level', 'debug'] + commands = append_additional_arguments(commands) + backporter.run(commands) + end + + desc "Backport PR to v1.19 branch" + task :v1_19 do + commands = ['--branch', 'v1.19', '--log-level', 'debug'] + commands = append_additional_arguments(commands) + backporter = PullRequestBackporter.new + backporter.run(commands) + end +end diff --git a/tasks/backport/backporter.rb b/tasks/backport/backporter.rb new file mode 100644 index 0000000000..bab1127dc3 --- /dev/null +++ b/tasks/backport/backporter.rb @@ -0,0 +1,158 @@ +require 'open-uri' +require 'json' +require 'optparse' +require 'logger' + +class PullRequestBackporter + + def initialize + @logger = Logger.new(STDOUT) + @options = { + upstream: "fluent/fluentd", + branch: "v1.16", + dry_run: false, + log_level: Logger::Severity::INFO, + remote: 'origin' + } + end + + def current_branch + branch = IO.popen(["git", "branch", "--contains"]) do |io| + io.read + end + branch.split.last + end + + def parse_command_line(argv) + opt = OptionParser.new + opt.on('--upstream REPOSITORY', + 'Specify upstream repository (e.g. fluent/fluentd)') {|v| @options[:upstream] = v } + opt.on('--branch BRANCH') {|v| @options[:branch] = v } + opt.on('--dry-run') {|v| @options[:dry_run] = true } + opt.on('--log-level LOG_LEVEL (e.g. debug,info)') {|v| + @options[:log_level] = case v + when "error" + Logger::Severity::ERROR + when "warn" + Logger::Severity::WARN + when "debug" + Logger::Severity::DEBUG + when "info" + Logger::Severity::INFO + else + puts "unknown log level: <#{v}>" + exit 1 + end + } + opt.on('--remote REMOTE') {|v| @options[:remote] = v } + opt.parse!(argv) + end + + def collect_backports + backports = [] + pages = 5 + pages.times.each do |page| + @logger.debug "Collecting backport information (#{page + 1}/#{pages})" + URI.open("https://api.github.com/repos/fluent/fluentd/pulls?state=closed&per_page=100&page=#{page+1}", + "Accept" => "application/vnd.github+json", + "Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}", + "X-GitHub-Api-Version" => "2022-11-28") do |request| + JSON.parse(request.read).each do |pull_request| + unless pull_request["labels"].empty? + labels = pull_request["labels"].collect { |label| label["name"] } + unless labels.include?("backport to #{@options[:branch]}") + next + end + if labels.include?("backported") + @logger.info "[DONE] \##{pull_request['number']} #{pull_request['title']} LABELS: #{pull_request['labels'].collect { |label| label['name'] }}" + next + end + @logger.info "* \##{pull_request['number']} #{pull_request['title']} LABELS: #{pull_request['labels'].collect { |label| label['name'] }}" + # merged into this commit + @logger.debug "MERGE_COMMIT_SHA: #{pull_request['merge_commit_sha']}" + body = pull_request["body"].gsub(/\*\*Which issue\(s\) this PR fixes\*\*: \r\n/, + "**Which issue(s) this PR fixes**: \r\nBackport \##{pull_request['number']}\r\n") + backports << { + number: pull_request["number"], + merge_commit_sha: pull_request["merge_commit_sha"], + title: "Backport(#{@options[:branch]}): #{pull_request['title']} (\##{pull_request['number']})", + body: body + } + end + end + end + end + backports + end + + def create_pull_requests + backports = collect_backports + if backports.empty? + @logger.info "No need to backport pull requests" + return + end + + failed = [] + original_branch = current_branch + backports.each do |backport| + @logger.info "Backport #{backport[:number]} #{backport[:title]}" + if @options[:dry_run] + @logger.info "DRY_RUN: PR was created: \##{backport[:number]} #{backport[:title]}" + next + end + begin + branch = "backport-to-#{@options[:branch]}/pr#{backport[:number]}" + @logger.debug "git switch --create #{branch} --track #{@options[:remote]}/#{@options[:branch]}" + IO.popen(["git", "switch", "--create", branch, "--track", "#{@options[:remote]}/#{@options[:branch]}"]) do |io| + @logger.debug io.read + end + @logger.info `git branch` + @logger.info "cherry-pick for #{backport[:number]}" + @logger.debug "git cherry-pick --signoff #{backport[:merge_commit_sha]}" + IO.popen(["git", "cherry-pick", "--signoff", backport[:merge_commit_sha]]) do |io| + @logger.debug io.read + end + if $? != 0 + @logger.warn "Give up cherry-pick for #{backport[:number]}" + @logger.debug `git cherry-pick --abort` + failed << backport + next + else + @logger.info "Push branch: #{branch}" + @logger.debug `git push origin #{branch}` + end + + upstream_repo = "/repos/#{@options[:upstream]}/pulls" + owner = @options[:upstream].split('/').first + head = "#{owner}:#{branch}" + @logger.debug "Create pull request repo: #{upstream_repo} head: #{head} base: #{@options[:branch]}" + IO.popen(["gh", "api", "--method", "POST", + "-H", "Accept: application/vnd.github+json", + "-H", "X-GitHub-Api-Version: 2022-11-28", + upstream_repo, + "-f", "title=#{backport[:title]}", + "-f", "body=#{backport[:body]}", + "-f", "head=#{head}", + "-f", "base=#{@options[:branch]}"]) do |io| + json = JSON.parse(io.read) + @logger.info "PR was created: #{json['url']}" + end + rescue => e + @logger.error "ERROR: #{backport[:number]} #{e.message}" + ensure + IO.popen(["git", "checkout", original_branch]) do |io| + @logger.debug io.read + end + end + end + failed.each do |backport| + @logger.error "FAILED: #{backport[:number]} #{backport[:title]}" + end + end + + def run(argv) + parse_command_line(argv) + @logger.info("Target upstream: #{@options[:upstream]} target branch: #{@options[:branch]}") + create_pull_requests + end +end From 8d047923ffaf2ffedc94c28fc24142ee26482253 Mon Sep 17 00:00:00 2001 From: Kentaro Hayashi Date: Fri, 5 Dec 2025 17:20:55 +0900 Subject: [PATCH 2/2] Update tasks/backport/backporter.rb Co-authored-by: Shizuo Fujita Signed-off-by: Kentaro Hayashi --- tasks/backport/backporter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/backport/backporter.rb b/tasks/backport/backporter.rb index bab1127dc3..697b5a1eb2 100644 --- a/tasks/backport/backporter.rb +++ b/tasks/backport/backporter.rb @@ -53,7 +53,7 @@ def collect_backports pages = 5 pages.times.each do |page| @logger.debug "Collecting backport information (#{page + 1}/#{pages})" - URI.open("https://api.github.com/repos/fluent/fluentd/pulls?state=closed&per_page=100&page=#{page+1}", + URI.open("https://api.github.com/repos/#{@options[:upstream]}/pulls?state=closed&per_page=100&page=#{page+1}", "Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}", "X-GitHub-Api-Version" => "2022-11-28") do |request|