kettle-dev 2.5.11 → 2.5.13
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 +88 -1
- data/README.md +1 -1
- data/exe/kettle-changelog +25 -3
- data/exe/kettle-release +26 -2
- data/exe/kettle-release-secrets-doctor +39 -0
- data/lib/kettle/dev/bundler_env_guard.rb +1 -0
- data/lib/kettle/dev/changelog_cli.rb +43 -1
- data/lib/kettle/dev/ci_monitor.rb +235 -7
- 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 +49 -4
- data/lib/kettle/dev/release_cli.rb +342 -18
- 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 +0 -0
- metadata +10 -6
- metadata.gz.sig +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "net/http"
|
|
7
|
+
require "kettle/ndjson"
|
|
7
8
|
|
|
8
9
|
module Kettle
|
|
9
10
|
module Dev
|
|
@@ -47,10 +48,10 @@ module Kettle
|
|
|
47
48
|
#
|
|
48
49
|
# @param restart_hint [String] guidance command shown on failure
|
|
49
50
|
# @return [void]
|
|
50
|
-
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, **options)
|
|
51
|
+
def monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: nil, keepalive: nil, event_recorder: nil, **options)
|
|
51
52
|
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)
|
|
53
|
+
checks_any |= monitor_github_internal!(restart_hint: restart_hint, workflows: workflows, keepalive: keepalive, event_recorder: event_recorder)
|
|
54
|
+
checks_any |= monitor_gitlab_internal!(restart_hint: restart_hint, keepalive: keepalive, event_recorder: event_recorder)
|
|
54
55
|
abort("CI configuration not detected (GitHub or GitLab). Ensure CI is configured and remotes point to the correct hosts.") unless checks_any
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -267,7 +268,7 @@ module Kettle
|
|
|
267
268
|
end
|
|
268
269
|
module_function :collect_gitlab
|
|
269
270
|
|
|
270
|
-
def monitor_github_internal!(restart_hint:, workflows: nil)
|
|
271
|
+
def monitor_github_internal!(restart_hint:, workflows: nil, keepalive: nil, event_recorder: nil)
|
|
271
272
|
root = Kettle::Dev::CIHelpers.project_root
|
|
272
273
|
workflows = Array(workflows).empty? ? Kettle::Dev::CIHelpers.workflows_list(root) : Array(workflows)
|
|
273
274
|
gh_remote = preferred_github_remote
|
|
@@ -288,8 +289,31 @@ module Kettle
|
|
|
288
289
|
|
|
289
290
|
passed = {}
|
|
290
291
|
started = {}
|
|
292
|
+
emit_ci_monitor_event(
|
|
293
|
+
event_recorder,
|
|
294
|
+
action: "github_discover",
|
|
295
|
+
status: "started",
|
|
296
|
+
provider: "github",
|
|
297
|
+
remote: gh_remote,
|
|
298
|
+
owner: owner,
|
|
299
|
+
repo: repo,
|
|
300
|
+
branch: branch,
|
|
301
|
+
head_sha: head_sha,
|
|
302
|
+
workflows: workflows,
|
|
303
|
+
total: total
|
|
304
|
+
)
|
|
291
305
|
puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
|
|
292
306
|
puts "Waiting for GitHub Actions runs to start for HEAD #{head_sha[0, 12]}."
|
|
307
|
+
emit_ci_monitor_event(
|
|
308
|
+
event_recorder,
|
|
309
|
+
action: "github_wait",
|
|
310
|
+
status: "started",
|
|
311
|
+
provider: "github",
|
|
312
|
+
branch: branch,
|
|
313
|
+
head_sha: head_sha,
|
|
314
|
+
completed: 0,
|
|
315
|
+
total: total
|
|
316
|
+
)
|
|
293
317
|
pbar = if defined?(ProgressBar)
|
|
294
318
|
ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
|
|
295
319
|
end
|
|
@@ -306,27 +330,107 @@ module Kettle
|
|
|
306
330
|
start_timeout = github_start_timeout
|
|
307
331
|
poll_interval = github_poll_interval
|
|
308
332
|
start_deadline = monotonic_time + start_timeout
|
|
333
|
+
keepalive_timer = keepalive_timer(keepalive)
|
|
309
334
|
idx = 0
|
|
335
|
+
all_started_emitted = false
|
|
310
336
|
loop do
|
|
337
|
+
keepalive_timer.call
|
|
311
338
|
wf = workflows[idx]
|
|
312
339
|
run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch, require_head: true, head_sha: head_sha)
|
|
340
|
+
completed_workflow = nil
|
|
313
341
|
if run
|
|
314
|
-
started[wf]
|
|
342
|
+
unless started[wf]
|
|
343
|
+
started[wf] = true
|
|
344
|
+
emit_ci_monitor_event(
|
|
345
|
+
event_recorder,
|
|
346
|
+
action: "github_workflow",
|
|
347
|
+
status: "started",
|
|
348
|
+
provider: "github",
|
|
349
|
+
workflow: wf,
|
|
350
|
+
url: run["html_url"],
|
|
351
|
+
run_status: run["status"],
|
|
352
|
+
conclusion: run["conclusion"],
|
|
353
|
+
completed: passed.size,
|
|
354
|
+
total: total
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
if started.size == total && !all_started_emitted
|
|
358
|
+
all_started_emitted = true
|
|
359
|
+
emit_ci_monitor_event(
|
|
360
|
+
event_recorder,
|
|
361
|
+
action: "github_started",
|
|
362
|
+
status: "ok",
|
|
363
|
+
provider: "github",
|
|
364
|
+
completed: passed.size,
|
|
365
|
+
total: total,
|
|
366
|
+
started: started.size
|
|
367
|
+
)
|
|
368
|
+
end
|
|
315
369
|
if Kettle::Dev::CIHelpers.success?(run)
|
|
316
370
|
unless passed[wf]
|
|
317
371
|
passed[wf] = true
|
|
372
|
+
completed_workflow = wf
|
|
318
373
|
pbar&.increment
|
|
374
|
+
emit_ci_monitor_event(
|
|
375
|
+
event_recorder,
|
|
376
|
+
action: "github_workflow",
|
|
377
|
+
status: "ok",
|
|
378
|
+
provider: "github",
|
|
379
|
+
workflow: wf,
|
|
380
|
+
url: run["html_url"],
|
|
381
|
+
run_status: run["status"],
|
|
382
|
+
conclusion: run["conclusion"],
|
|
383
|
+
completed: passed.size,
|
|
384
|
+
total: total
|
|
385
|
+
)
|
|
319
386
|
end
|
|
320
387
|
elsif Kettle::Dev::CIHelpers.failed?(run)
|
|
321
388
|
puts
|
|
322
389
|
wf_url = run["html_url"] || "https://github.com/#{owner}/#{repo}/actions/workflows/#{wf}"
|
|
390
|
+
emit_ci_monitor_event(
|
|
391
|
+
event_recorder,
|
|
392
|
+
action: "github_workflow",
|
|
393
|
+
status: "failed",
|
|
394
|
+
provider: "github",
|
|
395
|
+
workflow: wf,
|
|
396
|
+
url: wf_url,
|
|
397
|
+
run_status: run["status"],
|
|
398
|
+
conclusion: run["conclusion"],
|
|
399
|
+
completed: passed.size,
|
|
400
|
+
total: total,
|
|
401
|
+
reason: "Workflow failed"
|
|
402
|
+
)
|
|
323
403
|
abort("Workflow failed: #{wf} -> #{wf_url} Fix the workflow, then restart this tool from CI validation with: #{restart_hint}")
|
|
324
404
|
end
|
|
325
405
|
end
|
|
406
|
+
emit_ci_monitor_event(
|
|
407
|
+
event_recorder,
|
|
408
|
+
action: "github_tick",
|
|
409
|
+
status: "started",
|
|
410
|
+
provider: "github",
|
|
411
|
+
workflow: wf,
|
|
412
|
+
run_status: run && run["status"],
|
|
413
|
+
conclusion: run && run["conclusion"],
|
|
414
|
+
completed_workflow: completed_workflow,
|
|
415
|
+
completed: passed.size,
|
|
416
|
+
total: total,
|
|
417
|
+
started: started.size
|
|
418
|
+
)
|
|
326
419
|
break if passed.size == total
|
|
327
420
|
if started.size < total && monotonic_time >= start_deadline
|
|
328
421
|
missing = (workflows - started.keys).join(", ")
|
|
329
422
|
puts
|
|
423
|
+
emit_ci_monitor_event(
|
|
424
|
+
event_recorder,
|
|
425
|
+
action: "github_timeout",
|
|
426
|
+
status: "failed",
|
|
427
|
+
provider: "github",
|
|
428
|
+
missing: workflows - started.keys,
|
|
429
|
+
head_sha: head_sha,
|
|
430
|
+
total: total,
|
|
431
|
+
completed: passed.size,
|
|
432
|
+
reason: "Timed out waiting for workflows to start"
|
|
433
|
+
)
|
|
330
434
|
abort("Timed out after #{start_timeout}s waiting for GitHub Actions workflows to start for HEAD #{head_sha[0, 12]}: #{missing}. Confirm GitHub Actions started, then restart this tool from CI validation with: #{restart_hint}")
|
|
331
435
|
end
|
|
332
436
|
|
|
@@ -334,6 +438,15 @@ module Kettle
|
|
|
334
438
|
sleep(poll_interval)
|
|
335
439
|
end
|
|
336
440
|
pbar&.finish unless pbar&.finished?
|
|
441
|
+
emit_ci_monitor_event(
|
|
442
|
+
event_recorder,
|
|
443
|
+
action: "github_complete",
|
|
444
|
+
status: "ok",
|
|
445
|
+
provider: "github",
|
|
446
|
+
workflows: workflows,
|
|
447
|
+
completed: passed.size,
|
|
448
|
+
total: total
|
|
449
|
+
)
|
|
337
450
|
puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
|
|
338
451
|
true
|
|
339
452
|
end
|
|
@@ -364,7 +477,32 @@ module Kettle
|
|
|
364
477
|
end
|
|
365
478
|
module_function :github_poll_interval
|
|
366
479
|
|
|
367
|
-
def
|
|
480
|
+
def keepalive_timer(keepalive)
|
|
481
|
+
interval = keepalive_interval
|
|
482
|
+
last_run = nil
|
|
483
|
+
lambda do
|
|
484
|
+
if keepalive
|
|
485
|
+
now = monotonic_time
|
|
486
|
+
if !last_run || (now - last_run) >= interval
|
|
487
|
+
keepalive.call
|
|
488
|
+
last_run = now
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
module_function :keepalive_timer
|
|
494
|
+
|
|
495
|
+
def keepalive_interval
|
|
496
|
+
seconds = begin
|
|
497
|
+
Float(ENV["KETTLE_RELEASE_SECRET_KEEPALIVE_INTERVAL"])
|
|
498
|
+
rescue
|
|
499
|
+
nil
|
|
500
|
+
end
|
|
501
|
+
(seconds && seconds >= 0) ? seconds : 240.0
|
|
502
|
+
end
|
|
503
|
+
module_function :keepalive_interval
|
|
504
|
+
|
|
505
|
+
def monitor_gitlab_internal!(restart_hint:, keepalive: nil, event_recorder: nil)
|
|
368
506
|
root = Kettle::Dev::CIHelpers.project_root
|
|
369
507
|
gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))
|
|
370
508
|
gl_remote = gitlab_remote_candidates.first
|
|
@@ -376,15 +514,39 @@ module Kettle
|
|
|
376
514
|
owner, repo = Kettle::Dev::CIHelpers.repo_info_gitlab
|
|
377
515
|
return false unless owner && repo
|
|
378
516
|
|
|
517
|
+
emit_ci_monitor_event(
|
|
518
|
+
event_recorder,
|
|
519
|
+
action: "gitlab_discover",
|
|
520
|
+
status: "started",
|
|
521
|
+
provider: "gitlab",
|
|
522
|
+
remote: gl_remote,
|
|
523
|
+
owner: owner,
|
|
524
|
+
repo: repo,
|
|
525
|
+
branch: branch,
|
|
526
|
+
total: 1
|
|
527
|
+
)
|
|
379
528
|
puts "Ensuring GitLab pipeline passes on #{branch} (#{owner}/#{repo}) via remote '#{gl_remote}'"
|
|
380
529
|
pbar = if defined?(ProgressBar)
|
|
381
530
|
ProgressBar.create(title: "CI", total: 1, format: "%t %b %c/%C", length: 30)
|
|
382
531
|
end
|
|
532
|
+
keepalive_timer = keepalive_timer(keepalive)
|
|
383
533
|
loop do
|
|
534
|
+
keepalive_timer.call
|
|
384
535
|
pipe = Kettle::Dev::CIHelpers.gitlab_latest_pipeline(owner: owner, repo: repo, branch: branch)
|
|
385
536
|
if pipe
|
|
537
|
+
pipe_url = pipe["web_url"] || "https://gitlab.com/#{owner}/#{repo}/-/pipelines"
|
|
386
538
|
if Kettle::Dev::CIHelpers.gitlab_success?(pipe)
|
|
387
539
|
pbar&.increment unless pbar&.finished?
|
|
540
|
+
emit_ci_monitor_event(
|
|
541
|
+
event_recorder,
|
|
542
|
+
action: "gitlab_pipeline",
|
|
543
|
+
status: "ok",
|
|
544
|
+
provider: "gitlab",
|
|
545
|
+
url: pipe_url,
|
|
546
|
+
run_status: pipe["status"],
|
|
547
|
+
completed: 1,
|
|
548
|
+
total: 1
|
|
549
|
+
)
|
|
388
550
|
break
|
|
389
551
|
elsif Kettle::Dev::CIHelpers.gitlab_failed?(pipe)
|
|
390
552
|
# Special-case: if failure is due to exhausted minutes/insufficient quota, treat as unknown and continue
|
|
@@ -392,27 +554,93 @@ module Kettle
|
|
|
392
554
|
if /insufficient|quota|minute/i.match?(reason)
|
|
393
555
|
puts "\nGitLab reports pipeline cannot run due to quota/minutes exhaustion. Result is unknown; continuing."
|
|
394
556
|
pbar&.finish unless pbar&.finished?
|
|
557
|
+
emit_ci_monitor_event(
|
|
558
|
+
event_recorder,
|
|
559
|
+
action: "gitlab_pipeline",
|
|
560
|
+
status: "skipped",
|
|
561
|
+
provider: "gitlab",
|
|
562
|
+
url: pipe_url,
|
|
563
|
+
run_status: pipe["status"],
|
|
564
|
+
reason: reason,
|
|
565
|
+
completed: 0,
|
|
566
|
+
total: 1
|
|
567
|
+
)
|
|
395
568
|
break
|
|
396
569
|
else
|
|
397
570
|
puts
|
|
398
|
-
|
|
571
|
+
emit_ci_monitor_event(
|
|
572
|
+
event_recorder,
|
|
573
|
+
action: "gitlab_pipeline",
|
|
574
|
+
status: "failed",
|
|
575
|
+
provider: "gitlab",
|
|
576
|
+
url: pipe_url,
|
|
577
|
+
run_status: pipe["status"],
|
|
578
|
+
reason: reason,
|
|
579
|
+
completed: 0,
|
|
580
|
+
total: 1
|
|
581
|
+
)
|
|
582
|
+
url = pipe_url
|
|
399
583
|
abort("Pipeline failed: #{url} Fix the pipeline, then restart this tool from CI validation with: #{restart_hint}")
|
|
400
584
|
end
|
|
401
585
|
elsif pipe["status"] == "blocked"
|
|
402
586
|
# Blocked pipeline (e.g., awaiting approvals) — treat as unknown and continue
|
|
403
587
|
puts "\nGitLab pipeline is blocked. Result is unknown; continuing."
|
|
404
588
|
pbar&.finish unless pbar&.finished?
|
|
589
|
+
emit_ci_monitor_event(
|
|
590
|
+
event_recorder,
|
|
591
|
+
action: "gitlab_pipeline",
|
|
592
|
+
status: "skipped",
|
|
593
|
+
provider: "gitlab",
|
|
594
|
+
url: pipe_url,
|
|
595
|
+
run_status: pipe["status"],
|
|
596
|
+
reason: "blocked",
|
|
597
|
+
completed: 0,
|
|
598
|
+
total: 1
|
|
599
|
+
)
|
|
405
600
|
break
|
|
406
601
|
end
|
|
407
602
|
end
|
|
408
603
|
sleep(1)
|
|
409
604
|
end
|
|
410
605
|
pbar&.finish unless pbar&.finished?
|
|
606
|
+
emit_ci_monitor_event(
|
|
607
|
+
event_recorder,
|
|
608
|
+
action: "gitlab_complete",
|
|
609
|
+
status: "ok",
|
|
610
|
+
provider: "gitlab",
|
|
611
|
+
completed: 1,
|
|
612
|
+
total: 1
|
|
613
|
+
)
|
|
411
614
|
puts "\nGitLab pipeline passing."
|
|
412
615
|
true
|
|
413
616
|
end
|
|
414
617
|
module_function :monitor_gitlab_internal!
|
|
415
618
|
|
|
619
|
+
def emit_ci_monitor_event(event_recorder, action:, status:, provider:, **payload)
|
|
620
|
+
mark = case status.to_s
|
|
621
|
+
when "started"
|
|
622
|
+
">"
|
|
623
|
+
when "ok", "skipped"
|
|
624
|
+
"."
|
|
625
|
+
when "failed"
|
|
626
|
+
"!"
|
|
627
|
+
else
|
|
628
|
+
"?"
|
|
629
|
+
end
|
|
630
|
+
Kettle::Ndjson.emit_event(
|
|
631
|
+
event_recorder,
|
|
632
|
+
"ci_monitor",
|
|
633
|
+
payload.merge(
|
|
634
|
+
action: action,
|
|
635
|
+
phase: "release",
|
|
636
|
+
provider: provider,
|
|
637
|
+
status: status,
|
|
638
|
+
mark: mark
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
end
|
|
642
|
+
module_function :emit_ci_monitor_event
|
|
643
|
+
|
|
416
644
|
# -- tiny wrappers around GitAdapter-like helpers used by ReleaseCLI --
|
|
417
645
|
def remotes_with_urls
|
|
418
646
|
Kettle::Dev::GitAdapter.new.remotes_with_urls
|
|
@@ -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 |
|
|
@@ -10,6 +10,7 @@ require "net/http"
|
|
|
10
10
|
require "openssl"
|
|
11
11
|
require "time"
|
|
12
12
|
require_relative "cache_progress"
|
|
13
|
+
require "kettle/ndjson"
|
|
13
14
|
begin
|
|
14
15
|
require "addressable/uri"
|
|
15
16
|
rescue LoadError
|
|
@@ -271,9 +272,10 @@ module Kettle
|
|
|
271
272
|
end
|
|
272
273
|
|
|
273
274
|
# @param check_num [Integer] 1-based index to resume from
|
|
274
|
-
def initialize(check_num: 1)
|
|
275
|
+
def initialize(check_num: 1, event_recorder: nil)
|
|
275
276
|
@check_num = (check_num || 1).to_i
|
|
276
277
|
@check_num = 1 if @check_num < 1
|
|
278
|
+
@event_recorder = event_recorder
|
|
277
279
|
@image_url_cache_path = configured_image_url_cache_path
|
|
278
280
|
@refresh_image_url_cache = env_truthy?(ENV["KETTLE_IMAGE_URL_CACHE_REFRESH"])
|
|
279
281
|
@image_url_skip_patterns = configured_image_url_skip_patterns
|
|
@@ -294,7 +296,14 @@ module Kettle
|
|
|
294
296
|
checks[begin_idx..-1].each_with_index do |check, i|
|
|
295
297
|
idx = begin_idx + i + 1
|
|
296
298
|
puts "[kettle-pre-release] Running check ##{idx} of #{checks.size}"
|
|
297
|
-
check.
|
|
299
|
+
emit_pre_release_event(action: "check", status: "started", check: check_name(check), index: idx, total: checks.size)
|
|
300
|
+
begin
|
|
301
|
+
check.call
|
|
302
|
+
emit_pre_release_event(action: "check", status: "ok", check: check_name(check), index: idx, total: checks.size)
|
|
303
|
+
rescue => error
|
|
304
|
+
emit_pre_release_event(action: "check", status: "failed", check: check_name(check), index: idx, total: checks.size, reason: "#{error.class}: #{error.message}")
|
|
305
|
+
raise
|
|
306
|
+
end
|
|
298
307
|
end
|
|
299
308
|
nil
|
|
300
309
|
end
|
|
@@ -335,8 +344,7 @@ module Kettle
|
|
|
335
344
|
modified = false
|
|
336
345
|
|
|
337
346
|
urls.each do |url_str|
|
|
338
|
-
|
|
339
|
-
normalized = addr.normalize.to_s
|
|
347
|
+
normalized = normalized_markdown_image_url(url_str)
|
|
340
348
|
next if normalized == url_str
|
|
341
349
|
|
|
342
350
|
# Replace exact occurrences of the URL in the markdown content
|
|
@@ -356,9 +364,17 @@ module Kettle
|
|
|
356
364
|
end
|
|
357
365
|
|
|
358
366
|
puts "[kettle-pre-release] Normalization candidates: #{total_candidates}. Files changed: #{changed.uniq.size}."
|
|
367
|
+
emit_pre_release_event(action: "markdown_urls", status: "ok", candidates: total_candidates, changed_files: changed.uniq.size)
|
|
359
368
|
nil
|
|
360
369
|
end
|
|
361
370
|
|
|
371
|
+
def normalized_markdown_image_url(url_str)
|
|
372
|
+
addr = Addressable::URI.parse(url_str)
|
|
373
|
+
normalized = addr.normalize
|
|
374
|
+
normalized.query = addr.query if addr.query && normalized.query != addr.query
|
|
375
|
+
normalized.to_s
|
|
376
|
+
end
|
|
377
|
+
|
|
362
378
|
# Check 3: Validate Markdown image links by cached HTTP HEAD/GET.
|
|
363
379
|
# @return [void]
|
|
364
380
|
def check_markdown_images_http!
|
|
@@ -402,10 +418,12 @@ module Kettle
|
|
|
402
418
|
puts "[kettle-pre-release] Image URL checks: #{progress.cached_count} cached, #{progress.live_count} live."
|
|
403
419
|
puts "[kettle-pre-release] Skipped #{progress.skipped_count} image URL check(s)." if skipped.any?
|
|
404
420
|
if failures.any?
|
|
421
|
+
emit_pre_release_event(action: "image_links", status: "failed", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: failures.size)
|
|
405
422
|
warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
|
|
406
423
|
failures.each { |u| warn(" - #{u}") }
|
|
407
424
|
Kettle::Dev::ExitAdapter.abort("Image link validation failed")
|
|
408
425
|
else
|
|
426
|
+
emit_pre_release_event(action: "image_links", status: "ok", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: 0)
|
|
409
427
|
puts "[kettle-pre-release] All image links validated."
|
|
410
428
|
end
|
|
411
429
|
nil
|
|
@@ -413,6 +431,33 @@ module Kettle
|
|
|
413
431
|
|
|
414
432
|
private
|
|
415
433
|
|
|
434
|
+
def check_name(check)
|
|
435
|
+
check.name.to_s.sub(/\Acheck_/, "").sub(/!\z/, "")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def emit_pre_release_event(action:, status:, **payload)
|
|
439
|
+
mark = case status.to_s
|
|
440
|
+
when "started"
|
|
441
|
+
">"
|
|
442
|
+
when "ok", "skipped"
|
|
443
|
+
"."
|
|
444
|
+
when "failed"
|
|
445
|
+
"!"
|
|
446
|
+
else
|
|
447
|
+
"?"
|
|
448
|
+
end
|
|
449
|
+
Kettle::Ndjson.emit_event(
|
|
450
|
+
@event_recorder,
|
|
451
|
+
"pre_release",
|
|
452
|
+
payload.merge(
|
|
453
|
+
action: action,
|
|
454
|
+
phase: "release",
|
|
455
|
+
status: status,
|
|
456
|
+
mark: mark
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
end
|
|
460
|
+
|
|
416
461
|
def image_url_cache
|
|
417
462
|
return nil if @image_url_cache_path.to_s.empty?
|
|
418
463
|
|