-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add action backporter * Fix specs * Fix latest issues * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena <[email protected]> * Apply review recommendations --------- Co-authored-by: Andrés Pereira de Lucena <[email protected]>
- Loading branch information
1 parent
e3ed6e8
commit 3ddec41
Showing
9 changed files
with
362 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require "thor" | ||
|
||
require_relative "../lib/decidim/maintainers_toolbox/github_manager/querier" | ||
require_relative "../lib/decidim/maintainers_toolbox/action_backporter" | ||
|
||
class ActionBackporterCLI < Thor | ||
desc "", "Backport a pull request to another branch. This is intended to be run in the GitHub Action environment, for automating the backport processes." | ||
option :github_token, required: true, desc: <<~HELP | ||
Required. Github Personal Access Token (PAT). It can be obtained from https://github.com/settings/tokens/new. You will need to create one with `public_repo` access. | ||
Alternatively, you can use the `gh` CLI tool to authenticate with `gh auth token` (i.e. --github-token=$(gh auth token)) | ||
HELP | ||
option :pull_request_id, required: true, desc: "Required. The ID of the pull request that you want to make the backport from. It should have the \"type: fix\" label." | ||
option :exit_with_unstaged_changes, type: :boolean, default: true, desc: <<~HELP | ||
Optional. Whether the script should exit with an error if there are unstaged changes in the current project. | ||
HELP | ||
default_task :backport | ||
|
||
def backport | ||
Decidim::MaintainersToolbox::ActionBackporter.new( | ||
token: options[:github_token], | ||
pull_request_id: options[:pull_request_id], | ||
exit_with_unstaged_changes: options[:exit_with_unstaged_changes] | ||
).call | ||
rescue Decidim::MaintainersToolbox::GithubManager::Querier::Base::InvalidMetadataError | ||
puts "Metadata was not returned from the server. Please check that the provided pull request ID and GitHub token are correct." | ||
end | ||
|
||
def help | ||
super("backport") | ||
end | ||
|
||
def self.exit_on_failure? | ||
true | ||
end | ||
end | ||
|
||
ActionBackporterCLI.start(ARGV) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "github_manager/querier" | ||
require_relative "github_manager/poster" | ||
require_relative "git_backport_manager" | ||
|
||
module Decidim | ||
module MaintainersToolbox | ||
class ActionBackporter | ||
class InvalidMetadataError < StandardError; end | ||
|
||
DECIDIM_MAINTAINERS = ["alecslupu", "andreslucena"] | ||
|
||
# @param token [String] token for GitHub authentication | ||
# @param pull_request_id [String] the ID of the pull request that we want to backport | ||
# @param exit_with_unstaged_changes [Boolean] wheter we should exit cowardly if there is any unstaged change | ||
def initialize(token: , pull_request_id: ,exit_with_unstaged_changes: ) | ||
@token = token | ||
@pull_request_id = pull_request_id | ||
@exit_with_unstaged_changes = exit_with_unstaged_changes | ||
end | ||
|
||
def call | ||
exit_with_errors("The requested PR #{pull_request_id} does not contain `type: fix`") unless pull_request_metadata[:labels].include?("type: fix") | ||
exit_with_errors("The requested PR #{pull_request_id} is not merged") unless pull_request_metadata[:is_merged] | ||
exit_with_errors("The requested PR #{pull_request_id} cannot be backported") if pull_request_metadata[:labels].include?("no-backport") | ||
|
||
extract_versions.each do |version| | ||
next if extract_backport_pull_request_for_version(related_issues, version) | ||
system("decidim-backporter --github_token=#{token} --pull_request_id=#{pull_request_id} --version_number=#{version} --exit_with_unstaged_changes=#{exit_with_unstaged_changes} --with-console=false", exception: true) | ||
rescue RuntimeError => e | ||
puts e.message | ||
create_backport_issue(version) | ||
end | ||
end | ||
|
||
private | ||
|
||
attr_reader :token, :pull_request_id, :exit_with_unstaged_changes | ||
|
||
def create_backport_issue(version) | ||
some_params = { | ||
title: "Fail: automatic backport of \"#{pull_request_metadata[:title]}\"", | ||
body: "Automatic backport of ##{pull_request_id} has failed for version #{version}. Please do this action manually.", | ||
assignee: DECIDIM_MAINTAINERS, | ||
labels: pull_request_metadata[:labels] | ||
} | ||
|
||
uri = "https://api.github.com/repos/decidim/decidim/issues" | ||
Faraday.post(uri, some_params.to_json, { Authorization: "token #{token}" }) | ||
end | ||
|
||
# same method exists in lib/decidim/maintainers_toolbox/backports_reporter/report.rb | ||
def extract_backport_pull_request_for_version(related_issues, version) | ||
related_issues = related_issues.select do |pull_request| | ||
pull_request[:title].start_with?("Backport") && pull_request[:title].include?(version) | ||
end | ||
return if related_issues.empty? | ||
|
||
related_issues.first | ||
end | ||
|
||
def extract_versions | ||
return [] unless pull_request_metadata[:labels] | ||
|
||
pull_request_metadata[:labels].map do |item| | ||
item.match(/release: v(\d+\.\d+)/) { |m| m[1] } | ||
end.compact | ||
end | ||
|
||
# Asks the metadata for a given issue or pull request on GitHub API | ||
# | ||
# @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request | ||
# Same method exists in lib/decidim/maintainers_toolbox/backporter.rb | ||
def pull_request_metadata | ||
@pull_request_metadata ||= Decidim::MaintainersToolbox::GithubManager::Querier::ByIssueId.new( | ||
token: token, | ||
issue_id: pull_request_id | ||
).call | ||
end | ||
|
||
def related_issues | ||
@related_issues ||= Decidim::MaintainersToolbox::GithubManager::Querier::RelatedIssues.new( | ||
token: token, | ||
issue_id: pull_request_id | ||
).call | ||
end | ||
|
||
# Exit the script execution with a message | ||
# | ||
# @return [void] | ||
def exit_with_errors(message) | ||
puts message | ||
exit 1 | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
spec/lib/decidim/maintainers_toolbox/action_backporter_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# frozen_string_literal: true | ||
|
||
require "decidim/maintainers_toolbox/action_backporter" | ||
require "webmock/rspec" | ||
|
||
describe Decidim::MaintainersToolbox::ActionBackporter do | ||
|
||
subject { described_class.new(token: token, pull_request_id: pull_request_id, exit_with_unstaged_changes: exit_with_unstaged_changes) } | ||
|
||
let(:token) { "1234" } | ||
let(:pull_request_id) { 123 } | ||
let(:exit_with_unstaged_changes) { true } | ||
|
||
before do | ||
stub_request(:get, "https://api.github.com/repos/decidim/decidim/issues/123"). | ||
to_return(status: 200, body: '{"number": 12345, "pull_request": {"merged_at": "" }, "title": "Fix whatever", "labels": [{"name": "type: fix"}, {"name": "module: admin"}]}', headers: {}) | ||
|
||
stub_request(:post, "https://api.github.com/repos/decidim/decidim/issues") | ||
.to_return(status: 200, body: "{}", headers: {}) | ||
end | ||
|
||
describe ".exit_with_errors" do | ||
it "exit with a custom message" do | ||
expect { subject.send(:exit_with_errors, "Bye") }.to raise_error(SystemExit).and output(/Bye/).to_stdout | ||
end | ||
end | ||
|
||
describe ".call" do | ||
it "exists when the PR is not a fix" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({labels: ["type: change"], is_merged: true }) | ||
expect{ subject.call }.to raise_error(SystemExit).and output(/does not contain `type: fix`/).to_stdout | ||
end | ||
|
||
it "exists when the PR is not merged" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({labels: ["type: fix"], is_merged: false }) | ||
expect{ subject.call }.to raise_error(SystemExit).and output(/is not merged/).to_stdout | ||
end | ||
|
||
it "exists when the PR is not backportable" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({labels: ["type: fix", "no-backport"], is_merged: true }) | ||
expect{ subject.call }.to raise_error(SystemExit).and output(/cannot be backported/).to_stdout | ||
end | ||
|
||
it "calls extract versions" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "release: v0.28", "release: v0.29"], is_merged: true }) | ||
allow(subject).to receive(:extract_versions).and_return(["0.28", "0.29"]) | ||
allow(subject).to receive(:related_issues).and_return([]) | ||
allow(subject).to receive(:system).and_return(true) | ||
|
||
expect(subject).to receive(:extract_versions) | ||
|
||
subject.call | ||
end | ||
|
||
it "runs the system command" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "release: v0.28", "release: v0.29"], is_merged: true }) | ||
allow(subject).to receive(:extract_versions).and_return(["0.28", "0.29"]) | ||
allow(subject).to receive(:related_issues).and_return([]) | ||
|
||
expect(subject).to receive(:system).with("decidim-backporter --github_token=#{token} --pull_request_id=#{pull_request_id} --version_number=0.28 --exit_with_unstaged_changes=#{exit_with_unstaged_changes} --with-console=false", exception: true) | ||
expect(subject).to receive(:system).with("decidim-backporter --github_token=#{token} --pull_request_id=#{pull_request_id} --version_number=0.29 --exit_with_unstaged_changes=#{exit_with_unstaged_changes} --with-console=false", exception: true) | ||
|
||
subject.call | ||
end | ||
|
||
it "skips the creation" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "release: v0.28", "release: v0.29"], is_merged: true }) | ||
allow(subject).to receive(:extract_versions).and_return(["0.28", "0.29"]) | ||
allow(subject).to receive(:related_issues).and_return([{title: "Backport 0.28"}, {title: "Backport 0.29"}]) | ||
|
||
expect(subject).to receive(:extract_backport_pull_request_for_version).with(kind_of(Array), "0.28").and_return({}) | ||
expect(subject).to receive(:extract_backport_pull_request_for_version).with(kind_of(Array), "0.29").and_return({}) | ||
|
||
subject.call | ||
end | ||
|
||
it "creates the ticket" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "release: v0.28", "release: v0.29"], is_merged: true }) | ||
allow(subject).to receive(:extract_versions).and_return(["0.28", "0.29"]) | ||
allow(subject).to receive(:related_issues).and_return([]) | ||
allow(subject).to receive(:extract_backport_pull_request_for_version).with(kind_of(Array), "0.28").and_return(nil) | ||
allow(subject).to receive(:extract_backport_pull_request_for_version).with(kind_of(Array), "0.29").and_return(nil) | ||
|
||
allow(subject).to receive(:system).and_raise(RuntimeError) | ||
|
||
expect(subject).to receive(:create_backport_issue).with("0.28") | ||
expect(subject).to receive(:create_backport_issue).with("0.29") | ||
|
||
subject.call | ||
end | ||
end | ||
|
||
describe ".extract_versions" do | ||
it "returns the versions" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "release: v0.28", "release: v0.29"], is_merged: true }) | ||
expect(subject.send(:extract_versions)).to eq(["0.28", "0.29"]) | ||
expect(subject.send(:extract_versions).size).to eq(2) | ||
end | ||
|
||
it "returns empty array" do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ labels: ["type: fix", "team: documentation", "module: initiatives"], is_merged: true }) | ||
expect(subject.send(:extract_versions)).to eq([]) | ||
expect(subject.send(:extract_versions).size).to eq(0) | ||
end | ||
end | ||
|
||
describe ".create_backport_task" do | ||
|
||
before do | ||
allow(subject).to receive(:pull_request_metadata).and_return({ title: "Foo Bar", labels: ["type: fix", "release: v0.28"]}) | ||
end | ||
|
||
it "returns the respose from the server" do | ||
expect(subject.send(:create_backport_issue, "0.29")).to be_a Faraday::Response | ||
end | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.