fastlane-plugin-better_semantic_release 1.0.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: 56cbc72476d971316f6fa98a4a8d761270ea92ee1c1cc07662f0ff0121a123f5
4
+ data.tar.gz: 17c1f14c265f83d8fb37c518b5759afbe9a93c7c65de46981bd85bffa70cc765
5
+ SHA512:
6
+ metadata.gz: 65b093fb5a663eb41c29b5fb5194a58e47f0f68c025485d80620f9c5b94ac1f5ad5b9e9d0276d0c7344a0a96517309f8932bc4485870e793ecc84130f82f9ff8
7
+ data.tar.gz: e87fd555bcdd5e41da77c6b2febe97132febc31e0b46e39ed45d750ec9b1f617ba91a365f43692922f706ef544286b802922aaa094acbbc28fb1063b9362fdf3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Jiří Otáhal <xotahal@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # better_semantic_release plugin for `fastlane`
2
+
3
+ [![License](https://img.shields.io/github/license/SiarheiFedartsou/fastlane-plugin-versioning.svg)](https://github.com/SiarheiFedartsou/fastlane-plugin-versioning/blob/master/LICENSE) [![Gem Version](https://badge.fury.io/rb/fastlane-plugin-better_semantic_release.svg)](https://badge.fury.io/rb/fastlane-plugin-better_semantic_release) [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-versioning)
4
+
5
+ ## Getting Started
6
+
7
+ ```
8
+ fastlane add_plugin better_semantic_release
9
+ ```
10
+
11
+ ## About
12
+
13
+ Original plugin not maintained and not working out of the box
14
+
15
+ Automated version managment and generator of release notes. Inspired by [semantic-release](https://github.com/semantic-release/semantic-release) for npm packages. Based on [conventional commits](https://www.conventionalcommits.org/).
16
+
17
+ ### Articles
18
+
19
+ [Semantic Release for Fastlane](https://medium.com/@xotahal/semantic-release-for-fastlane-781df4cf5888?source=friends_link&sk=5c02e32daca7a68539e27e0e1bac1092) @ Medium - By Jiri Otahal
20
+
21
+ ## Available Actions
22
+
23
+ ### `conventional_changelog`
24
+
25
+ - parses all commits since last version
26
+ - groups those commits by their type (fix, feat, docs, refactor, chore, etc)
27
+ - and creates formated release notes either in markdown or in slack format
28
+
29
+ Available parameters:
30
+
31
+ - `format: 'slack|markdown|plain'` (defaults to `markdown`). This formats the changelog for the destination you need. If you're using this for TestFlight changelogs, we suggest using the `plain` option
32
+ - `title: 'My Title'` - is appended to the release notes title, "1.1.8 My Title (YYYY-MM-DD)"
33
+ - `display_title: true|false` (defaults to true) - allows you to hide the entire first line of the changelog
34
+ - `display_links: true|false` (defaults to true) - allows you to hide links to commits from your changelog
35
+ - `commit_url: 'https://github.com/username/repository/commit'` - prepended to the commit ID to build usable links
36
+ - View other options by searching for `available_options` in `conventional_changelog.rb`
37
+
38
+ Example:
39
+
40
+ ```
41
+ notes = conventional_changelog(format: 'slack', title: 'Android Alpha')
42
+ ```
43
+
44
+ <img src="https://raw.githubusercontent.com/xotahal/fastlane-plugin-semantic_release/master/docs/Changelog.png" />
45
+
46
+ ### `analyze_commits`
47
+
48
+ - analyzes your git history
49
+ - finds last tag on current branch (for example ios/beta/1.3.2)
50
+ - parses the last version from tag (1.3.2)
51
+ - gets all commits since this tag
52
+ - analyzes subject of every single commit and increases version number if there is a need (check conventional commit rules)
53
+ - if next version number is higher then last version number it will recommend you to release this version
54
+
55
+ Options:
56
+
57
+ - `ignore_scopes: ['android','windows']`: allows you to ignore any commits which include a given scope, like this one: `feat(android): add functionality not relevant to the release we are producing`
58
+
59
+ Example usage:
60
+
61
+ ```
62
+ isReleasable = analyze_commits(match: 'ios/beta*')
63
+ ```
64
+
65
+ It provides these variables in `lane_context`.
66
+
67
+ ```
68
+ ['RELEASE_ANALYZED', 'True if commits were analyzed.'],
69
+ ['RELEASE_IS_NEXT_VERSION_HIGHER', 'True if next version is higher then last version'],
70
+ ['RELEASE_LAST_TAG_HASH', 'Hash of commit that is tagged as a last version'],
71
+ ['RELEASE_LAST_VERSION', 'Last version number - parsed from last tag.'],
72
+ ['RELEASE_NEXT_MAJOR_VERSION', 'Major number of the next version'],
73
+ ['RELEASE_NEXT_MINOR_VERSION', 'Minor number of the next version'],
74
+ ['RELEASE_NEXT_PATCH_VERSION', 'Patch number of the next version'],
75
+ ['RELEASE_NEXT_VERSION', 'Next version string in format (major.minor.patch)'],
76
+ ```
77
+
78
+ And you can access these like this:
79
+
80
+ `next_version = lane_context[SharedValues::RELEASE_NEXT_VERSION]`
81
+
82
+ <img src="https://raw.githubusercontent.com/xotahal/fastlane-plugin-semantic_release/master/docs/Analyze.png" />
83
+
84
+ ## Tests
85
+
86
+ To run the test suite (contained in `./spec`), call `bundle exec rake`
87
+
88
+ ## Questions
89
+
90
+ If you need anything ping us on [twitter](http://bit.ly/t-xotahal).
91
+
92
+ | Jiri Otahal |
93
+ | -------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | [<img src="https://avatars3.githubusercontent.com/u/3531955?v=4" width="100px;" style="border-radius:50px"/>](http://bit.ly/t-xotahal) |
@@ -0,0 +1,374 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/better_semantic_release_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ RELEASE_ANALYZED = :RELEASE_ANALYZED
8
+ RELEASE_IS_NEXT_VERSION_HIGHER = :RELEASE_IS_NEXT_VERSION_HIGHER
9
+ RELEASE_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH = :RELEASE_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH
10
+ RELEASE_LAST_TAG_HASH = :RELEASE_LAST_TAG_HASH
11
+ RELEASE_LAST_VERSION = :RELEASE_LAST_VERSION
12
+ RELEASE_NEXT_MAJOR_VERSION = :RELEASE_NEXT_MAJOR_VERSION
13
+ RELEASE_NEXT_MINOR_VERSION = :RELEASE_NEXT_MINOR_VERSION
14
+ RELEASE_NEXT_PATCH_VERSION = :RELEASE_NEXT_PATCH_VERSION
15
+ RELEASE_NEXT_VERSION = :RELEASE_NEXT_VERSION
16
+ RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION = :RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION
17
+ CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN = :CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN
18
+ end
19
+
20
+ class AnalyzeCommitsAction < Action
21
+ def self.get_last_tag(params)
22
+ # Try to find the tag
23
+ command = "git describe --tags --match=#{params[:match]}"
24
+ Actions.sh(command, log: params[:debug])
25
+ rescue
26
+ UI.message("Tag was not found for match pattern - #{params[:match]}")
27
+ ''
28
+ end
29
+
30
+ def self.get_last_tag_hash(params)
31
+ command = "git rev-list -n 1 refs/tags/#{params[:tag_name]}"
32
+ Actions.sh(command, log: params[:debug]).chomp
33
+ end
34
+
35
+ def self.get_commits_from_hash(params)
36
+ commits = Helper::BetterSemanticReleaseHelper.git_log(
37
+ pretty: '%s|%b|>',
38
+ start: params[:hash],
39
+ debug: params[:debug]
40
+ )
41
+ commits.split("|>")
42
+ end
43
+
44
+ def self.get_beginning_of_next_sprint(params)
45
+ # command to get first commit
46
+ git_command = 'git rev-list --max-parents=0 HEAD'
47
+
48
+ tag = get_last_tag(match: params[:match], debug: params[:debug])
49
+
50
+ # if tag doesn't exist it get's first commit or fallback tag (v*.*.*)
51
+ if tag.empty?
52
+ UI.message("It couldn't match tag for #{params[:match]}. Check if first commit can be taken as a beginning of next release")
53
+ # If there is no tag found we taking the first commit of current branch
54
+ hash_lines = Actions.sh("#{git_command} | wc -l", log: params[:debug]).chomp
55
+
56
+ if hash_lines.to_i == 1
57
+ UI.message("First commit of the branch is taken as a begining of next release")
58
+ return {
59
+ # here we know this command will return 1 line
60
+ hash: Actions.sh(git_command, log: params[:debug]).chomp
61
+ }
62
+ end
63
+
64
+ # neighter matched tag and first hash could be used - as fallback we try vX.Y.Z
65
+ UI.message("It couldn't match tag for #{params[:match]} and couldn't use first commit. Check if tag vX.Y.Z can be taken as a begining of next release")
66
+ tag = get_last_tag(match: "v*", debug: params[:debug])
67
+
68
+ # even fallback tag doesn't work
69
+ if tag.empty?
70
+ return false
71
+ end
72
+ end
73
+
74
+ # Tag's format is v2.3.4-5-g7685948
75
+ # See git describe man page for more info
76
+ version = tag.split('/')[2]
77
+
78
+ if version.nil?
79
+ UI.user_error!("Error while parsing version from tag #{tag}")
80
+ end
81
+
82
+ # Get a hash of last version tag
83
+ hash = get_last_tag_hash(
84
+ tag_name: tag,
85
+ debug: params[:debug]
86
+ )
87
+
88
+ UI.message("Found a tag #{tag} associated with version #{version}")
89
+
90
+ return {
91
+ hash: hash,
92
+ version: version
93
+ }
94
+ end
95
+
96
+ def self.is_releasable(params)
97
+ # Hash of the commit where is the last version
98
+ beginning = get_beginning_of_next_sprint(params)
99
+
100
+ unless beginning
101
+ UI.error('It could not find a begining of this sprint. How to fix this:')
102
+ UI.error('-- ensure there is only one commit with --max-parents=0 (this command should return one line: "git rev-list --max-parents=0 HEAD")')
103
+ UI.error('-- tell us explicitely where the release starts by adding tag like this: vX.Y.Z (where X.Y.Z is version from which it starts computing next version number)')
104
+ return false
105
+ end
106
+
107
+ # Default last version
108
+ version = beginning[:version] || '0.0.0'
109
+ # If the tag is not found we are taking HEAD as reference
110
+ hash = beginning[:hash] || 'HEAD'
111
+
112
+ # converts last version string to the int numbers
113
+ next_major = (version.split('.')[0] || 0).to_i
114
+ next_minor = (version.split('.')[1] || 0).to_i
115
+ next_patch = (version.split('.')[2] || 0).to_i
116
+
117
+ is_next_version_compatible_with_codepush = true
118
+
119
+ # Get commits log between last version and head
120
+ splitted = get_commits_from_hash(
121
+ hash: hash,
122
+ debug: params[:debug]
123
+ )
124
+
125
+ UI.message("Found #{splitted.length} commits since last release")
126
+ releases = params[:releases]
127
+
128
+ format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
129
+ splitted.each do |line|
130
+ parts = line.split("|")
131
+ subject = parts[0].strip
132
+ # conventional commits are in format
133
+ # type: subject (fix: app crash - for example)
134
+ commit = Helper::BetterSemanticReleaseHelper.parse_commit(
135
+ commit_subject: subject,
136
+ commit_body: parts[1],
137
+ releases: releases,
138
+ pattern: format_pattern
139
+ )
140
+
141
+ unless commit[:scope].nil?
142
+ # if this commit has a scope, then we need to inspect to see if that is one of the scopes we're trying to exclude
143
+ scope = commit[:scope]
144
+ scopes_to_ignore = params[:ignore_scopes]
145
+ # if it is, we'll skip this commit when bumping versions
146
+ next if scopes_to_ignore.include?(scope) #=> true
147
+ end
148
+
149
+ if commit[:release] == "major" || commit[:is_breaking_change]
150
+ next_major += 1
151
+ next_minor = 0
152
+ next_patch = 0
153
+ elsif commit[:release] == "minor"
154
+ next_minor += 1
155
+ next_patch = 0
156
+ elsif commit[:release] == "patch"
157
+ next_patch += 1
158
+ end
159
+
160
+ unless commit[:is_codepush_friendly]
161
+ is_next_version_compatible_with_codepush = false
162
+ end
163
+
164
+ next_version = "#{next_major}.#{next_minor}.#{next_patch}"
165
+ UI.message("#{next_version}: #{subject}") if params[:show_version_path]
166
+ end
167
+
168
+ next_version = "#{next_major}.#{next_minor}.#{next_patch}"
169
+
170
+ is_next_version_releasable = Helper::BetterSemanticReleaseHelper.semver_gt(next_version, version)
171
+
172
+ Actions.lane_context[SharedValues::RELEASE_ANALYZED] = true
173
+ Actions.lane_context[SharedValues::RELEASE_IS_NEXT_VERSION_HIGHER] = is_next_version_releasable
174
+ Actions.lane_context[SharedValues::RELEASE_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH] = is_next_version_compatible_with_codepush
175
+ # Last release analysis
176
+ Actions.lane_context[SharedValues::RELEASE_LAST_TAG_HASH] = hash
177
+ Actions.lane_context[SharedValues::RELEASE_LAST_VERSION] = version
178
+ # Next release analysis
179
+ Actions.lane_context[SharedValues::RELEASE_NEXT_MAJOR_VERSION] = next_major
180
+ Actions.lane_context[SharedValues::RELEASE_NEXT_MINOR_VERSION] = next_minor
181
+ Actions.lane_context[SharedValues::RELEASE_NEXT_PATCH_VERSION] = next_patch
182
+ Actions.lane_context[SharedValues::RELEASE_NEXT_VERSION] = next_version
183
+
184
+ success_message = "Next version (#{next_version}) is higher than last version (#{version}). This version should be released."
185
+ UI.success(success_message) if is_next_version_releasable
186
+
187
+ is_next_version_releasable
188
+ end
189
+
190
+ def self.is_codepush_friendly(params)
191
+ git_command = 'git rev-list --max-parents=0 HEAD'
192
+ # Begining of the branch is taken for codepush analysis
193
+ hash_lines = Actions.sh("#{git_command} | wc -l", log: params[:debug]).chomp
194
+ hash = Actions.sh(git_command, log: params[:debug]).chomp
195
+ next_major = 0
196
+ next_minor = 0
197
+ next_patch = 0
198
+ last_incompatible_codepush_version = '0.0.0'
199
+
200
+ if hash_lines.to_i > 1
201
+ UI.error("#{git_command} resulted to more than 1 hash")
202
+ UI.error('This usualy happens when you pull only part of a git history. Check out how you pull the repo! "git fetch" should be enough.')
203
+ Actions.sh(git_command, log: true).chomp
204
+ return false
205
+ end
206
+
207
+ # Get commits log between last version and head
208
+ splitted = get_commits_from_hash(
209
+ hash: hash,
210
+ debug: params[:debug]
211
+ )
212
+ releases = params[:releases]
213
+ codepush_friendly = params[:codepush_friendly]
214
+
215
+ format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
216
+ splitted.each do |line|
217
+ # conventional commits are in format
218
+ # type: subject (fix: app crash - for example)
219
+ commit = Helper::BetterSemanticReleaseHelper.parse_commit(
220
+ commit_subject: line.split("|")[0],
221
+ commit_body: line.split("|")[1],
222
+ releases: releases,
223
+ pattern: format_pattern,
224
+ codepush_friendly: codepush_friendly
225
+ )
226
+
227
+ if commit[:release] == "major" || commit[:is_breaking_change]
228
+ next_major += 1
229
+ next_minor = 0
230
+ next_patch = 0
231
+ elsif commit[:release] == "minor"
232
+ next_minor += 1
233
+ next_patch = 0
234
+ elsif commit[:release] == "patch"
235
+ next_patch += 1
236
+ end
237
+
238
+ unless commit[:is_codepush_friendly]
239
+ last_incompatible_codepush_version = "#{next_major}.#{next_minor}.#{next_patch}"
240
+ end
241
+ end
242
+
243
+ Actions.lane_context[SharedValues::RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION] = last_incompatible_codepush_version
244
+ end
245
+
246
+ def self.run(params)
247
+ is_next_version_releasable = is_releasable(params)
248
+ is_codepush_friendly(params)
249
+
250
+ is_next_version_releasable
251
+ end
252
+
253
+ #####################################################
254
+ # @!group Documentation
255
+ #####################################################
256
+
257
+ def self.description
258
+ "Finds a tag of last release and determinates version of next release"
259
+ end
260
+
261
+ def self.details
262
+ "This action will find a last release tag and analyze all commits since the tag. It uses conventional commits. Every time when commit is marked as fix or feat it will increase patch or minor number (you can setup this default behaviour). After all it will suggest if the version should be released or not."
263
+ end
264
+
265
+ def self.available_options
266
+ # Define all options your action supports.
267
+
268
+ # Below a few examples
269
+ [
270
+ FastlaneCore::ConfigItem.new(
271
+ key: :match,
272
+ description: "Match parameter of git describe. See man page of git describe for more info",
273
+ verify_block: proc do |value|
274
+ UI.user_error!("No match for analyze_commits action given, pass using `match: 'expr'`") unless value && !value.empty?
275
+ end
276
+ ),
277
+ FastlaneCore::ConfigItem.new(
278
+ key: :commit_format,
279
+ description: "The commit format to apply. Presets are 'default' or 'angular', or you can provide your own Regexp. Note: the supplied regex _must_ have 4 capture groups, in order: type, scope, has_exclamation_mark, and subject",
280
+ default_value: "default",
281
+ is_string: false,
282
+ verify_block: proc do |value|
283
+ case value
284
+ when String
285
+ unless Helper::BetterSemanticReleaseHelper.format_patterns.key?(value)
286
+ UI.user_error!("Invalid format preset: #{value}")
287
+ end
288
+
289
+ pattern = Helper::BetterSemanticReleaseHelper.format_patterns[value]
290
+ when Regexp
291
+ pattern = value
292
+ else
293
+ UI.user_error!("Invalid option type: #{value.inspect}")
294
+ end
295
+ Actions.lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN] = pattern
296
+ end
297
+ ),
298
+ FastlaneCore::ConfigItem.new(
299
+ key: :releases,
300
+ description: "Map types of commit to release (major, minor, patch)",
301
+ default_value: { fix: "patch", feat: "minor" },
302
+ type: Hash
303
+ ),
304
+ FastlaneCore::ConfigItem.new(
305
+ key: :codepush_friendly,
306
+ description: "These types are consider as codepush friendly automatically",
307
+ default_value: ["chore", "test", "docs"],
308
+ type: Array,
309
+ optional: true
310
+ ),
311
+ FastlaneCore::ConfigItem.new(
312
+ key: :tag_version_match,
313
+ description: "To parse version number from tag name",
314
+ default_value: '\d+\.\d+\.\d+'
315
+ ),
316
+ FastlaneCore::ConfigItem.new(
317
+ key: :ignore_scopes,
318
+ description: "To ignore certain scopes when calculating releases",
319
+ default_value: [],
320
+ type: Array,
321
+ optional: true
322
+ ),
323
+ FastlaneCore::ConfigItem.new(
324
+ key: :show_version_path,
325
+ description: "True if you want to print out the version calculated for each commit",
326
+ default_value: true,
327
+ type: Boolean,
328
+ optional: true
329
+ ),
330
+ FastlaneCore::ConfigItem.new(
331
+ key: :debug,
332
+ description: "True if you want to log out a debug info",
333
+ default_value: false,
334
+ type: Boolean,
335
+ optional: true
336
+ )
337
+ ]
338
+ end
339
+
340
+ def self.output
341
+ # Define the shared values you are going to provide
342
+ # Example
343
+ [
344
+ ['RELEASE_ANALYZED', 'True if commits were analyzed.'],
345
+ ['RELEASE_IS_NEXT_VERSION_HIGHER', 'True if next version is higher then last version'],
346
+ ['RELEASE_IS_NEXT_VERSION_COMPATIBLE_WITH_CODEPUSH', 'True if next version is compatible with codepush'],
347
+ ['RELEASE_LAST_TAG_HASH', 'Hash of commit that is tagged as a last version'],
348
+ ['RELEASE_LAST_VERSION', 'Last version number - parsed from last tag.'],
349
+ ['RELEASE_NEXT_MAJOR_VERSION', 'Major number of the next version'],
350
+ ['RELEASE_NEXT_MINOR_VERSION', 'Minor number of the next version'],
351
+ ['RELEASE_NEXT_PATCH_VERSION', 'Patch number of the next version'],
352
+ ['RELEASE_NEXT_VERSION', 'Next version string in format (major.minor.patch)'],
353
+ ['RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION', 'Last commit without codepush'],
354
+ ['CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN', 'The format pattern Regexp used to match commits (mainly for internal use)']
355
+ ]
356
+ end
357
+
358
+ def self.return_value
359
+ # If your method provides a return value, you can describe here what it does
360
+ "Returns true if the next version is higher then the last version"
361
+ end
362
+
363
+ def self.authors
364
+ # So no one will ever forget your contribution to fastlane :) You are awesome btw!
365
+ ["xotahal"]
366
+ end
367
+
368
+ def self.is_supported?(platform)
369
+ # you can do things like
370
+ true
371
+ end
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,323 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/better_semantic_release_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ end
8
+
9
+ class ConventionalChangelogAction < Action
10
+ def self.get_commits_from_hash(params)
11
+ commits = Helper::BetterSemanticReleaseHelper.git_log(
12
+ pretty: '%s|%b|%H|%h|%an|%at|>',
13
+ start: params[:hash],
14
+ debug: params[:debug]
15
+ )
16
+ commits.split("|>")
17
+ end
18
+
19
+ def self.run(params)
20
+ # Get next version number from shared values
21
+ analyzed = lane_context[SharedValues::RELEASE_ANALYZED]
22
+
23
+ # If analyze commits action was not run there will be no version in shared
24
+ # values. We need to run the action to get next version number
25
+ unless analyzed
26
+ UI.user_error!("Release hasn't been analyzed yet. Run analyze_commits action first please.")
27
+ # version = other_action.analyze_commits(match: params[:match])
28
+ end
29
+
30
+ last_tag_hash = lane_context[SharedValues::RELEASE_LAST_TAG_HASH]
31
+ version = lane_context[SharedValues::RELEASE_NEXT_VERSION]
32
+
33
+ # Get commits log between last version and head
34
+ commits = get_commits_from_hash(
35
+ hash: last_tag_hash,
36
+ debug: params[:debug]
37
+ )
38
+ parsed = parse_commits(commits, params)
39
+
40
+ commit_url = params[:commit_url]
41
+ format = params[:format]
42
+
43
+ result = note_builder(format, parsed, version, commit_url, params)
44
+
45
+ result
46
+ end
47
+
48
+ def self.note_builder(format, commits, version, commit_url, params)
49
+ sections = params[:sections]
50
+
51
+ result = ""
52
+
53
+ # Begining of release notes
54
+ if params[:display_title] == true
55
+ title = version
56
+ title += " #{params[:title]}" if params[:title]
57
+ title += " (#{Date.today})"
58
+
59
+ result = style_text(title, format, "title").to_s
60
+ result += "\n\n"
61
+ end
62
+
63
+ params[:order].each do |type|
64
+ # write section only if there is at least one commit
65
+ next if commits.none? { |commit| commit[:type] == type }
66
+
67
+ result += style_text(sections[type.to_sym], format, "heading").to_s
68
+ result += "\n"
69
+
70
+ commits.each do |commit|
71
+ next if commit[:type] != type || commit[:is_merge]
72
+
73
+ result += "-"
74
+
75
+ unless commit[:scope].nil?
76
+ formatted_text = style_text("#{commit[:scope]}:", format, "bold").to_s
77
+ result += " #{formatted_text}"
78
+ end
79
+
80
+ result += " #{commit[:subject]}"
81
+
82
+ if params[:display_links] == true
83
+ styled_link = build_commit_link(commit, commit_url, format).to_s
84
+ result += " (#{styled_link})"
85
+ end
86
+
87
+ if params[:display_author]
88
+ result += " - #{commit[:author_name]}"
89
+ end
90
+
91
+ result += "\n"
92
+ end
93
+ result += "\n"
94
+ end
95
+
96
+ if commits.any? { |commit| commit[:is_breaking_change] == true }
97
+ result += style_text("BREAKING CHANGES", format, "heading").to_s
98
+ result += "\n"
99
+
100
+ commits.each do |commit|
101
+ next unless commit[:is_breaking_change]
102
+ result += "- #{commit[:breaking_change]}" # This is the only unique part of this loop
103
+
104
+ if params[:display_links] == true
105
+ styled_link = build_commit_link(commit, commit_url, format).to_s
106
+ result += " (#{styled_link})"
107
+ end
108
+
109
+ if params[:display_author]
110
+ result += " - #{commit[:author_name]}"
111
+ end
112
+
113
+ result += "\n"
114
+ end
115
+
116
+ result += "\n"
117
+ end
118
+
119
+ # Trim any trailing newlines
120
+ result = result.rstrip!
121
+
122
+ result
123
+ end
124
+
125
+ def self.style_text(text, format, style)
126
+ # formats the text according to the style we're looking to use
127
+
128
+ # Skips all styling
129
+ case style
130
+ when "title"
131
+ if format == "markdown"
132
+ "# #{text}"
133
+ elsif format == "slack"
134
+ "*#{text}*"
135
+ else
136
+ text
137
+ end
138
+ when "heading"
139
+ if format == "markdown"
140
+ "### #{text}"
141
+ elsif format == "slack"
142
+ "*#{text}*"
143
+ else
144
+ "#{text}:"
145
+ end
146
+ when "bold"
147
+ if format == "markdown"
148
+ "**#{text}**"
149
+ elsif format == "slack"
150
+ "*#{text}*"
151
+ else
152
+ text
153
+ end
154
+ else
155
+ text # catchall, shouldn't be needed
156
+ end
157
+ end
158
+
159
+ def self.build_commit_link(commit, commit_url, format)
160
+ # formats the link according to the output format we need
161
+ short_hash = commit[:short_hash]
162
+ hash = commit[:hash]
163
+ url = "#{commit_url}/#{hash}"
164
+
165
+ case format
166
+ when "slack"
167
+ "<#{url}|#{short_hash}>"
168
+ when "markdown"
169
+ "[#{short_hash}](#{url})"
170
+ else
171
+ url
172
+ end
173
+ end
174
+
175
+ def self.parse_commits(commits, params)
176
+ parsed = []
177
+ # %s|%b|%H|%h|%an|%at
178
+ format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
179
+ commits.each do |line|
180
+ splitted = line.split("|")
181
+
182
+ commit = Helper::BetterSemanticReleaseHelper.parse_commit(
183
+ commit_subject: splitted[0],
184
+ commit_body: splitted[1],
185
+ pattern: format_pattern
186
+ )
187
+
188
+ unless commit[:scope].nil?
189
+ # if this commit has a scope, then we need to inspect to see if that is one of the scopes we're trying to exclude
190
+ scope = commit[:scope]
191
+ scopes_to_ignore = params[:ignore_scopes]
192
+ # if it is, we'll skip this commit when bumping versions
193
+ next if scopes_to_ignore.include?(scope) #=> true
194
+ end
195
+
196
+ commit[:hash] = splitted[2]
197
+ commit[:short_hash] = splitted[3]
198
+ commit[:author_name] = splitted[4]
199
+ commit[:commit_date] = splitted[5]
200
+
201
+ parsed.push(commit)
202
+ end
203
+
204
+ parsed
205
+ end
206
+
207
+ #####################################################
208
+ # @!group Documentation
209
+ #####################################################
210
+
211
+ def self.description
212
+ "Get commits since last version and generates release notes"
213
+ end
214
+
215
+ def self.details
216
+ "Uses conventional commits. It groups commits by their types and generates release notes in markdown or slack format."
217
+ end
218
+
219
+ def self.available_options
220
+ # Define all options your action supports.
221
+
222
+ # Below a few examples
223
+ [
224
+ FastlaneCore::ConfigItem.new(
225
+ key: :format,
226
+ description: "You can use either markdown, slack or plain",
227
+ default_value: "markdown",
228
+ optional: true
229
+ ),
230
+ FastlaneCore::ConfigItem.new(
231
+ key: :title,
232
+ description: "Title for release notes",
233
+ optional: true
234
+ ),
235
+ FastlaneCore::ConfigItem.new(
236
+ key: :commit_url,
237
+ description: "Uses as a link to the commit",
238
+ optional: true
239
+ ),
240
+ FastlaneCore::ConfigItem.new(
241
+ key: :order,
242
+ description: "You can change the order of groups in release notes",
243
+ default_value: ["feat", "fix", "refactor", "perf", "chore", "test", "docs", "no_type"],
244
+ type: Array,
245
+ optional: true
246
+ ),
247
+ FastlaneCore::ConfigItem.new(
248
+ key: :sections,
249
+ description: "Map type to section title",
250
+ default_value: {
251
+ feat: "Features",
252
+ fix: "Bug fixes",
253
+ refactor: "Code refactoring",
254
+ perf: "Performance improvements",
255
+ chore: "Building system",
256
+ test: "Testing",
257
+ docs: "Documentation",
258
+ no_type: "Other work"
259
+ },
260
+ type: Hash,
261
+ optional: true
262
+ ),
263
+ FastlaneCore::ConfigItem.new(
264
+ key: :display_author,
265
+ description: "Whether you want to show the author of the commit",
266
+ default_value: false,
267
+ type: Boolean,
268
+ optional: true
269
+ ),
270
+ FastlaneCore::ConfigItem.new(
271
+ key: :display_title,
272
+ description: "Whether you want to hide the title/header with the version details at the top of the changelog",
273
+ default_value: true,
274
+ type: Boolean,
275
+ optional: true
276
+ ),
277
+ FastlaneCore::ConfigItem.new(
278
+ key: :display_links,
279
+ description: "Whether you want to display the links to commit IDs",
280
+ default_value: true,
281
+ type: Boolean,
282
+ optional: true
283
+ ),
284
+ FastlaneCore::ConfigItem.new(
285
+ key: :ignore_scopes,
286
+ description: "To ignore certain scopes when calculating releases",
287
+ default_value: [],
288
+ type: Array,
289
+ optional: true
290
+ ),
291
+ FastlaneCore::ConfigItem.new(
292
+ key: :debug,
293
+ description: "True if you want to log out a debug info",
294
+ default_value: false,
295
+ type: Boolean,
296
+ optional: true
297
+ )
298
+ ]
299
+ end
300
+
301
+ def self.output
302
+ # Define the shared values you are going to provide
303
+ # Example
304
+ []
305
+ end
306
+
307
+ def self.return_value
308
+ # If your method provides a return value, you can describe here what it does
309
+ "Returns generated release notes as a string"
310
+ end
311
+
312
+ def self.authors
313
+ # So no one will ever forget your contribution to fastlane :) You are awesome btw!
314
+ ["xotahal"]
315
+ end
316
+
317
+ def self.is_supported?(platform)
318
+ # you can do things like
319
+ true
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,104 @@
1
+ require 'fastlane_core/ui/ui'
2
+
3
+ module Fastlane
4
+ UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
5
+
6
+ module Helper
7
+ class SemanticReleaseHelper
8
+ def self.format_patterns
9
+ return {
10
+ "default" => /^(docs|fix|feat|chore|style|refactor|perf|test)(?:\((.*)\))?(!?)\: (.*)/,
11
+ "angular" => /^(\w*)(?:\((.*)\))?(): (.*)/
12
+ }
13
+ end
14
+
15
+ # class methods that you define here become available in your action
16
+ # as `Helper::SemanticReleaseHelper.your_method`
17
+ #
18
+ def self.git_log(params)
19
+ command = "git log --pretty='#{params[:pretty]}' --reverse #{params[:start]}..HEAD"
20
+ Actions.sh(command, log: params[:debug]).chomp
21
+ end
22
+
23
+ def self.parse_commit(params)
24
+ commit_subject = params[:commit_subject].strip
25
+ commit_body = params[:commit_body]
26
+ releases = params[:releases]
27
+ codepush_friendly = params[:codepush_friendly]
28
+ pattern = params[:pattern]
29
+ breaking_change_pattern = /BREAKING CHANGES?: (.*)/
30
+ codepush_pattern = /codepush?: (.*)/
31
+
32
+ matched = commit_subject.match(pattern)
33
+ result = {
34
+ is_valid: false,
35
+ subject: commit_subject,
36
+ is_merge: !(commit_subject =~ /^Merge/).nil?,
37
+ type: 'no_type'
38
+ }
39
+
40
+ unless matched.nil?
41
+ type = matched[1]
42
+ scope = matched[2]
43
+
44
+ result[:is_valid] = true
45
+ result[:type] = type
46
+ result[:scope] = scope
47
+ result[:has_exclamation_mark] = matched[3] == '!'
48
+ result[:subject] = matched[4]
49
+
50
+ unless releases.nil?
51
+ result[:release] = releases[type.to_sym]
52
+ end
53
+ unless codepush_friendly.nil?
54
+ result[:is_codepush_friendly] = codepush_friendly.include?(type)
55
+ end
56
+
57
+ unless commit_body.nil?
58
+ breaking_change_matched = commit_body.match(breaking_change_pattern)
59
+ codepush_matched = commit_body.match(codepush_pattern)
60
+
61
+ unless breaking_change_matched.nil?
62
+ result[:is_breaking_change] = true
63
+ result[:breaking_change] = breaking_change_matched[1]
64
+ end
65
+ unless codepush_matched.nil?
66
+ result[:is_codepush_friendly] = codepush_matched[1] == 'ok'
67
+ end
68
+ end
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ def self.semver_gt(first, second)
75
+ first_major = (first.split('.')[0] || 0).to_i
76
+ first_minor = (first.split('.')[1] || 0).to_i
77
+ first_patch = (first.split('.')[2] || 0).to_i
78
+
79
+ second_major = (second.split('.')[0] || 0).to_i
80
+ second_minor = (second.split('.')[1] || 0).to_i
81
+ second_patch = (second.split('.')[2] || 0).to_i
82
+
83
+ # Check if next version is higher then last version
84
+ if first_major > second_major
85
+ return true
86
+ elsif first_major == second_major
87
+ if first_minor > second_minor
88
+ return true
89
+ elsif first_minor == second_minor
90
+ if first_patch > second_patch
91
+ return true
92
+ end
93
+ end
94
+ end
95
+
96
+ return false
97
+ end
98
+
99
+ def self.semver_lt(first, second)
100
+ return !semver_gt(first, second)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1 @@
1
+ module Fastlane module BetterSemanticRelease VERSION = "1.0.0" end end
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/better_semantic_release/version'
2
+
3
+ module Fastlane
4
+ module BetterSemanticRelease
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::BetterSemanticRelease.all_classes.each do |current|
15
+ require current
16
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-better_semantic_release
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Greco
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec_junit_formatter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.49.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.49.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-require_tools
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: fastlane
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 2.117.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 2.117.1
139
+ description:
140
+ email: tom.greco@scanifly.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - LICENSE
146
+ - README.md
147
+ - lib/fastlane/plugin/better_semantic_release/actions/analyze_commits.rb
148
+ - lib/fastlane/plugin/better_semantic_release/actions/conventional_changelog.rb
149
+ - lib/fastlane/plugin/better_semantic_release/helper/semantic_release_helper.rb
150
+ - lib/fastlane/plugin/better_semantic_release/version.rb
151
+ - lib/fastlane/plugin/semantic_release.rb
152
+ homepage: https://github.com/tomgreco/fastlane-plugin-better_semantic_release
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubygems_version: 3.1.4
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: Automated version managment and generator of release notes.
175
+ test_files: []