kettle-dev 1.0.11 → 1.0.12
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.junie/guidelines.md +1 -0
- data/Appraisals +1 -0
- data/Appraisals.example +2 -0
- data/CHANGELOG.md +27 -2
- data/Gemfile.example +35 -0
- data/README.md +1 -1
- data/exe/kettle-changelog +3 -0
- data/exe/kettle-release +8 -2
- data/gemfiles/modular/optional.gemfile +1 -5
- data/lib/kettle/dev/ci_helpers.rb +19 -0
- data/lib/kettle/dev/ci_monitor.rb +192 -0
- data/lib/kettle/dev/input_adapter.rb +4 -0
- data/lib/kettle/dev/release_cli.rb +146 -171
- data/lib/kettle/dev/tasks/ci_task.rb +18 -0
- data/lib/kettle/dev/tasks/template_task.rb +1 -1
- data/lib/kettle/dev/version.rb +1 -1
- data/sig/kettle/dev/ci_helpers.rbs +1 -1
- data/sig/kettle/dev/ci_monitor.rbs +8 -0
- data/sig/kettle/dev/release_cli.rbs +1 -1
- data.tar.gz.sig +0 -0
- metadata +7 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18dc37569a34008ec953339ce7fd69e285bf007f2f1dd035de13856fb2d621ec
|
4
|
+
data.tar.gz: b3afeca255b2fb10fd86c4e48a47679b8652ff7ab9f75c4b8637c7738f83e28b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d12f7394b9aba54d138cbddc74598e03908280f6e4102c0b98a9d326e00909cc5d1cac16f1a5bced75ee694c2606e026bc772088b005abf1a69d9a1d23bf0c93
|
7
|
+
data.tar.gz: 5fc72941ddcc1c21e9e56fd0fffd2eb0b603eff7d1fe60d6cdf55108b8e4c314320be1ed243e5064d89cb604e74e42bdc6756764c42dcf920bc204514fbe539b
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.junie/guidelines.md
CHANGED
@@ -128,6 +128,7 @@ Notes
|
|
128
128
|
- NEVER run vanilla rubocop, as it won't handle the linting config properly. Always run rubocop_gradual:autocorrect or rubocop_gradual.
|
129
129
|
- Running only a subset of specs is supported but in order to bypass the hard failure due to coverage thresholds, you need to run with K_SOUP_COV_MIN_HARD=false.
|
130
130
|
- When adding code that writes to STDOUT, remember most specs silence output unless tagged with :check_output or DEBUG=true.
|
131
|
+
- Completion criteria after changes: Only consider your change “done” when the relevant examples pass, as verified by .rspec_status. Do not rely on STDOUT impressions; consult .rspec_status (and example IDs) to confirm green results for the affected files/examples. If you ran a subset, re-run the full suite before finalizing to restore coverage thresholds.
|
131
132
|
- Coverage reports: NEVER review the HTML report. Use JSON (preferred), XML, LCOV, or RCOV. For this project, always run tests with K_SOUP_COV_FORMATTERS set to "json".
|
132
133
|
- Do NOT modify .envrc in tasks; when running tests locally or in scripts, manually prefix each run, e.g.: K_SOUP_COV_FORMATTERS="json" bin/rspec
|
133
134
|
- For all the kettle-soup-cover options, see .envrc and find the K_SOUP_COV_* env vars.
|
data/Appraisals
CHANGED
@@ -26,6 +26,7 @@ appraise "unlocked_deps" do
|
|
26
26
|
eval_gemfile "modular/coverage.gemfile"
|
27
27
|
eval_gemfile "modular/documentation.gemfile"
|
28
28
|
eval_gemfile "modular/style.gemfile"
|
29
|
+
eval_gemfile "modular/optional.gemfile"
|
29
30
|
end
|
30
31
|
|
31
32
|
# Used for head (nightly) releases of ruby, truffleruby, and jruby.
|
data/Appraisals.example
CHANGED
@@ -25,6 +25,7 @@ appraise "unlocked_deps" do
|
|
25
25
|
eval_gemfile "modular/coverage.gemfile"
|
26
26
|
eval_gemfile "modular/documentation.gemfile"
|
27
27
|
eval_gemfile "modular/style.gemfile"
|
28
|
+
eval_gemfile "modular/optional.gemfile"
|
28
29
|
end
|
29
30
|
|
30
31
|
# Used for head (nightly) releases of ruby, truffleruby, and jruby.
|
@@ -92,6 +93,7 @@ appraise "coverage" do
|
|
92
93
|
gem "mutex_m", "~> 0.2"
|
93
94
|
gem "stringio", "~> 3.0"
|
94
95
|
eval_gemfile "modular/coverage.gemfile"
|
96
|
+
eval_gemfile "modular/optional.gemfile"
|
95
97
|
end
|
96
98
|
|
97
99
|
# Only run linter on latest Ruby version (but, in support of oldest supported Ruby version)
|
data/CHANGELOG.md
CHANGED
@@ -24,6 +24,30 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
24
24
|
### Fixed
|
25
25
|
### Security
|
26
26
|
|
27
|
+
## [1.0.12] - 2025-08-28
|
28
|
+
- TAG: [v1.0.12][1.0.12t]
|
29
|
+
- COVERAGE: 97.80% -- 1957/2001 lines in 19 files
|
30
|
+
- BRANCH COVERAGE: 79.98% -- 763/954 branches in 19 files
|
31
|
+
- 78.70% documented
|
32
|
+
### Added
|
33
|
+
- CIMonitor to consolidate workflow / pipeline monitoring logic for GH/GL across kettle-release and rake tasks, with handling for:
|
34
|
+
- minutes exhausted
|
35
|
+
- blocked
|
36
|
+
- not configured
|
37
|
+
- normal failures
|
38
|
+
- pending
|
39
|
+
- queued
|
40
|
+
- running
|
41
|
+
- success
|
42
|
+
- Ability to restart kettle-release from any failed step, so manual fixed can be applied.
|
43
|
+
- Example (after intermittent failure of CI): `bundle exec kettle-release start_step=10`
|
44
|
+
### Fixed
|
45
|
+
- added optional.gemfile.example, and handling for it in templating
|
46
|
+
- kettle-changelog: ensure a blank line at end of file
|
47
|
+
- add sleep(0.2) to ci:act to prevent race condition with stdout flushing
|
48
|
+
- kettle-release: ensure SKIP_GEM_SIGNING works as expected with values of "true" or "false"
|
49
|
+
- ensure it doesn't abort the process in CI
|
50
|
+
|
27
51
|
## [1.0.11] - 2025-08-28
|
28
52
|
- TAG: [v1.0.11][1.0.11t]
|
29
53
|
- COVERAGE: 97.90% -- 1959/2001 lines in 19 files
|
@@ -190,7 +214,7 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
190
214
|
- Selecting will run the selected workflow via `act`
|
191
215
|
- This may move to its own gem in the future.
|
192
216
|
|
193
|
-
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.
|
217
|
+
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.12...HEAD
|
194
218
|
[1.0.0]: https://github.com/kettle-rb/kettle-dev/compare/a427c302df09cfe4253a7c8d400333f9a4c1a208...v1.0.0
|
195
219
|
[1.0.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.0
|
196
220
|
[1.0.1]: https://gitlab.com/kettle-rb/kettle-dev/-/compare/v1.0.0...v1.0.1
|
@@ -215,4 +239,5 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
215
239
|
[1.0.10t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.10
|
216
240
|
[1.0.11]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.10...v1.0.11
|
217
241
|
[1.0.11t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.11
|
218
|
-
|
242
|
+
[1.0.12]: https://github.com/kettle-rb/kettle-dev/compare/v1.0.11...v1.0.12
|
243
|
+
[1.0.12t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.0.12
|
data/Gemfile.example
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" }
|
7
|
+
|
8
|
+
#### IMPORTANT #######################################################
|
9
|
+
# Gemfile is for local development ONLY; Gemfile is NOT loaded in CI #
|
10
|
+
####################################################### IMPORTANT ####
|
11
|
+
|
12
|
+
# Include dependencies from <gem name>.gemspec
|
13
|
+
gemspec
|
14
|
+
|
15
|
+
platform :mri do
|
16
|
+
# Debugging - Ensure ENV["DEBUG"] == "true" to use debuggers within spec suite
|
17
|
+
# Use binding.break, binding.b, or debugger in code
|
18
|
+
gem "debug", ">= 1.0.0" # ruby >= 2.7
|
19
|
+
gem "gem_bench", "~> 2.0", ">= 2.0.5"
|
20
|
+
|
21
|
+
# Dev Console - Binding.pry - Irb replacement
|
22
|
+
gem "pry", "~> 0.14" # ruby >= 2.0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Code Coverage
|
26
|
+
eval_gemfile "gemfiles/modular/coverage.gemfile"
|
27
|
+
|
28
|
+
# Linting
|
29
|
+
eval_gemfile "gemfiles/modular/style.gemfile"
|
30
|
+
|
31
|
+
# Documentation
|
32
|
+
eval_gemfile "gemfiles/modular/documentation.gemfile"
|
33
|
+
|
34
|
+
# Optional
|
35
|
+
eval_gemfile "gemfiles/modular/optional.gemfile"
|
data/README.md
CHANGED
@@ -697,7 +697,7 @@ Thanks for RTFM. ☺️
|
|
697
697
|
[📌gitmoji]:https://gitmoji.dev
|
698
698
|
[📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20😜%20😍-34495e.svg?style=flat-square
|
699
699
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
700
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
700
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.001-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
701
701
|
[🔐security]: SECURITY.md
|
702
702
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
703
703
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/exe/kettle-changelog
CHANGED
@@ -106,6 +106,9 @@ module Kettle
|
|
106
106
|
|
107
107
|
updated = update_link_refs(updated, owner, repo, prev_version, version)
|
108
108
|
|
109
|
+
# Ensure exactly one trailing newline at EOF
|
110
|
+
updated = updated.rstrip + "\n"
|
111
|
+
|
109
112
|
File.write(@changelog_path, updated)
|
110
113
|
puts "CHANGELOG.md updated with v#{version} section."
|
111
114
|
end
|
data/exe/kettle-release
CHANGED
@@ -25,9 +25,12 @@ require "kettle/dev"
|
|
25
25
|
# Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
|
26
26
|
if ARGV.include?("-h") || ARGV.include?("--help")
|
27
27
|
puts <<~USAGE
|
28
|
-
Usage: kettle-release
|
28
|
+
Usage: kettle-release [start_step=<number>]
|
29
29
|
|
30
30
|
Automates the release flow for a Ruby gem in the host project:
|
31
|
+
|
32
|
+
Options:
|
33
|
+
start_step=<number> # skip directly to the numbered step (e.g., 10 for CI validation)
|
31
34
|
- Runs bin/setup and bin/rake sanity checks
|
32
35
|
- Prompts to confirm version and changelog updates
|
33
36
|
- Commits a release prep change
|
@@ -46,8 +49,11 @@ if ARGV.include?("-h") || ARGV.include?("--help")
|
|
46
49
|
end
|
47
50
|
|
48
51
|
puts "== kettle-release v#{Kettle::Dev::Version::VERSION} =="
|
52
|
+
# Parse start_step=<n> from ARGV
|
53
|
+
start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
|
54
|
+
start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 1
|
49
55
|
begin
|
50
|
-
Kettle::Dev::ReleaseCLI.new.run
|
56
|
+
Kettle::Dev::ReleaseCLI.new(start_step: start_step).run
|
51
57
|
rescue LoadError => e
|
52
58
|
warn("kettle-release: could not load dependency: #{e.message}")
|
53
59
|
warn(e.backtrace.join("\n")) if ENV["DEBUG"]
|
@@ -1,5 +1 @@
|
|
1
|
-
# Optional dependencies are not dependended on directly, but
|
2
|
-
# git gem is not a direct dependency for two reasons:
|
3
|
-
# 1. it is incompatible with Truffleruby v23
|
4
|
-
# 2. it depends on activesupport, which is too heavy
|
5
|
-
gem "git", ">= 1.19.1" # ruby >= 2.3
|
1
|
+
# Optional dependencies are not dependended on directly, but may be used if present.
|
@@ -177,10 +177,29 @@ module Kettle
|
|
177
177
|
data = JSON.parse(res.body)
|
178
178
|
pipe = data&.first
|
179
179
|
return unless pipe
|
180
|
+
# Attempt to enrich with failure_reason by querying the single pipeline endpoint
|
181
|
+
begin
|
182
|
+
if pipe["id"]
|
183
|
+
detail_uri = URI("https://#{host}/api/v4/projects/#{project}/pipelines/#{pipe["id"]}")
|
184
|
+
dreq = Net::HTTP::Get.new(detail_uri)
|
185
|
+
dreq["User-Agent"] = "kettle-dev/ci-helpers"
|
186
|
+
dreq["PRIVATE-TOKEN"] = token if token && !token.empty?
|
187
|
+
dres = Net::HTTP.start(detail_uri.hostname, detail_uri.port, use_ssl: true) { |http| http.request(dreq) }
|
188
|
+
if dres.is_a?(Net::HTTPSuccess)
|
189
|
+
det = JSON.parse(dres.body)
|
190
|
+
pipe["failure_reason"] = det["failure_reason"] if det.is_a?(Hash)
|
191
|
+
pipe["status"] = det["status"] if det["status"]
|
192
|
+
pipe["web_url"] = det["web_url"] if det["web_url"]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
rescue StandardError
|
196
|
+
# ignore enrichment errors; fall back to basic fields
|
197
|
+
end
|
180
198
|
{
|
181
199
|
"status" => pipe["status"],
|
182
200
|
"web_url" => pipe["web_url"],
|
183
201
|
"id" => pipe["id"],
|
202
|
+
"failure_reason" => pipe["failure_reason"],
|
184
203
|
}
|
185
204
|
rescue StandardError
|
186
205
|
nil
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# External stdlib
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
require "net/http"
|
7
|
+
|
8
|
+
# Internal
|
9
|
+
require "kettle/dev/ci_helpers"
|
10
|
+
require "kettle/dev/exit_adapter"
|
11
|
+
require "kettle/dev/git_adapter"
|
12
|
+
|
13
|
+
module Kettle
|
14
|
+
module Dev
|
15
|
+
# CIMonitor centralizes CI monitoring logic (GitHub Actions and GitLab pipelines)
|
16
|
+
# so it can be reused by both kettle-release and Rake tasks (e.g., ci:act).
|
17
|
+
#
|
18
|
+
# Public API is intentionally small and based on environment/project introspection
|
19
|
+
# via CIHelpers, matching the behavior historically implemented in ReleaseCLI.
|
20
|
+
module CIMonitor
|
21
|
+
module_function
|
22
|
+
|
23
|
+
# Abort helper (delegates through ExitAdapter so specs can trap exits)
|
24
|
+
def abort(msg)
|
25
|
+
Kettle::Dev::ExitAdapter.abort(msg)
|
26
|
+
end
|
27
|
+
module_function :abort
|
28
|
+
|
29
|
+
# Monitor both GitHub and GitLab CI for the current project/branch.
|
30
|
+
# This mirrors ReleaseCLI behavior.
|
31
|
+
#
|
32
|
+
# @param restart_hint [String] guidance command shown on failure
|
33
|
+
# @return [void]
|
34
|
+
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
|
35
|
+
checks_any = false
|
36
|
+
checks_any |= monitor_github_internal!(restart_hint: restart_hint)
|
37
|
+
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint)
|
38
|
+
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
39
|
+
end
|
40
|
+
|
41
|
+
# Monitor only the GitLab pipeline for current project/branch.
|
42
|
+
# Used by ci:act after running 'act'.
|
43
|
+
#
|
44
|
+
# @param restart_hint [String] guidance command shown on failure
|
45
|
+
# @return [Boolean] true if check performed (gitlab configured), false otherwise
|
46
|
+
def monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
|
47
|
+
monitor_gitlab_internal!(restart_hint: restart_hint)
|
48
|
+
end
|
49
|
+
|
50
|
+
# -- internals --
|
51
|
+
|
52
|
+
def monitor_github_internal!(restart_hint:)
|
53
|
+
root = Kettle::Dev::CIHelpers.project_root
|
54
|
+
workflows = Kettle::Dev::CIHelpers.workflows_list(root)
|
55
|
+
gh_remote = preferred_github_remote
|
56
|
+
return false unless gh_remote && !workflows.empty?
|
57
|
+
|
58
|
+
branch = Kettle::Dev::CIHelpers.current_branch
|
59
|
+
abort("Could not determine current branch for CI checks.") unless branch
|
60
|
+
|
61
|
+
url = remote_url(gh_remote)
|
62
|
+
owner, repo = parse_github_owner_repo(url)
|
63
|
+
return false unless owner && repo
|
64
|
+
|
65
|
+
total = workflows.size
|
66
|
+
abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
|
67
|
+
|
68
|
+
passed = {}
|
69
|
+
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
|
70
|
+
pbar = if defined?(ProgressBar)
|
71
|
+
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
72
|
+
end
|
73
|
+
idx = 0
|
74
|
+
loop do
|
75
|
+
wf = workflows[idx]
|
76
|
+
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch)
|
77
|
+
if run
|
78
|
+
if Kettle::Dev::CIHelpers.success?(run)
|
79
|
+
unless passed[wf]
|
80
|
+
passed[wf] = true
|
81
|
+
pbar&.increment
|
82
|
+
end
|
83
|
+
elsif Kettle::Dev::CIHelpers.failed?(run)
|
84
|
+
puts
|
85
|
+
wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
|
86
|
+
abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
break if passed.size == total
|
90
|
+
idx = (idx + 1) % total
|
91
|
+
sleep(1)
|
92
|
+
end
|
93
|
+
pbar&.finish unless pbar&.finished?
|
94
|
+
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
95
|
+
true
|
96
|
+
end
|
97
|
+
module_function :monitor_github_internal!
|
98
|
+
|
99
|
+
def monitor_gitlab_internal!(restart_hint:)
|
100
|
+
root = Kettle::Dev::CIHelpers.project_root
|
101
|
+
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|
102
|
+
gl_remote = gitlab_remote_candidates.first
|
103
|
+
return false unless gitlab_ci && gl_remote
|
104
|
+
|
105
|
+
branch = Kettle::Dev::CIHelpers.current_branch
|
106
|
+
abort("Could not determine current branch for CI checks.") unless branch
|
107
|
+
|
108
|
+
owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
|
109
|
+
return false unless owner && repo
|
110
|
+
|
111
|
+
puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
|
112
|
+
pbar = if defined?(ProgressBar)
|
113
|
+
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
114
|
+
end
|
115
|
+
loop do
|
116
|
+
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
117
|
+
if pipe
|
118
|
+
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
119
|
+
pbar&.increment unless pbar&.finished?
|
120
|
+
break
|
121
|
+
elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
|
122
|
+
# Special-case: if failure is due to exhausted minutes/insufficient quota, treat as unknown and continue
|
123
|
+
reason = (pipe["failure_reason"] || "").to_s
|
124
|
+
if reason =~ /insufficient|quota|minute/i
|
125
|
+
puts "\nGitLab reports pipeline cannot run due to quota/minutes exhaustion. Result is unknown; continuing."
|
126
|
+
pbar&.finish unless pbar&.finished?
|
127
|
+
break
|
128
|
+
else
|
129
|
+
puts
|
130
|
+
url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
|
131
|
+
abort("Pipeline failed: #{url} Fix the pipeline, then restart this tool from CI validation with: #{restart_hint}")
|
132
|
+
end
|
133
|
+
elsif pipe["status"] == "blocked"
|
134
|
+
# Blocked pipeline (e.g., awaiting approvals) — treat as unknown and continue
|
135
|
+
puts "\nGitLab pipeline is blocked. Result is unknown; continuing."
|
136
|
+
pbar&.finish unless pbar&.finished?
|
137
|
+
break
|
138
|
+
end
|
139
|
+
end
|
140
|
+
sleep(1)
|
141
|
+
end
|
142
|
+
pbar&.finish unless pbar&.finished?
|
143
|
+
puts "\nGitLab pipeline passing."
|
144
|
+
true
|
145
|
+
end
|
146
|
+
module_function :monitor_gitlab_internal!
|
147
|
+
|
148
|
+
# -- tiny wrappers around GitAdapter-like helpers used by ReleaseCLI --
|
149
|
+
def remotes_with_urls
|
150
|
+
Kettle::Dev::GitAdapter.new.remotes_with_urls
|
151
|
+
end
|
152
|
+
module_function :remotes_with_urls
|
153
|
+
|
154
|
+
def remote_url(name)
|
155
|
+
Kettle::Dev::GitAdapter.new.remote_url(name)
|
156
|
+
end
|
157
|
+
module_function :remote_url
|
158
|
+
|
159
|
+
def github_remote_candidates
|
160
|
+
remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
|
161
|
+
end
|
162
|
+
module_function :github_remote_candidates
|
163
|
+
|
164
|
+
def gitlab_remote_candidates
|
165
|
+
remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys
|
166
|
+
end
|
167
|
+
module_function :gitlab_remote_candidates
|
168
|
+
|
169
|
+
def preferred_github_remote
|
170
|
+
cands = github_remote_candidates
|
171
|
+
return if cands.empty?
|
172
|
+
explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" }
|
173
|
+
return explicit if explicit
|
174
|
+
return "origin" if cands.include?("origin")
|
175
|
+
cands.first
|
176
|
+
end
|
177
|
+
module_function :preferred_github_remote
|
178
|
+
|
179
|
+
def parse_github_owner_repo(url)
|
180
|
+
return [nil, nil] unless url
|
181
|
+
if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
|
182
|
+
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
|
183
|
+
elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
|
184
|
+
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
|
185
|
+
else
|
186
|
+
[nil, nil]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
module_function :parse_github_owner_repo
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -30,124 +30,177 @@ module Kettle
|
|
30
30
|
|
31
31
|
public
|
32
32
|
|
33
|
-
def initialize
|
33
|
+
def initialize(start_step: 1)
|
34
34
|
@root = Kettle::Dev::CIHelpers.project_root
|
35
35
|
@git = Kettle::Dev::GitAdapter.new
|
36
|
+
@start_step = (start_step || 1).to_i
|
37
|
+
@start_step = 1 if @start_step < 1
|
36
38
|
end
|
37
39
|
|
38
40
|
def run
|
41
|
+
# 1. Ensure Bundler version ✓
|
39
42
|
ensure_bundler_2_7_plus!
|
40
43
|
|
41
|
-
version =
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
msg += " (matches current series)"
|
59
|
-
end
|
60
|
-
puts msg
|
61
|
-
|
62
|
-
cur = Gem::Version.new(version)
|
63
|
-
overall = Gem::Version.new(latest_overall)
|
64
|
-
cur_series = cur.segments[0, 2]
|
65
|
-
overall_series = overall.segments[0, 2]
|
66
|
-
target = if (cur_series <=> overall_series) == -1
|
67
|
-
latest_for_series
|
68
|
-
else
|
69
|
-
latest_overall
|
44
|
+
version = nil
|
45
|
+
committed = nil
|
46
|
+
trunk = nil
|
47
|
+
feature = nil
|
48
|
+
|
49
|
+
# 2. Version detection and sanity checks + prompt
|
50
|
+
if @start_step <= 2
|
51
|
+
version = detect_version
|
52
|
+
puts "Detected version: #{version.inspect}"
|
53
|
+
|
54
|
+
latest_overall = nil
|
55
|
+
latest_for_series = nil
|
56
|
+
begin
|
57
|
+
gem_name = detect_gem_name
|
58
|
+
latest_overall, latest_for_series = latest_released_versions(gem_name, version)
|
59
|
+
rescue StandardError => e
|
60
|
+
warn("Warning: failed to check RubyGems for latest version (#{e.class}: #{e.message}). Proceeding.")
|
70
61
|
end
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
series
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
62
|
+
|
63
|
+
if latest_overall
|
64
|
+
msg = "Latest released: #{latest_overall}"
|
65
|
+
if latest_for_series && latest_for_series != latest_overall
|
66
|
+
msg += " | Latest for series #{Gem::Version.new(version).segments[0, 2].join(".")}.x: #{latest_for_series}"
|
67
|
+
elsif latest_for_series
|
68
|
+
msg += " (matches current series)"
|
69
|
+
end
|
70
|
+
puts msg
|
71
|
+
|
72
|
+
cur = Gem::Version.new(version)
|
73
|
+
overall = Gem::Version.new(latest_overall)
|
74
|
+
cur_series = cur.segments[0, 2]
|
75
|
+
overall_series = overall.segments[0, 2]
|
76
|
+
target = if (cur_series <=> overall_series) == -1
|
77
|
+
latest_for_series
|
82
78
|
else
|
83
|
-
|
84
|
-
|
79
|
+
latest_overall
|
80
|
+
end
|
81
|
+
if target
|
82
|
+
bump = Kettle::Dev::Versioning.classify_bump(target, version)
|
83
|
+
case bump
|
84
|
+
when :same
|
85
|
+
series = cur_series.join(".")
|
86
|
+
warn("version.rb (#{version}) matches the latest released version for series #{series} (#{target}).")
|
87
|
+
abort("Aborting: version bump required. Bump PATCH/MINOR/MAJOR/EPIC.")
|
88
|
+
when :downgrade
|
89
|
+
series = cur_series.join(".")
|
90
|
+
warn("version.rb (#{version}) is lower than the latest released version for series #{series} (#{target}).")
|
91
|
+
abort("Aborting: version must be bumped above #{target}.")
|
92
|
+
else
|
93
|
+
label = {epic: "EPIC", major: "MAJOR", minor: "MINOR", patch: "PATCH"}[bump] || bump.to_s.upcase
|
94
|
+
puts "Proposed bump type: #{label} (from #{target} -> #{version})"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
|
85
98
|
end
|
86
99
|
else
|
87
100
|
puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
|
88
101
|
end
|
89
|
-
else
|
90
|
-
puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
|
91
|
-
end
|
92
102
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
103
|
+
puts "Have you updated lib/**/version.rb and CHANGELOG.md for v#{version}? [y/N]"
|
104
|
+
print("> ")
|
105
|
+
ans = Kettle::Dev::InputAdapter.gets&.strip
|
106
|
+
abort("Aborted: please update version.rb and CHANGELOG.md, then re-run.") unless ans&.downcase&.start_with?("y")
|
107
|
+
end
|
97
108
|
|
98
|
-
|
99
|
-
run_cmd!("bin/
|
109
|
+
# 3. bin/setup
|
110
|
+
run_cmd!("bin/setup") if @start_step <= 3
|
111
|
+
# 4. bin/rake
|
112
|
+
run_cmd!("bin/rake") if @start_step <= 4
|
113
|
+
|
114
|
+
# 5. appraisal:update (optional)
|
115
|
+
if @start_step <= 5
|
116
|
+
appraisals_path = File.join(@root, "Appraisals")
|
117
|
+
if File.file?(appraisals_path)
|
118
|
+
puts "Appraisals detected at #{appraisals_path}. Running: bin/rake appraisal:update"
|
119
|
+
run_cmd!("bin/rake appraisal:update")
|
120
|
+
else
|
121
|
+
puts "No Appraisals file found; skipping appraisal:update"
|
122
|
+
end
|
123
|
+
end
|
100
124
|
|
101
|
-
|
102
|
-
if
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
puts "No Appraisals file found; skipping appraisal:update"
|
125
|
+
# 6. git user + commit release prep
|
126
|
+
if @start_step <= 6
|
127
|
+
ensure_git_user!
|
128
|
+
version ||= detect_version
|
129
|
+
committed = commit_release_prep!(version)
|
107
130
|
end
|
108
131
|
|
109
|
-
|
110
|
-
committed
|
132
|
+
# 7. optional local CI via act
|
133
|
+
maybe_run_local_ci_before_push!(committed) if @start_step <= 7
|
111
134
|
|
112
|
-
|
135
|
+
# 8. ensure trunk synced
|
136
|
+
if @start_step <= 8
|
137
|
+
trunk = detect_trunk_branch
|
138
|
+
feature = current_branch
|
139
|
+
puts "Trunk branch detected: #{trunk}"
|
140
|
+
ensure_trunk_synced_before_push!(trunk, feature)
|
141
|
+
end
|
113
142
|
|
114
|
-
|
115
|
-
|
116
|
-
puts "Trunk branch detected: #{trunk}"
|
117
|
-
ensure_trunk_synced_before_push!(trunk, feature)
|
143
|
+
# 9. push branches
|
144
|
+
push! if @start_step <= 9
|
118
145
|
|
119
|
-
push
|
146
|
+
# 10. monitor CI after push
|
147
|
+
monitor_workflows_after_push! if @start_step <= 10
|
120
148
|
|
121
|
-
|
149
|
+
# 11. merge feature into trunk and push
|
150
|
+
if @start_step <= 11
|
151
|
+
trunk ||= detect_trunk_branch
|
152
|
+
feature ||= current_branch
|
153
|
+
merge_feature_into_trunk_and_push!(trunk, feature)
|
154
|
+
end
|
122
155
|
|
123
|
-
|
156
|
+
# 12. checkout trunk and pull
|
157
|
+
if @start_step <= 12
|
158
|
+
trunk ||= detect_trunk_branch
|
159
|
+
checkout!(trunk)
|
160
|
+
pull!(trunk)
|
161
|
+
end
|
124
162
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
163
|
+
# 13. signing guidance and checks
|
164
|
+
if @start_step <= 13
|
165
|
+
if ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
|
166
|
+
puts "TIP: For local dry-runs or testing the release workflow, set SKIP_GEM_SIGNING=true to avoid PEM password prompts."
|
167
|
+
if Kettle::Dev::InputAdapter.tty?
|
168
|
+
# In CI, avoid interactive prompts when no TTY is present (e.g., act or GitHub Actions "CI validation").
|
169
|
+
# Non-interactive CI runs should not abort here; later signing checks are either stubbed in tests
|
170
|
+
# or will be handled explicitly by ensure_signing_setup_or_skip!.
|
171
|
+
print("Proceed with signing enabled? This may hang waiting for a PEM password. [y/N]: ")
|
172
|
+
ans = Kettle::Dev::InputAdapter.gets&.strip
|
173
|
+
unless ans&.downcase&.start_with?("y")
|
174
|
+
abort("Aborted. Re-run with SKIP_GEM_SIGNING=true bundle exec kettle-release (or set it in your environment).")
|
175
|
+
end
|
176
|
+
else
|
177
|
+
warn("Non-interactive shell detected (non-TTY); skipping interactive signing confirmation.")
|
137
178
|
end
|
138
179
|
end
|
180
|
+
|
181
|
+
ensure_signing_setup_or_skip!
|
139
182
|
end
|
140
183
|
|
141
|
-
|
142
|
-
|
143
|
-
|
184
|
+
# 14. build
|
185
|
+
if @start_step <= 14
|
186
|
+
puts "Running build (you may be prompted for the signing key password)..."
|
187
|
+
run_cmd!("bundle exec rake build")
|
188
|
+
end
|
144
189
|
|
145
|
-
|
146
|
-
|
190
|
+
# 15. checksums validate
|
191
|
+
if @start_step <= 15
|
192
|
+
run_cmd!("bin/gem_checksums")
|
193
|
+
version ||= detect_version
|
194
|
+
validate_checksums!(version, stage: "after build + gem_checksums")
|
195
|
+
end
|
147
196
|
|
148
|
-
|
149
|
-
|
150
|
-
|
197
|
+
# 16. release and validate
|
198
|
+
if @start_step <= 16
|
199
|
+
puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
|
200
|
+
run_cmd!("bundle exec rake release")
|
201
|
+
version ||= detect_version
|
202
|
+
validate_checksums!(version, stage: "after release")
|
203
|
+
end
|
151
204
|
|
152
205
|
puts "\nRelease complete. Don't forget to push the checksums commit if needed."
|
153
206
|
end
|
@@ -155,87 +208,9 @@ module Kettle
|
|
155
208
|
private
|
156
209
|
|
157
210
|
def monitor_workflows_after_push!
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
branch = Kettle::Dev::CIHelpers.current_branch
|
163
|
-
abort("Could not determine current branch for CI checks.") unless branch
|
164
|
-
|
165
|
-
gh_remote = preferred_github_remote
|
166
|
-
gh_owner = nil
|
167
|
-
gh_repo = nil
|
168
|
-
if gh_remote && !workflows.empty?
|
169
|
-
url = remote_url(gh_remote)
|
170
|
-
gh_owner, gh_repo = parse_github_owner_repo(url)
|
171
|
-
end
|
172
|
-
|
173
|
-
checks_any = false
|
174
|
-
|
175
|
-
if gh_owner && gh_repo && !workflows.empty?
|
176
|
-
checks_any = true
|
177
|
-
total = workflows.size
|
178
|
-
abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
|
179
|
-
|
180
|
-
passed = {}
|
181
|
-
idx = 0
|
182
|
-
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{gh_owner}/#{gh_repo}) via remote '#{gh_remote}'"
|
183
|
-
pbar = if defined?(ProgressBar)
|
184
|
-
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
185
|
-
end
|
186
|
-
|
187
|
-
loop do
|
188
|
-
wf = workflows[idx]
|
189
|
-
run = Kettle::Dev::CIHelpers.latest_run(owner: gh_owner, repo: gh_repo, workflow_file: wf, branch: branch)
|
190
|
-
if run
|
191
|
-
if Kettle::Dev::CIHelpers.success?(run)
|
192
|
-
unless passed[wf]
|
193
|
-
passed[wf] = true
|
194
|
-
pbar&.increment
|
195
|
-
end
|
196
|
-
elsif Kettle::Dev::CIHelpers.failed?(run)
|
197
|
-
puts
|
198
|
-
url = run["html_url"] || "https://github.com/#{gh_owner}/#{gh_repo}/actions/workflows/#{wf}"
|
199
|
-
abort("Workflow failed: #{wf} -> #{url}")
|
200
|
-
end
|
201
|
-
end
|
202
|
-
break if passed.size == total
|
203
|
-
idx = (idx + 1) % total
|
204
|
-
sleep(1)
|
205
|
-
end
|
206
|
-
pbar&.finish unless pbar&.finished?
|
207
|
-
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
208
|
-
end
|
209
|
-
|
210
|
-
gl_remote = gitlab_remote_candidates.first
|
211
|
-
if gitlab_ci && gl_remote
|
212
|
-
owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
|
213
|
-
if owner && repo
|
214
|
-
checks_any = true
|
215
|
-
puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
|
216
|
-
pbar = if defined?(ProgressBar)
|
217
|
-
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
218
|
-
end
|
219
|
-
loop do
|
220
|
-
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
221
|
-
if pipe
|
222
|
-
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
223
|
-
pbar&.increment unless pbar&.finished?
|
224
|
-
break
|
225
|
-
elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
|
226
|
-
puts
|
227
|
-
url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
|
228
|
-
abort("Pipeline failed: #{url}")
|
229
|
-
end
|
230
|
-
end
|
231
|
-
sleep(1)
|
232
|
-
end
|
233
|
-
pbar&.finish unless pbar&.finished?
|
234
|
-
puts "\nGitLab pipeline passing."
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
211
|
+
# Delegate to shared CI monitor to keep logic DRY across release flow and rake tasks
|
212
|
+
require "kettle/dev/ci_monitor"
|
213
|
+
Kettle::Dev::CIMonitor.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10")
|
239
214
|
end
|
240
215
|
|
241
216
|
def run_cmd!(cmd)
|
@@ -608,8 +583,8 @@ module Kettle
|
|
608
583
|
end
|
609
584
|
|
610
585
|
def ensure_signing_setup_or_skip!
|
611
|
-
# Treat any non
|
612
|
-
return if ENV
|
586
|
+
# Treat any non-/true/i value as an explicit skip signal
|
587
|
+
return if ENV.fetch("SKIP_GEM_SIGNING", "").casecmp("true").zero?
|
613
588
|
|
614
589
|
user = ENV.fetch("GEM_CERT_USER", ENV["USER"])
|
615
590
|
cert_path = File.join(@root, "certs", "#{user}.pem")
|
@@ -132,6 +132,13 @@ module Kettle
|
|
132
132
|
end
|
133
133
|
fetch_and_print_status.call(file)
|
134
134
|
run_act_for.call(file_path)
|
135
|
+
# After running locally, check upstream GitLab pipeline status if configured
|
136
|
+
begin
|
137
|
+
require "kettle/dev/ci_monitor"
|
138
|
+
Kettle::Dev::CIMonitor.monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
|
139
|
+
rescue LoadError
|
140
|
+
# ignore if not available
|
141
|
+
end
|
135
142
|
return
|
136
143
|
end
|
137
144
|
|
@@ -186,6 +193,10 @@ module Kettle
|
|
186
193
|
print(prompt)
|
187
194
|
$stdout.flush
|
188
195
|
|
196
|
+
# We need to sleep a bit here to ensure the terminal is ready for both
|
197
|
+
# input and writing status updates to each workflow's line
|
198
|
+
sleep(0.2) unless Kettle::Dev::IS_CI
|
199
|
+
|
189
200
|
selected = nil
|
190
201
|
input_thread = Thread.new do
|
191
202
|
begin
|
@@ -330,6 +341,13 @@ module Kettle
|
|
330
341
|
abort("ci:act aborted: workflow not found: #{file_path}") unless File.file?(file_path)
|
331
342
|
fetch_and_print_status.call(chosen_file)
|
332
343
|
run_act_for.call(file_path)
|
344
|
+
# After running locally, check upstream GitLab pipeline status if configured
|
345
|
+
begin
|
346
|
+
require "kettle/dev/ci_monitor"
|
347
|
+
Kettle::Dev::CIMonitor.monitor_gitlab!(restart_hint: "bundle exec rake ci:act")
|
348
|
+
rescue LoadError
|
349
|
+
# ignore if not available
|
350
|
+
end
|
333
351
|
end
|
334
352
|
end
|
335
353
|
end
|
@@ -96,7 +96,7 @@ module Kettle
|
|
96
96
|
)
|
97
97
|
|
98
98
|
# 4) gemfiles/modular/*.gemfile (from gem's gemfiles/modular)
|
99
|
-
[%w[coverage.gemfile], %w[documentation.gemfile], %w[style.gemfile]].each do |base|
|
99
|
+
[%w[coverage.gemfile], %w[documentation.gemfile], %w[style.gemfile], %w[optional.gemfile]].each do |base|
|
100
100
|
src = helpers.prefer_example(File.join(gem_checkout_root, "gemfiles/modular", base[0]))
|
101
101
|
dest = File.join(project_root, "gemfiles/modular", base[0])
|
102
102
|
if File.basename(src).sub(/\.example\z/, "") == "style.gemfile"
|
data/lib/kettle/dev/version.rb
CHANGED
@@ -31,7 +31,7 @@ module Kettle
|
|
31
31
|
?branch: String?,
|
32
32
|
?host: String,
|
33
33
|
?token: String?
|
34
|
-
) -> { "status" => String, "web_url" => String, "id" => Integer }?
|
34
|
+
) -> { "status" => String, "web_url" => String, "id" => Integer, "failure_reason" => String? }?
|
35
35
|
def self.gitlab_success?: ({ "status" => String }?) -> bool
|
36
36
|
def self.gitlab_failed?: ({ "status" => String }?) -> bool
|
37
37
|
end
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kettle-dev
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter H. Boling
|
@@ -270,6 +270,7 @@ files:
|
|
270
270
|
- CODE_OF_CONDUCT.md
|
271
271
|
- CONTRIBUTING.md
|
272
272
|
- Gemfile
|
273
|
+
- Gemfile.example
|
273
274
|
- LICENSE.txt
|
274
275
|
- README.md
|
275
276
|
- README.md.example
|
@@ -288,6 +289,7 @@ files:
|
|
288
289
|
- lib/kettle-dev.rb
|
289
290
|
- lib/kettle/dev.rb
|
290
291
|
- lib/kettle/dev/ci_helpers.rb
|
292
|
+
- lib/kettle/dev/ci_monitor.rb
|
291
293
|
- lib/kettle/dev/commit_msg.rb
|
292
294
|
- lib/kettle/dev/exit_adapter.rb
|
293
295
|
- lib/kettle/dev/git_adapter.rb
|
@@ -317,6 +319,7 @@ files:
|
|
317
319
|
- sig/kettle-dev.rbs
|
318
320
|
- sig/kettle/dev.rbs
|
319
321
|
- sig/kettle/dev/ci_helpers.rbs
|
322
|
+
- sig/kettle/dev/ci_monitor.rbs
|
320
323
|
- sig/kettle/dev/commit_msg.rbs
|
321
324
|
- sig/kettle/dev/exit_adapter.rbs
|
322
325
|
- sig/kettle/dev/git_adapter.rbs
|
@@ -336,10 +339,10 @@ licenses:
|
|
336
339
|
- MIT
|
337
340
|
metadata:
|
338
341
|
homepage_uri: https://kettle-dev.galtzo.com/
|
339
|
-
source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.0.
|
340
|
-
changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.0.
|
342
|
+
source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.0.12
|
343
|
+
changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.0.12/CHANGELOG.md
|
341
344
|
bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
|
342
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.
|
345
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.0.12
|
343
346
|
funding_uri: https://github.com/sponsors/pboling
|
344
347
|
wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
|
345
348
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
Binary file
|