Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
@@ -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 }}
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require 'rake/testtask'
require 'rake/clean'

require_relative 'tasks/benchmark'
require_relative 'tasks/backport'

task test: [:base_test]

Expand Down
32 changes: 32 additions & 0 deletions tasks/backport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require_relative 'backport/backporter'

namespace :backport do

desc "Backport PR to v1.16 branch"
task :v1_16 do
backporter = PullRequestBackporter.new
commands = ['--branch', 'v1.16', '--log-level', 'debug']
if ENV['DRY_RUN']
commands << '--dry-run'
end
if ENV['GITHUB_REPOSITORY']
commands << '--upstream'
commands << ENV['GITHUB_REPOSITORY']
end
backporter.run(commands)
end

desc "Backport PR to v1.19 branch"
task :v1_19 do
commands = ['--branch', 'v1.19', '--log-level', 'debug']
if ENV['DRY_RUN']
commands << '--dry-run'
end
if ENV['GITHUB_REPOSITORY']
commands << '--upstream'
commands << ENV['GITHUB_REPOSITORY']
end
backporter = PullRequestBackporter.new
backporter.run(commands)
end
end
143 changes: 143 additions & 0 deletions tasks/backport/backporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
require 'open-uri'
require 'json'
require 'optparse'
require 'logger'

class PullRequestBackporter

def initialize
@logger = Logger.new(STDOUT)
@options = {
upstream: "kenhys/fluentd",
branch: "v1.16",
dry_run: false,
log_level: Logger::Severity::INFO
}
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.parse!(argv)
end

def collect_backports
backports = []
5.times.each do |page|
@logger.debug "Collecting backport information"
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.size == 0
@logger.debug "No need to backport: #{backports.size} PR"
return
end

failed = []
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-#{backport[:number]}"
@logger.debug "git switch --create #{branch} --track origin/#{@options[:branch]}"
IO.popen(["git", "switch", "--create", branch, "--track", "origin/#{@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

@logger.debug "Create pull request: #{branch}"
upstream_repo = "/repos/#{@options[:upstream]}/pulls"
owner = @options[:upstream].split('/').first
head = "#{owner}:#{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}"
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
Loading