kettle-dev 1.0.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.
data/REEK ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
@@ -0,0 +1 @@
1
+ b4c6725b40f3e0906cd314309dfa6a9f4a8fda0394dacd99f16aa32376275ab9
@@ -0,0 +1 @@
1
+ 1273b5c26da368293af8c2d0b87efabf5f8af66e89fbeea1a20e26c960dedb130eb1388c151cba85682110cf4fea577680c3a1e7171606b0f913dce7750e5f45
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "net/http"
5
+ require "json"
6
+ require "uri"
7
+
8
+ module Kettle
9
+ module Dev
10
+ # CI-related helper functions used by Rake tasks and release tooling.
11
+ #
12
+ # This module only exposes module-functions (no instance state) and is
13
+ # intentionally small so it can be required by both Rake tasks and the
14
+ # kettle-release executable.
15
+ module CIHelpers
16
+ module_function
17
+
18
+ # Determine the project root directory.
19
+ #
20
+ # Prefers the directory Rake was invoked from (Rake.application.original_dir)
21
+ # so that tasks shipped with this gem operate relative to the host project.
22
+ # Falls back to the current working directory when Rake context is absent.
23
+ #
24
+ # @return [String] absolute path to the project root
25
+ def project_root
26
+ # Too difficult to test every possible branch here, so ignoring
27
+ # :nocov:
28
+ dir = if defined?(Rake) && Rake&.application&.respond_to?(:original_dir)
29
+ Rake.application.original_dir
30
+ end
31
+ # :nocov:
32
+ dir || Dir.pwd
33
+ end
34
+
35
+ # Parse the GitHub owner/repo from the configured origin remote.
36
+ #
37
+ # Supports SSH (git@github.com:owner/repo(.git)) and HTTPS
38
+ # (https://github.com/owner/repo(.git)) forms.
39
+ #
40
+ # @return [Array(String, String), nil] [owner, repo] or nil when unavailable
41
+ def repo_info
42
+ out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
43
+ return unless status.success?
44
+ url = out.strip
45
+ if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
46
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
47
+ elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
48
+ [Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
49
+ end
50
+ end
51
+
52
+ # Current git branch name, or nil when not in a repository.
53
+ # @return [String, nil]
54
+ def current_branch
55
+ out, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
56
+ status.success? ? out.strip : nil
57
+ end
58
+
59
+ # List workflow YAML basenames under .github/workflows at the given root.
60
+ #
61
+ # Excludes maintenance workflows defined by {#exclusions}.
62
+ #
63
+ # @param root [String] project root (defaults to {#project_root})
64
+ # @return [Array<String>] sorted list of basenames (e.g., "ci.yml")
65
+ def workflows_list(root = project_root)
66
+ workflows_dir = File.join(root, ".github", "workflows")
67
+ files = if Dir.exist?(workflows_dir)
68
+ Dir[File.join(workflows_dir, "*.yml")] + Dir[File.join(workflows_dir, "*.yaml")]
69
+ else
70
+ []
71
+ end
72
+ basenames = files.map { |p| File.basename(p) }
73
+ basenames = basenames.uniq - exclusions
74
+ basenames.sort
75
+ end
76
+
77
+ # List of workflow files to exclude from interactive menus and checks.
78
+ #
79
+ # For reference...
80
+ #
81
+ # A list of all worlflows,
82
+ # with each marked relative to if they exist in this repo,
83
+ # or at the top of the README marked.
84
+ #
85
+ # - ancient (+)
86
+ # - auto-assign.yml (-)
87
+ # - codeql-analysis.yml (-)
88
+ # - coverage.yml (+)
89
+ # - current.yml (+)
90
+ # - danger.yml (x)
91
+ # - dependency-review.yml (-)
92
+ # - discord-notifier.yml (-)
93
+ # - heads.yml (+)
94
+ # - jruby.yml (+)
95
+ # - legacy.yml (+)
96
+ # - locked_deps.yml (+)
97
+ # - opencollective.yml (-)
98
+ # - style.yml (+)
99
+ # - supported.yml (+)
100
+ # - truffle.yml (+)
101
+ # - unlocked_deps.yml (+)
102
+ # - unsupported.yml (+)
103
+ #
104
+ # All those marked as (-) or (x) are excluded from interactive menus and checks.
105
+ # The (x) exist because they may be common in other repos.
106
+ #
107
+ # @return [Array<String>]
108
+ def exclusions
109
+ %w[
110
+ auto-assign.yml
111
+ codeql-analysis.yml
112
+ danger.yml
113
+ dependency-review.yml
114
+ discord-notifier.yml
115
+ opencollective.yml
116
+ ]
117
+ end
118
+
119
+ # Fetch latest workflow run info for a given workflow and branch via GitHub API.
120
+ #
121
+ # @param owner [String]
122
+ # @param repo [String]
123
+ # @param workflow_file [String] the workflow basename (e.g., "ci.yml")
124
+ # @param branch [String, nil] branch to query; defaults to {#current_branch}
125
+ # @param token [String, nil] OAuth token for higher rate limits; defaults to {#default_token}
126
+ # @return [Hash{String=>String,Integer}, nil] minimal run info or nil on error/none
127
+ def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token)
128
+ return unless owner && repo
129
+ b = branch || current_branch
130
+ return unless b
131
+ uri = URI("https://api.github.com/repos/#{owner}/#{repo}/actions/workflows/#{workflow_file}/runs?branch=#{URI.encode_www_form_component(b)}&per_page=1")
132
+ req = Net::HTTP::Get.new(uri)
133
+ req["User-Agent"] = "kettle-dev/ci-helpers"
134
+ req["Authorization"] = "token #{token}" if token && !token.empty?
135
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
136
+ return unless res.is_a?(Net::HTTPSuccess)
137
+ data = JSON.parse(res.body)
138
+ run = data["workflow_runs"]&.first
139
+ return unless run
140
+ {
141
+ "status" => run["status"],
142
+ "conclusion" => run["conclusion"],
143
+ "html_url" => run["html_url"],
144
+ "id" => run["id"],
145
+ }
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ # Whether a run has completed successfully.
151
+ # @param run [Hash, nil]
152
+ # @return [Boolean]
153
+ def success?(run)
154
+ run && run["status"] == "completed" && run["conclusion"] == "success"
155
+ end
156
+
157
+ # Whether a run has completed with a non-success conclusion.
158
+ # @param run [Hash, nil]
159
+ # @return [Boolean]
160
+ def failed?(run)
161
+ run && run["status"] == "completed" && run["conclusion"] && run["conclusion"] != "success"
162
+ end
163
+
164
+ # Default GitHub token sourced from environment.
165
+ # @return [String, nil]
166
+ def default_token
167
+ ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,8 @@
1
+ # Load from a "rakelib" directory is automatic!
2
+ # Adding a custom directory of tasks as a "rakelib" directory makes them available.
3
+ #
4
+ # This file is loaded by Kettle::Dev.install_tasks to register the
5
+ # gem-provided Rake tasks with the host application's Rake context.
6
+ abs_path = File.expand_path(__dir__)
7
+ rakelib = "#{abs_path}/rakelib"
8
+ Rake.add_rakelib(rakelib)
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ require "find"
5
+ require_relative "ci_helpers"
6
+
7
+ module Kettle
8
+ module Dev
9
+ # Helpers shared by kettle:dev Rake tasks for templating and file ops.
10
+ module TemplateHelpers
11
+ # Track results of templating actions across a single process run.
12
+ # Keys: absolute destination paths (String)
13
+ # Values: Hash with keys: :action (Symbol, one of :create, :replace, :skip, :dir_create, :dir_replace), :timestamp (Time)
14
+ @@template_results = {}
15
+
16
+ module_function
17
+
18
+ # Root of the host project where Rake was invoked
19
+ # @return [String]
20
+ def project_root
21
+ CIHelpers.project_root
22
+ end
23
+
24
+ # Root of this gem's checkout (repository root when working from source)
25
+ # Calculated relative to lib/kettle/dev/
26
+ # @return [String]
27
+ def gem_checkout_root
28
+ File.expand_path("../../..", __dir__)
29
+ end
30
+
31
+ # Simple yes/no prompt.
32
+ # @param prompt [String]
33
+ # @param default [Boolean]
34
+ # @return [Boolean]
35
+ def ask(prompt, default)
36
+ # Force mode: any prompt resolves to Yes when ENV["force"] is set truthy
37
+ if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
38
+ puts "#{prompt} #{default ? "[Y/n]" : "[y/N]"}: Y (forced)"
39
+ return true
40
+ end
41
+ print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
42
+ ans = $stdin.gets&.strip
43
+ ans = "" if ans.nil?
44
+ if default
45
+ ans.empty? || ans =~ /\Ay(es)?\z/i
46
+ else
47
+ ans =~ /\Ay(es)?\z/i
48
+ end
49
+ end
50
+
51
+ # Write file content creating directories as needed
52
+ # @param dest_path [String]
53
+ # @param content [String]
54
+ # @return [void]
55
+ def write_file(dest_path, content)
56
+ FileUtils.mkdir_p(File.dirname(dest_path))
57
+ File.open(dest_path, "w") { |f| f.write(content) }
58
+ end
59
+
60
+ # Prefer an .example variant for a given source path when present
61
+ # For a given intended source path (e.g., "/src/Rakefile"), this will return
62
+ # "/src/Rakefile.example" if it exists, otherwise returns the original path.
63
+ # If the given path already ends with .example, it is returned as-is.
64
+ # @param src_path [String]
65
+ # @return [String]
66
+ def prefer_example(src_path)
67
+ return src_path if src_path.end_with?(".example")
68
+ example = src_path + ".example"
69
+ File.exist?(example) ? example : src_path
70
+ end
71
+
72
+ # Record a template action for a destination path
73
+ # @param dest_path [String]
74
+ # @param action [Symbol] one of :create, :replace, :skip, :dir_create, :dir_replace
75
+ # @return [void]
76
+ def record_template_result(dest_path, action)
77
+ abs = File.expand_path(dest_path.to_s)
78
+ if action == :skip && @@template_results.key?(abs)
79
+ # Preserve the last meaningful action; do not downgrade to :skip
80
+ return
81
+ end
82
+ @@template_results[abs] = {action: action, timestamp: Time.now}
83
+ end
84
+
85
+ # Access all template results (read-only clone)
86
+ # @return [Hash]
87
+ def template_results
88
+ @@template_results.clone
89
+ end
90
+
91
+ # Returns true if the given path was created or replaced by the template task in this run
92
+ # @param dest_path [String]
93
+ # @return [Boolean]
94
+ def modified_by_template?(dest_path)
95
+ rec = @@template_results[File.expand_path(dest_path.to_s)]
96
+ return false unless rec
97
+ [:create, :replace, :dir_create, :dir_replace].include?(rec[:action])
98
+ end
99
+
100
+ # Ensure git working tree is clean before making changes in a task.
101
+ # If not a git repo, this is a no-op.
102
+ # @param root [String] project root to run git commands in
103
+ # @param task_label [String] name of the rake task for user-facing messages (e.g., "kettle:dev:install")
104
+ # @return [void]
105
+ def ensure_clean_git!(root:, task_label:)
106
+ inside_repo = begin
107
+ system("git", "-C", root.to_s, "rev-parse", "--is-inside-work-tree", out: File::NULL, err: File::NULL)
108
+ rescue StandardError
109
+ false
110
+ end
111
+ return unless inside_repo
112
+
113
+ status_output = begin
114
+ IO.popen(["git", "-C", root.to_s, "status", "--porcelain"], &:read).to_s
115
+ rescue StandardError
116
+ ""
117
+ end
118
+ return if status_output.strip.empty?
119
+
120
+ puts "ERROR: Your git working tree has uncommitted changes."
121
+ puts "#{task_label} may modify files (e.g., .github/, .gitignore, *.gemspec)."
122
+ puts "Please commit or stash your changes, then re-run: rake #{task_label}"
123
+ preview = status_output.lines.take(10).map(&:rstrip)
124
+ unless preview.empty?
125
+ puts "Detected changes:"
126
+ preview.each { |l| puts " #{l}" }
127
+ puts "(showing up to first 10 lines)"
128
+ end
129
+ raise Kettle::Dev::Error, "Aborting: git working tree is not clean."
130
+ end
131
+
132
+ # Copy a single file with interactive prompts for create/replace.
133
+ # Yields content for transformation when block given.
134
+ # @return [void]
135
+ def copy_file_with_prompt(src_path, dest_path, allow_create: true, allow_replace: true)
136
+ return unless File.exist?(src_path)
137
+ dest_exists = File.exist?(dest_path)
138
+ action = nil
139
+ if dest_exists
140
+ if allow_replace
141
+ action = ask("Replace #{dest_path}?", true) ? :replace : :skip
142
+ else
143
+ puts "Skipping #{dest_path} (replace not allowed)."
144
+ action = :skip
145
+ end
146
+ elsif allow_create
147
+ action = ask("Create #{dest_path}?", true) ? :create : :skip
148
+ else
149
+ puts "Skipping #{dest_path} (create not allowed)."
150
+ action = :skip
151
+ end
152
+ if action == :skip
153
+ record_template_result(dest_path, :skip)
154
+ return
155
+ end
156
+
157
+ content = File.read(src_path)
158
+ content = yield(content) if block_given?
159
+ write_file(dest_path, content)
160
+ record_template_result(dest_path, dest_exists ? :replace : :create)
161
+ puts "Wrote #{dest_path}"
162
+ end
163
+
164
+ # Copy a directory tree, prompting before creating or overwriting.
165
+ # @return [void]
166
+ def copy_dir_with_prompt(src_dir, dest_dir)
167
+ return unless Dir.exist?(src_dir)
168
+ dest_exists = Dir.exist?(dest_dir)
169
+ if dest_exists
170
+ if ask("Replace directory #{dest_dir} (will overwrite files)?", true)
171
+ Find.find(src_dir) do |path|
172
+ rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
173
+ next if rel.empty?
174
+ target = File.join(dest_dir, rel)
175
+ if File.directory?(path)
176
+ FileUtils.mkdir_p(target)
177
+ else
178
+ FileUtils.mkdir_p(File.dirname(target))
179
+ FileUtils.cp(path, target)
180
+ end
181
+ end
182
+ puts "Updated #{dest_dir}"
183
+ record_template_result(dest_dir, :dir_replace)
184
+ else
185
+ puts "Skipped #{dest_dir}"
186
+ record_template_result(dest_dir, :skip)
187
+ end
188
+ elsif ask("Create directory #{dest_dir}?", true)
189
+ FileUtils.mkdir_p(dest_dir)
190
+ Find.find(src_dir) do |path|
191
+ rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
192
+ next if rel.empty?
193
+ target = File.join(dest_dir, rel)
194
+ if File.directory?(path)
195
+ FileUtils.mkdir_p(target)
196
+ else
197
+ FileUtils.mkdir_p(File.dirname(target))
198
+ FileUtils.cp(path, target)
199
+ end
200
+ end
201
+ puts "Created #{dest_dir}"
202
+ record_template_result(dest_dir, :dir_create)
203
+ end
204
+ end
205
+
206
+ # Apply common token replacements used when templating text files
207
+ # @param content [String]
208
+ # @param gh_org [String, nil]
209
+ # @param gem_name [String]
210
+ # @param namespace [String]
211
+ # @param namespace_shield [String]
212
+ # @param gem_shield [String]
213
+ # @return [String]
214
+ def apply_common_replacements(content, gh_org:, gem_name:, namespace:, namespace_shield:, gem_shield:)
215
+ c = content.dup
216
+ c = c.gsub("kettle-rb", gh_org.to_s) if gh_org && !gh_org.empty?
217
+ if gem_name && !gem_name.empty?
218
+ # Replace occurrences of the template gem name in text, including inside
219
+ # markdown reference labels like [🖼️kettle-dev] and identifiers like kettle-dev-i
220
+ c = c.gsub("kettle-dev", gem_name)
221
+ c = c.gsub(/\bKettle::Dev\b/u, namespace) unless namespace.empty?
222
+ c = c.gsub("Kettle%3A%3ADev", namespace_shield) unless namespace_shield.empty?
223
+ c = c.gsub("kettle--dev", gem_shield)
224
+ end
225
+ c
226
+ end
227
+
228
+ # Parse gemspec metadata and derive useful strings
229
+ # @param root [String] project root
230
+ # @return [Hash]
231
+ def gemspec_metadata(root = project_root)
232
+ gemspecs = Dir.glob(File.join(root, "*.gemspec"))
233
+ gemspec_path = gemspecs.first
234
+ gemspec_text = (gemspec_path && File.file?(gemspec_path)) ? File.read(gemspec_path) : ""
235
+ gem_name = (gemspec_text[/\bspec\.name\s*=\s*["']([^"']+)["']/, 1] || "").strip
236
+ min_ruby = (
237
+ gemspec_text[/\bspec\.minimum_ruby_version\s*=\s*["'](?:>=\s*)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
238
+ gemspec_text[/\bspec\.required_ruby_version\s*=\s*["']>=\s*([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
239
+ gemspec_text[/\brequired_ruby_version\s*[:=]\s*["'](?:>=\s*)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
240
+ ""
241
+ ).strip
242
+ homepage_line = gemspec_text.lines.find { |l| l =~ /\bspec\.homepage\s*=\s*/ }
243
+ homepage_val = homepage_line ? homepage_line.split("=", 2).last.to_s.strip : ""
244
+ if (homepage_val.start_with?("\"") && homepage_val.end_with?("\"")) || (homepage_val.start_with?("'") && homepage_val.end_with?("'"))
245
+ homepage_val = begin
246
+ homepage_val[1..-2]
247
+ rescue
248
+ homepage_val
249
+ end
250
+ end
251
+ gh_match = homepage_val&.match(%r{github\.com/([^/]+)/([^/]+)}i)
252
+ gh_org = gh_match && gh_match[1]
253
+ gh_repo = gh_match && gh_match[2]&.sub(/\.git\z/, "")
254
+ if gh_org.nil?
255
+ begin
256
+ origin_url = IO.popen(["git", "-C", root.to_s, "remote", "get-url", "origin"], &:read).to_s.strip
257
+ if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
258
+ gh_org = m[1]
259
+ gh_repo = m[2]&.sub(/\.git\z/, "")
260
+ end
261
+ rescue StandardError
262
+ # ignore
263
+ end
264
+ end
265
+
266
+ camel = lambda do |s|
267
+ s.split(/[_-]/).map { |p| p.gsub(/\b([a-z])/) { Regexp.last_match(1).upcase } }.join
268
+ end
269
+ namespace = gem_name.to_s.split("-").map { |seg| camel.call(seg) }.join("::")
270
+ namespace_shield = namespace.gsub("::", "%3A%3A")
271
+ entrypoint_require = gem_name.to_s.tr("-", "/")
272
+ gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")
273
+
274
+ {
275
+ gemspec_path: gemspec_path,
276
+ gem_name: gem_name,
277
+ min_ruby: min_ruby,
278
+ homepage: homepage_val,
279
+ gh_org: gh_org,
280
+ gh_repo: gh_repo,
281
+ namespace: namespace,
282
+ namespace_shield: namespace_shield,
283
+ entrypoint_require: entrypoint_require,
284
+ gem_shield: gem_shield,
285
+ }
286
+ end
287
+ end
288
+ end
289
+ end
290
+ # :nocov:
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Dev
5
+ # Version namespace for kettle-dev.
6
+ module Version
7
+ # The gem version.
8
+ # @return [String]
9
+ VERSION = "1.0.0"
10
+ end
11
+ end
12
+ end