git-version-bump 0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/release.yml +33 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENCE +674 -0
- data/README.md +170 -0
- data/Rakefile +17 -0
- data/bin/git-version-bump +69 -0
- data/git-version-bump.gemspec +26 -0
- data/lib/git-version-bump/rake-tasks.rb +49 -0
- data/lib/git-version-bump/version.rb +33 -0
- data/lib/git-version-bump.rb +386 -0
- metadata +110 -0
@@ -0,0 +1,386 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'open3'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module GitVersionBump
|
7
|
+
class VersionUnobtainable < StandardError; end
|
8
|
+
class CommandFailure < StandardError
|
9
|
+
attr_accessor :output, :exitstatus
|
10
|
+
|
11
|
+
def initialize(m, output, exitstatus)
|
12
|
+
super(m)
|
13
|
+
@output, @exitstatus = output, exitstatus
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
VERSION_TAG_GLOB = 'v[0-9]*.[0-9]*.*[0-9]'
|
18
|
+
private_constant :VERSION_TAG_GLOB
|
19
|
+
|
20
|
+
DEVNULL = Gem.win_platform? ? "NUL" : "/dev/null"
|
21
|
+
private_constant :DEVNULL
|
22
|
+
|
23
|
+
def self.version(use_local_dir=false, include_lite_tags=false)
|
24
|
+
if use_local_dir
|
25
|
+
repo_version(true, include_lite_tags)
|
26
|
+
else
|
27
|
+
gem_version || repo_version(false, include_lite_tags)
|
28
|
+
end.tap { |v| p :GVB_VERSION, v if debug? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.major_version(use_local_dir=false, include_lite_tags=false)
|
32
|
+
ver = version(use_local_dir, include_lite_tags)
|
33
|
+
v = ver.split('.')[0]
|
34
|
+
|
35
|
+
unless v =~ /^[0-9]+$/
|
36
|
+
raise ArgumentError,
|
37
|
+
"#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
|
38
|
+
end
|
39
|
+
|
40
|
+
return v.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.minor_version(use_local_dir=false, include_lite_tags=false)
|
44
|
+
ver = version(use_local_dir, include_lite_tags)
|
45
|
+
v = ver.split('.')[1]
|
46
|
+
|
47
|
+
unless v =~ /^[0-9]+$/
|
48
|
+
raise ArgumentError,
|
49
|
+
"#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
|
50
|
+
end
|
51
|
+
|
52
|
+
return v.to_i
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.patch_version(use_local_dir=false, include_lite_tags=false)
|
56
|
+
ver = version(use_local_dir, include_lite_tags)
|
57
|
+
v = ver.split('.')[2]
|
58
|
+
|
59
|
+
unless v =~ /^[0-9]+$/
|
60
|
+
raise ArgumentError,
|
61
|
+
"#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
|
62
|
+
end
|
63
|
+
|
64
|
+
return v.to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.internal_revision(use_local_dir=false, include_lite_tags=false)
|
68
|
+
version(use_local_dir, include_lite_tags).split('.', 4)[3].to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.date(use_local_dir=false, include_lite_tags = false)
|
72
|
+
if use_local_dir
|
73
|
+
repo_date(true, include_lite_tags)
|
74
|
+
else
|
75
|
+
gem_date || repo_date(false, include_lite_tags)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.tag_version(v, release_notes = false, include_lite_tags=false)
|
80
|
+
if dirty_tree?
|
81
|
+
puts "You have uncommitted files. Refusing to tag a dirty tree."
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
if release_notes
|
85
|
+
log_file = Tempfile.new('gvb')
|
86
|
+
|
87
|
+
begin
|
88
|
+
# We need to find the tag before this one, so we can list all the commits
|
89
|
+
# between the two. This is not a trivial operation.
|
90
|
+
git_cmd = ["git", "describe", "--match=#{VERSION_TAG_GLOB}", "--always"]
|
91
|
+
git_cmd << "--tags" if include_lite_tags
|
92
|
+
|
93
|
+
prev_tag = run_command(git_cmd, "getting previous release tag").strip.gsub(/-\d+-g[0-9a-f]+$/, '')
|
94
|
+
|
95
|
+
log_file.puts <<-EOF.gsub(/^\t\t\t\t\t/, '')
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
# Write your release notes above. The first line should be the release name.
|
100
|
+
# To help you remember what's in here, the commits since your last release
|
101
|
+
# are listed below. This will become v#{v}
|
102
|
+
#
|
103
|
+
EOF
|
104
|
+
log_file.puts run_command(["git", "log", "--no-show-signature", "--format=# %h %s", "#{prev_tag}..HEAD"], "getting commit range of release")
|
105
|
+
|
106
|
+
log_file.close
|
107
|
+
|
108
|
+
pre_hash = Digest::SHA1.hexdigest(File.read(log_file.path))
|
109
|
+
run_command(["git", "config", "-e", "-f", log_file.path], "editing release notes", false)
|
110
|
+
if Digest::SHA1.hexdigest(File.read(log_file.path)) == pre_hash
|
111
|
+
puts "Release notes not edited; not making release"
|
112
|
+
log_file.unlink
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
puts "Tagging version #{v}..."
|
117
|
+
run_command(["git", "tag", "-a", "-F", log_file.path, "v#{v}"], "tagging release with annotations")
|
118
|
+
ensure
|
119
|
+
log_file.unlink
|
120
|
+
end
|
121
|
+
else
|
122
|
+
# Crikey this is a lot simpler
|
123
|
+
run_command(["git", "tag", "-a", "-m", "Version v#{v}", "v#{v}"], "tagging release")
|
124
|
+
end
|
125
|
+
|
126
|
+
run_command(["git", "push"], "pushing commits to the default remote repository")
|
127
|
+
run_command(["git", "push", "--tags"], "pushing tags to the default remote repository")
|
128
|
+
end
|
129
|
+
|
130
|
+
# Calculate a version number based on the date of the most recent git commit.
|
131
|
+
#
|
132
|
+
# Return a version format string of the form `"0.YYYYMMDD.N"`, where
|
133
|
+
# `YYYYMMDD` is the date of the "top-most" commit in the tree, and `N` is
|
134
|
+
# the number of other commits also made on that date.
|
135
|
+
#
|
136
|
+
# This version format is not recommented for general use. It has benefit
|
137
|
+
# only in situations where the principles of Semantic Versioning have no
|
138
|
+
# real meaning, such as packages where there is little or no concept of
|
139
|
+
# "backwards compatibility" (eg packages which only contain images and
|
140
|
+
# other assets), or where the package can, for reasons outside that of
|
141
|
+
# the package itself, never break backwards compatibility (definitions of
|
142
|
+
# binary-packed structures shared amongst multiple systems).
|
143
|
+
#
|
144
|
+
# The format of this commit-date-based version format allows for a strictly
|
145
|
+
# monotonically-increasing version number, aligned with the progression of the
|
146
|
+
# underlying git commit log.
|
147
|
+
#
|
148
|
+
# One limitation of the format is that it doesn't deal with the issue of
|
149
|
+
# package builds made from multiple divergent trees. Unlike
|
150
|
+
# `git-describe`-based output, there is no "commit hash" identity
|
151
|
+
# included in the version string. This is because of (ludicrous)
|
152
|
+
# limitations of the Rubygems format definition -- the moment there's a
|
153
|
+
# letter in the version number, the package is considered a "pre-release"
|
154
|
+
# version. Since hashes are hex, we're boned. Sorry about that. Don't
|
155
|
+
# make builds off a branch, basically.
|
156
|
+
#
|
157
|
+
def self.commit_date_version(use_local_dir = false)
|
158
|
+
if use_local_dir
|
159
|
+
commit_date_version_string(true)
|
160
|
+
else
|
161
|
+
gem_version || commit_date_version_string(false)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.commit_date_version_string(use_local_dir = false)
|
166
|
+
commit_dates = run_command(["git", "-C", git_dir(use_local_dir).to_s, "log", "--no-show-signature", "--format=%at"], "getting dates of all commits").
|
167
|
+
split("\n").
|
168
|
+
map { |l| Time.at(Integer(l)).strftime("%Y%m%d") }
|
169
|
+
|
170
|
+
version_date = commit_dates.first
|
171
|
+
commit_count = commit_dates.select { |d| d == version_date }.length - 1
|
172
|
+
dirty_suffix = if dirty_tree?
|
173
|
+
".dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}"
|
174
|
+
else
|
175
|
+
""
|
176
|
+
end
|
177
|
+
|
178
|
+
return "0.#{version_date}.#{commit_count}#{dirty_suffix}"
|
179
|
+
rescue CommandFailure => ex
|
180
|
+
p :GVB_CDVS_CMD_FAIL, ex.output if debug?
|
181
|
+
if ex.output =~ /fatal: your current branch .* does not have any commits yet/
|
182
|
+
return "0.0.0.1.ENOCOMMITS"
|
183
|
+
else
|
184
|
+
raise VersionUnobtainable, "Could not get commit date-based version from git repository at #{git_dir(use_local_dir)}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.git_available?
|
189
|
+
try_command(["git", "--version"])
|
190
|
+
end
|
191
|
+
private_class_method :git_available?
|
192
|
+
|
193
|
+
def self.dirty_tree?(dir='.')
|
194
|
+
# Are we in a dirty, dirty tree?
|
195
|
+
! run_command(["git", "-C", dir.to_s, "status", "--porcelain"], "checking for tree cleanliness").empty?
|
196
|
+
end
|
197
|
+
private_class_method :dirty_tree?
|
198
|
+
|
199
|
+
# Execute a command, specified as an array.
|
200
|
+
#
|
201
|
+
# On success, the full output of the command (stdout+stderr, interleaved) is returned unless capture_output is not true.
|
202
|
+
# On error, a `CommandFailure` exception is raised.
|
203
|
+
#
|
204
|
+
def self.run_command(cmd, desc, capture_output = true)
|
205
|
+
unless cmd.is_a?(Array)
|
206
|
+
raise ArgumentError, "Must pass command line arguments in an array"
|
207
|
+
end
|
208
|
+
|
209
|
+
unless cmd.all? { |s| s.is_a?(String) }
|
210
|
+
raise ArgumentError, "Command line arguments must be strings"
|
211
|
+
end
|
212
|
+
|
213
|
+
if debug?
|
214
|
+
p :GVB_CMD, desc, cmd
|
215
|
+
end
|
216
|
+
|
217
|
+
if capture_output == true
|
218
|
+
out, status = Open3.capture2e({"LC_MESSAGES" => "C"}, *cmd)
|
219
|
+
else
|
220
|
+
out = '(output not captured)'
|
221
|
+
pid = spawn(*cmd)
|
222
|
+
(retpid, status) = Process.wait2 pid
|
223
|
+
end
|
224
|
+
|
225
|
+
if status.exitstatus != 0
|
226
|
+
raise CommandFailure.new("Failed while #{desc}", out, status.exitstatus)
|
227
|
+
else
|
228
|
+
out
|
229
|
+
end
|
230
|
+
end
|
231
|
+
private_class_method :run_command
|
232
|
+
|
233
|
+
# Execute a command, and return whether it succeeded or failed.
|
234
|
+
#
|
235
|
+
def self.try_command(cmd)
|
236
|
+
begin
|
237
|
+
run_command(cmd, "try_command")
|
238
|
+
true
|
239
|
+
rescue CommandFailure
|
240
|
+
false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
private_class_method :try_command
|
244
|
+
|
245
|
+
def self.run_git(git_args, desc, use_local_dir)
|
246
|
+
run_command(["git", "-C", git_dir(use_local_dir).to_s] + git_args, desc)
|
247
|
+
end
|
248
|
+
private_class_method :run_git
|
249
|
+
|
250
|
+
def self.caller_file
|
251
|
+
# Who called us? Because this method gets called from other methods
|
252
|
+
# within this file, we can't just look at Gem.location_of_caller, but
|
253
|
+
# instead we need to parse the caller stack ourselves to find which
|
254
|
+
# gem we're trying to version all over.
|
255
|
+
Pathname(
|
256
|
+
caller_locations.
|
257
|
+
map(&:path).
|
258
|
+
tap { |c| p :CALLER_LOCATIONS, c if debug? }.
|
259
|
+
find { |l| l != __FILE__ }
|
260
|
+
).realpath rescue nil
|
261
|
+
end
|
262
|
+
private_class_method :caller_file
|
263
|
+
|
264
|
+
def self.caller_gemspec
|
265
|
+
cf = caller_file or return nil
|
266
|
+
|
267
|
+
# Grovel through all the loaded gems to try and find the gem
|
268
|
+
# that contains the caller's file.
|
269
|
+
Gem.loaded_specs.values.each do |spec|
|
270
|
+
# On Windows I have encountered gems that already have an absolute
|
271
|
+
# path, verify that the path is relative before appending to it
|
272
|
+
search_dirs = spec.require_paths.map do |path|
|
273
|
+
if Pathname(path).absolute?
|
274
|
+
path
|
275
|
+
else
|
276
|
+
File.join(spec.full_gem_path, path)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
search_dirs << File.join(spec.full_gem_path, spec.bindir)
|
280
|
+
search_dirs.map! do |d|
|
281
|
+
begin
|
282
|
+
Pathname(d).realpath.to_s
|
283
|
+
rescue Errno::ENOENT
|
284
|
+
nil
|
285
|
+
end
|
286
|
+
end.compact!
|
287
|
+
|
288
|
+
if search_dirs.find { |d| cf.to_s.index(d) == 0 }
|
289
|
+
return spec
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
if debug?
|
294
|
+
p :GVB_NO_GEMSPEC, cf
|
295
|
+
end
|
296
|
+
|
297
|
+
nil
|
298
|
+
end
|
299
|
+
private_class_method :caller_gemspec
|
300
|
+
|
301
|
+
def self.gem_version
|
302
|
+
return "" if caller_gemspec.nil?
|
303
|
+
|
304
|
+
caller_gemspec.version.to_s
|
305
|
+
end
|
306
|
+
private_class_method :gem_version
|
307
|
+
|
308
|
+
def self.gem_date
|
309
|
+
return nil if caller_gemspec.nil?
|
310
|
+
return nil if caller_gemspec.date.nil?
|
311
|
+
|
312
|
+
caller_gemspec.date.strftime("%F")
|
313
|
+
end
|
314
|
+
private_class_method :gem_version
|
315
|
+
|
316
|
+
def self.repo_version(use_local_dir, include_lite_tags)
|
317
|
+
begin
|
318
|
+
run_git(["config", "versionBump.versionOverride"], "getting versionOverride", use_local_dir).chomp
|
319
|
+
rescue CommandFailure => ex
|
320
|
+
p :NO_OVERRIDE_VERSION, [ex.class, ex.message] if debug?
|
321
|
+
repo_version_from_tag(use_local_dir, include_lite_tags)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
private_class_method :repo_version
|
325
|
+
|
326
|
+
def self.repo_version_from_tag(use_local_dir, include_lite_tags)
|
327
|
+
git_cmd = ["git", "-C", git_dir(use_local_dir).to_s, "describe", "--dirty=.1.dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}", "--match=#{VERSION_TAG_GLOB}"]
|
328
|
+
git_cmd << "--tags" if include_lite_tags
|
329
|
+
|
330
|
+
begin
|
331
|
+
run_command(git_cmd, "getting current version descriptor").
|
332
|
+
strip.
|
333
|
+
gsub(/^v/, '').
|
334
|
+
gsub('-', '.')
|
335
|
+
rescue CommandFailure => ex
|
336
|
+
p :GVB_REPO_VERSION_FAILURE, ex.output if debug?
|
337
|
+
if ex.output =~ /fatal: No names found, cannot describe anything/
|
338
|
+
# aka "no tags, bro"
|
339
|
+
"0.0.0.1.ENOTAG"
|
340
|
+
else
|
341
|
+
raise VersionUnobtainable, "Could not get version from gemspec or git repository at #{git_dir(use_local_dir)}"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
private_class_method :repo_version_from_tag
|
346
|
+
|
347
|
+
def self.repo_date(use_local_dir, include_lite_tags)
|
348
|
+
begin
|
349
|
+
run_git(["config", "versionBump.dateOverride"], "getting dateOverride", use_local_dir).chomp
|
350
|
+
rescue CommandFailure => ex
|
351
|
+
p :NO_OVERRIDE_DATE, [ex.class, ex.message] if debug?
|
352
|
+
repo_date_from_commit(use_local_dir, include_lite_tags)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
private_class_method :repo_date
|
356
|
+
|
357
|
+
def self.repo_date_from_commit(use_local_dir, include_lite_tags)
|
358
|
+
if dirty_tree?(git_dir(use_local_dir))
|
359
|
+
Time.now.strftime("%F")
|
360
|
+
else
|
361
|
+
# Clean tree. Date of last commit is needed.
|
362
|
+
(run_command(["git", "-C", git_dir(use_local_dir).to_s, "show", "--no-show-signature", "--format=format:%cd", "--date=short"], "getting date of last commit").lines.first || "").strip
|
363
|
+
end
|
364
|
+
rescue CommandFailure
|
365
|
+
raise VersionUnobtainable, "Could not get commit date from git repository at #{git_dir(use_local_dir)}"
|
366
|
+
end
|
367
|
+
private_class_method :repo_date_from_commit
|
368
|
+
|
369
|
+
def self.git_dir(use_local_dir = false)
|
370
|
+
if use_local_dir
|
371
|
+
Dir.pwd
|
372
|
+
else
|
373
|
+
(caller_file && caller_file.dirname) || Dir.pwd
|
374
|
+
end.tap { |d| p :GVB_GIT_DIR, use_local_dir, d if debug? }
|
375
|
+
end
|
376
|
+
private_class_method :git_dir
|
377
|
+
|
378
|
+
def self.debug?
|
379
|
+
ENV.key?("GVB_DEBUG")
|
380
|
+
end
|
381
|
+
private_class_method :debug?
|
382
|
+
end
|
383
|
+
|
384
|
+
GVB = GitVersionBump unless defined? GVB
|
385
|
+
|
386
|
+
require 'git-version-bump/version'
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git-version-bump
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Palmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: github-release
|
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: rake
|
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: bundler
|
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: rdoc
|
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
|
+
description:
|
70
|
+
email:
|
71
|
+
executables:
|
72
|
+
- git-version-bump
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files:
|
75
|
+
- README.md
|
76
|
+
files:
|
77
|
+
- ".github/workflows/release.yml"
|
78
|
+
- ".gitignore"
|
79
|
+
- Gemfile
|
80
|
+
- LICENCE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/git-version-bump
|
84
|
+
- git-version-bump.gemspec
|
85
|
+
- lib/git-version-bump.rb
|
86
|
+
- lib/git-version-bump/rake-tasks.rb
|
87
|
+
- lib/git-version-bump/version.rb
|
88
|
+
homepage: https://github.com/mpalmer/git-version-bump
|
89
|
+
licenses: []
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 2.1.0
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.1.6
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Manage your app version entirely via git tags
|
110
|
+
test_files: []
|