kettle-dev 1.0.10 → 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/.envrc +1 -1
- data/.github/workflows/coverage.yml +2 -2
- data/.github/workflows/coverage.yml.example +127 -0
- data/.github/workflows/discord-notifier.yml +2 -1
- data/.github/workflows/truffle.yml +0 -8
- data/.junie/guidelines.md +1 -0
- data/Appraisals +4 -1
- data/Appraisals.example +104 -0
- data/CHANGELOG.md +88 -29
- data/CHANGELOG.md.example +4 -4
- data/CONTRIBUTING.md +37 -1
- data/Gemfile +3 -0
- data/Gemfile.example +35 -0
- data/README.md +48 -10
- data/README.md.example +515 -0
- data/{Rakefile → Rakefile.example} +13 -27
- data/exe/kettle-changelog +404 -0
- data/exe/kettle-commit-msg +2 -0
- data/exe/kettle-readme-backers +2 -0
- data/exe/kettle-release +10 -9
- data/gemfiles/modular/optional.gemfile +1 -0
- data/lib/kettle/dev/ci_helpers.rb +19 -0
- data/lib/kettle/dev/ci_monitor.rb +192 -0
- data/lib/kettle/dev/git_adapter.rb +98 -33
- data/lib/kettle/dev/git_commit_footer.rb +1 -1
- data/lib/kettle/dev/input_adapter.rb +44 -0
- data/lib/kettle/dev/release_cli.rb +154 -177
- data/lib/kettle/dev/tasks/ci_task.rb +22 -1
- data/lib/kettle/dev/tasks/install_task.rb +313 -95
- data/lib/kettle/dev/tasks/template_task.rb +176 -74
- data/lib/kettle/dev/template_helpers.rb +61 -8
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev/versioning.rb +68 -0
- data/sig/kettle/dev/ci_helpers.rbs +1 -1
- data/sig/kettle/dev/ci_monitor.rbs +8 -0
- data/sig/kettle/dev/input_adapter.rbs +8 -0
- data/sig/kettle/dev/release_cli.rbs +1 -1
- data/sig/kettle/dev/template_helpers.rbs +3 -1
- data.tar.gz.sig +0 -0
- metadata +24 -22
- metadata.gz.sig +0 -0
- data/.gitlab-ci.yml +0 -45
@@ -1,13 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "open3"
|
4
|
+
|
3
5
|
module Kettle
|
4
6
|
module Dev
|
5
7
|
# Minimal Git adapter used by kettle-dev to avoid invoking live shell commands
|
6
|
-
# directly from the library code. In tests, mock this adapter's
|
7
|
-
# prevent any real network or repository mutations.
|
8
|
+
# directly from the higher-level library code. In tests, mock this adapter's
|
9
|
+
# methods to prevent any real network or repository mutations.
|
8
10
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
+
# Behavior:
|
12
|
+
# - Prefer the 'git' gem when available.
|
13
|
+
# - If the 'git' gem is not present (LoadError), fall back to shelling out to
|
14
|
+
# the system `git` executable for the small set of operations we need.
|
11
15
|
#
|
12
16
|
# Public API is intentionally small and only includes what we need right now.
|
13
17
|
class GitAdapter
|
@@ -15,10 +19,21 @@ module Kettle
|
|
15
19
|
# @return [void]
|
16
20
|
def initialize
|
17
21
|
begin
|
18
|
-
|
19
|
-
|
22
|
+
# Allow users/CI to opt out of using the 'git' gem even when available.
|
23
|
+
# Set KETTLE_DEV_DISABLE_GIT_GEM to a truthy value ("1", "true", "yes") to force CLI backend.
|
24
|
+
env_val = ENV["KETTLE_DEV_DISABLE_GIT_GEM"]
|
25
|
+
# Ruby 2.3 compatibility: String#match? was added in 2.4; use Regexp#=== / =~ instead
|
26
|
+
disable_gem = env_val && !!(/\A(1|true|yes)\z/i =~ env_val)
|
27
|
+
if disable_gem
|
28
|
+
@backend = :cli
|
29
|
+
else
|
30
|
+
Kernel.require "git"
|
31
|
+
@backend = :gem
|
32
|
+
@git = ::Git.open(Dir.pwd)
|
33
|
+
end
|
20
34
|
rescue LoadError
|
21
|
-
|
35
|
+
# Optional dependency: fall back to CLI
|
36
|
+
@backend = :cli
|
22
37
|
rescue StandardError => e
|
23
38
|
raise Kettle::Dev::Error, "Failed to open git repository: #{e.message}"
|
24
39
|
end
|
@@ -30,42 +45,73 @@ module Kettle
|
|
30
45
|
# @param force [Boolean] whether to force push
|
31
46
|
# @return [Boolean] true when the push is reported successful
|
32
47
|
def push(remote, branch, force: false)
|
33
|
-
|
34
|
-
|
48
|
+
if @backend == :gem
|
49
|
+
begin
|
50
|
+
if remote
|
51
|
+
@git.push(remote, branch, force: force)
|
52
|
+
else
|
53
|
+
# Default remote according to repo config
|
54
|
+
@git.push(nil, branch, force: force)
|
55
|
+
end
|
56
|
+
true
|
57
|
+
rescue StandardError
|
58
|
+
false
|
59
|
+
end
|
60
|
+
else
|
61
|
+
args = ["git", "push"]
|
62
|
+
args << "--force" if force
|
35
63
|
if remote
|
36
|
-
|
37
|
-
else
|
38
|
-
# Default remote according to repo config
|
39
|
-
@git.push(nil, branch, force: force)
|
64
|
+
args << remote.to_s << branch.to_s
|
40
65
|
end
|
41
|
-
|
42
|
-
rescue StandardError
|
43
|
-
false
|
66
|
+
system(*args)
|
44
67
|
end
|
45
68
|
end
|
46
69
|
|
47
70
|
# @return [String, nil] current branch name, or nil on error
|
48
71
|
def current_branch
|
49
|
-
@
|
72
|
+
if @backend == :gem
|
73
|
+
@git.current_branch
|
74
|
+
else
|
75
|
+
out, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
|
76
|
+
status.success? ? out.strip : nil
|
77
|
+
end
|
50
78
|
rescue StandardError
|
51
79
|
nil
|
52
80
|
end
|
53
81
|
|
54
82
|
# @return [Array<String>] list of remote names
|
55
83
|
def remotes
|
56
|
-
@
|
84
|
+
if @backend == :gem
|
85
|
+
@git.remotes.map(&:name)
|
86
|
+
else
|
87
|
+
out, status = Open3.capture2("git", "remote")
|
88
|
+
status.success? ? out.split(/\r?\n/).map(&:strip).reject(&:empty?) : []
|
89
|
+
end
|
57
90
|
rescue StandardError
|
58
91
|
[]
|
59
92
|
end
|
60
93
|
|
61
94
|
# @return [Hash{String=>String}] remote name => fetch URL
|
62
95
|
def remotes_with_urls
|
63
|
-
@
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
96
|
+
if @backend == :gem
|
97
|
+
@git.remotes.each_with_object({}) do |r, h|
|
98
|
+
begin
|
99
|
+
h[r.name] = r.url
|
100
|
+
rescue StandardError
|
101
|
+
# ignore
|
102
|
+
end
|
103
|
+
end
|
104
|
+
else
|
105
|
+
out, status = Open3.capture2("git", "remote", "-v")
|
106
|
+
return {} unless status.success?
|
107
|
+
urls = {}
|
108
|
+
out.each_line do |line|
|
109
|
+
# Example: origin https://github.com/me/repo.git (fetch)
|
110
|
+
if line =~ /^(\S+)\s+(\S+)\s+\(fetch\)/
|
111
|
+
urls[Regexp.last_match(1)] = Regexp.last_match(2)
|
112
|
+
end
|
68
113
|
end
|
114
|
+
urls
|
69
115
|
end
|
70
116
|
rescue StandardError
|
71
117
|
{}
|
@@ -74,8 +120,13 @@ module Kettle
|
|
74
120
|
# @param name [String]
|
75
121
|
# @return [String, nil]
|
76
122
|
def remote_url(name)
|
77
|
-
|
78
|
-
|
123
|
+
if @backend == :gem
|
124
|
+
r = @git.remotes.find { |x| x.name == name }
|
125
|
+
r&.url
|
126
|
+
else
|
127
|
+
out, status = Open3.capture2("git", "config", "--get", "remote.#{name}.url")
|
128
|
+
status.success? ? out.strip : nil
|
129
|
+
end
|
79
130
|
rescue StandardError
|
80
131
|
nil
|
81
132
|
end
|
@@ -84,8 +135,12 @@ module Kettle
|
|
84
135
|
# @param branch [String]
|
85
136
|
# @return [Boolean]
|
86
137
|
def checkout(branch)
|
87
|
-
@
|
88
|
-
|
138
|
+
if @backend == :gem
|
139
|
+
@git.checkout(branch)
|
140
|
+
true
|
141
|
+
else
|
142
|
+
system("git", "checkout", branch.to_s)
|
143
|
+
end
|
89
144
|
rescue StandardError
|
90
145
|
false
|
91
146
|
end
|
@@ -95,8 +150,12 @@ module Kettle
|
|
95
150
|
# @param branch [String]
|
96
151
|
# @return [Boolean]
|
97
152
|
def pull(remote, branch)
|
98
|
-
@
|
99
|
-
|
153
|
+
if @backend == :gem
|
154
|
+
@git.pull(remote, branch)
|
155
|
+
true
|
156
|
+
else
|
157
|
+
system("git", "pull", remote.to_s, branch.to_s)
|
158
|
+
end
|
100
159
|
rescue StandardError
|
101
160
|
false
|
102
161
|
end
|
@@ -106,12 +165,18 @@ module Kettle
|
|
106
165
|
# @param ref [String, nil]
|
107
166
|
# @return [Boolean]
|
108
167
|
def fetch(remote, ref = nil)
|
109
|
-
if
|
110
|
-
|
168
|
+
if @backend == :gem
|
169
|
+
if ref
|
170
|
+
@git.fetch(remote, ref)
|
171
|
+
else
|
172
|
+
@git.fetch(remote)
|
173
|
+
end
|
174
|
+
true
|
175
|
+
elsif ref
|
176
|
+
system("git", "fetch", remote.to_s, ref.to_s)
|
111
177
|
else
|
112
|
-
|
178
|
+
system("git", "fetch", remote.to_s)
|
113
179
|
end
|
114
|
-
true
|
115
180
|
rescue StandardError
|
116
181
|
false
|
117
182
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kettle
|
4
|
+
module Dev
|
5
|
+
# Input indirection layer to make interactive prompts safe in tests.
|
6
|
+
#
|
7
|
+
# Production/default behavior delegates to $stdin.gets (or Kernel#gets)
|
8
|
+
# so application code does not read from STDIN directly. In specs, mock
|
9
|
+
# this adapter's methods to return deterministic answers without touching
|
10
|
+
# global IO.
|
11
|
+
#
|
12
|
+
# Example (RSpec):
|
13
|
+
# allow(Kettle::Dev::InputAdapter).to receive(:gets).and_return("y\n")
|
14
|
+
#
|
15
|
+
# This mirrors the "mockable adapter" approach used for GitAdapter and ExitAdapter.
|
16
|
+
module InputAdapter
|
17
|
+
module_function
|
18
|
+
|
19
|
+
# Read one line from the standard input, including the trailing newline if
|
20
|
+
# present. Returns nil on EOF, consistent with IO#gets.
|
21
|
+
#
|
22
|
+
# @param args [Array] any args are forwarded to $stdin.gets for compatibility
|
23
|
+
# @return [String, nil]
|
24
|
+
def gets(*args)
|
25
|
+
$stdin.gets(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def tty?
|
29
|
+
$stdin.tty?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Read one line from standard input, raising EOFError on end-of-file.
|
33
|
+
# Provided for convenience symmetry with IO#readline when needed.
|
34
|
+
#
|
35
|
+
# @param args [Array]
|
36
|
+
# @return [String]
|
37
|
+
def readline(*args)
|
38
|
+
line = gets(*args)
|
39
|
+
raise EOFError, "end of file reached" if line.nil?
|
40
|
+
line
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -16,6 +16,8 @@ require "ruby-progressbar"
|
|
16
16
|
# Internal
|
17
17
|
require "kettle/dev/git_adapter"
|
18
18
|
require "kettle/dev/exit_adapter"
|
19
|
+
require "kettle/dev/input_adapter"
|
20
|
+
require "kettle/dev/versioning"
|
19
21
|
|
20
22
|
module Kettle
|
21
23
|
module Dev
|
@@ -28,114 +30,177 @@ module Kettle
|
|
28
30
|
|
29
31
|
public
|
30
32
|
|
31
|
-
def initialize
|
33
|
+
def initialize(start_step: 1)
|
32
34
|
@root = Kettle::Dev::CIHelpers.project_root
|
33
35
|
@git = Kettle::Dev::GitAdapter.new
|
36
|
+
@start_step = (start_step || 1).to_i
|
37
|
+
@start_step = 1 if @start_step < 1
|
34
38
|
end
|
35
39
|
|
36
40
|
def run
|
37
|
-
|
38
|
-
|
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)"
|
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.")
|
59
61
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
78
|
+
else
|
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."
|
98
|
+
end
|
68
99
|
else
|
69
|
-
|
70
|
-
end
|
71
|
-
if target && Gem::Version.new(version) <= Gem::Version.new(target)
|
72
|
-
series = cur_series.join(".")
|
73
|
-
warn("version.rb (#{version}) must be greater than the latest released version for series #{series}. Latest for series: #{target}.")
|
74
|
-
warn("Tip: bump PATCH for a stable branch release, or bump MINOR/MAJOR when on trunk.")
|
75
|
-
abort("Aborting: version bump required.")
|
100
|
+
puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
|
76
101
|
end
|
77
|
-
else
|
78
|
-
puts "Could not determine latest released version from RubyGems (offline?). Proceeding without sanity check."
|
79
|
-
end
|
80
102
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
85
108
|
|
86
|
-
|
87
|
-
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
|
88
124
|
|
89
|
-
|
90
|
-
if
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
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)
|
95
130
|
end
|
96
131
|
|
97
|
-
|
98
|
-
committed
|
132
|
+
# 7. optional local CI via act
|
133
|
+
maybe_run_local_ci_before_push!(committed) if @start_step <= 7
|
99
134
|
|
100
|
-
|
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
|
101
142
|
|
102
|
-
|
103
|
-
|
104
|
-
puts "Trunk branch detected: #{trunk}"
|
105
|
-
ensure_trunk_synced_before_push!(trunk, feature)
|
143
|
+
# 9. push branches
|
144
|
+
push! if @start_step <= 9
|
106
145
|
|
107
|
-
push
|
146
|
+
# 10. monitor CI after push
|
147
|
+
monitor_workflows_after_push! if @start_step <= 10
|
108
148
|
|
109
|
-
|
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
|
110
155
|
|
111
|
-
|
156
|
+
# 12. checkout trunk and pull
|
157
|
+
if @start_step <= 12
|
158
|
+
trunk ||= detect_trunk_branch
|
159
|
+
checkout!(trunk)
|
160
|
+
pull!(trunk)
|
161
|
+
end
|
112
162
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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.")
|
125
178
|
end
|
126
179
|
end
|
180
|
+
|
181
|
+
ensure_signing_setup_or_skip!
|
127
182
|
end
|
128
183
|
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
132
189
|
|
133
|
-
|
134
|
-
|
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
|
135
196
|
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
139
204
|
|
140
205
|
puts "\nRelease complete. Don't forget to push the checksums commit if needed."
|
141
206
|
end
|
@@ -143,87 +208,9 @@ module Kettle
|
|
143
208
|
private
|
144
209
|
|
145
210
|
def monitor_workflows_after_push!
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
branch = Kettle::Dev::CIHelpers.current_branch
|
151
|
-
abort("Could not determine current branch for CI checks.") unless branch
|
152
|
-
|
153
|
-
gh_remote = preferred_github_remote
|
154
|
-
gh_owner = nil
|
155
|
-
gh_repo = nil
|
156
|
-
if gh_remote && !workflows.empty?
|
157
|
-
url = remote_url(gh_remote)
|
158
|
-
gh_owner, gh_repo = parse_github_owner_repo(url)
|
159
|
-
end
|
160
|
-
|
161
|
-
checks_any = false
|
162
|
-
|
163
|
-
if gh_owner && gh_repo && !workflows.empty?
|
164
|
-
checks_any = true
|
165
|
-
total = workflows.size
|
166
|
-
abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
|
167
|
-
|
168
|
-
passed = {}
|
169
|
-
idx = 0
|
170
|
-
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{gh_owner}/#{gh_repo}) via remote '#{gh_remote}'"
|
171
|
-
pbar = if defined?(ProgressBar)
|
172
|
-
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
173
|
-
end
|
174
|
-
|
175
|
-
loop do
|
176
|
-
wf = workflows[idx]
|
177
|
-
run = Kettle::Dev::CIHelpers.latest_run(owner: gh_owner, repo: gh_repo, workflow_file: wf, branch: branch)
|
178
|
-
if run
|
179
|
-
if Kettle::Dev::CIHelpers.success?(run)
|
180
|
-
unless passed[wf]
|
181
|
-
passed[wf] = true
|
182
|
-
pbar&.increment
|
183
|
-
end
|
184
|
-
elsif Kettle::Dev::CIHelpers.failed?(run)
|
185
|
-
puts
|
186
|
-
url = run["html_url"] || "https://github.com/#{gh_owner}/#{gh_repo}/actions/workflows/#{wf}"
|
187
|
-
abort("Workflow failed: #{wf} -> #{url}")
|
188
|
-
end
|
189
|
-
end
|
190
|
-
break if passed.size == total
|
191
|
-
idx = (idx + 1) % total
|
192
|
-
sleep(1)
|
193
|
-
end
|
194
|
-
pbar&.finish unless pbar&.finished?
|
195
|
-
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
196
|
-
end
|
197
|
-
|
198
|
-
gl_remote = gitlab_remote_candidates.first
|
199
|
-
if gitlab_ci && gl_remote
|
200
|
-
owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
|
201
|
-
if owner && repo
|
202
|
-
checks_any = true
|
203
|
-
puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
|
204
|
-
pbar = if defined?(ProgressBar)
|
205
|
-
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
206
|
-
end
|
207
|
-
loop do
|
208
|
-
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
209
|
-
if pipe
|
210
|
-
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
211
|
-
pbar&.increment unless pbar&.finished?
|
212
|
-
break
|
213
|
-
elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
|
214
|
-
puts
|
215
|
-
url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
|
216
|
-
abort("Pipeline failed: #{url}")
|
217
|
-
end
|
218
|
-
end
|
219
|
-
sleep(1)
|
220
|
-
end
|
221
|
-
pbar&.finish unless pbar&.finished?
|
222
|
-
puts "\nGitLab pipeline passing."
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
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")
|
227
214
|
end
|
228
215
|
|
229
216
|
def run_cmd!(cmd)
|
@@ -269,7 +256,7 @@ module Kettle
|
|
269
256
|
when "true", "1", "yes", "y" then true
|
270
257
|
when "ask"
|
271
258
|
print("Run local CI with 'act' before pushing? [Y/n] ")
|
272
|
-
ans =
|
259
|
+
ans = Kettle::Dev::InputAdapter.gets&.strip
|
273
260
|
ans.nil? || ans.empty? || ans =~ /\Ay(es)?\z/i
|
274
261
|
else
|
275
262
|
false
|
@@ -329,17 +316,7 @@ module Kettle
|
|
329
316
|
end
|
330
317
|
|
331
318
|
def detect_version
|
332
|
-
|
333
|
-
abort("Could not find version.rb under lib/**.") if candidates.empty?
|
334
|
-
versions = candidates.map do |path|
|
335
|
-
content = File.read(path)
|
336
|
-
m = content.match(/VERSION\s*=\s*(["'])([^"']+)\1/)
|
337
|
-
next unless m
|
338
|
-
m[2]
|
339
|
-
end.compact
|
340
|
-
abort("VERSION constant not found in #{@root}/lib/**/version.rb") if versions.none?
|
341
|
-
abort("Multiple VERSION constants found to be out of sync (#{versions.inspect}) in #{@root}/lib/**/version.rb") unless versions.uniq.length == 1
|
342
|
-
versions.first
|
319
|
+
Kettle::Dev::Versioning.detect_version(@root)
|
343
320
|
end
|
344
321
|
|
345
322
|
def detect_gem_name
|
@@ -571,7 +548,7 @@ module Kettle
|
|
571
548
|
puts " [m] Merge --no-ff #{gh_remote}/#{trunk} into #{trunk} (push to origin and #{gh_remote})"
|
572
549
|
puts " [a] Abort"
|
573
550
|
print("> ")
|
574
|
-
choice =
|
551
|
+
choice = Kettle::Dev::InputAdapter.gets&.strip&.downcase
|
575
552
|
case choice
|
576
553
|
when "r"
|
577
554
|
run_cmd!("git rebase #{Shellwords.escape("#{gh_remote}/#{trunk}")}")
|
@@ -606,8 +583,8 @@ module Kettle
|
|
606
583
|
end
|
607
584
|
|
608
585
|
def ensure_signing_setup_or_skip!
|
609
|
-
# Treat any non
|
610
|
-
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?
|
611
588
|
|
612
589
|
user = ENV.fetch("GEM_CERT_USER", ENV["USER"])
|
613
590
|
cert_path = File.join(@root, "certs", "#{user}.pem")
|