fastlane-plugin-semantic_convention_release 1.0.1

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: 889dc05b04c719703292518e04758eadb79446e68fdd15c1d4d1b945ce9b0f80
4
+ data.tar.gz: 0361a0e836e85a121a6b903e76cc2dba98db3aa43447e50a5f231e7fed5ae65f
5
+ SHA512:
6
+ metadata.gz: 6736d92b82f9b338461bec515c20e5c3052706f95542544d79448a488677fab4ec30c35ed3a83fe5a251b1f4f66ccb31506b752abf3c9a0999bfec8368d4a338
7
+ data.tar.gz: 9460a31c83dcd918292cf1c961fbda1676599d1268e3fffa616ab648bc8d7c12889a50204cea3276c1f32b2dfb163096470ab4819ac5fbd4141fa031adb02aac
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,92 @@
1
+ # semantic_release plugin for `fastlane`
2
+
3
+ [![CircleCI](https://circleci.com/gh/xotahal/fastlane-plugin-semantic_release.svg?style=svg)](https://circleci.com/gh/xotahal/fastlane-plugin-semantic_release) [![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-semantic_release.svg)](https://badge.fury.io/rb/fastlane-plugin-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 fastlane-plugin-semantic_convention_release
9
+ ```
10
+
11
+ ## About
12
+
13
+ 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/).
14
+
15
+ ### Articles
16
+
17
+ [Semantic Release for Fastlane](https://medium.com/@xotahal/semantic-release-for-fastlane-781df4cf5888?source=friends_link&sk=5c02e32daca7a68539e27e0e1bac1092) @ Medium - By Jiri Otahal
18
+
19
+ ## Available Actions
20
+
21
+ ### `conventional_changelog`
22
+
23
+ - parses all commits since last version
24
+ - groups those commits by their type (fix, feat, docs, refactor, chore, etc)
25
+ - and creates formated release notes either in markdown or in slack format
26
+
27
+ Available parameters:
28
+
29
+ - `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
30
+ - `title: 'My Title'` - is appended to the release notes title, "1.1.8 My Title (YYYY-MM-DD)"
31
+ - `display_title: true|false` (defaults to true) - allows you to hide the entire first line of the changelog
32
+ - `display_links: true|false` (defaults to true) - allows you to hide links to commits from your changelog
33
+ - `commit_url: 'https://github.com/username/repository/commit'` - prepended to the commit ID to build usable links
34
+ - View other options by searching for `available_options` in `conventional_changelog.rb`
35
+
36
+ Example:
37
+
38
+ ```
39
+ notes = conventional_changelog(format: 'slack', title: 'Android Alpha')
40
+ ```
41
+
42
+ <img src="https://raw.githubusercontent.com/xotahal/-plugin-semantic_release/master/docs/Changelog.png" />
43
+
44
+ ### `analyze_commits`
45
+
46
+ - analyzes your git history
47
+ - finds last tag on current branch (for example ios/beta/1.3.2)
48
+ - parses the last version from tag (1.3.2)
49
+ - gets all commits since this tag
50
+ - analyzes subject of every single commit and increases version number if there is a need (check conventional commit rules)
51
+ - if next version number is higher then last version number it will recommend you to release this version
52
+
53
+ Options:
54
+
55
+ - `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`
56
+
57
+ Example usage:
58
+
59
+ ```
60
+ isReleasable = analyze_commits(match: 'ios/beta*')
61
+ ```
62
+
63
+ It provides these variables in `lane_context`.
64
+
65
+ ```
66
+ ['RELEASE_ANALYZED', 'True if commits were analyzed.'],
67
+ ['RELEASE_IS_NEXT_VERSION_HIGHER', 'True if next version is higher then last version'],
68
+ ['RELEASE_LAST_TAG_HASH', 'Hash of commit that is tagged as a last version'],
69
+ ['RELEASE_LAST_VERSION', 'Last version number - parsed from last tag.'],
70
+ ['RELEASE_NEXT_MAJOR_VERSION', 'Major number of the next version'],
71
+ ['RELEASE_NEXT_MINOR_VERSION', 'Minor number of the next version'],
72
+ ['RELEASE_NEXT_PATCH_VERSION', 'Patch number of the next version'],
73
+ ['RELEASE_NEXT_VERSION', 'Next version string in format (major.minor.patch)'],
74
+ ```
75
+
76
+ And you can access these like this:
77
+
78
+ `next_version = lane_context[SharedValues::RELEASE_NEXT_VERSION]`
79
+
80
+ <img src="https://raw.githubusercontent.com/xotahal/fastlane-plugin-semantic_release/master/docs/Analyze.png" />
81
+
82
+ ## Tests
83
+
84
+ To run the test suite (contained in `./spec`), call `bundle exec rake`
85
+
86
+ ## Questions
87
+
88
+ If you need anything ping us on [twitter](http://bit.ly/t-xotahal).
89
+
90
+ | Jiri Otahal |
91
+ | -------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | [<img src="https://avatars3.githubusercontent.com/u/3531955?v=4" width="100px;" style="border-radius:50px"/>](http://bit.ly/t-xotahal) |
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/semantic_convention_release/version'
2
+
3
+ module Fastlane
4
+ module SemanticConventionRelease
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::SemanticConventionRelease.all_classes.each do |current|
15
+ require current
16
+ end
@@ -0,0 +1,333 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/semantic_convention_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_LAST_TAG_HASH = :RELEASE_LAST_TAG_HASH
10
+ RELEASE_LAST_VERSION = :RELEASE_LAST_VERSION
11
+ RELEASE_NEXT_MAJOR_VERSION = :RELEASE_NEXT_MAJOR_VERSION
12
+ RELEASE_NEXT_MINOR_VERSION = :RELEASE_NEXT_MINOR_VERSION
13
+ RELEASE_NEXT_PATCH_VERSION = :RELEASE_NEXT_PATCH_VERSION
14
+ RELEASE_NEXT_VERSION = :RELEASE_NEXT_VERSION
15
+ RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION = :RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION
16
+ CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN = :CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN
17
+ end
18
+
19
+ class AnalyzeCommitsAction < Action
20
+ def self.get_last_tag(params)
21
+ # Try to find the tag
22
+ command = "git describe --tags --match=#{params[:match]}"
23
+ Actions.sh(command, log: params[:debug])
24
+ rescue
25
+ UI.message("Tag was not found for match pattern - #{params[:match]}")
26
+ ''
27
+ end
28
+
29
+ def self.get_last_tag_hash(params)
30
+ command = "git rev-list -n 1 refs/tags/#{params[:tag_name]}"
31
+ Actions.sh(command, log: params[:debug]).chomp
32
+ end
33
+
34
+ def self.get_commits_from_hash(params)
35
+ commits = Helper::SemanticConventionReleaseHelper.git_log(
36
+ pretty: '%s|%b|>',
37
+ start: params[:hash],
38
+ debug: params[:debug]
39
+ )
40
+ commits.split("|>")
41
+ end
42
+
43
+ def self.is_releasable(params)
44
+ # Hash of the commit where is the last version
45
+ # If the tag is not found we are taking HEAD as reference
46
+ hash = 'HEAD'
47
+ # Default last version
48
+ version = '0.0.0'
49
+
50
+ tag = get_last_tag(
51
+ match: params[:match],
52
+ debug: params[:debug]
53
+ )
54
+
55
+ if tag.empty?
56
+ UI.message("First commit of the branch is taken as a begining of next release")
57
+ # If there is no tag found we taking the first commit of current branch
58
+ hash = Actions.sh('git rev-list --max-parents=0 HEAD', log: params[:debug]).chomp
59
+ else
60
+ # Tag's format is v2.3.4-5-g7685948
61
+ # See git describe man page for more info
62
+ tag_name = tag.split('-')[0...-2].join('-').strip
63
+ parsed_version = tag_name.match(params[:tag_version_match])
64
+
65
+ if parsed_version.nil?
66
+ UI.user_error!("Error while parsing version from tag #{tag_name} by using tag_version_match - #{params[:tag_version_match]}. Please check if the tag contains version as you expect and if you are using single brackets for tag_version_match parameter.")
67
+ end
68
+
69
+ version = parsed_version[0]
70
+ # Get a hash of last version tag
71
+ hash = get_last_tag_hash(
72
+ tag_name: tag_name,
73
+ debug: params[:debug]
74
+ )
75
+
76
+ UI.message("Found a tag #{tag_name} associated with version #{version}")
77
+ end
78
+
79
+ # converts last version string to the int numbers
80
+ next_major = (version.split('.')[0] || 0).to_i
81
+ next_minor = (version.split('.')[1] || 0).to_i
82
+ next_patch = (version.split('.')[2] || 0).to_i
83
+
84
+ # Get commits log between last version and head
85
+ splitted = get_commits_from_hash(
86
+ hash: hash,
87
+ debug: params[:debug]
88
+ )
89
+
90
+ UI.message("Found #{splitted.length} commits since last release")
91
+ releases = params[:releases]
92
+
93
+ format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
94
+ splitted.each do |line|
95
+ parts = line.split("|")
96
+ subject = parts[0].strip
97
+ # conventional commits are in format
98
+ # type: subject (fix: app crash - for example)
99
+ commit = Helper::SemanticConventionReleaseHelper.parse_commit(
100
+ commit_subject: subject,
101
+ commit_body: parts[1],
102
+ releases: releases,
103
+ pattern: format_pattern
104
+ )
105
+
106
+ unless commit[:scope].nil?
107
+ # 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
108
+ scope = commit[:scope]
109
+ scopes_to_ignore = params[:ignore_scopes]
110
+ # if it is, we'll skip this commit when bumping versions
111
+ next if scopes_to_ignore.include?(scope) #=> true
112
+ end
113
+
114
+ if commit[:release] == "major" || commit[:is_breaking_change]
115
+ next_major += 1
116
+ next_minor = 0
117
+ next_patch = 0
118
+ elsif commit[:release] == "minor"
119
+ next_minor += 1
120
+ next_patch = 0
121
+ elsif commit[:release] == "patch"
122
+ next_patch += 1
123
+ end
124
+
125
+ next_version = "#{next_major}.#{next_minor}.#{next_patch}"
126
+ UI.message("#{next_version}: #{subject}") if params[:show_version_path]
127
+ end
128
+
129
+ next_version = "#{next_major}.#{next_minor}.#{next_patch}"
130
+
131
+ is_next_version_releasable = Helper::SemanticConventionReleaseHelper.semver_gt(next_version, version)
132
+
133
+ Actions.lane_context[SharedValues::RELEASE_ANALYZED] = true
134
+ Actions.lane_context[SharedValues::RELEASE_IS_NEXT_VERSION_HIGHER] = is_next_version_releasable
135
+ # Last release analysis
136
+ Actions.lane_context[SharedValues::RELEASE_LAST_TAG_HASH] = hash
137
+ Actions.lane_context[SharedValues::RELEASE_LAST_VERSION] = version
138
+ # Next release analysis
139
+ Actions.lane_context[SharedValues::RELEASE_NEXT_MAJOR_VERSION] = next_major
140
+ Actions.lane_context[SharedValues::RELEASE_NEXT_MINOR_VERSION] = next_minor
141
+ Actions.lane_context[SharedValues::RELEASE_NEXT_PATCH_VERSION] = next_patch
142
+ Actions.lane_context[SharedValues::RELEASE_NEXT_VERSION] = next_version
143
+
144
+ success_message = "Next version (#{next_version}) is higher than last version (#{version}). This version should be released."
145
+ UI.success(success_message) if is_next_version_releasable
146
+
147
+ is_next_version_releasable
148
+ end
149
+
150
+ def self.is_codepush_friendly(params)
151
+ git_command = 'git rev-list --max-parents=0 HEAD'
152
+ # Begining of the branch is taken for codepush analysis
153
+ hash_lines = Actions.sh("#{git_command} | wc -l", log: params[:debug]).chomp
154
+ hash = Actions.sh(git_command, log: params[:debug]).chomp
155
+ next_major = 0
156
+ next_minor = 0
157
+ next_patch = 0
158
+ last_incompatible_codepush_version = '0.0.0'
159
+
160
+ if hash_lines.to_i > 1
161
+ UI.error("#{git_command} resulted to more than 1 hash")
162
+ 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.')
163
+ Actions.sh(git_command, log: true).chomp
164
+ return false
165
+ end
166
+
167
+ # Get commits log between last version and head
168
+ splitted = get_commits_from_hash(
169
+ hash: hash,
170
+ debug: params[:debug]
171
+ )
172
+ releases = params[:releases]
173
+ codepush_friendly = params[:codepush_friendly]
174
+
175
+ format_pattern = lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN]
176
+ splitted.each do |line|
177
+ # conventional commits are in format
178
+ # type: subject (fix: app crash - for example)
179
+ commit = Helper::SemanticConventionReleaseHelper.parse_commit(
180
+ commit_subject: line.split("|")[0],
181
+ commit_body: line.split("|")[1],
182
+ releases: releases,
183
+ pattern: format_pattern,
184
+ codepush_friendly: codepush_friendly
185
+ )
186
+
187
+ if commit[:release] == "major" || commit[:is_breaking_change]
188
+ next_major += 1
189
+ next_minor = 0
190
+ next_patch = 0
191
+ elsif commit[:release] == "minor"
192
+ next_minor += 1
193
+ next_patch = 0
194
+ elsif commit[:release] == "patch"
195
+ next_patch += 1
196
+ end
197
+
198
+ unless commit[:is_codepush_friendly]
199
+ last_incompatible_codepush_version = "#{next_major}.#{next_minor}.#{next_patch}"
200
+ end
201
+ end
202
+
203
+ Actions.lane_context[SharedValues::RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION] = last_incompatible_codepush_version
204
+ end
205
+
206
+ def self.run(params)
207
+ is_next_version_releasable = is_releasable(params)
208
+ is_codepush_friendly(params)
209
+
210
+ is_next_version_releasable
211
+ end
212
+
213
+ #####################################################
214
+ # @!group Documentation
215
+ #####################################################
216
+
217
+ def self.description
218
+ "Finds a tag of last release and determinates version of next release"
219
+ end
220
+
221
+ def self.details
222
+ "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."
223
+ end
224
+
225
+ def self.available_options
226
+ # Define all options your action supports.
227
+
228
+ # Below a few examples
229
+ [
230
+ FastlaneCore::ConfigItem.new(
231
+ key: :match,
232
+ description: "Match parameter of git describe. See man page of git describe for more info",
233
+ verify_block: proc do |value|
234
+ UI.user_error!("No match for analyze_commits action given, pass using `match: 'expr'`") unless value && !value.empty?
235
+ end
236
+ ),
237
+ FastlaneCore::ConfigItem.new(
238
+ key: :commit_format,
239
+ 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",
240
+ default_value: "default",
241
+ is_string: false,
242
+ verify_block: proc do |value|
243
+ case value
244
+ when String
245
+ unless Helper::SemanticConventionReleaseHelper.format_patterns.key?(value)
246
+ UI.user_error!("Invalid format preset: #{value}")
247
+ end
248
+
249
+ pattern = Helper::SemanticConventionReleaseHelper.format_patterns[value]
250
+ when Regexp
251
+ pattern = value
252
+ else
253
+ UI.user_error!("Invalid option type: #{value.inspect}")
254
+ end
255
+ Actions.lane_context[SharedValues::CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN] = pattern
256
+ end
257
+ ),
258
+ FastlaneCore::ConfigItem.new(
259
+ key: :releases,
260
+ description: "Map types of commit to release (major, minor, patch)",
261
+ default_value: { fix: "patch", feat: "minor" },
262
+ type: Hash
263
+ ),
264
+ FastlaneCore::ConfigItem.new(
265
+ key: :codepush_friendly,
266
+ description: "These types are consider as codepush friendly automatically",
267
+ default_value: ["chore", "test", "docs"],
268
+ type: Array,
269
+ optional: true
270
+ ),
271
+ FastlaneCore::ConfigItem.new(
272
+ key: :tag_version_match,
273
+ description: "To parse version number from tag name",
274
+ default_value: '\d+\.\d+\.\d+'
275
+ ),
276
+ FastlaneCore::ConfigItem.new(
277
+ key: :ignore_scopes,
278
+ description: "To ignore certain scopes when calculating releases",
279
+ default_value: [],
280
+ type: Array,
281
+ optional: true
282
+ ),
283
+ FastlaneCore::ConfigItem.new(
284
+ key: :show_version_path,
285
+ description: "True if you want to print out the version calculated for each commit",
286
+ default_value: true,
287
+ type: Boolean,
288
+ optional: true
289
+ ),
290
+ FastlaneCore::ConfigItem.new(
291
+ key: :debug,
292
+ description: "True if you want to log out a debug info",
293
+ default_value: false,
294
+ type: Boolean,
295
+ optional: true
296
+ )
297
+ ]
298
+ end
299
+
300
+ def self.output
301
+ # Define the shared values you are going to provide
302
+ # Example
303
+ [
304
+ ['RELEASE_ANALYZED', 'True if commits were analyzed.'],
305
+ ['RELEASE_IS_NEXT_VERSION_HIGHER', 'True if next version is higher then last version'],
306
+ ['RELEASE_LAST_TAG_HASH', 'Hash of commit that is tagged as a last version'],
307
+ ['RELEASE_LAST_VERSION', 'Last version number - parsed from last tag.'],
308
+ ['RELEASE_NEXT_MAJOR_VERSION', 'Major number of the next version'],
309
+ ['RELEASE_NEXT_MINOR_VERSION', 'Minor number of the next version'],
310
+ ['RELEASE_NEXT_PATCH_VERSION', 'Patch number of the next version'],
311
+ ['RELEASE_NEXT_VERSION', 'Next version string in format (major.minor.patch)'],
312
+ ['RELEASE_LAST_INCOMPATIBLE_CODEPUSH_VERSION', 'Last commit without codepush'],
313
+ ['CONVENTIONAL_CHANGELOG_ACTION_FORMAT_PATTERN', 'The format pattern Regexp used to match commits (mainly for internal use)']
314
+ ]
315
+ end
316
+
317
+ def self.return_value
318
+ # If your method provides a return value, you can describe here what it does
319
+ "Returns true if the next version is higher then the last version"
320
+ end
321
+
322
+ def self.authors
323
+ # So no one will ever forget your contribution to fastlane :) You are awesome btw!
324
+ ["xotahal"]
325
+ end
326
+
327
+ def self.is_supported?(platform)
328
+ # you can do things like
329
+ true
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,309 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/semantic_convention_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::SemanticConventionReleaseHelper.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)
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)
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::SemanticConventionReleaseHelper.parse_commit(
183
+ commit_subject: splitted[0],
184
+ commit_body: splitted[1],
185
+ pattern: format_pattern
186
+ )
187
+
188
+ commit[:hash] = splitted[2]
189
+ commit[:short_hash] = splitted[3]
190
+ commit[:author_name] = splitted[4]
191
+ commit[:commit_date] = splitted[5]
192
+
193
+ parsed.push(commit)
194
+ end
195
+
196
+ parsed
197
+ end
198
+
199
+ #####################################################
200
+ # @!group Documentation
201
+ #####################################################
202
+
203
+ def self.description
204
+ "Get commits since last version and generates release notes"
205
+ end
206
+
207
+ def self.details
208
+ "Uses conventional commits. It groups commits by their types and generates release notes in markdown or slack format."
209
+ end
210
+
211
+ def self.available_options
212
+ # Define all options your action supports.
213
+
214
+ # Below a few examples
215
+ [
216
+ FastlaneCore::ConfigItem.new(
217
+ key: :format,
218
+ description: "You can use either markdown, slack or plain",
219
+ default_value: "markdown",
220
+ optional: true
221
+ ),
222
+ FastlaneCore::ConfigItem.new(
223
+ key: :title,
224
+ description: "Title for release notes",
225
+ optional: true
226
+ ),
227
+ FastlaneCore::ConfigItem.new(
228
+ key: :commit_url,
229
+ description: "Uses as a link to the commit",
230
+ optional: true
231
+ ),
232
+ FastlaneCore::ConfigItem.new(
233
+ key: :order,
234
+ description: "You can change the order of groups in release notes",
235
+ default_value: ["feat", "fix", "refactor", "perf", "chore", "test", "docs", "style", "no_type"],
236
+ type: Array,
237
+ optional: true
238
+ ),
239
+ FastlaneCore::ConfigItem.new(
240
+ key: :sections,
241
+ description: "Map type to section title",
242
+ default_value: {
243
+ feat: "Features",
244
+ fix: "Bug fixes",
245
+ refactor: "Code refactoring",
246
+ perf: "Performance improvements",
247
+ chore: "Building system",
248
+ test: "Testing",
249
+ docs: "Documentation",
250
+ style: "Code style",
251
+ no_type: "Other work"
252
+ },
253
+ type: Hash,
254
+ optional: true
255
+ ),
256
+ FastlaneCore::ConfigItem.new(
257
+ key: :display_author,
258
+ description: "Whether you want to show the author of the commit",
259
+ default_value: false,
260
+ type: Boolean,
261
+ optional: true
262
+ ),
263
+ FastlaneCore::ConfigItem.new(
264
+ key: :display_title,
265
+ description: "Whether you want to hide the title/header with the version details at the top of the changelog",
266
+ default_value: true,
267
+ type: Boolean,
268
+ optional: true
269
+ ),
270
+ FastlaneCore::ConfigItem.new(
271
+ key: :display_links,
272
+ description: "Whether you want to display the links to commit IDs",
273
+ default_value: true,
274
+ type: Boolean,
275
+ optional: true
276
+ ),
277
+ FastlaneCore::ConfigItem.new(
278
+ key: :debug,
279
+ description: "True if you want to log out a debug info",
280
+ default_value: false,
281
+ type: Boolean,
282
+ optional: true
283
+ )
284
+ ]
285
+ end
286
+
287
+ def self.output
288
+ # Define the shared values you are going to provide
289
+ # Example
290
+ []
291
+ end
292
+
293
+ def self.return_value
294
+ # If your method provides a return value, you can describe here what it does
295
+ "Returns generated release notes as a string"
296
+ end
297
+
298
+ def self.authors
299
+ # So no one will ever forget your contribution to fastlane :) You are awesome btw!
300
+ ["xotahal"]
301
+ end
302
+
303
+ def self.is_supported?(platform)
304
+ # you can do things like
305
+ true
306
+ end
307
+ end
308
+ end
309
+ 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 SemanticConventionReleaseHelper
8
+ def self.format_patterns
9
+ return {
10
+ "default" => /^(docs|fix|feat|chore|style|refactor|perf|test|skip)(?:\((.*)\))?(!?)\: (.*)/,
11
+ "angular" => /^(\w*)(?:\((.*)\))?(): (.*)/
12
+ }
13
+ end
14
+
15
+ # class methods that you define here become available in your action
16
+ # as `Helper::SemanticConventionReleaseHelper.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 SemanticConventionRelease VERSION = "1.0.1" end end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-semantic_convention_release
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jiří Otáhal
8
+ - Sergio Fierro
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-08-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pry
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec_junit_formatter
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '='
89
+ - !ruby/object:Gem::Version
90
+ version: 0.49.1
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '='
96
+ - !ruby/object:Gem::Version
97
+ version: 0.49.1
98
+ - !ruby/object:Gem::Dependency
99
+ name: rubocop-require_tools
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: simplecov
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: fastlane
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 2.117.1
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 2.117.1
140
+ description:
141
+ email: xotahal@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - LICENSE
147
+ - README.md
148
+ - lib/fastlane/plugin/semantic_convention_release.rb
149
+ - lib/fastlane/plugin/semantic_convention_release/actions/analyze_commits.rb
150
+ - lib/fastlane/plugin/semantic_convention_release/actions/conventional_changelog.rb
151
+ - lib/fastlane/plugin/semantic_convention_release/helper/semantic_convention_release_helper.rb
152
+ - lib/fastlane/plugin/semantic_convention_release/version.rb
153
+ homepage: https://github.com/SergioFierro/fastlane-plugin-semantic_release
154
+ licenses:
155
+ - MIT
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.1.2
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Automated version managment and generator of release notes.
176
+ test_files: []