kettle-dev 2.5.11 → 2.5.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/CHANGELOG.md +44 -1
- data/README.md +1 -1
- data/exe/kettle-release-secrets-doctor +39 -0
- data/lib/kettle/dev/bundler_env_guard.rb +1 -0
- data/lib/kettle/dev/ci_monitor.rb +34 -5
- data/lib/kettle/dev/interactive_release_command.rb +16 -5
- data/lib/kettle/dev/lockfile_reset.rb +22 -0
- data/lib/kettle/dev/pre_release_cli.rb +8 -2
- data/lib/kettle/dev/release_cli.rb +69 -3
- data/lib/kettle/dev/release_notifier.rb +27 -0
- data/lib/kettle/dev/release_secrets.rb +16 -1
- data/lib/kettle/dev/release_secrets_doctor.rb +204 -0
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +2 -0
- data.tar.gz.sig +3 -2
- metadata +10 -6
- 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: 60069f78cc45c33bf7d2616ea76c456e98cf9885da75331a553f5c9e2b96bbe8
|
|
4
|
+
data.tar.gz: d58499d2f31cbffc408e15af977b93b3254f310d021822d90726c7407f4436c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b34e918fa0cce724b098ab97e46abd768a65938d52f908f87515a5f64b8c3c8d68e6717c05529045da5ce1670e407fa1fa955cfb3c9c2cbd6a4c72d46b64577b
|
|
7
|
+
data.tar.gz: d9dcb4194c6f22c6f6ba1b3311b8f7e644766b52bb36f80d4f59130e48f32d2d4917541c3aebf25e253adb4e3914485b9d66b7f77ebf64b73e56804686dc4276
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,47 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [2.5.12] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
- TAG: [v2.5.12][2.5.12t]
|
|
36
|
+
- COVERAGE: 91.73% -- 4961/5408 lines in 46 files
|
|
37
|
+
- BRANCH COVERAGE: 75.98% -- 1850/2435 branches in 46 files
|
|
38
|
+
- 50.68% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `kettle-release-secrets-doctor` can probe 1Password release secret
|
|
43
|
+
authorization across same-process, child-process, and threaded shapes.
|
|
44
|
+
|
|
45
|
+
- `kettle-release-secrets-doctor` now probes additional 1Password
|
|
46
|
+
authorization boundaries, including parent-child, Bundler child, and
|
|
47
|
+
Bundler-env-reset child process shapes.
|
|
48
|
+
|
|
49
|
+
- `kettle-release --events` now emits `secret_provider` events around release
|
|
50
|
+
secret keepalives and provider-backed prompt responses.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `kettle-release` now keeps configured release secrets providers warm during
|
|
55
|
+
CI waits and alerts before provider lookups, reducing missed 1Password
|
|
56
|
+
authorization prompts during long release runs.
|
|
57
|
+
|
|
58
|
+
- `kettle-release` keepalive alerts now include the elapsed release time so
|
|
59
|
+
delayed 1Password prompts are easier to correlate with the run.
|
|
60
|
+
|
|
61
|
+
- `kettle-dev` now requires `kettle-ndjson` 0.1.4 or newer so
|
|
62
|
+
`kettle-release --events=secret_provider` cannot resolve against an older
|
|
63
|
+
event protocol.
|
|
64
|
+
- `kettle-release` lockfile reset now preserves existing bundle platforms and
|
|
65
|
+
adds the current host platform, preventing release-prep lockfiles from losing
|
|
66
|
+
Linux support during full rebuilds.
|
|
67
|
+
- `kettle-pre-release` Markdown image URL normalization now keeps encoded query
|
|
68
|
+
values stable, avoiding release-time churn in shields URLs with nested URL
|
|
69
|
+
parameters.
|
|
70
|
+
- `kettle-release` now runs `bin/setup` with inherited Bundler bootstrap
|
|
71
|
+
variables unset, so a release lockfile reset can install newly selected gems
|
|
72
|
+
before later release tasks run.
|
|
73
|
+
|
|
33
74
|
## [2.5.11] - 2026-07-30
|
|
34
75
|
|
|
35
76
|
- TAG: [v2.5.11][2.5.11t]
|
|
@@ -3077,7 +3118,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
3077
3118
|
- Selecting will run the selected workflow via `act`
|
|
3078
3119
|
- This may move to its own gem in the future.
|
|
3079
3120
|
|
|
3080
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.
|
|
3121
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.12...HEAD
|
|
3122
|
+
[2.5.12]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.11...v2.5.12
|
|
3123
|
+
[2.5.12t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.12
|
|
3081
3124
|
[2.5.11]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.10...v2.5.11
|
|
3082
3125
|
[2.5.11t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.5.11
|
|
3083
3126
|
[2.5.10]: https://github.com/kettle-dev/kettle-dev/compare/v2.5.9...v2.5.10
|
data/README.md
CHANGED
|
@@ -1005,7 +1005,7 @@ Thanks for RTFM. ☺️
|
|
|
1005
1005
|
[📌gitmoji]: https://gitmoji.dev
|
|
1006
1006
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1007
1007
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1008
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.
|
|
1008
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.408-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1009
1009
|
[🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
|
|
1010
1010
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1011
1011
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
$stderr.sync = true
|
|
8
|
+
|
|
9
|
+
script_basename = File.basename(__FILE__)
|
|
10
|
+
repo_lib = File.expand_path("../lib", __dir__)
|
|
11
|
+
$LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include?(repo_lib)
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
require "kettle/dev"
|
|
15
|
+
require "kettle/dev/executable_version"
|
|
16
|
+
Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV, value_option: true)
|
|
17
|
+
Kettle::Dev::ExecutableVersion.print_header(script_basename)
|
|
18
|
+
rescue LoadError
|
|
19
|
+
warn "kettle-release-secrets-doctor must run with kettle-dev on the load path"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
options = Kettle::Dev::ReleaseSecretsDoctor.options_from_env
|
|
24
|
+
|
|
25
|
+
OptionParser.new do |parser|
|
|
26
|
+
parser.banner = "Usage: kettle-release-secrets-doctor [options]"
|
|
27
|
+
parser.on("--provider NAME", "Secrets provider name, default: op") { |value| options[:provider] = value }
|
|
28
|
+
parser.on("--sleep SECONDS", Float, "Seconds to wait between probes") { |value| options[:sleep_seconds] = value }
|
|
29
|
+
parser.on("--keepalive SECONDS", Float, "Run keepalive every N seconds while waiting; 0 disables") { |value| options[:keepalive_seconds] = value }
|
|
30
|
+
parser.on("--shape SHAPE", "Shape: #{Kettle::Dev::ReleaseSecretsDoctor::SHAPES.join(", ")}") { |value| options[:shape] = value }
|
|
31
|
+
parser.on("--otp", "Fetch RubyGems OTP after the wait") { options[:otp] = true }
|
|
32
|
+
end.parse!
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
Kettle::Dev::ReleaseSecretsDoctor.new(options: options, program_name: $PROGRAM_NAME).run
|
|
36
|
+
rescue Kettle::Dev::Error => e
|
|
37
|
+
warn e.message
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
@@ -47,10 +47,10 @@ module Kettle
|
|
|
47
47
|
#
|
|
48
48
|
# @param restart_hint [String] guidance command shown on failure
|
|
49
49
|
# @return [void]
|
|
50
|
-
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, **options)
|
|
50
|
+
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, keepalive: nil, **options)
|
|
51
51
|
checks_any = false
|
|
52
|
-
checks_any |= monitor_github_internal!(restart_hint: restart_hint, workflows: workflows)
|
|
53
|
-
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint)
|
|
52
|
+
checks_any |= monitor_github_internal!(restart_hint: restart_hint, workflows: workflows, keepalive: keepalive)
|
|
53
|
+
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint, keepalive: keepalive)
|
|
54
54
|
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -267,7 +267,7 @@ module Kettle
|
|
|
267
267
|
end
|
|
268
268
|
module_function :collect_gitlab
|
|
269
269
|
|
|
270
|
-
def monitor_github_internal!(restart_hint:, workflows: nil)
|
|
270
|
+
def monitor_github_internal!(restart_hint:, workflows: nil, keepalive: nil)
|
|
271
271
|
root = Kettle::Dev::CIHelpers.project_root
|
|
272
272
|
workflows = Array(workflows).empty? ? Kettle::Dev::CIHelpers.workflows_list(root) : Array(workflows)
|
|
273
273
|
gh_remote = preferred_github_remote
|
|
@@ -306,8 +306,10 @@ module Kettle
|
|
|
306
306
|
start_timeout = github_start_timeout
|
|
307
307
|
poll_interval = github_poll_interval
|
|
308
308
|
start_deadline = monotonic_time + start_timeout
|
|
309
|
+
keepalive_timer = keepalive_timer(keepalive)
|
|
309
310
|
idx = 0
|
|
310
311
|
loop do
|
|
312
|
+
keepalive_timer.call
|
|
311
313
|
wf = workflows[idx]
|
|
312
314
|
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch, require_head: true, head_sha: head_sha)
|
|
313
315
|
if run
|
|
@@ -364,7 +366,32 @@ module Kettle
|
|
|
364
366
|
end
|
|
365
367
|
module_function :github_poll_interval
|
|
366
368
|
|
|
367
|
-
def
|
|
369
|
+
def keepalive_timer(keepalive)
|
|
370
|
+
interval = keepalive_interval
|
|
371
|
+
last_run = nil
|
|
372
|
+
lambda do
|
|
373
|
+
if keepalive
|
|
374
|
+
now = monotonic_time
|
|
375
|
+
if !last_run || (now - last_run) >= interval
|
|
376
|
+
keepalive.call
|
|
377
|
+
last_run = now
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
module_function :keepalive_timer
|
|
383
|
+
|
|
384
|
+
def keepalive_interval
|
|
385
|
+
seconds = begin
|
|
386
|
+
Float(ENV["KETTLE_RELEASE_SECRET_KEEPALIVE_INTERVAL"])
|
|
387
|
+
rescue
|
|
388
|
+
nil
|
|
389
|
+
end
|
|
390
|
+
(seconds && seconds >= 0) ? seconds : 240.0
|
|
391
|
+
end
|
|
392
|
+
module_function :keepalive_interval
|
|
393
|
+
|
|
394
|
+
def monitor_gitlab_internal!(restart_hint:, keepalive: nil)
|
|
368
395
|
root = Kettle::Dev::CIHelpers.project_root
|
|
369
396
|
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|
|
370
397
|
gl_remote = gitlab_remote_candidates.first
|
|
@@ -380,7 +407,9 @@ module Kettle
|
|
|
380
407
|
pbar = if defined?(ProgressBar)
|
|
381
408
|
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
|
382
409
|
end
|
|
410
|
+
keepalive_timer = keepalive_timer(keepalive)
|
|
383
411
|
loop do
|
|
412
|
+
keepalive_timer.call
|
|
384
413
|
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
|
385
414
|
if pipe
|
|
386
415
|
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
|
@@ -5,11 +5,12 @@ require "open3"
|
|
|
5
5
|
module Kettle
|
|
6
6
|
module Dev
|
|
7
7
|
class InteractiveReleaseCommand
|
|
8
|
-
def initialize(secrets_provider:, input: $stdin, output: $stdout, error: $stderr)
|
|
8
|
+
def initialize(secrets_provider:, input: $stdin, output: $stdout, error: $stderr, secret_event_handler: nil)
|
|
9
9
|
@secrets_provider = secrets_provider
|
|
10
10
|
@input = input
|
|
11
11
|
@output = output
|
|
12
12
|
@error = error
|
|
13
|
+
@secret_event_handler = secret_event_handler
|
|
13
14
|
@gem_signing_passphrase = nil
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ module Kettle
|
|
|
21
22
|
|
|
22
23
|
private
|
|
23
24
|
|
|
24
|
-
attr_reader :secrets_provider
|
|
25
|
+
attr_reader :secrets_provider, :secret_event_handler
|
|
25
26
|
|
|
26
27
|
def call_with_pty(env, cmd)
|
|
27
28
|
stdout_str = +""
|
|
@@ -77,18 +78,20 @@ module Kettle
|
|
|
77
78
|
|
|
78
79
|
def handle_prompt(input, chunk)
|
|
79
80
|
if otp_prompt?(chunk)
|
|
80
|
-
write_secret(input,
|
|
81
|
+
write_secret(input, label: "RubyGems MFA code", source: "rubygems_otp") { secrets_provider.rubygems_otp } if otp_response_prompt?(chunk)
|
|
81
82
|
return
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
write_secret(input,
|
|
85
|
+
write_secret(input, label: "gem signing passphrase", source: "gem_signing_passphrase") { gem_signing_passphrase } if signing_password_prompt?(chunk)
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
def gem_signing_passphrase
|
|
88
89
|
@gem_signing_passphrase ||= secrets_provider.gem_signing_passphrase.to_s
|
|
89
90
|
end
|
|
90
91
|
|
|
91
|
-
def write_secret(input,
|
|
92
|
+
def write_secret(input, label:, source:)
|
|
93
|
+
emit_secret_event(source: source, action: "prompt_response", status: "started", label: label)
|
|
94
|
+
value = yield
|
|
92
95
|
secret = value.to_s
|
|
93
96
|
if secret.empty?
|
|
94
97
|
raise Kettle::Dev::Error, "#{label} prompt reached, but the configured release secrets provider returned no value. Aborting because secret prompts are not allowed when a secrets provider is configured."
|
|
@@ -97,6 +100,14 @@ module Kettle
|
|
|
97
100
|
@output.puts("#{label} loaded from configured secrets provider.")
|
|
98
101
|
input.write("#{secret}\n")
|
|
99
102
|
input.flush
|
|
103
|
+
emit_secret_event(source: source, action: "prompt_response", status: "ok", label: label)
|
|
104
|
+
rescue Kettle::Dev::Error => error
|
|
105
|
+
emit_secret_event(source: source, action: "prompt_response", status: "failed", label: label, reason: error.message)
|
|
106
|
+
raise
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def emit_secret_event(payload)
|
|
110
|
+
secret_event_handler&.call(payload)
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
def otp_prompt?(chunk)
|
|
@@ -79,12 +79,34 @@ module Kettle
|
|
|
79
79
|
command << " #{key}=#{Shellwords.escape(value)}"
|
|
80
80
|
end
|
|
81
81
|
command << " bundle lock"
|
|
82
|
+
reset_platforms(path).each do |platform|
|
|
83
|
+
command << " --add-platform=#{Shellwords.escape(platform)}"
|
|
84
|
+
end
|
|
82
85
|
command << " --update"
|
|
83
86
|
command << " #{update_gems.map { |gem_name| Shellwords.escape(gem_name) }.join(" ")}" unless update_gems.empty?
|
|
84
87
|
command << " --add-checksums"
|
|
85
88
|
command
|
|
86
89
|
end
|
|
87
90
|
|
|
91
|
+
def reset_platforms(path)
|
|
92
|
+
(lockfile_platforms(path) | [Gem::Platform.local.to_s]).reject(&:empty?).sort
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def lockfile_platforms(path)
|
|
96
|
+
in_platforms = false
|
|
97
|
+
File.readlines(path).filter_map do |line|
|
|
98
|
+
stripped = line.strip
|
|
99
|
+
if stripped.match?(/\A[A-Z][A-Z ]*\z/)
|
|
100
|
+
in_platforms = stripped == "PLATFORMS"
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
next unless in_platforms
|
|
104
|
+
next if stripped.empty?
|
|
105
|
+
|
|
106
|
+
stripped
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
88
110
|
def reset_update_gems(path)
|
|
89
111
|
(
|
|
90
112
|
path_source_gems(path).to_a |
|
|
@@ -335,8 +335,7 @@ module Kettle
|
|
|
335
335
|
modified = false
|
|
336
336
|
|
|
337
337
|
urls.each do |url_str|
|
|
338
|
-
|
|
339
|
-
normalized = addr.normalize.to_s
|
|
338
|
+
normalized = normalized_markdown_image_url(url_str)
|
|
340
339
|
next if normalized == url_str
|
|
341
340
|
|
|
342
341
|
# Replace exact occurrences of the URL in the markdown content
|
|
@@ -359,6 +358,13 @@ module Kettle
|
|
|
359
358
|
nil
|
|
360
359
|
end
|
|
361
360
|
|
|
361
|
+
def normalized_markdown_image_url(url_str)
|
|
362
|
+
addr = Addressable::URI.parse(url_str)
|
|
363
|
+
normalized = addr.normalize
|
|
364
|
+
normalized.query = addr.query if addr.query && normalized.query != addr.query
|
|
365
|
+
normalized.to_s
|
|
366
|
+
end
|
|
367
|
+
|
|
362
368
|
# Check 3: Validate Markdown image links by cached HTTP HEAD/GET.
|
|
363
369
|
# @return [void]
|
|
364
370
|
def check_markdown_images_http!
|
|
@@ -276,7 +276,7 @@ module Kettle
|
|
|
276
276
|
prepare_release_lockfiles_for_release_tasks! if release_lockfile_preflight_needed?
|
|
277
277
|
|
|
278
278
|
# 3. bin/setup
|
|
279
|
-
run_cmd!(
|
|
279
|
+
run_cmd!(release_setup_command) if run_step?(3)
|
|
280
280
|
# 4. bin/rake
|
|
281
281
|
run_cmd!(release_default_task_command) if run_step?(4)
|
|
282
282
|
|
|
@@ -773,6 +773,11 @@ module Kettle
|
|
|
773
773
|
"bin/rake"
|
|
774
774
|
end
|
|
775
775
|
|
|
776
|
+
def release_setup_command
|
|
777
|
+
unbundled_keys = (BundlerEnvGuard::RESET_ENV_KEYS + %w[RUBYLIB RUBYOPT]).uniq
|
|
778
|
+
"env #{unbundled_keys.map { |key| "-u #{Shellwords.escape(key)}" }.join(" ")} bin/setup"
|
|
779
|
+
end
|
|
780
|
+
|
|
776
781
|
def confirm_yes!(message, prompt, abort_message)
|
|
777
782
|
puts(message)
|
|
778
783
|
if @yes
|
|
@@ -1004,8 +1009,13 @@ module Kettle
|
|
|
1004
1009
|
|
|
1005
1010
|
def monitor_workflows_after_push!
|
|
1006
1011
|
ensure_github_pull_request_for_ci!
|
|
1012
|
+
keep_release_secrets_alive!("CI monitoring")
|
|
1007
1013
|
# Use abort-on-failure CI monitor to match historical behavior and specs
|
|
1008
|
-
Kettle::Dev::CIMonitor.monitor_all!(
|
|
1014
|
+
Kettle::Dev::CIMonitor.monitor_all!(
|
|
1015
|
+
restart_hint: "bundle exec kettle-release start_step=10",
|
|
1016
|
+
workflows: @ci_workflows,
|
|
1017
|
+
keepalive: release_secrets_configured? ? -> { keep_release_secrets_alive!("CI monitoring") } : nil
|
|
1018
|
+
)
|
|
1009
1019
|
end
|
|
1010
1020
|
|
|
1011
1021
|
def ensure_github_pull_request_for_ci!
|
|
@@ -1110,8 +1120,12 @@ module Kettle
|
|
|
1110
1120
|
def run_command_with_release_secrets!(cmd)
|
|
1111
1121
|
return self.class.run_cmd!(cmd) unless release_secret_command?(cmd) && release_secrets_configured?
|
|
1112
1122
|
|
|
1123
|
+
keep_release_secrets_alive!("prompt-bearing command")
|
|
1113
1124
|
puts "$ #{cmd}"
|
|
1114
|
-
_stdout_str, stderr_str, status = Kettle::Dev::InteractiveReleaseCommand.new(
|
|
1125
|
+
_stdout_str, stderr_str, status = Kettle::Dev::InteractiveReleaseCommand.new(
|
|
1126
|
+
secrets_provider: @secrets_provider,
|
|
1127
|
+
secret_event_handler: ->(payload) { emit_secret_event(payload) }
|
|
1128
|
+
).call(
|
|
1115
1129
|
self.class.send(:command_env),
|
|
1116
1130
|
cmd
|
|
1117
1131
|
)
|
|
@@ -1137,12 +1151,41 @@ module Kettle
|
|
|
1137
1151
|
end
|
|
1138
1152
|
|
|
1139
1153
|
def ensure_release_secrets_ready_for_signing!
|
|
1154
|
+
keep_release_secrets_alive!("signing preflight")
|
|
1140
1155
|
value = @secrets_provider.gem_signing_passphrase.to_s
|
|
1141
1156
|
abort(release_secrets_configuration_message("gem signing passphrase was empty")) if value.empty?
|
|
1142
1157
|
rescue Kettle::Dev::Error => error
|
|
1143
1158
|
abort(release_secrets_configuration_message(error.message))
|
|
1144
1159
|
end
|
|
1145
1160
|
|
|
1161
|
+
def keep_release_secrets_alive!(purpose)
|
|
1162
|
+
return unless release_secrets_configured?
|
|
1163
|
+
return unless @secrets_provider.respond_to?(:keepalive!)
|
|
1164
|
+
|
|
1165
|
+
emit_secret_event(action: "keepalive", status: "started", purpose: purpose, source: release_secrets_provider_label, elapsed: release_elapsed_label)
|
|
1166
|
+
@secrets_provider.keepalive!(elapsed: release_elapsed_label)
|
|
1167
|
+
emit_secret_event(action: "keepalive", status: "ok", purpose: purpose, source: release_secrets_provider_label, elapsed: release_elapsed_label)
|
|
1168
|
+
rescue Kettle::Dev::Error => error
|
|
1169
|
+
emit_secret_event(action: "keepalive", status: "failed", purpose: purpose, source: release_secrets_provider_label, elapsed: release_elapsed_label, reason: error.message)
|
|
1170
|
+
abort("Release secrets provider keepalive failed before #{purpose}: #{error.message}")
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
def release_elapsed_label
|
|
1174
|
+
return nil unless @started_at
|
|
1175
|
+
|
|
1176
|
+
format_elapsed_seconds(monotonic_time - @started_at)
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def format_elapsed_seconds(seconds)
|
|
1180
|
+
total = seconds.to_f.round
|
|
1181
|
+
hours = total / 3600
|
|
1182
|
+
minutes = (total % 3600) / 60
|
|
1183
|
+
remaining_seconds = total % 60
|
|
1184
|
+
return format("%d:%02d:%02d", hours, minutes, remaining_seconds) if hours.positive?
|
|
1185
|
+
|
|
1186
|
+
format("%02d:%02d", minutes, remaining_seconds)
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1146
1189
|
def release_secrets_configuration_message(reason)
|
|
1147
1190
|
<<~MSG.strip
|
|
1148
1191
|
Release secrets provider was requested, but #{reason}.
|
|
@@ -1569,6 +1612,29 @@ module Kettle
|
|
|
1569
1612
|
Kettle::Ndjson.emit_diagnostic_event(@event_recorder, diagnostic, index: @diagnostics.length)
|
|
1570
1613
|
end
|
|
1571
1614
|
|
|
1615
|
+
def emit_secret_event(payload)
|
|
1616
|
+
status = payload.fetch(:status).to_s
|
|
1617
|
+
mark = case status
|
|
1618
|
+
when "started"
|
|
1619
|
+
">"
|
|
1620
|
+
when "ok"
|
|
1621
|
+
"."
|
|
1622
|
+
when "failed"
|
|
1623
|
+
"!"
|
|
1624
|
+
else
|
|
1625
|
+
"?"
|
|
1626
|
+
end
|
|
1627
|
+
Kettle::Ndjson.emit_event(
|
|
1628
|
+
@event_recorder,
|
|
1629
|
+
"secret_provider",
|
|
1630
|
+
payload.merge(
|
|
1631
|
+
phase: "release",
|
|
1632
|
+
provider: release_secrets_provider_label,
|
|
1633
|
+
mark: mark
|
|
1634
|
+
)
|
|
1635
|
+
)
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1572
1638
|
def finish_release_report(status:, error:)
|
|
1573
1639
|
return if @finished_report
|
|
1574
1640
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kettle
|
|
4
|
+
module Dev
|
|
5
|
+
module ReleaseNotifier
|
|
6
|
+
class << self
|
|
7
|
+
def alert(message, stream: $stderr)
|
|
8
|
+
return unless enabled?
|
|
9
|
+
|
|
10
|
+
stream.write("\a") if terminal_bell?
|
|
11
|
+
stream.puts(message)
|
|
12
|
+
stream.flush if stream.respond_to?(:flush)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def enabled?
|
|
18
|
+
!ENV.fetch("KETTLE_RELEASE_SECRET_ALERT", "true").match?(/\A(?:false|0|no|off|disabled)\z/i)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def terminal_bell?
|
|
22
|
+
!ENV.fetch("KETTLE_RELEASE_SECRET_BELL", "true").match?(/\A(?:false|0|no|off|disabled)\z/i)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "kettle/dev/release_notifier"
|
|
4
5
|
|
|
5
6
|
module Kettle
|
|
6
7
|
module Dev
|
|
@@ -15,6 +16,10 @@ module Kettle
|
|
|
15
16
|
def rubygems_otp
|
|
16
17
|
nil
|
|
17
18
|
end
|
|
19
|
+
|
|
20
|
+
def keepalive!(elapsed: nil)
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
class OnePassword < Provider
|
|
@@ -64,6 +69,14 @@ module Kettle
|
|
|
64
69
|
run_op(argv, purpose: "RubyGems OTP")
|
|
65
70
|
end
|
|
66
71
|
|
|
72
|
+
def keepalive!(elapsed: nil)
|
|
73
|
+
argv = [op_cli, "account", "get"]
|
|
74
|
+
account = string_config("account")
|
|
75
|
+
argv.concat(["--account", account]) unless account.empty?
|
|
76
|
+
run_op(argv, purpose: "authorization keepalive", elapsed: elapsed)
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
private
|
|
68
81
|
|
|
69
82
|
attr_reader :config
|
|
@@ -99,7 +112,9 @@ module Kettle
|
|
|
99
112
|
required_config("cli")
|
|
100
113
|
end
|
|
101
114
|
|
|
102
|
-
def run_op(argv, purpose:)
|
|
115
|
+
def run_op(argv, purpose:, elapsed: nil)
|
|
116
|
+
elapsed_suffix = elapsed.to_s.empty? ? "" : " (elapsed #{elapsed})"
|
|
117
|
+
Kettle::Dev::ReleaseNotifier.alert("1Password #{purpose} lookup starting#{elapsed_suffix}; watch for authorization prompt.")
|
|
103
118
|
stdout, stderr, status = Open3.capture3(*argv)
|
|
104
119
|
return stdout.to_s.strip if status.success? && !stdout.to_s.strip.empty?
|
|
105
120
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Kettle
|
|
8
|
+
module Dev
|
|
9
|
+
class ReleaseSecretsDoctor
|
|
10
|
+
DEFAULT_SLEEP_SECONDS = 30.0
|
|
11
|
+
DEFAULT_KEEPALIVE_SECONDS = 0.0
|
|
12
|
+
DEFAULT_PROVIDER = "op"
|
|
13
|
+
DEFAULT_SHAPE = "same-process"
|
|
14
|
+
CHILD_SHAPE = "same-process"
|
|
15
|
+
SHAPES = %w[same-process child-process parent-child thread bundler-child env-reset-child].freeze
|
|
16
|
+
BUNDLER_ENV_UNSETS = %w[
|
|
17
|
+
BUNDLE_BIN_PATH
|
|
18
|
+
BUNDLE_FROZEN
|
|
19
|
+
BUNDLE_GEMFILE
|
|
20
|
+
BUNDLE_LOCKFILE
|
|
21
|
+
BUNDLER_SETUP
|
|
22
|
+
BUNDLER_VERSION
|
|
23
|
+
RUBYLIB
|
|
24
|
+
RUBYOPT
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def self.options_from_env(env = ENV)
|
|
28
|
+
{
|
|
29
|
+
provider: env.fetch("KETTLE_RELEASE_SECRETS_PROVIDER", DEFAULT_PROVIDER),
|
|
30
|
+
sleep_seconds: Float(env.fetch("KETTLE_RELEASE_SECRETS_DOCTOR_SLEEP", DEFAULT_SLEEP_SECONDS.to_s)),
|
|
31
|
+
keepalive_seconds: Float(env.fetch("KETTLE_RELEASE_SECRETS_DOCTOR_KEEPALIVE", DEFAULT_KEEPALIVE_SECONDS.to_s)),
|
|
32
|
+
shape: env.fetch("KETTLE_RELEASE_SECRETS_DOCTOR_SHAPE", DEFAULT_SHAPE),
|
|
33
|
+
otp: false
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(options:, program_name:, output: $stdout, system_runner: Kernel)
|
|
38
|
+
@options = normalize_options(options)
|
|
39
|
+
@program_name = program_name
|
|
40
|
+
@output = output
|
|
41
|
+
@system_runner = system_runner
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
case options.fetch(:shape)
|
|
46
|
+
when "same-process"
|
|
47
|
+
run_same_process(options)
|
|
48
|
+
when "child-process"
|
|
49
|
+
run_child_process(options)
|
|
50
|
+
when "parent-child"
|
|
51
|
+
run_parent_child(options)
|
|
52
|
+
when "thread"
|
|
53
|
+
run_thread(options)
|
|
54
|
+
when "bundler-child"
|
|
55
|
+
run_bundler_child(options)
|
|
56
|
+
when "env-reset-child"
|
|
57
|
+
run_env_reset_child(options)
|
|
58
|
+
else
|
|
59
|
+
raise Kettle::Dev::Error, "unknown shape #{options.fetch(:shape).inspect}; expected #{SHAPES.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def child_env(shape:, options: self.options)
|
|
64
|
+
env = {
|
|
65
|
+
"KETTLE_RELEASE_SECRETS_PROVIDER" => options.fetch(:provider),
|
|
66
|
+
"KETTLE_RELEASE_SECRETS_DOCTOR_SHAPE" => shape,
|
|
67
|
+
"KETTLE_RELEASE_SECRETS_DOCTOR_SLEEP" => options.fetch(:sleep_seconds).to_s,
|
|
68
|
+
"KETTLE_RELEASE_SECRETS_DOCTOR_KEEPALIVE" => options.fetch(:keepalive_seconds).to_s
|
|
69
|
+
}
|
|
70
|
+
env["KETTLE_RELEASE_SECRETS_DOCTOR_CHILD"] = "true"
|
|
71
|
+
env
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def child_args(otp: options.fetch(:otp))
|
|
75
|
+
args = [RbConfig.ruby, program_name]
|
|
76
|
+
args << "--otp" if otp
|
|
77
|
+
args
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bundler_child_args(otp: options.fetch(:otp))
|
|
81
|
+
args = ["bundle", "exec", RbConfig.ruby, program_name]
|
|
82
|
+
args << "--otp" if otp
|
|
83
|
+
args
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
attr_reader :options, :program_name, :output, :system_runner
|
|
89
|
+
|
|
90
|
+
def normalize_options(raw_options)
|
|
91
|
+
{
|
|
92
|
+
provider: raw_options.fetch(:provider, DEFAULT_PROVIDER).to_s,
|
|
93
|
+
sleep_seconds: Float(raw_options.fetch(:sleep_seconds, DEFAULT_SLEEP_SECONDS)),
|
|
94
|
+
keepalive_seconds: Float(raw_options.fetch(:keepalive_seconds, DEFAULT_KEEPALIVE_SECONDS)),
|
|
95
|
+
shape: raw_options.fetch(:shape, DEFAULT_SHAPE).to_s,
|
|
96
|
+
otp: !!raw_options.fetch(:otp, false)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def provider_for(current_options)
|
|
101
|
+
Kettle::Dev::ReleaseSecrets::Factory.build(provider_name: current_options.fetch(:provider))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run_same_process(current_options)
|
|
105
|
+
provider = provider_for(current_options)
|
|
106
|
+
output.puts("[#{timestamp}] initial keepalive")
|
|
107
|
+
provider.keepalive!
|
|
108
|
+
wait_with_keepalive(provider, current_options)
|
|
109
|
+
output.puts("[#{timestamp}] final keepalive")
|
|
110
|
+
provider.keepalive!
|
|
111
|
+
run_otp_lookup(provider) if current_options.fetch(:otp)
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run_parent_child(current_options)
|
|
116
|
+
provider = provider_for(current_options)
|
|
117
|
+
output.puts("[#{timestamp}] parent initial keepalive")
|
|
118
|
+
provider.keepalive!
|
|
119
|
+
wait_with_keepalive(provider, current_options)
|
|
120
|
+
output.puts("[#{timestamp}] parent spawning child after keepalive")
|
|
121
|
+
run_child_process(current_options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def run_child_process(current_options)
|
|
125
|
+
env = child_env(shape: CHILD_SHAPE, options: current_options)
|
|
126
|
+
args = child_args(otp: current_options.fetch(:otp))
|
|
127
|
+
output.puts("[#{timestamp}] spawning child: #{args.shelljoin}")
|
|
128
|
+
run_system(env, args)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def run_bundler_child(current_options)
|
|
132
|
+
env = child_env(shape: CHILD_SHAPE, options: current_options)
|
|
133
|
+
args = bundler_child_args(otp: current_options.fetch(:otp))
|
|
134
|
+
output.puts("[#{timestamp}] spawning bundler child: #{args.shelljoin}")
|
|
135
|
+
run_system(env, args)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def run_env_reset_child(current_options)
|
|
139
|
+
env = child_env(shape: CHILD_SHAPE, options: current_options)
|
|
140
|
+
BUNDLER_ENV_UNSETS.each { |key| env[key] = nil }
|
|
141
|
+
args = child_args(otp: current_options.fetch(:otp))
|
|
142
|
+
output.puts("[#{timestamp}] spawning env-reset child: #{args.shelljoin}")
|
|
143
|
+
run_system(env, args)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def run_thread(current_options)
|
|
147
|
+
error = nil
|
|
148
|
+
# rubocop:disable ThreadSafety/NewThread -- this doctor intentionally probes threaded secret-provider behavior.
|
|
149
|
+
thread = Thread.new do
|
|
150
|
+
begin
|
|
151
|
+
run_same_process(current_options)
|
|
152
|
+
rescue => e
|
|
153
|
+
error = e
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
157
|
+
thread.join
|
|
158
|
+
raise error if error
|
|
159
|
+
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def run_system(env, args)
|
|
164
|
+
success = system_runner.system(env, *args)
|
|
165
|
+
raise Kettle::Dev::Error, "child shape failed: #{args.shelljoin}" unless success
|
|
166
|
+
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def wait_with_keepalive(provider, current_options)
|
|
171
|
+
sleep_seconds = current_options.fetch(:sleep_seconds)
|
|
172
|
+
keepalive_seconds = current_options.fetch(:keepalive_seconds)
|
|
173
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + sleep_seconds
|
|
174
|
+
if keepalive_seconds <= 0
|
|
175
|
+
output.puts("[#{timestamp}] sleeping #{sleep_seconds}s without keepalive")
|
|
176
|
+
sleep(sleep_seconds)
|
|
177
|
+
return
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
output.puts("[#{timestamp}] sleeping #{sleep_seconds}s with #{keepalive_seconds}s keepalive")
|
|
181
|
+
loop do
|
|
182
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
183
|
+
break if remaining <= 0
|
|
184
|
+
|
|
185
|
+
sleep([remaining, keepalive_seconds].min)
|
|
186
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
187
|
+
|
|
188
|
+
output.puts("[#{timestamp}] keepalive")
|
|
189
|
+
provider.keepalive!
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def run_otp_lookup(provider)
|
|
194
|
+
output.puts("[#{timestamp}] OTP lookup")
|
|
195
|
+
otp = provider.rubygems_otp
|
|
196
|
+
output.puts("[#{timestamp}] OTP lookup returned #{otp.to_s.empty? ? "empty" : "#{otp.length} chars"}")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def timestamp
|
|
200
|
+
Time.now.iso8601
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
data/lib/kettle/dev/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "2.5.
|
|
8
|
+
VERSION = "2.5.12"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/kettle/dev.rb
CHANGED
|
@@ -46,8 +46,10 @@ module Kettle
|
|
|
46
46
|
autoload :LockfileReset, "kettle/dev/lockfile_reset"
|
|
47
47
|
autoload :ReadmeBackers, "kettle/dev/readme_backers"
|
|
48
48
|
autoload :OpenCollectiveConfig, "kettle/dev/open_collective_config"
|
|
49
|
+
autoload :ReleaseNotifier, "kettle/dev/release_notifier"
|
|
49
50
|
autoload :ReleaseCLI, "kettle/dev/release_cli"
|
|
50
51
|
autoload :ReleaseSecrets, "kettle/dev/release_secrets"
|
|
52
|
+
autoload :ReleaseSecretsDoctor, "kettle/dev/release_secrets_doctor"
|
|
51
53
|
autoload :ResetCLI, "kettle/dev/reset_cli"
|
|
52
54
|
autoload :PreReleaseCLI, "kettle/dev/pre_release_cli"
|
|
53
55
|
autoload :Version, "kettle/dev/version"
|
data.tar.gz.sig
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
w2��Y�S�s��E�xx���阻Qc��X���_�%�%X.��C⹎���f�,~�d�
|
|
2
|
+
O�OE�_m�Ф�P���s�!j���=�4� ��q�{��0Ӕ�Ʈ}���:���ܪ}ȿ����7yO?�����qy��z�';�%��j�5ˠ�����%F��}��*���0fVk2Cp{�
|
|
3
|
+
��#H�٩��� 嫕�d KgHV���'Ꮍ����.�!�s�U=����@�����|pq����&P���Dqd߹�?�3j2�s�}�� $�ҳ�?R|�2���T
|
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: 2.5.
|
|
4
|
+
version: 2.5.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -80,7 +80,7 @@ dependencies:
|
|
|
80
80
|
version: '0.1'
|
|
81
81
|
- - ">="
|
|
82
82
|
- !ruby/object:Gem::Version
|
|
83
|
-
version: 0.1.
|
|
83
|
+
version: 0.1.4
|
|
84
84
|
type: :runtime
|
|
85
85
|
prerelease: false
|
|
86
86
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -90,7 +90,7 @@ dependencies:
|
|
|
90
90
|
version: '0.1'
|
|
91
91
|
- - ">="
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
|
-
version: 0.1.
|
|
93
|
+
version: 0.1.4
|
|
94
94
|
- !ruby/object:Gem::Dependency
|
|
95
95
|
name: kettle-rb
|
|
96
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -327,6 +327,7 @@ executables:
|
|
|
327
327
|
- kettle-pre-release
|
|
328
328
|
- kettle-readme-backers
|
|
329
329
|
- kettle-release
|
|
330
|
+
- kettle-release-secrets-doctor
|
|
330
331
|
- kettle-reset
|
|
331
332
|
extensions: []
|
|
332
333
|
extra_rdoc_files: []
|
|
@@ -346,6 +347,7 @@ files:
|
|
|
346
347
|
- exe/kettle-pre-release
|
|
347
348
|
- exe/kettle-readme-backers
|
|
348
349
|
- exe/kettle-release
|
|
350
|
+
- exe/kettle-release-secrets-doctor
|
|
349
351
|
- exe/kettle-reset
|
|
350
352
|
- lib/kettle-dev.rb
|
|
351
353
|
- lib/kettle/dev.rb
|
|
@@ -381,7 +383,9 @@ files:
|
|
|
381
383
|
- lib/kettle/dev/rakelib/yard.rake
|
|
382
384
|
- lib/kettle/dev/readme_backers.rb
|
|
383
385
|
- lib/kettle/dev/release_cli.rb
|
|
386
|
+
- lib/kettle/dev/release_notifier.rb
|
|
384
387
|
- lib/kettle/dev/release_secrets.rb
|
|
388
|
+
- lib/kettle/dev/release_secrets_doctor.rb
|
|
385
389
|
- lib/kettle/dev/reset_cli.rb
|
|
386
390
|
- lib/kettle/dev/ruby_gems_versions.rb
|
|
387
391
|
- lib/kettle/dev/tasks.rb
|
|
@@ -397,10 +401,10 @@ licenses:
|
|
|
397
401
|
- AGPL-3.0-only
|
|
398
402
|
metadata:
|
|
399
403
|
homepage_uri: https://kettle-dev.galtzo.com
|
|
400
|
-
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.
|
|
401
|
-
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.
|
|
404
|
+
source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.12
|
|
405
|
+
changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.12/CHANGELOG.md
|
|
402
406
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
|
|
403
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.
|
|
407
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.12
|
|
404
408
|
funding_uri: https://github.com/sponsors/pboling
|
|
405
409
|
wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
|
|
406
410
|
news_uri: https://www.railsbling.com/tags/kettle-dev
|
metadata.gz.sig
CHANGED
|
Binary file
|