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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 98bbc7bc7f2fe89db2430fe013218ed8f73139241148f81e2b836001341a9edb
4
+ data.tar.gz: b2fcf684927d644818e40a3b47542bc9e71159ff8a220de40f45f7c02d479d90
5
+ SHA512:
6
+ metadata.gz: e9fcb49b88f506a3d8522ed38afe5253bb6cbdff6f66287ce9e5d9b24800f37d10756784999b9ef3944c7a338ffb088d0216da9232d2b77f604a36dbb40cb7d9
7
+ data.tar.gz: 919e40cba79bb9d64c4cdaf6a7ec485423ffb64017f063ea696be0a5249e9d9eafd4bbf68c7cc85ee7aaf2059c3e083a637273e7f8b132953294f1fdc07c6c7b
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Decidim::MaintainersToolbox
2
+
3
+ Release related tools for the Decidim project.
4
+
5
+ Tools for releasing, backporting, changelog generating, and working with GitHub
6
+
7
+ ## Installation
8
+
9
+ This gem is meant to be used outside of bundler/Gemfile so we do not need to bump the version every time we release a new one to each of the releases branch.
10
+
11
+ ```console
12
+ gem install decidim-maintainers_toolbox
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ This gem allows preparing and working with Decidim releases. Is it meant to be used by maintainers of the project. In the near future most of these tools will be used by `decidim-bot`.
18
+
19
+ There are a couple differences with the rest of the gems of this repository:
20
+
21
+ * About the versioning: as it has not decidim nor decidim-core as dependencies, and to keep it easy to work with, we will not have the same versioning as the others gems.
22
+ * About the ruby version: to make it possible to work with older decidim versions, we will support the lowest supported ruby version.
23
+
24
+ This is the reason why its in a different repository and not in the decidim repository.
25
+
26
+ The main scripts are `decidim-backporter`, `decidim-backports-checker`, `decidim-changelog-generator` and `decidim-releaser`.
27
+
28
+ ### decidim-backporter
29
+
30
+ See [Backports documentation](https://docs.decidim.org/en/develop/develop/backports)
31
+
32
+ ### decidim-backports-checker
33
+
34
+ See [Backports documentation](https://docs.decidim.org/en/develop/develop/backports)
35
+
36
+ ### decidim-changelog-generator
37
+
38
+ Used for generating the changelog with all the Pull Requests that goes to the current release. To be used automatically by the `releaser` script.
39
+
40
+ ### decidim-releaser
41
+
42
+ See [Releasing new versions documentation](https://docs.decidim.org/en/develop/develop/maintainers/releases)
43
+
44
+ ## Development
45
+
46
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+
48
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+
50
+ ## Releases
51
+
52
+ As this gem is meant to be used outside of the main decidim gems, we will not follow the same versioning. We will release a new version of this gem every time we have a new feature or bugfix that we need to use. This also means that we will not follow the same release process.
53
+
54
+ To release this gem, follow these steps:
55
+
56
+ 1. Bump the version number in `lib/decidim/maintainers_toolbox/version.rb` following [Semantic Versioning](https://semver.org/).
57
+ 1. Update the `CHANGELOG.md` with the new version and the changes.
58
+ 1. Commit the changes.
59
+ 1. Create a new tag with the version number.
60
+ 1. Push the changes and the tag to the repository.
61
+ 1. Run `rake build` to build the gem.
62
+ 1. Run `rake release` to publish the gem to [rubygems.org](https://rubygems.org).
63
+
64
+ ## Contributing
65
+
66
+ Bug reports and pull requests are welcome on GitHub at https://github.com/decidim/decidim.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/decidim/maintainers_toolbox/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "decidim-maintainers_toolbox"
7
+ spec.version = Decidim::MaintainersToolbox::VERSION
8
+ spec.authors = ["Andrés Pereira de Lucena"]
9
+ spec.email = ["andreslucena@gmail.com"]
10
+
11
+ spec.summary = "Release related tools for the Decidim project"
12
+ spec.description = "Tools for releasing, backporting, changelog generating, and working with GitHub"
13
+
14
+ spec.license = "AGPL-3.0"
15
+ spec.homepage = "https://decidim.org"
16
+ spec.metadata = {
17
+ "bug_tracker_uri" => "https://github.com/decidim/decidim/issues",
18
+ "documentation_uri" => "https://docs.decidim.org/",
19
+ "funding_uri" => "https://opencollective.com/decidim",
20
+ "homepage_uri" => "https://decidim.org",
21
+ "source_code_uri" => "https://github.com/decidim/decidim"
22
+ }
23
+ spec.required_ruby_version = ">= 2.7.5"
24
+
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").select do |f|
27
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w(
29
+ exe/
30
+ lib/
31
+ README.md
32
+ ))
33
+ end
34
+ end
35
+
36
+ spec.bindir = "exe"
37
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
38
+ spec.require_paths = ["lib"]
39
+
40
+ spec.add_dependency "faraday", "~> 1.10"
41
+ spec.add_dependency "ruby-progressbar", "~> 1.7"
42
+ spec.add_dependency "thor", "~> 1.0"
43
+
44
+ spec.add_development_dependency "activesupport", "~> 6.1.7"
45
+ spec.add_development_dependency "rspec", "~> 3.12"
46
+ spec.add_development_dependency "webmock", "~> 3.18"
47
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ require_relative "../lib/decidim/maintainers_toolbox/github_manager/querier"
7
+ require_relative "../lib/decidim/maintainers_toolbox/github_manager/poster"
8
+ require_relative "../lib/decidim/maintainers_toolbox/git_backport_manager"
9
+ require_relative "../lib/decidim/maintainers_toolbox/backporter"
10
+
11
+ class BackporterCLI < Thor
12
+ desc "", "Backport a pull request to another branch"
13
+ option :github_token, required: true, desc: <<~HELP
14
+ 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.
15
+ Alternatively, you can use the `gh` CLI tool to authenticate with `gh auth token` (i.e. --github-token=$(gh auth token))
16
+ HELP
17
+ option :version_number, required: true, desc: "Required. The version number that you want to do the backport to. It must have the format MAJOR.MINOR."
18
+ 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."
19
+ option :exit_with_unstaged_changes, type: :boolean, default: true, desc: <<~HELP
20
+ Optional. Whether the script should exit with an error if there are unstaged changes in the current project.
21
+ HELP
22
+ default_task :backport
23
+
24
+ def backport
25
+ Decidim::MaintainersToolbox::Backporter.new(
26
+ token: options[:github_token],
27
+ pull_request_id: options[:pull_request_id],
28
+ version_number: options[:version_number],
29
+ exit_with_unstaged_changes: options[:exit_with_unstaged_changes]
30
+ ).call
31
+ rescue Decidim::MaintainersToolbox::GithubManager::Querier::Base::InvalidMetadataError
32
+ puts "Metadata was not returned from the server. Please check that the provided pull request ID and GitHub token are correct."
33
+ end
34
+
35
+ def help
36
+ super("backport")
37
+ end
38
+
39
+ def self.exit_on_failure?
40
+ true
41
+ end
42
+ end
43
+
44
+ BackporterCLI.start(ARGV)
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ require_relative "../lib/decidim/maintainers_toolbox/git_backport_checker"
7
+
8
+ class BackportsCheckerCLI < Thor
9
+ desc "", <<~HELP
10
+ Backports checker. Shows the status of the pull requests opened in the last days
11
+
12
+ The output by default is for the terminal. It uses a color code to differentiate the status of the Pull Requests, following the colors of GitHub:
13
+ - \e[34mPurple\e[0m: closed with a merge
14
+ - \e[35mRed\e[0m: closed without being merge
15
+ - \e[32mGreen\e[0m: opened without being merge
16
+ HELP
17
+ option :github_token, required: true, desc: <<~HELP
18
+ 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.
19
+ Alternatively, you can use the `gh` CLI tool to authenticate with `gh auth token` (i.e. --github-token=$(gh auth token))
20
+ HELP
21
+ option :last_version_number, required: true, desc: <<~HELP
22
+ Required. The version number of the last supported version that you want to do the backports to. It must have the format MAJOR.MINOR.
23
+ HELP
24
+ option :days_to_check_from, required: false, default: 90, type: :numeric, desc: "How many days since the pull requests were merged we will check from."
25
+ default_task :backports_checker
26
+
27
+ def backports_checker
28
+ checker = Decidim::MaintainersToolbox::GitBackportChecker.new(
29
+ token: options[:github_token],
30
+ last_version_number: options[:last_version_number],
31
+ days_to_check_from: options[:days_to_check_from]
32
+ )
33
+ checker.call
34
+ puts checker.cli_report
35
+ rescue Decidim::MaintainersToolbox::GithubManager::Querier::Base::InvalidMetadataError
36
+ puts "Metadata was not returned from the server. Please check that the provided GitHub token is correct."
37
+ end
38
+
39
+ def help
40
+ super("backports_checker")
41
+ end
42
+
43
+ def self.exit_on_failure?
44
+ true
45
+ end
46
+ end
47
+
48
+ BackportsCheckerCLI.start(ARGV)
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ require_relative "../lib/decidim/maintainers_toolbox/changelog_generator"
7
+
8
+ class ChangeLogGeneratorCLI < Thor
9
+ desc "", <<~HELP
10
+ Decidim's changelog generator. It will generate a changelog based on the pull requests merged since a given SHA
11
+ This script will generate the sections needed for the changelog, using the
12
+ labels of the merged PRs as a source of truth. It will add a section at the
13
+ end, "Unsorted", with the list of PRs that could not be classified.
14
+ It ignores any Crowdin PR.
15
+ HELP
16
+
17
+ option :github_token, required: true, desc: <<~HELP
18
+ 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.
19
+ Alternatively, you can use the `gh` CLI tool to authenticate with `gh auth token` (i.e. --github-token=$(gh auth token))
20
+ HELP
21
+ option :since_sha, required: true, desc: <<~HELP
22
+ Required. The git commit SHA from which to consider the changes. It is
23
+ usually the last commit that modified the `.decidim_version` file.
24
+ HELP
25
+
26
+ default_task :changelog_generator
27
+
28
+ def changelog_generator
29
+ Decidim::MaintainersToolbox::ChangeLogGenerator.new(
30
+ token: options[:github_token],
31
+ since_sha: options[:since_sha]
32
+ ).call
33
+ rescue Decidim::MaintainersToolbox::ChangeLogGenerator::InvalidMetadataError
34
+ puts "Metadata was not returned from the server. Please check that the provided GitHub token is correct."
35
+ end
36
+
37
+ def help
38
+ super("changelog_generator")
39
+ end
40
+
41
+ def self.exit_on_failure?
42
+ true
43
+ end
44
+ end
45
+
46
+ ChangeLogGeneratorCLI.start(ARGV)
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+
6
+ require_relative "../lib/decidim/maintainers_toolbox/releaser"
7
+
8
+ class ReleaserCLI < Thor
9
+ desc "", "Make the branch for preparing a release"
10
+ option :github_token, required: true, desc: <<~HELP
11
+ 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.
12
+ Alternatively, you can use the `gh` CLI tool to authenticate with `gh auth token` (i.e. --github-token=$(gh auth token))
13
+ HELP
14
+ option :version_type, enum: %w(rc minor patch), required: true, desc: <<~HELP
15
+ Required. The kind of release that you want to prepare.
16
+ HELP
17
+ option :exit_with_unstaged_changes, type: :boolean, default: true, desc: <<~HELP
18
+ Optional. Whether the script should exit with an error if there are unstaged changes in the current project.
19
+ HELP
20
+ default_task :releaser
21
+
22
+ def releaser
23
+ Decidim::MaintainersToolbox::Releaser.new(
24
+ token: options[:github_token],
25
+ version_type: options[:version_type],
26
+ exit_with_unstaged_changes: options[:exit_with_unstaged_changes]
27
+ ).call
28
+ rescue Decidim::MaintainersToolbox::Releaser::InvalidMetadataError
29
+ puts "Metadata was not returned from the server. Please check that the GitHub token is correct."
30
+ end
31
+
32
+ def help
33
+ super("releaser")
34
+ end
35
+
36
+ def self.exit_on_failure?
37
+ true
38
+ end
39
+ end
40
+
41
+ ReleaserCLI.start(ARGV)
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module MaintainersToolbox
5
+ class Backporter
6
+ class InvalidMetadataError < StandardError; end
7
+
8
+ # @param token [String] token for GitHub authentication
9
+ # @param pull_request_id [String] the ID of the pull request that we want to backport
10
+ # @param version_number [String] the version number of the release that we want to make the backport to
11
+ # @param exit_with_unstaged_changes [Boolean] wheter we should exit cowardly if there is any unstaged change
12
+ def initialize(token:, pull_request_id:, version_number:, exit_with_unstaged_changes:)
13
+ @token = token
14
+ @pull_request_id = pull_request_id
15
+ @version_number = version_number
16
+ @exit_with_unstaged_changes = exit_with_unstaged_changes
17
+ end
18
+
19
+ # Handles the different tasks to create a backport:
20
+ # * Gets the metadata of a pull request on GitHub
21
+ # * Appls thi commit to another brach and push it to the remote repository
22
+ # * Creates the pull request on GitHub
23
+ #
24
+ # @raise [InvalidMetadataError] if we could not get the information of this pull quest
25
+ # @return [void]
26
+ def call
27
+ metadata = pull_request_metadata
28
+ make_cherrypick_and_branch(metadata)
29
+ create_pull_request(metadata)
30
+ Decidim::MaintainersToolbox::GitBackportManager.checkout_develop
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :token, :pull_request_id, :version_number, :exit_with_unstaged_changes
36
+
37
+ # Asks the metadata for a given issue or pull request on GitHub API
38
+ #
39
+ # @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
40
+ def pull_request_metadata
41
+ Decidim::MaintainersToolbox::GithubManager::Querier::ByIssueId.new(
42
+ token: token,
43
+ issue_id: pull_request_id
44
+ ).call
45
+ end
46
+
47
+ # Handles all the different tasks involved on a backport with the git command line utility
48
+ #
49
+ # @return [void]
50
+ def make_cherrypick_and_branch(metadata)
51
+ Decidim::MaintainersToolbox::GitBackportManager.new(
52
+ pull_request_id: pull_request_id,
53
+ release_branch: release_branch,
54
+ backport_branch: backport_branch(metadata[:title]),
55
+ exit_with_unstaged_changes: exit_with_unstaged_changes
56
+ ).call
57
+ end
58
+
59
+ # Creates the pull request with GitHub API
60
+ #
61
+ # @return [Faraday::Response] An instance that represents an HTTP response from making an HTTP request
62
+ def create_pull_request(metadata)
63
+ params = {
64
+ title: "Backport '#{metadata[:title]}' to v#{version_number}",
65
+ body: "#### :tophat: What? Why?\n\nBackport ##{pull_request_id} to v#{version_number}\n\n:hearts: Thank you!",
66
+ labels: (metadata[:labels] << "backport"),
67
+ head: backport_branch(metadata[:title]),
68
+ base: release_branch
69
+ }
70
+
71
+ Decidim::MaintainersToolbox::GithubManager::Poster.new(
72
+ token: token,
73
+ params: params
74
+ ).call
75
+ end
76
+
77
+ # Name of the release branch
78
+ #
79
+ # @return [String] name of the release branch
80
+ def release_branch
81
+ "release/#{version_number}-stable"
82
+ end
83
+
84
+ # Name of the backport branch
85
+ #
86
+ # @return [String] name of the backport branch
87
+ def backport_branch(pull_request_title)
88
+ "backport/#{version_number}/#{slugify(pull_request_title).slice!(0, 30)}-#{pull_request_id}"
89
+ end
90
+
91
+ # Converts a string with spaces to a slug
92
+ # It changes it to lowercase and removes spaces
93
+ #
94
+ # @return [String] slugged string
95
+ def slugify(string)
96
+ string.downcase.strip.gsub(" ", "-").gsub(/[^\w-]/, "")
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require_relative "report"
5
+
6
+ module Decidim
7
+ module MaintainersToolbox
8
+ module BackportsReporter
9
+ class CLIReport < Decidim::MaintainersToolbox::BackportsReporter::Report
10
+ private
11
+
12
+ def output_head
13
+ head = "| #{"ID".center(6)} | #{"Title".center(83)} | Backport v#{last_version_number} | Backport v#{penultimate_version_number} |\n"
14
+ head += "|#{"-" * 8}|#{"-" * 85}|#{"-" * 16}|#{"-" * 16}|\n"
15
+ head
16
+ end
17
+
18
+ def output_line(line)
19
+ output = "| ##{line[:id].to_s.center(5)} "
20
+ output += "| #{line[:title].truncate(83).ljust(84, " ")}"
21
+ output += "| #{format_backport(line[:related_issues], "v#{last_version_number}")}"
22
+ output += "| #{format_backport(line[:related_issues], "v#{penultimate_version_number}")}|\n"
23
+ output
24
+ end
25
+
26
+ def format_backport(related_issues, version)
27
+ none = "None".center(15, " ")
28
+ return none if related_issues.empty?
29
+
30
+ pull_request = extract_backport_pull_request_for_version(related_issues, version)
31
+ return none if pull_request.nil?
32
+
33
+ "\e[#{state_color(pull_request[:state])}m##{pull_request[:id]}\e[0m".center(24, " ")
34
+ end
35
+
36
+ def state_color(state)
37
+ {
38
+ closed: "35",
39
+ merged: "34",
40
+ open: "32"
41
+ }[state.to_sym]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "report"
4
+
5
+ module Decidim
6
+ module MaintainersToolbox
7
+ module BackportsReporter
8
+ class CSVReport < Decidim::MaintainersToolbox::BackportsReporter::Report
9
+ private
10
+
11
+ def output_head
12
+ "ID;Title;Backport v#{last_version_number};Backport v#{penultimate_version_number}\n"
13
+ end
14
+
15
+ def output_line(line)
16
+ output = "#{line[:id]};"
17
+ output += "#{line[:title]};"
18
+ output += "#{format_backport(line[:related_issues], "v#{last_version_number}")};"
19
+ output += "#{format_backport(line[:related_issues], "v#{penultimate_version_number}")}\n"
20
+ output
21
+ end
22
+
23
+ def format_backport(related_issues, version)
24
+ return if related_issues.empty?
25
+
26
+ pull_request = extract_backport_pull_request_for_version(related_issues, version)
27
+ return if pull_request.nil?
28
+
29
+ "#{pull_request[:state]}|#{pull_request[:id]}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module MaintainersToolbox
5
+ module BackportsReporter
6
+ # Abstract class for the different formats
7
+ class Report
8
+ attr_reader :report, :last_version_number
9
+
10
+ def initialize(report:, last_version_number:)
11
+ @report = report
12
+ @last_version_number = last_version_number
13
+ end
14
+
15
+ def call
16
+ output_report
17
+ end
18
+
19
+ private
20
+
21
+ def penultimate_version_number
22
+ major, minor = last_version_number.split(".")
23
+
24
+ "#{major}.#{minor.to_i - 1}"
25
+ end
26
+
27
+ def output_report
28
+ output = output_head
29
+ report.each do |line|
30
+ next if backports_merged?(line[:related_issues])
31
+
32
+ output += output_line(line)
33
+ end
34
+ output
35
+ end
36
+
37
+ def output_head
38
+ raise "Called abstract method: output_head"
39
+ end
40
+
41
+ def output_line(_line)
42
+ raise "Called abstract method: output_line"
43
+ end
44
+
45
+ def extract_backport_pull_request_for_version(related_issues, version)
46
+ related_issues = related_issues.select do |pull_request|
47
+ pull_request[:title].start_with?("Backport") && pull_request[:title].include?(version)
48
+ end
49
+ return if related_issues.empty?
50
+
51
+ related_issues.first
52
+ end
53
+
54
+ def backports_merged?(related_issues)
55
+ return if related_issues.empty?
56
+
57
+ latest_pr = extract_backport_pull_request_for_version(related_issues, "v#{last_version_number}")
58
+ penultimate_pr = extract_backport_pull_request_for_version(related_issues, "v#{penultimate_version_number}")
59
+
60
+ return unless [latest_pr, penultimate_pr].all?
61
+
62
+ latest_pr[:state] == "merged" && penultimate_pr[:state] == "merged"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end