semverve 0.1.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.
@@ -0,0 +1,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+
5
+ require_relative "error"
6
+ require_relative "presets"
7
+ require_relative "project_metadata"
8
+
9
+ module Semverve
10
+ ##
11
+ # Mutable configuration used before Semverve resolves project defaults.
12
+ class Configuration
13
+ ##
14
+ # Version-maintenance surfaces supported by umbrella check and fix tasks.
15
+ #
16
+ # @return [Array<Symbol>]
17
+ VALID_VERSION_CHECKS = [:doc_references, :code_references, :metadata].freeze
18
+
19
+ ##
20
+ # Default Ruby pattern for code version literals that are safe to rewrite.
21
+ #
22
+ # @return [Regexp]
23
+ DEFAULT_VERSION_CODE_REFERENCE_PATTERN = /^\s*(?:(?:[A-Z]\w*::)*(?:[A-Z]\w*VERSION[A-Z0-9_]*|VERSION)|(?:[a-z_]\w*|self)\.version)\s*=\s*(?<quote>["'])(?<version>\d+\.\d+\.\d+)\k<quote>/
24
+
25
+ ##
26
+ # Whether increments should run +bundle lock+ after writing a version.
27
+ #
28
+ # @return [Boolean]
29
+ attr_accessor :bundle_lock
30
+
31
+ ##
32
+ # Callable used to run shell commands such as +bundle lock+.
33
+ #
34
+ # @return [#call]
35
+ attr_accessor :command_runner
36
+
37
+ ##
38
+ # Version-file format to read, replace, or generate.
39
+ #
40
+ # @return [Symbol, String]
41
+ attr_reader :format
42
+
43
+ ##
44
+ # Explicit gem name to use instead of inferring one from the project.
45
+ #
46
+ # @return [String, nil]
47
+ attr_reader :gem_name
48
+
49
+ ##
50
+ # Explicit Ruby module name to use in generated version files.
51
+ #
52
+ # @return [String, nil]
53
+ attr_reader :module_name
54
+
55
+ ##
56
+ # Framework preset used to apply project defaults.
57
+ #
58
+ # @return [Symbol, String, nil]
59
+ attr_reader :preset
60
+
61
+ ##
62
+ # Project root used when inferring metadata and expanding paths.
63
+ #
64
+ # @return [String, nil]
65
+ attr_reader :root
66
+
67
+ ##
68
+ # Explicit version-file path relative to the project root.
69
+ #
70
+ # @return [String, nil]
71
+ attr_reader :version_file
72
+
73
+ ##
74
+ # Code files to scan for safe version literals.
75
+ #
76
+ # @return [Rake::FileList]
77
+ attr_accessor :version_code_reference_files
78
+
79
+ ##
80
+ # Pattern used to find safe version literals in code files.
81
+ #
82
+ # @return [Regexp]
83
+ attr_reader :version_code_reference_pattern
84
+
85
+ ##
86
+ # Documentation files to scan for version references.
87
+ #
88
+ # @return [Rake::FileList]
89
+ attr_accessor :version_doc_reference_files
90
+
91
+ ##
92
+ # Version-maintenance surfaces run by the umbrella check and fix tasks.
93
+ #
94
+ # @return [Array<Symbol, String>]
95
+ attr_reader :version_checks
96
+
97
+ ##
98
+ # Version-reference comparison mode.
99
+ #
100
+ # @return [Symbol, String]
101
+ attr_accessor :version_reference_mode
102
+
103
+ ##
104
+ # Initializes configuration with Semverve's default settings.
105
+ #
106
+ # @return [Semverve::Configuration]
107
+ def initialize
108
+ @explicit_attributes = []
109
+ @bundle_lock = false
110
+ @command_runner = ->(command) { system(command) }
111
+ @format = :module
112
+ @preset = nil
113
+ @version_code_reference_files = Rake::FileList[]
114
+ self.version_code_reference_pattern = DEFAULT_VERSION_CODE_REFERENCE_PATTERN
115
+ @version_doc_reference_files = Rake::FileList["README*", "**/README*"].exclude(
116
+ ".git/**/*",
117
+ "coverage/**/*",
118
+ "tmp/**/*",
119
+ "vendor/**/*"
120
+ )
121
+ @version_reference_mode = :older
122
+ self.version_checks = VALID_VERSION_CHECKS
123
+ end
124
+
125
+ ##
126
+ # Resolves explicit configuration and inferred project metadata.
127
+ #
128
+ # @return [Semverve::ResolvedConfiguration]
129
+ def resolved
130
+ metadata = ProjectMetadata.new(self)
131
+
132
+ ResolvedConfiguration.new(
133
+ bundle_lock: bundle_lock,
134
+ command_runner: command_runner,
135
+ format: normalized_format,
136
+ gem_name: metadata.gem_name,
137
+ module_name: metadata.module_name,
138
+ root: expanded_root,
139
+ version_file: metadata.version_file,
140
+ version_checks: normalized_version_checks,
141
+ version_code_reference_files: version_code_reference_files,
142
+ version_code_reference_pattern: version_code_reference_pattern,
143
+ version_doc_reference_files: version_doc_reference_files,
144
+ version_reference_mode: normalized_version_reference_mode
145
+ )
146
+ end
147
+
148
+ ##
149
+ # Sets the version-file format.
150
+ #
151
+ # @param [Symbol, String] format
152
+ #
153
+ # @return [Symbol, String]
154
+ def format=(format)
155
+ set_explicit(:format, format)
156
+ end
157
+
158
+ ##
159
+ # Sets the gem name used for metadata checks.
160
+ #
161
+ # @param [String, nil] gem_name
162
+ #
163
+ # @return [String, nil]
164
+ def gem_name=(gem_name)
165
+ set_explicit(:gem_name, gem_name)
166
+ end
167
+
168
+ ##
169
+ # Sets the module name used for generated version files.
170
+ #
171
+ # @param [String, nil] module_name
172
+ #
173
+ # @return [String, nil]
174
+ def module_name=(module_name)
175
+ set_explicit(:module_name, module_name)
176
+ end
177
+
178
+ ##
179
+ # Sets the framework preset used for project defaults.
180
+ #
181
+ # @param [Symbol, String, nil] preset
182
+ #
183
+ # @return [Symbol, nil]
184
+ def preset=(preset)
185
+ @preset_defaults = nil
186
+ @preset = preset.nil? ? nil : Presets.fetch(preset).name
187
+ end
188
+
189
+ ##
190
+ # Sets the project root used when expanding paths.
191
+ #
192
+ # @param [String, nil] root
193
+ #
194
+ # @return [String, nil]
195
+ def root=(root)
196
+ set_explicit(:root, root)
197
+ end
198
+
199
+ ##
200
+ # Sets the version-file path relative to the project root.
201
+ #
202
+ # @param [String, nil] version_file
203
+ #
204
+ # @return [String, nil]
205
+ def version_file=(version_file)
206
+ set_explicit(:version_file, version_file)
207
+ end
208
+
209
+ ##
210
+ # Sets the pattern used to find safe version literals in code files.
211
+ #
212
+ # @param [Regexp] pattern
213
+ #
214
+ # @return [Regexp]
215
+ def version_code_reference_pattern=(pattern)
216
+ validate_version_code_reference_pattern(pattern)
217
+ @version_code_reference_pattern = pattern
218
+ end
219
+
220
+ ##
221
+ # Sets the version-maintenance surfaces run by umbrella tasks.
222
+ #
223
+ # @param [Array<Symbol, String>] checks
224
+ #
225
+ # @return [Array<Symbol>]
226
+ def version_checks=(checks)
227
+ @version_checks = normalize_version_checks(checks)
228
+ end
229
+
230
+ ##
231
+ # Absolute project root.
232
+ #
233
+ # @return [String]
234
+ def expanded_root
235
+ File.expand_path(resolved_value(:root) || Dir.pwd)
236
+ end
237
+
238
+ ##
239
+ # Configured format normalized for lookup.
240
+ #
241
+ # @return [Symbol]
242
+ def normalized_format
243
+ resolved_value(:format).to_sym
244
+ end
245
+
246
+ ##
247
+ # Resolved value for a configurable attribute before metadata inference.
248
+ #
249
+ # @param [Symbol] attribute
250
+ #
251
+ # @return [Object]
252
+ def resolved_value(attribute)
253
+ return public_send(attribute) if explicit_attribute?(attribute)
254
+ return preset_defaults[attribute] if preset_defaults.key?(attribute)
255
+
256
+ public_send(attribute)
257
+ end
258
+
259
+ ##
260
+ # Configured version checks normalized for lookup.
261
+ #
262
+ # @return [Array<Symbol>]
263
+ def normalized_version_checks
264
+ normalize_version_checks(version_checks)
265
+ end
266
+
267
+ ##
268
+ # Configured version-reference mode normalized for lookup.
269
+ #
270
+ # @return [Symbol]
271
+ def normalized_version_reference_mode
272
+ version_reference_mode.to_sym
273
+ end
274
+
275
+ ##
276
+ # Normalizes and validates umbrella version checks.
277
+ #
278
+ # @param [Array<Symbol, String>] checks
279
+ #
280
+ # @return [Array<Symbol>]
281
+ def normalize_version_checks(checks)
282
+ normalized_checks = Array(checks).map do |check|
283
+ check.respond_to?(:to_sym) ? check.to_sym : check
284
+ end
285
+ return normalized_checks if normalized_checks.all? { |check| VALID_VERSION_CHECKS.include?(check) }
286
+
287
+ invalid_checks = normalized_checks.reject { |check| VALID_VERSION_CHECKS.include?(check) }
288
+ valid_check_names = VALID_VERSION_CHECKS.map(&:inspect)
289
+ valid_checks = "#{valid_check_names[0...-1].join(", ")}, or #{valid_check_names.last}"
290
+ raise Error, "Unknown version check #{invalid_checks.map(&:inspect).join(", ")}. Use #{valid_checks}."
291
+ end
292
+
293
+ ##
294
+ # Validates the configured code reference pattern.
295
+ #
296
+ # @param [Object] pattern
297
+ #
298
+ # @return [void]
299
+ def validate_version_code_reference_pattern(pattern)
300
+ unless pattern.is_a?(Regexp)
301
+ raise Error, "version_code_reference_pattern must be a Regexp."
302
+ end
303
+
304
+ unless pattern.named_captures.key?("version")
305
+ raise Error, "version_code_reference_pattern must include a named capture called version."
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ ##
312
+ # Whether an attribute was explicitly set by the user.
313
+ #
314
+ # @param [Symbol] attribute
315
+ #
316
+ # @return [Boolean]
317
+ def explicit_attribute?(attribute)
318
+ @explicit_attributes.include?(attribute)
319
+ end
320
+
321
+ ##
322
+ # Stores an explicit user-provided setting.
323
+ #
324
+ # @param [Symbol] attribute
325
+ # @param [Object] value
326
+ #
327
+ # @return [Object]
328
+ def set_explicit(attribute, value)
329
+ @preset_defaults = nil
330
+ @explicit_attributes << attribute unless explicit_attribute?(attribute)
331
+ instance_variable_set(:"@#{attribute}", value)
332
+ end
333
+
334
+ ##
335
+ # Defaults supplied by the configured preset.
336
+ #
337
+ # @return [Hash]
338
+ def preset_defaults
339
+ @preset_defaults ||= Presets.defaults_for(preset, self)
340
+ end
341
+ end
342
+
343
+ ##
344
+ # Immutable configuration with all project defaults resolved.
345
+ class ResolvedConfiguration
346
+ ##
347
+ # Whether increments should run +bundle lock+ after writing a version.
348
+ #
349
+ # @return [Boolean]
350
+ attr_reader :bundle_lock
351
+
352
+ ##
353
+ # Callable used to run shell commands such as +bundle lock+.
354
+ #
355
+ # @return [#call]
356
+ attr_reader :command_runner
357
+
358
+ ##
359
+ # Resolved version-file format.
360
+ #
361
+ # @return [Symbol]
362
+ attr_reader :format
363
+
364
+ ##
365
+ # Resolved gem name.
366
+ #
367
+ # @return [String]
368
+ attr_reader :gem_name
369
+
370
+ ##
371
+ # Resolved Ruby module name.
372
+ #
373
+ # @return [String]
374
+ attr_reader :module_name
375
+
376
+ ##
377
+ # Absolute project root.
378
+ #
379
+ # @return [String]
380
+ attr_reader :root
381
+
382
+ ##
383
+ # Resolved version-file path relative to the project root.
384
+ #
385
+ # @return [String]
386
+ attr_reader :version_file
387
+
388
+ ##
389
+ # Resolved version-maintenance surfaces run by umbrella tasks.
390
+ #
391
+ # @return [Array<Symbol>]
392
+ attr_reader :version_checks
393
+
394
+ ##
395
+ # Resolved code files to scan for safe version literals.
396
+ #
397
+ # @return [Rake::FileList]
398
+ attr_reader :version_code_reference_files
399
+
400
+ ##
401
+ # Resolved code pattern used to scan safe version literals.
402
+ #
403
+ # @return [Regexp]
404
+ attr_reader :version_code_reference_pattern
405
+
406
+ ##
407
+ # Resolved documentation files to scan for version references.
408
+ #
409
+ # @return [Rake::FileList]
410
+ attr_reader :version_doc_reference_files
411
+
412
+ ##
413
+ # Resolved version-reference comparison mode.
414
+ #
415
+ # @return [Symbol]
416
+ attr_reader :version_reference_mode
417
+
418
+ ##
419
+ # Initializes a resolved configuration.
420
+ #
421
+ # @param [Boolean] bundle_lock
422
+ # @param [#call] command_runner
423
+ # @param [Symbol] format
424
+ # @param [String] gem_name
425
+ # @param [String] module_name
426
+ # @param [String] root
427
+ # @param [String] version_file
428
+ # @param [Array<Symbol>] version_checks
429
+ # @param [Rake::FileList] version_code_reference_files
430
+ # @param [Regexp] version_code_reference_pattern
431
+ # @param [Rake::FileList] version_doc_reference_files
432
+ # @param [Symbol] version_reference_mode
433
+ #
434
+ # @return [Semverve::ResolvedConfiguration]
435
+ def initialize(
436
+ bundle_lock:,
437
+ command_runner:,
438
+ format:,
439
+ gem_name:,
440
+ module_name:,
441
+ root:,
442
+ version_file:,
443
+ version_checks:,
444
+ version_code_reference_files:,
445
+ version_code_reference_pattern:,
446
+ version_doc_reference_files:,
447
+ version_reference_mode:
448
+ )
449
+ @bundle_lock = bundle_lock
450
+ @command_runner = command_runner
451
+ @format = format
452
+ @gem_name = gem_name
453
+ @module_name = module_name
454
+ @root = root
455
+ @version_file = version_file
456
+ @version_checks = version_checks
457
+ @version_code_reference_files = version_code_reference_files
458
+ @version_code_reference_pattern = version_code_reference_pattern
459
+ @version_doc_reference_files = version_doc_reference_files
460
+ @version_reference_mode = version_reference_mode
461
+ end
462
+
463
+ ##
464
+ # Absolute path to the resolved version file.
465
+ #
466
+ # @return [String]
467
+ def absolute_version_file
468
+ File.expand_path(version_file, root)
469
+ end
470
+ end
471
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+
5
+ require_relative "../docs_publisher"
6
+
7
+ module Semverve
8
+ class DocsPublisher
9
+ ##
10
+ # Defines Rake tasks for publishing generated documentation.
11
+ class Task
12
+ include Rake::DSL
13
+
14
+ ##
15
+ # Source project root.
16
+ #
17
+ # @return [String]
18
+ attr_accessor :root
19
+
20
+ ##
21
+ # Rake task that builds documentation before publishing.
22
+ #
23
+ # @return [String]
24
+ attr_accessor :build_task
25
+
26
+ ##
27
+ # Directory containing generated documentation.
28
+ #
29
+ # @return [String]
30
+ attr_accessor :source_dir
31
+
32
+ ##
33
+ # Documentation directory on the publishing branch.
34
+ #
35
+ # @return [String]
36
+ attr_accessor :target_dir
37
+
38
+ ##
39
+ # Branch that receives generated documentation.
40
+ #
41
+ # @return [String]
42
+ attr_accessor :branch
43
+
44
+ ##
45
+ # Remote used when pushing the publishing branch.
46
+ #
47
+ # @return [String]
48
+ attr_accessor :remote
49
+
50
+ ##
51
+ # Commit message for generated documentation updates.
52
+ #
53
+ # @return [String]
54
+ attr_accessor :commit_message
55
+
56
+ ##
57
+ # Optional path for the temporary worktree.
58
+ #
59
+ # @return [String, nil]
60
+ attr_accessor :worktree_path
61
+
62
+ ##
63
+ # Whether dirty source working trees are allowed.
64
+ #
65
+ # @return [Boolean]
66
+ attr_accessor :allow_dirty
67
+
68
+ ##
69
+ # Whether the publishing branch should be pushed.
70
+ #
71
+ # @return [Boolean]
72
+ attr_accessor :push
73
+
74
+ ##
75
+ # Output stream for status messages.
76
+ #
77
+ # @return [#puts]
78
+ attr_accessor :output
79
+
80
+ ##
81
+ # Namespace for the generated tasks.
82
+ #
83
+ # @return [Symbol, String]
84
+ attr_accessor :task_namespace
85
+
86
+ ##
87
+ # Initializes and defines documentation publishing tasks.
88
+ #
89
+ # @yieldparam [Semverve::DocsPublisher::Task] task
90
+ #
91
+ # @return [Semverve::DocsPublisher::Task]
92
+ def initialize
93
+ @root = Dir.pwd
94
+ @build_task = "rerdoc"
95
+ @source_dir = "docs"
96
+ @target_dir = "docs"
97
+ @branch = "gh-pages"
98
+ @remote = "origin"
99
+ @commit_message = "Update generated documentation"
100
+ @worktree_path = nil
101
+ @allow_dirty = false
102
+ @push = true
103
+ @output = $stdout
104
+ @task_namespace = :docs
105
+
106
+ yield self if block_given?
107
+
108
+ define
109
+ end
110
+
111
+ ##
112
+ # Defines the documentation publishing tasks.
113
+ #
114
+ # @return [void]
115
+ def define
116
+ namespace namespace_name do
117
+ desc "Publish generated documentation to #{branch}"
118
+ task :publish do
119
+ publish(dry_run: false)
120
+ end
121
+
122
+ namespace :publish do
123
+ desc "Show whether generated documentation would change #{branch}"
124
+ task :dry_run do
125
+ publish(dry_run: true)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ ##
134
+ # Normalized namespace name.
135
+ #
136
+ # @return [Symbol]
137
+ def namespace_name
138
+ task_namespace.to_sym
139
+ end
140
+
141
+ ##
142
+ # Builds and publishes documentation.
143
+ #
144
+ # @param [Boolean] dry_run
145
+ #
146
+ # @return [void]
147
+ def publish(dry_run:)
148
+ Rake::Task[build_task].invoke
149
+
150
+ DocsPublisher.new do |publisher|
151
+ publisher.root = root
152
+ publisher.source_dir = source_dir
153
+ publisher.target_dir = target_dir
154
+ publisher.branch = branch
155
+ publisher.remote = remote
156
+ publisher.commit_message = commit_message
157
+ publisher.worktree_path = worktree_path
158
+ publisher.allow_dirty = allow_dirty
159
+ publisher.push = push
160
+ publisher.dry_run = dry_run
161
+ publisher.output = output
162
+ end.publish
163
+ end
164
+ end
165
+ end
166
+ end