decidim-maintainers_toolbox 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +66 -0
- data/decidim-maintainers_toolbox.gemspec +47 -0
- data/exe/decidim-backporter +44 -0
- data/exe/decidim-backports-checker +48 -0
- data/exe/decidim-changelog-generator +46 -0
- data/exe/decidim-releaser +41 -0
- data/lib/decidim/maintainers_toolbox/backporter.rb +100 -0
- data/lib/decidim/maintainers_toolbox/backports_reporter/cli_report.rb +46 -0
- data/lib/decidim/maintainers_toolbox/backports_reporter/csv_report.rb +34 -0
- data/lib/decidim/maintainers_toolbox/backports_reporter/report.rb +67 -0
- data/lib/decidim/maintainers_toolbox/changelog_generator.rb +166 -0
- data/lib/decidim/maintainers_toolbox/git_backport_checker.rb +81 -0
- data/lib/decidim/maintainers_toolbox/git_backport_manager.rb +184 -0
- data/lib/decidim/maintainers_toolbox/github_manager/poster.rb +75 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier/base.rb +80 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier/by_issue_id.rb +48 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier/by_label.rb +97 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier/by_title.rb +59 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier/related_issues.rb +53 -0
- data/lib/decidim/maintainers_toolbox/github_manager/querier.rb +23 -0
- data/lib/decidim/maintainers_toolbox/releaser.rb +287 -0
- data/lib/decidim/maintainers_toolbox/version.rb +7 -0
- data/lib/decidim/maintainers_toolbox.rb +10 -0
- metadata +160 -0
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "github_manager/querier"
|
4
|
+
|
5
|
+
require "json"
|
6
|
+
require "ruby-progressbar"
|
7
|
+
|
8
|
+
module Decidim
|
9
|
+
module MaintainersToolbox
|
10
|
+
class ChangeLogGenerator
|
11
|
+
class InvalidMetadataError < StandardError; end
|
12
|
+
|
13
|
+
TYPES = {
|
14
|
+
"Added" => {
|
15
|
+
label: "type: feature",
|
16
|
+
skip_modules: false
|
17
|
+
},
|
18
|
+
"Changed" => {
|
19
|
+
label: "type: change",
|
20
|
+
skip_modules: false
|
21
|
+
},
|
22
|
+
"Fixed" => {
|
23
|
+
label: "type: fix",
|
24
|
+
skip_modules: false
|
25
|
+
},
|
26
|
+
"Removed" => {
|
27
|
+
label: "type: removal",
|
28
|
+
skip_modules: false
|
29
|
+
},
|
30
|
+
"Developer improvements" => {
|
31
|
+
label: "target: developer-experience",
|
32
|
+
skip_modules: true
|
33
|
+
},
|
34
|
+
"Internal" => {
|
35
|
+
label: "type: internal",
|
36
|
+
skip_modules: false
|
37
|
+
}
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
def initialize(token:, since_sha:)
|
41
|
+
@token = token
|
42
|
+
@since_sha = since_sha
|
43
|
+
@output_file = []
|
44
|
+
@handled_ids = []
|
45
|
+
|
46
|
+
@progress_bar = ProgressBar.create(title: "PRs", total: list_of_commits.count)
|
47
|
+
end
|
48
|
+
|
49
|
+
def call
|
50
|
+
raise InvalidMetadataError if token.nil?
|
51
|
+
|
52
|
+
TYPES.each do |type_title, type_data|
|
53
|
+
type_prs = prs.select do |_commit_title, data|
|
54
|
+
next unless data
|
55
|
+
|
56
|
+
data[:type].include?(type_data[:label])
|
57
|
+
end
|
58
|
+
|
59
|
+
output "### #{type_title}"
|
60
|
+
output ""
|
61
|
+
|
62
|
+
if type_prs.any?
|
63
|
+
type_prs.each do |_pr_title, data|
|
64
|
+
process_single_pr(data, type_data)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
output "Nothing."
|
68
|
+
end
|
69
|
+
|
70
|
+
output ""
|
71
|
+
end
|
72
|
+
|
73
|
+
process_unsorted_prs
|
74
|
+
write_data_file!
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def write_data_file!
|
80
|
+
File.write("temporary_changelog.md", @output_file.join("\n"))
|
81
|
+
puts "Written file: temporary_changelog.md"
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_single_pr(data, type_data)
|
85
|
+
modules_list = data[:modules].map { |l| "**decidim-#{l.delete_prefix("module: ")}**" }
|
86
|
+
id = data[:id]
|
87
|
+
title = data[:title]
|
88
|
+
|
89
|
+
@handled_ids << id
|
90
|
+
|
91
|
+
if type_data[:skip_modules] || modules_list.empty?
|
92
|
+
output "- #{title} #{pr_link(id)}"
|
93
|
+
else
|
94
|
+
output "- #{modules_list.join(", ")}: #{title} #{pr_link(id)}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def process_unsorted_prs
|
99
|
+
return unless unsorted_prs.any?
|
100
|
+
|
101
|
+
output "### Unsorted"
|
102
|
+
output ""
|
103
|
+
|
104
|
+
unsorted_prs.map do |title, data|
|
105
|
+
pr_data = data || {}
|
106
|
+
output "- #{title} #{pr_link(pr_data[:id])} || #{data}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def unsorted_prs
|
111
|
+
@unsorted_prs ||= prs.reject do |_commit_title, data|
|
112
|
+
pr_data = data || {}
|
113
|
+
|
114
|
+
@handled_ids.include?(pr_data[:id])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
attr_reader :token, :since_sha
|
119
|
+
|
120
|
+
def prs
|
121
|
+
@prs ||= list_of_commits.inject({}) do |acc, commit|
|
122
|
+
next acc if crowdin?(commit)
|
123
|
+
|
124
|
+
acc.update(commit => get_pr_data(commit))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def list_of_commits
|
129
|
+
@list_of_commits ||= `git log #{since_sha}..HEAD --oneline`.split("\n").reverse
|
130
|
+
end
|
131
|
+
|
132
|
+
def crowdin?(commit)
|
133
|
+
!commit.match(/New Crowdin updates/).nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_pr_id(commit)
|
137
|
+
id = commit.scan(/#\d+/).last
|
138
|
+
return unless id
|
139
|
+
|
140
|
+
id.delete_prefix("#")
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_pr_data(commit)
|
144
|
+
@progress_bar.increment
|
145
|
+
|
146
|
+
id = get_pr_id(commit)
|
147
|
+
return nil unless id
|
148
|
+
|
149
|
+
Decidim::MaintainersToolbox::GithubManager::Querier::ByIssueId.new(
|
150
|
+
token: token,
|
151
|
+
issue_id: id
|
152
|
+
).call
|
153
|
+
end
|
154
|
+
|
155
|
+
def pr_link(id)
|
156
|
+
# We need to do this so that it generates the expected Markdown format.
|
157
|
+
# String interpolation messes with the format.
|
158
|
+
"[#{"\\#" + id.to_s}](https://github.com/decidim/decidim/pull/#{id})" # rubocop:disable Style/StringConcatenation
|
159
|
+
end
|
160
|
+
|
161
|
+
def output(str)
|
162
|
+
@output_file << str
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby-progressbar"
|
4
|
+
|
5
|
+
require_relative "github_manager/querier/by_label"
|
6
|
+
require_relative "github_manager/querier/related_issues"
|
7
|
+
require_relative "backports_reporter/csv_report"
|
8
|
+
require_relative "backports_reporter/cli_report"
|
9
|
+
|
10
|
+
module Decidim
|
11
|
+
module MaintainersToolbox
|
12
|
+
# Extracts the status of the Pull Requests on the decidim repository
|
13
|
+
# with the label "type: fix" and shows the status of the related Pull Requests,
|
14
|
+
# so we can check which PRs have pending backports
|
15
|
+
class GitBackportChecker
|
16
|
+
# @param token [String] token for GitHub authentication
|
17
|
+
# @param days_to_check_from [Integer] the number of days since the pull requests were merged from when we will start the check
|
18
|
+
# @param last_version_number [String] the version number of the last release that we want to make the backport to
|
19
|
+
def initialize(token:, days_to_check_from:, last_version_number:)
|
20
|
+
@token = token
|
21
|
+
@days_to_check_from = days_to_check_from
|
22
|
+
@last_version_number = last_version_number
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
pull_requests_with_labels = by_label(
|
27
|
+
label: "type: fix",
|
28
|
+
exclude_labels: ["backport", "no-backport"],
|
29
|
+
days_to_check_from: @days_to_check_from
|
30
|
+
)
|
31
|
+
|
32
|
+
progress_bar = ProgressBar.create(title: "PRs", total: pull_requests_with_labels.count)
|
33
|
+
@report = []
|
34
|
+
|
35
|
+
pull_requests_with_labels.each do |pull_request|
|
36
|
+
progress_bar.increment
|
37
|
+
|
38
|
+
@report << {
|
39
|
+
id: pull_request[:id],
|
40
|
+
title: pull_request[:title],
|
41
|
+
related_issues: related_issues(pull_request[:id])
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def csv_report
|
47
|
+
Decidim::MaintainersToolbox::BackportsReporter::CSVReport.new(
|
48
|
+
report: @report,
|
49
|
+
last_version_number: @last_version_number
|
50
|
+
).call
|
51
|
+
end
|
52
|
+
|
53
|
+
def cli_report
|
54
|
+
Decidim::MaintainersToolbox::BackportsReporter::CLIReport.new(
|
55
|
+
report: @report,
|
56
|
+
last_version_number: @last_version_number
|
57
|
+
).call
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :token
|
63
|
+
|
64
|
+
def by_label(label:, exclude_labels:, days_to_check_from:)
|
65
|
+
Decidim::MaintainersToolbox::GithubManager::Querier::ByLabel.new(
|
66
|
+
token: token,
|
67
|
+
label: label,
|
68
|
+
exclude_labels: exclude_labels,
|
69
|
+
days_to_check_from: days_to_check_from
|
70
|
+
).call
|
71
|
+
end
|
72
|
+
|
73
|
+
def related_issues(issue_id)
|
74
|
+
Decidim::MaintainersToolbox::GithubManager::Querier::RelatedIssues.new(
|
75
|
+
token: token,
|
76
|
+
issue_id: issue_id
|
77
|
+
).call
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "English"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module MaintainersToolbox
|
8
|
+
# Handles the backport of a given pull request to a branch
|
9
|
+
# Uses the git commnad line client
|
10
|
+
# rubocop:disable Rails/Output
|
11
|
+
class GitBackportManager
|
12
|
+
# @param pull_request_id [String] the ID of the pull request that we want to backport
|
13
|
+
# @param release_branch [String] the name of the branch that we want to backport to
|
14
|
+
# @param backport_branch [String] the name of the branch that we want to create
|
15
|
+
# @param working_dir [String] current working directory. Useful for testing purposes
|
16
|
+
# @param exit_with_unstaged_changes [Boolean] wheter we should exit cowardly if there is any unstaged change
|
17
|
+
def initialize(pull_request_id:, release_branch:, backport_branch:, working_dir: Dir.pwd, exit_with_unstaged_changes: false)
|
18
|
+
@pull_request_id = pull_request_id
|
19
|
+
@release_branch = release_branch
|
20
|
+
@backport_branch = sanitize_branch(backport_branch)
|
21
|
+
@working_dir = working_dir
|
22
|
+
@exit_with_unstaged_changes = exit_with_unstaged_changes
|
23
|
+
end
|
24
|
+
|
25
|
+
# Handles all the different tasks involved on a backport with the git command line utility
|
26
|
+
# It does the following tasks:
|
27
|
+
# * Creates a branch based on a release branch
|
28
|
+
# * Apply a commit to this branch
|
29
|
+
# * Push it to the remote repository
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
def call
|
33
|
+
Dir.chdir(working_dir) do
|
34
|
+
exit_if_unstaged_changes if @exit_with_unstaged_changes
|
35
|
+
self.class.checkout_develop
|
36
|
+
sha_commit = sha_commit_to_backport
|
37
|
+
|
38
|
+
error_message = <<-EOERROR
|
39
|
+
Could not find commit for pull request #{pull_request_id}.
|
40
|
+
Please make sure you have pulled the latest changes.
|
41
|
+
EOERROR
|
42
|
+
exit_with_errors(error_message) unless sha_commit
|
43
|
+
|
44
|
+
create_backport_branch!
|
45
|
+
cherrypick_commit!(sha_commit)
|
46
|
+
clean_commit_message!
|
47
|
+
push_backport_branch!
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Switch to the develop branch
|
52
|
+
# In case that it cannot do that, exits
|
53
|
+
#
|
54
|
+
# @return [void]
|
55
|
+
def self.checkout_develop
|
56
|
+
`git checkout develop`
|
57
|
+
|
58
|
+
error_message = <<-EOERROR
|
59
|
+
Could not checkout the develop branch.
|
60
|
+
Please make sure you do not have any uncommitted changes in the current branch.
|
61
|
+
EOERROR
|
62
|
+
exit_with_errors(error_message) unless $CHILD_STATUS.exitstatus.zero?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
attr_reader :pull_request_id, :release_branch, :backport_branch, :working_dir
|
68
|
+
|
69
|
+
# Create the backport branch based on a release branch
|
70
|
+
# Checks that this branch does not exist already, if it does then exits
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def create_backport_branch!
|
74
|
+
`git checkout #{release_branch}`
|
75
|
+
|
76
|
+
diff_count = `git rev-list HEAD..#{remote}/#{release_branch} --count`.strip.to_i
|
77
|
+
`git pull #{remote} #{release_branch}` if diff_count.positive?
|
78
|
+
`git checkout -b #{backport_branch}`
|
79
|
+
|
80
|
+
error_message = <<-EOERROR
|
81
|
+
Branch already exists locally.
|
82
|
+
Delete it with 'git branch -D #{backport_branch}' and rerun the script.
|
83
|
+
EOERROR
|
84
|
+
exit_with_errors(error_message) unless $CHILD_STATUS.exitstatus.zero?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Cherrypick a commit from another branch
|
88
|
+
# Apply the changes introduced by some existing commits
|
89
|
+
# Drops to a shell in case that it needs a manual conflict resolution
|
90
|
+
#
|
91
|
+
# @return [void]
|
92
|
+
def cherrypick_commit!(sha_commit)
|
93
|
+
return unless sha_commit
|
94
|
+
|
95
|
+
puts "Cherrypicking commit #{sha_commit}"
|
96
|
+
`git cherry-pick #{sha_commit}`
|
97
|
+
|
98
|
+
unless $CHILD_STATUS.exitstatus.zero?
|
99
|
+
puts "Resolve the cherrypick conflict manually and exit your shell to keep with the process."
|
100
|
+
system ENV.fetch("SHELL")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Clean the commit message to remove the pull request ID of the last commit
|
105
|
+
# This is mostly cosmetic, but if we don´t do this, we will have the two IDs on the final commit:
|
106
|
+
# the ID of the original PR and the id of the backported PR.
|
107
|
+
#
|
108
|
+
# @return [void]
|
109
|
+
def clean_commit_message!
|
110
|
+
message = `git log --pretty=format:"%B" -1`
|
111
|
+
message = message.lines[0].gsub!(/ \(#[0-9]+\)$/, "").concat(*message.lines[1..-1])
|
112
|
+
message.gsub!('"', '"\""') # Escape the double quotes for bash as they are the message delimiters
|
113
|
+
|
114
|
+
`git commit --amend -m "#{message}"`
|
115
|
+
end
|
116
|
+
|
117
|
+
# Push the branch to a git remote repository
|
118
|
+
# Checks that there is actually something to push first, if not then it exits.
|
119
|
+
#
|
120
|
+
# @return [void]
|
121
|
+
def push_backport_branch!
|
122
|
+
if `git diff #{backport_branch}..#{release_branch}`.empty?
|
123
|
+
self.class.checkout_develop
|
124
|
+
|
125
|
+
error_message = <<-EOERROR
|
126
|
+
Nothing to push to remote server.
|
127
|
+
It was probably merged already or the cherry-pick was aborted.
|
128
|
+
EOERROR
|
129
|
+
exit_with_errors(error_message)
|
130
|
+
else
|
131
|
+
puts "Pushing branch #{backport_branch} to #{remote}"
|
132
|
+
`git push #{remote} #{backport_branch}`
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# The name of the git remote repository in the local git repository configuration
|
137
|
+
# Most of the times this would be origin or upstream.
|
138
|
+
#
|
139
|
+
# @return [String] the name of the git repository
|
140
|
+
def remote
|
141
|
+
`git remote -v | grep -e 'decidim/decidim\\([^ ]*\\) (push)' | sed 's/\\s.*//'`.strip
|
142
|
+
end
|
143
|
+
|
144
|
+
# The SHA1 commit to backport
|
145
|
+
# It needs to have a pull_request_id associated in the commit message
|
146
|
+
#
|
147
|
+
# @return [String] the SHA1 commit
|
148
|
+
def sha_commit_to_backport
|
149
|
+
`git log --format=oneline | grep "(##{pull_request_id})"`.split.first
|
150
|
+
end
|
151
|
+
|
152
|
+
# Replace all the characters from the user supplied input that are uncontrolled
|
153
|
+
# and could generate a command line injection
|
154
|
+
#
|
155
|
+
# @return [String] the sanitized branch name
|
156
|
+
def sanitize_branch(branch_name)
|
157
|
+
Shellwords.escape(branch_name.gsub("`", ""))
|
158
|
+
end
|
159
|
+
|
160
|
+
# Exit the script execution if there are any unstaged changes
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
def exit_if_unstaged_changes
|
164
|
+
return if `git diff`.empty?
|
165
|
+
|
166
|
+
error_message = <<-EOERROR
|
167
|
+
There are changes not staged in your project.
|
168
|
+
Please commit your changes or stash them.
|
169
|
+
EOERROR
|
170
|
+
exit_with_errors(error_message)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Exit the script execution with a message
|
174
|
+
#
|
175
|
+
# @return [void]
|
176
|
+
def exit_with_errors(message)
|
177
|
+
puts message
|
178
|
+
exit 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# rubocop:enable Rails/Output
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "faraday"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module Decidim
|
8
|
+
module MaintainersToolbox
|
9
|
+
module GithubManager
|
10
|
+
# Allows to make POST requests to GitHub Rest API about Pull Requests
|
11
|
+
# @see https://docs.github.com/en/rest
|
12
|
+
# rubocop:disable Rails/Output
|
13
|
+
class Poster
|
14
|
+
# @param token [String] token for GitHub authentication
|
15
|
+
# @param params [Hash] Parameters accepted by the GitHub API
|
16
|
+
def initialize(token:, params:)
|
17
|
+
@token = token
|
18
|
+
@params = params
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create the pull request or give error messages
|
22
|
+
#
|
23
|
+
# @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
|
24
|
+
def call
|
25
|
+
response = create_pull_request!
|
26
|
+
pull_request_id = JSON.parse(response.body)["number"]
|
27
|
+
unless pull_request_id
|
28
|
+
puts "Pull request could not be created!"
|
29
|
+
puts "Please make sure you have enabled the 'public_repo' scope for the access token"
|
30
|
+
return
|
31
|
+
end
|
32
|
+
puts "Pull request created at https://github.com/decidim/decidim/pull/#{pull_request_id}"
|
33
|
+
|
34
|
+
add_labels_to_issue!(pull_request_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :token, :params
|
40
|
+
|
41
|
+
# Make the POST request to GitHub API of the decidim repository
|
42
|
+
#
|
43
|
+
# @param path [String] The path to do the request to the GitHub API
|
44
|
+
# @param some_params [Hash] The parameters of the request
|
45
|
+
# @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
|
46
|
+
def post!(path, some_params)
|
47
|
+
uri = "https://api.github.com/repos/decidim/decidim/#{path}"
|
48
|
+
Faraday.post(uri, some_params.to_json, { Authorization: "token #{token}" })
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a pull request using the GitHub API
|
52
|
+
#
|
53
|
+
# @see https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request GitHub API documentation
|
54
|
+
# @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
|
55
|
+
def create_pull_request!
|
56
|
+
puts "Creating the PR in GitHub"
|
57
|
+
post!("pulls", params.except(:labels))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add labels to an issue or a pull request
|
61
|
+
# GitHub does not support adding labels while creating the PR, so we need to do it afterwards
|
62
|
+
#
|
63
|
+
# @see https://docs.github.com/en/rest/issues/labels#add-labels-to-an-issue GitHub API documentation
|
64
|
+
# @param issue_id [String] String of the issue to add the labels to
|
65
|
+
# @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
|
66
|
+
def add_labels_to_issue!(issue_id)
|
67
|
+
puts "Adding the labels to the PR"
|
68
|
+
post!("issues/#{issue_id}/labels", params.slice(:labels))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# rubocop:enable Rails/Output
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "faraday"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Decidim
|
8
|
+
module MaintainersToolbox
|
9
|
+
module GithubManager
|
10
|
+
module Querier
|
11
|
+
# Base class that allows making GET requests to GitHub Rest API about Issues and Pull Requests
|
12
|
+
# This must be inherited from other class with the following methods:
|
13
|
+
# - call
|
14
|
+
# - uri
|
15
|
+
# @see https://docs.github.com/en/rest
|
16
|
+
class Base
|
17
|
+
class InvalidMetadataError < StandardError; end
|
18
|
+
|
19
|
+
def initialize(token:)
|
20
|
+
@token = token
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
raise "Not implemented"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :token
|
30
|
+
|
31
|
+
def headers
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def authorization_header
|
36
|
+
{ Authorization: "token #{token}" }
|
37
|
+
end
|
38
|
+
|
39
|
+
def request(uri)
|
40
|
+
response = Faraday.get(uri, headers, authorization_header)
|
41
|
+
|
42
|
+
{ body: response.body, headers: response.headers }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get's the JSON response from a URI
|
46
|
+
# Supports pagination
|
47
|
+
#
|
48
|
+
# @param uri {String} - The URL that we want to get the JSON response from
|
49
|
+
# @param old_json {Array} - The Array with the old_json or an empty Array if it is the first time that we are calling this method
|
50
|
+
def json_response(uri, old_json = [])
|
51
|
+
body, headers = request(uri).values_at(:body, :headers)
|
52
|
+
json = JSON.parse(body)
|
53
|
+
json.concat(old_json) if json.is_a?(Array)
|
54
|
+
raise InvalidMetadataError if json.is_a?(Hash) && json["message"] == "Bad credentials"
|
55
|
+
|
56
|
+
# If there are more pages, then we call ourselves redundantly to fetch the next page
|
57
|
+
next_json = more_pages?(headers) ? json_response(next_page(headers), json) : []
|
58
|
+
|
59
|
+
if json.is_a?(Array)
|
60
|
+
# For some reason we have duplicated values, so we deduplicate them
|
61
|
+
json.concat(next_json).uniq { |issue| issue.has_key?("number") ? issue["number"] : issue }
|
62
|
+
else
|
63
|
+
json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def more_pages?(headers)
|
68
|
+
return false if headers["link"].nil?
|
69
|
+
|
70
|
+
headers["link"].include?('rel="next"')
|
71
|
+
end
|
72
|
+
|
73
|
+
def next_page(headers)
|
74
|
+
URI.extract(headers["link"].split(",").select { |url| url.end_with?('rel="next"') }[0])[0]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
module MaintainersToolbox
|
7
|
+
module GithubManager
|
8
|
+
module Querier
|
9
|
+
# Makes a GET request for the metadata of an Issue or Pull Request in GitHub
|
10
|
+
#
|
11
|
+
# @see https://docs.github.com/en/rest/issues/issues#get-an-issue GitHub API documentation
|
12
|
+
class ByIssueId < Decidim::MaintainersToolbox::GithubManager::Querier::Base
|
13
|
+
def initialize(issue_id:, token:)
|
14
|
+
@issue_id = issue_id
|
15
|
+
@token = token
|
16
|
+
end
|
17
|
+
|
18
|
+
# Makes the GET request and parses the response of an Issue or Pull Request in GitHub
|
19
|
+
#
|
20
|
+
# @return [Hash]
|
21
|
+
def call
|
22
|
+
data = json_response("https://api.github.com/repos/decidim/decidim/issues/#{@issue_id}")
|
23
|
+
return unless data["number"]
|
24
|
+
|
25
|
+
parse(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Parses the response of an Issue or Pull Request in GitHub
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
def parse(metadata)
|
34
|
+
labels = metadata["labels"].map { |l| l["name"] }.sort
|
35
|
+
|
36
|
+
{
|
37
|
+
id: metadata["number"],
|
38
|
+
title: metadata["title"],
|
39
|
+
labels: labels,
|
40
|
+
type: labels.select { |l| l.match(/^type: /) || l == "target: developer-experience" },
|
41
|
+
modules: labels.select { |l| l.match(/^module: /) }
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|