decidim-maintainers_toolbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/time/zones"
4
+ require_relative "base"
5
+
6
+ module Decidim
7
+ module MaintainersToolbox
8
+ module GithubManager
9
+ module Querier
10
+ # Makes a GET request for the list of Issues or Pull Requests in GitHub.
11
+ # They must comply the following conditions:
12
+ # * To be merged in the period between the days to check from and today. (90 days by default)
13
+ # * To have the label that we are querying ("type: fix" by default)
14
+ # * To not have any of the excluded labels (["backport", "no-backport"] by default)
15
+ # * To have been merged
16
+ #
17
+ # @param token [String] token for GitHub authentication
18
+ # @param days_to_check_from [Integer] the number of days since the pull requests were merged from when we will start the check
19
+ # @param label [String] the label that we want to search by
20
+ # @param exclude_labels [Array] the labels that we want to exclude in the search
21
+ #
22
+ # @see https://docs.github.com/en/rest/issues/issues#list-repository-issues GitHub API documentation
23
+ class ByLabel < Decidim::MaintainersToolbox::GithubManager::Querier::Base
24
+ def initialize(token:, days_to_check_from: 90, label: "type: fix", exclude_labels: ["backport", "no-backport"])
25
+ @label = label
26
+ @exclude_labels = exclude_labels
27
+ @token = token
28
+ @days_to_check_from = days_to_check_from
29
+ end
30
+
31
+ # Makes the GET request and parses the response of an Issue or Pull Request in GitHub
32
+ #
33
+ # @return [Hash]
34
+ def call
35
+ parse json_response("https://api.github.com/repos/decidim/decidim/issues")
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :label, :exclude_labels, :days_to_check_from
41
+
42
+ def headers
43
+ Time.zone = "UTC"
44
+
45
+ {
46
+ labels: label,
47
+ state: "closed",
48
+ per_page: 100,
49
+ since: since.iso8601
50
+ }
51
+ end
52
+
53
+ def since
54
+ Time.zone.today - days_to_check_from
55
+ end
56
+
57
+ # Parses the response of an Issue or Pull Request in GitHub
58
+ #
59
+ # @return [Hash]
60
+ def parse(metadata)
61
+ metadata.map do |item|
62
+ next if has_any_of_excluded_labels?(item)
63
+ next unless merged?(item)
64
+ next unless merged_in_date_range?(item)
65
+
66
+ {
67
+ id: item["number"],
68
+ title: item["title"]
69
+ }
70
+ end.compact
71
+ end
72
+
73
+ def has_any_of_excluded_labels?(item)
74
+ item["labels"].map { |label| label.map { |_key, val| exclude_labels.include?(val) } }.flatten.any? true
75
+ end
76
+
77
+ def merged_at(item)
78
+ Date.parse(item["pull_request"]["merged_at"])
79
+ end
80
+
81
+ def merged?(item)
82
+ return false if item["pull_request"].nil?
83
+ return false if item["pull_request"]["merged_at"].nil?
84
+
85
+ merged_at(item).present?
86
+ rescue TypeError, Date::Error
87
+ false
88
+ end
89
+
90
+ def merged_in_date_range?(item)
91
+ merged_at(item) > since
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,59 @@
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 list of Issues or Pull Requests in GitHub.
10
+ #
11
+ # @param token [String] token for GitHub authentication
12
+ # @param title [String] the title that we want to search by
13
+ # @param state [String] the state of the issue. By default is "open"
14
+ #
15
+ # @see https://docs.github.com/en/rest/issues/issues#list-repository-issues GitHub API documentation
16
+ class ByTitle < Decidim::MaintainersToolbox::GithubManager::Querier::Base
17
+ def initialize(title:, token:, state: "open")
18
+ @title = title
19
+ @token = token
20
+ @state = state
21
+ end
22
+
23
+ # Makes the GET request and parses the response of an Issue or Pull Request in GitHub
24
+ #
25
+ # @return [Hash]
26
+ def call
27
+ data = json_response("https://api.github.com/repos/decidim/decidim/issues")
28
+
29
+ parse(data)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :title, :state
35
+
36
+ def headers
37
+ {
38
+ title: title,
39
+ state: state,
40
+ per_page: 100
41
+ }
42
+ end
43
+
44
+ # Parses the response of an Issue or Pull Request in GitHub
45
+ #
46
+ # @return [Hash]
47
+ def parse(metadata)
48
+ metadata.map do |item|
49
+ {
50
+ id: item["number"],
51
+ title: item["title"]
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
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 related issues of an Issue or Pull Request in GitHub
10
+ # Uses the Timeline events API endpoint
11
+ #
12
+ # @see https://docs.github.com/en/rest/issues/timeline?apiVersion=2022-11-28 GitHub API documentation
13
+ class RelatedIssues < Decidim::MaintainersToolbox::GithubManager::Querier::Base
14
+ def initialize(issue_id:, token:)
15
+ @issue_id = issue_id
16
+ @token = token
17
+ end
18
+
19
+ # Makes the GET request and parses the response of an Issue or Pull Request in GitHub
20
+ #
21
+ # @return [Hash]
22
+ def call
23
+ parse(json_response("https://api.github.com/repos/decidim/decidim/issues/#{@issue_id}/timeline"))
24
+ end
25
+
26
+ private
27
+
28
+ def headers
29
+ { per_page: 100 }
30
+ end
31
+
32
+ # Parses the response of an Issue or Pull Request in GitHub
33
+ #
34
+ # @return [Hash]
35
+ def parse(metadata)
36
+ references = metadata.select do |item|
37
+ item["event"] == "cross-referenced" && item["source"]["issue"]["repository"]["full_name"] == "decidim/decidim"
38
+ end
39
+ references.map do |item|
40
+ issue = item["source"]["issue"]
41
+
42
+ {
43
+ id: issue["number"],
44
+ title: issue["title"].strip,
45
+ state: issue.dig("pull_request", "merged_at").nil? ? issue["state"] : "merged"
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ require_relative "querier/by_issue_id"
7
+ require_relative "querier/by_label"
8
+ require_relative "querier/related_issues"
9
+
10
+ module Decidim
11
+ module MaintainersToolbox
12
+ module GithubManager
13
+ # Allows to make GET requests to GitHub Rest API about Issues and Pull Requests
14
+ # @see https://docs.github.com/en/rest
15
+ module Querier
16
+ autoload :ByIssueId, "decidim/maintainers_toolbox/github_manager/querier/by_issue_id"
17
+ autoload :ByLabel, "decidim/maintainers_toolbox/github_manager/querier/by_label"
18
+ autoload :ByTitle, "decidim/maintainers_toolbox/github_manager/querier/by_title"
19
+ autoload :RelatedIssues, "decidim/maintainers_toolbox/github_manager/querier/related_issues"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "github_manager/poster"
5
+ require_relative "github_manager/querier/by_title"
6
+ require_relative "changelog_generator"
7
+
8
+ module Decidim
9
+ module MaintainersToolbox
10
+ class Releaser
11
+ class InvalidMetadataError < StandardError; end
12
+
13
+ class InvalidBranchError < StandardError; end
14
+
15
+ class InvalidVersionTypeError < StandardError; end
16
+
17
+ DECIDIM_VERSION_FILE = ".decidim-version"
18
+
19
+ # @param token [String] token for GitHub authentication
20
+ # @param version_type [String] The kind of release that you want to prepare. Supported values: rc, minor, patch
21
+ # @param working_dir [String] current working directory. Useful for testing purposes
22
+ # @param exit_with_unstaged_changes [Boolean] wheter we should exit cowardly if there is any unstaged change
23
+ def initialize(token:, version_type:, working_dir: Dir.pwd, exit_with_unstaged_changes: false)
24
+ @token = token
25
+ @version_type = version_type
26
+ @working_dir = working_dir
27
+ @exit_with_unstaged_changes = exit_with_unstaged_changes
28
+ end
29
+
30
+ def call
31
+ Dir.chdir(@working_dir) do
32
+ exit_if_unstaged_changes if @exit_with_unstaged_changes
33
+ exit_if_pending_crowdin_pull_request
34
+
35
+ puts "Starting the release process for #{version_number} in 10 seconds"
36
+ sleep 10
37
+
38
+ run("git checkout #{release_branch}")
39
+ run("git pull origin #{release_branch}")
40
+ bump_decidim_version
41
+ run("bin/rake update_versions")
42
+ run("bin/rake patch_generators")
43
+ run("bin/rake bundle")
44
+ run("npm install")
45
+
46
+ check_tests
47
+
48
+ generate_changelog
49
+
50
+ run("git checkout -b chore/prepare/#{version_number}")
51
+ run("git commit -a -m 'Prepare #{version_number} release'")
52
+ run("git push origin chore/prepare/#{version_number}")
53
+
54
+ create_pull_request
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # The git branch
61
+ #
62
+ # @return [String]
63
+ def branch
64
+ @branch ||= capture("git rev-parse --abbrev-ref HEAD")[0].strip
65
+ end
66
+
67
+ # Raise an error if the branch does not start with the preffix "release/"
68
+ # or returns the branch name
69
+ #
70
+ # @raise [InvalidBranchError]
71
+ #
72
+ # @return [String]
73
+ def release_branch
74
+ raise InvalidBranchError, "This is not a release branch, aborting" unless branch.start_with?("release/")
75
+
76
+ branch
77
+ end
78
+
79
+ # Changes the decidim version in the file
80
+ #
81
+ # @return [void]
82
+ def bump_decidim_version
83
+ File.write(DECIDIM_VERSION_FILE, version_number)
84
+ end
85
+
86
+ # The version number for the release that we are preparing
87
+ #
88
+ # @todo support the "minor" type version
89
+ #
90
+ # @return [String] the version number
91
+ def version_number
92
+ @version_number ||= case @version_type
93
+ when "rc"
94
+ next_version_number_for_release_candidate(old_version_number)
95
+ when "patch"
96
+ next_version_number_for_patch_release(old_version_number)
97
+ else
98
+ raise InvalidVersionTypeError, "This is not a supported version type"
99
+ end
100
+ end
101
+
102
+ def parsed_version_number(version_number)
103
+ /(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/ =~ version_number
104
+
105
+ [major.to_i, minor.to_i, patch.to_i]
106
+ end
107
+
108
+ # Given a version number, returns the next release candidate
109
+ #
110
+ # If the current version number is `dev`, then we return the `rc1` version
111
+ # If the current version number is `rc`, then we return the next `rc` version
112
+ # Else, it means is a `minor` or `patch` version. On those cases we raise an Exception, as releases candidates should
113
+ # be only done from a `dev` or a `rc` version.
114
+ #
115
+ # @raise [InvalidVersionTypeError]
116
+ #
117
+ # @param current_version_number [String] - The version number of the current version
118
+ #
119
+ # @return [String] - the new version number
120
+ def next_version_number_for_release_candidate(current_version_number)
121
+ if current_version_number.include? "dev"
122
+ major, minor, patch = parsed_version_number(current_version_number)
123
+ new_version_number = "#{major}.#{minor}.#{patch}.rc1"
124
+ elsif current_version_number.include? "rc"
125
+ new_rc_number = current_version_number.match(/rc(\d)/)[1].to_i + 1
126
+ new_version_number = current_version_number.gsub(/rc\d/, "rc#{new_rc_number}")
127
+ else
128
+ error_message = <<-EOMESSAGE
129
+ Trying to do a release candidate version from patch release. Bailing out.
130
+ You need to do a release candidate from a `dev` or from another `rc` version
131
+ EOMESSAGE
132
+ raise InvalidVersionTypeError, error_message
133
+ end
134
+
135
+ new_version_number
136
+ end
137
+
138
+ # Given a version number, returns the next patch release
139
+ #
140
+ # If the current version number is `dev`, then we raise an Exception, as you need to first do a release candidate.
141
+ # If the current version number is `rc`, then we return the `0` patch version
142
+ # Else, it means is a `patch` version, so we return the next patch version
143
+ #
144
+ # @raise [InvalidVersionTypeError]
145
+ #
146
+ # @param current_version_number [String] - The version number of the current version
147
+ #
148
+ # @return [String] - the new version number
149
+ def next_version_number_for_patch_release(current_version_number)
150
+ major, minor, patch = parsed_version_number(current_version_number)
151
+
152
+ if current_version_number.include? "dev"
153
+ error_message = <<-EOMESSAGE
154
+ Trying to do a patch version from dev release. Bailing out.
155
+ You need to do first a release candidate.
156
+ EOMESSAGE
157
+ raise InvalidVersionTypeError, error_message
158
+ elsif current_version_number.include? "rc"
159
+ new_version_number = "#{major}.#{minor}.0"
160
+ else
161
+ new_version_number = "#{major}.#{minor}.#{patch.to_i + 1}"
162
+ end
163
+
164
+ new_version_number
165
+ end
166
+
167
+ # The version number from the file
168
+ #
169
+ # @return [String] the version number
170
+ def old_version_number
171
+ File.read(DECIDIM_VERSION_FILE).strip
172
+ end
173
+
174
+ # Run the tests and if fails restore the changes using git and exit with an error
175
+ #
176
+ # @return [void]
177
+ def check_tests
178
+ puts "Running specs"
179
+ output, status = capture("bin/rspec")
180
+
181
+ unless status.success?
182
+ run("git restore .")
183
+ puts output
184
+ exit_with_errors("Tests execution failed. Fix the errors and run again.")
185
+ end
186
+ end
187
+
188
+ # Generates the changelog taking into account the last time the version changed
189
+ #
190
+ # @return [void]
191
+ def generate_changelog
192
+ sha_version = capture("git log -n 1 --pretty=format:%h -- .decidim-version")[0]
193
+ ChangeLogGenerator.new(token: @token, since_sha: sha_version).call
194
+ temporary_changelog = File.read("./temporary_changelog.md")
195
+ legacy_changelog = File.read("./CHANGELOG.md")
196
+ version_changelog = "## [#{version_number}](https://github.com/decidim/decidim/tree/#{version_number})\n\n#{temporary_changelog}\n"
197
+ changelog = legacy_changelog.gsub("# Changelog\n\n", "# Changelog\n\n#{version_changelog}")
198
+ File.write("./CHANGELOG.md", changelog)
199
+ end
200
+
201
+ # Creates the pull request for bumping the version
202
+ #
203
+ # @return [void]
204
+ def create_pull_request
205
+ base_branch = release_branch
206
+ head_branch = "chore/prepare/#{version_number}"
207
+
208
+ params = {
209
+ title: "Bump to v#{version_number} version",
210
+ body: "#### :tophat: What? Why?
211
+
212
+ This PR changes the version of the #{release_branch} branch, so we can publish the release once this is approved and merged.
213
+
214
+ #### Testing
215
+
216
+ All the tests should pass, except for some generators tests, that will fail because the gems and NPM packages have not
217
+ been actually published yet (as in sent to rubygems/npm).
218
+ You will see errors such as `No matching version found for @decidim/browserslist-config@~0.xx.y` in the CI logs.
219
+
220
+ :hearts: Thank you!
221
+ ",
222
+ labels: ["type: internal"],
223
+ head: head_branch,
224
+ base: base_branch
225
+ }
226
+ Decidim::MaintainersToolbox::GithubManager::Poster.new(token: @token, params: params).call
227
+ end
228
+
229
+ # Captures to output of a command
230
+ #
231
+ # @return [Array<String, Process::Status>] The stdout and stderr of the command and its status (aka error code)
232
+ def capture(cmd, env: {})
233
+ Open3.capture2e(env, cmd)
234
+ end
235
+
236
+ # Runs a command
237
+ #
238
+ # @return [void]
239
+ def run(cmd, out: $stdout)
240
+ system(cmd, out: out)
241
+ end
242
+
243
+ # Check if there is any open pull request from Crowdin in GitHub
244
+ #
245
+ # @return [Boolean] - true if there is any open PR
246
+ def pending_crowdin_pull_requests?
247
+ pull_requests = Decidim::MaintainersToolbox::GithubManager::Querier::ByTitle.new(token: @token, title: "New Crowdin updates").call
248
+ pull_requests.any?
249
+ end
250
+
251
+ # Exit the script execution if there are any pull request from Crowdin open
252
+ #
253
+ # @return [void]
254
+ def exit_if_pending_crowdin_pull_request
255
+ return unless pending_crowdin_pull_requests?
256
+
257
+ error_message = <<-EOERROR
258
+ There are open pull requests from Crowdin in GitHub
259
+ Merge them and run again this script.
260
+ EOERROR
261
+ exit_with_errors(error_message)
262
+ end
263
+
264
+ # Exit the script execution with a message
265
+ # Exit the script execution if there are any unstaged changes
266
+ #
267
+ # @return [void]
268
+ def exit_if_unstaged_changes
269
+ return if `git diff`.empty?
270
+
271
+ error_message = <<-EOERROR
272
+ There are changes not staged in your project.
273
+ Please commit your changes or stash them.
274
+ EOERROR
275
+ exit_with_errors(error_message)
276
+ end
277
+
278
+ # Exit the script execution with a message
279
+ #
280
+ # @return [void]
281
+ def exit_with_errors(message)
282
+ puts message
283
+ exit 1
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module MaintainersToolbox
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "maintainers_toolbox/version"
4
+
5
+ module Decidim
6
+ module MaintainersToolbox
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: decidim-maintainers_toolbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrés Pereira de Lucena
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-progressbar
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 6.1.7
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 6.1.7
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.12'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.18'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.18'
97
+ description: Tools for releasing, backporting, changelog generating, and working with
98
+ GitHub
99
+ email:
100
+ - andreslucena@gmail.com
101
+ executables:
102
+ - decidim-backporter
103
+ - decidim-backports-checker
104
+ - decidim-changelog-generator
105
+ - decidim-releaser
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - README.md
110
+ - decidim-maintainers_toolbox.gemspec
111
+ - exe/decidim-backporter
112
+ - exe/decidim-backports-checker
113
+ - exe/decidim-changelog-generator
114
+ - exe/decidim-releaser
115
+ - lib/decidim/maintainers_toolbox.rb
116
+ - lib/decidim/maintainers_toolbox/backporter.rb
117
+ - lib/decidim/maintainers_toolbox/backports_reporter/cli_report.rb
118
+ - lib/decidim/maintainers_toolbox/backports_reporter/csv_report.rb
119
+ - lib/decidim/maintainers_toolbox/backports_reporter/report.rb
120
+ - lib/decidim/maintainers_toolbox/changelog_generator.rb
121
+ - lib/decidim/maintainers_toolbox/git_backport_checker.rb
122
+ - lib/decidim/maintainers_toolbox/git_backport_manager.rb
123
+ - lib/decidim/maintainers_toolbox/github_manager/poster.rb
124
+ - lib/decidim/maintainers_toolbox/github_manager/querier.rb
125
+ - lib/decidim/maintainers_toolbox/github_manager/querier/base.rb
126
+ - lib/decidim/maintainers_toolbox/github_manager/querier/by_issue_id.rb
127
+ - lib/decidim/maintainers_toolbox/github_manager/querier/by_label.rb
128
+ - lib/decidim/maintainers_toolbox/github_manager/querier/by_title.rb
129
+ - lib/decidim/maintainers_toolbox/github_manager/querier/related_issues.rb
130
+ - lib/decidim/maintainers_toolbox/releaser.rb
131
+ - lib/decidim/maintainers_toolbox/version.rb
132
+ homepage: https://decidim.org
133
+ licenses:
134
+ - AGPL-3.0
135
+ metadata:
136
+ bug_tracker_uri: https://github.com/decidim/decidim/issues
137
+ documentation_uri: https://docs.decidim.org/
138
+ funding_uri: https://opencollective.com/decidim
139
+ homepage_uri: https://decidim.org
140
+ source_code_uri: https://github.com/decidim/decidim
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: 2.7.5
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 3.1.6
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Release related tools for the Decidim project
160
+ test_files: []