kettle-dev 3.0.25 → 3.0.27

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cd8d91c81ccdd9583d9891fca16e33847e99c5f27d7730c7d10f45cd4964c8f
4
- data.tar.gz: 71bfd99738051fb53d744386489a866a90c9477613d1af2a82921bb7a4a51523
3
+ metadata.gz: f2c4229bf9687d5eeb37b3969ae097bd1826819756082e26053970f9c953048f
4
+ data.tar.gz: 6eaf217455b0f4215bb3c4699e0a5651d1367524208567121ea869a482cf9ca9
5
5
  SHA512:
6
- metadata.gz: 350321f82985a5dc9fd911e2e0fb11c9c5441cbc3abe8d43d64a550bab64c5a9f5a327700780eb191eeb11dbb7d5215492f5c2150f37f46618a7440d493fefb1
7
- data.tar.gz: 45f92406c0e64bd0d16252bb14099746aac35b7a346309497ef3e8fc3d9b30adaf2497ced7b93aca7535406cecb7d6e9e840b85f2171b72ddfef18e9b3947e27
6
+ metadata.gz: 21ba6652f5ed37d6b67674b7869604c4700a94b8618185c8891e652271de5c71b85ea9eea576b5936ec1c43715c4a95f18ef9c117b3f2b74fac07546ad1d3fed
7
+ data.tar.gz: dc321c0c5531ff7b006bcbb83f3dee87a693b70b000619e4907e7e037fd597791446b1e75907a2895cc6135f0c25af62caaf4e782baa9ef4f089dca8ac504255
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,32 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.0.27] - 2026-08-31
34
+
35
+ - TAG: [v3.0.27][3.0.27t]
36
+ - COVERAGE: 91.68% -- 5998/6542 lines in 47 files
37
+ - BRANCH COVERAGE: 76.20% -- 2296/3013 branches in 47 files
38
+ - 50.24% documented
39
+
40
+ ### Added
41
+
42
+ - Emit GitHub Release creation and asset-upload lifecycle events, including retry attempts.
43
+
44
+ ### Fixed
45
+
46
+ - Retry transient GitHub Release asset uploads and report every failed asset.
47
+
48
+ ## [3.0.26] - 2026-08-31
49
+
50
+ - TAG: [v3.0.26][3.0.26t]
51
+ - COVERAGE: 91.60% -- 5967/6514 lines in 47 files
52
+ - BRANCH COVERAGE: 76.17% -- 2286/3001 branches in 47 files
53
+ - 50.47% documented
54
+
55
+ ### Fixed
56
+
57
+ - Refresh existing GitHub Release notes while backfilling missing assets.
58
+
33
59
  ## [3.0.25] - 2026-08-31
34
60
 
35
61
  - TAG: [v3.0.25][3.0.25t]
@@ -3736,7 +3762,11 @@ Please file a bug if you notice a violation of semantic versioning.
3736
3762
  - Selecting will run the selected workflow via `act`
3737
3763
  - This may move to its own gem in the future.
3738
3764
 
3739
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.25...HEAD
3765
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.27...HEAD
3766
+ [3.0.27]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.26...v3.0.27
3767
+ [3.0.27t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.27
3768
+ [3.0.26]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.25...v3.0.26
3769
+ [3.0.26t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.26
3740
3770
  [3.0.25]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.24...v3.0.25
3741
3771
  [3.0.25t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.25
3742
3772
  [3.0.24]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.23...v3.0.24
data/README.md CHANGED
@@ -1066,7 +1066,7 @@ Thanks for RTFM. ☺️
1066
1066
  [📌gitmoji]: https://gitmoji.dev
1067
1067
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1068
1068
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1069
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.516-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1069
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.542-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1070
1070
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1071
1071
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1072
1072
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -26,6 +26,7 @@ $LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include
26
26
 
27
27
  begin
28
28
  require "kettle/dev"
29
+ require "kettle/ndjson"
29
30
  require "kettle/dev/executable_version"
30
31
  Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV)
31
32
  Kettle::Dev::ExecutableVersion.print_header(script_basename)
@@ -52,7 +53,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
52
53
  version=<VERSION> # Backward-compatible explicit version form
53
54
  --check # Verify RubyGems, remote tag, changelog, and token without creating a release
54
55
  --update # Update an existing GitHub Release title/body from CHANGELOG.md
55
- --events # Emit the final result as one NDJSON event
56
+ --events # Emit GitHub Release lifecycle and asset NDJSON events
56
57
  --allow-unpublished # Skip the RubyGems publication requirement (for intentionally non-RubyGems projects)
57
58
  --asset PATH # Attach an artifact to the GitHub release (repeatable)
58
59
 
@@ -106,7 +107,8 @@ if !explicit_version.nil? && explicit_version.empty?
106
107
  end
107
108
 
108
109
  begin
109
- cli = Kettle::Dev::ReleaseCLI.new(start_step: 18)
110
+ event_stream = events ? Kettle::Ndjson.event_stream($stdout, types: "github_release") : nil
111
+ cli = Kettle::Dev::ReleaseCLI.new(start_step: 18, event_stream: event_stream)
110
112
  version = explicit_version || cli.send(:detect_version)
111
113
  result = cli.send(:github_release_backfill_check, version, require_rubygems: !allow_unpublished)
112
114
 
@@ -135,22 +137,11 @@ begin
135
137
  exit 1 unless result.fetch("ok")
136
138
  exit 0 if check_only
137
139
 
138
- created, message = if update_only
140
+ created, _message = if update_only
139
141
  cli.send(:maybe_update_github_release!, version)
140
142
  else
141
143
  cli.send(:maybe_create_github_release!, version, assets: assets)
142
144
  end
143
- if events
144
- puts JSON.generate(
145
- event_version: 1,
146
- type: "github_release",
147
- action: update_only ? "update" : "create",
148
- status: created ? "ok" : "failed",
149
- version: result.fetch("version"),
150
- tag: result.fetch("tag"),
151
- result: message
152
- )
153
- end
154
145
  exit(created ? 0 : 1)
155
146
  rescue LoadError => e
156
147
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
@@ -7,6 +7,7 @@ require "shellwords"
7
7
  require "time"
8
8
  require "fileutils"
9
9
  require "net/http"
10
+ require "openssl"
10
11
  require "json"
11
12
  require "uri"
12
13
  require "yaml"
@@ -27,6 +28,8 @@ module Kettle
27
28
  class ReleaseCLI
28
29
  RUBYGEMS_INVALID_OTP = /Your OTP code is incorrect\. Please check it and retry\./.freeze
29
30
  OTP_RETRY_DELAY_SECONDS = 2
31
+ GITHUB_RELEASE_ASSET_UPLOAD_ATTEMPTS = 3
32
+ GITHUB_RELEASE_ASSET_UPLOAD_RETRY_DELAY_SECONDS = 1
30
33
  QUIET_ENV = {
31
34
  "KETTLE_JEM_QUIET" => "true",
32
35
  "KETTLE_JEM_DEBUG" => "false",
@@ -2880,7 +2883,9 @@ module Kettle
2880
2883
 
2881
2884
  tag = "v#{version}"
2882
2885
  puts "Creating GitHub release #{owner}/#{repo} #{tag}..."
2886
+ emit_github_release_event(action: "create", status: "started", tag: tag, assets: Array(assets).length)
2883
2887
  ok, msg = github_create_release(owner: owner, repo: repo, token: token, tag: tag, title: tag, body: body, assets: assets)
2888
+ emit_github_release_event(action: "create", status: ok ? "ok" : "failed", tag: tag, assets: Array(assets).length, reason: msg)
2884
2889
  if ok
2885
2890
  puts "GitHub release created for #{tag}."
2886
2891
  else
@@ -3063,17 +3068,18 @@ module Kettle
3063
3068
 
3064
3069
  release = JSON.parse(res.body)
3065
3070
  asset_messages = Array(assets).map { |asset| github_upload_release_asset(release.fetch("id"), owner: owner, repo: repo, token: token, path: asset) }
3066
- failed_asset = asset_messages.find { |ok, _message| !ok }
3067
- failed_asset || [true, "created with #{asset_messages.length} assets"]
3071
+ github_release_asset_result(asset_messages, "created")
3068
3072
  else
3069
3073
  if res.code.to_s == "422" && res.body.to_s.include?("already_exists")
3070
3074
  return [true, "already exists"] if Array(assets).empty?
3071
3075
 
3072
- upload_missing_github_release_assets(
3076
+ update_existing_github_release_with_assets(
3073
3077
  owner: owner,
3074
3078
  repo: repo,
3075
3079
  token: token,
3076
3080
  tag: tag,
3081
+ title: title,
3082
+ body: body,
3077
3083
  assets: assets
3078
3084
  )
3079
3085
  else
@@ -3084,20 +3090,36 @@ module Kettle
3084
3090
  [false, "#{e.class}: #{e.message}"]
3085
3091
  end
3086
3092
 
3087
- def upload_missing_github_release_assets(owner:, repo:, token:, tag:, assets:)
3093
+ def update_existing_github_release_with_assets(owner:, repo:, token:, tag:, title:, body:, assets:)
3088
3094
  release, error = github_release_for_tag(owner: owner, repo: repo, token: token, tag: tag)
3089
3095
  return [false, error] unless release
3090
3096
 
3097
+ updated, error = github_update_release_by_id(
3098
+ release.fetch("id"),
3099
+ owner: owner,
3100
+ repo: repo,
3101
+ token: token,
3102
+ title: title,
3103
+ body: body
3104
+ )
3105
+ return [false, error] unless updated
3106
+
3091
3107
  existing_names = Array(release["assets"]).filter_map { |asset| asset["name"] }.to_set
3092
3108
  missing_assets = Array(assets).reject { |asset| existing_names.include?(File.basename(asset)) }
3093
- return [true, "already exists with all assets present"] if missing_assets.empty?
3109
+ return [true, "updated existing release with all assets present"] if missing_assets.empty?
3094
3110
 
3095
3111
  asset_messages = missing_assets.map do |asset|
3096
3112
  github_upload_release_asset(release.fetch("id"), owner: owner, repo: repo, token: token, path: asset)
3097
3113
  end
3098
- failed_asset = asset_messages.find { |ok, _message| !ok }
3114
+ github_release_asset_result(asset_messages, "updated existing release")
3115
+ end
3116
+
3117
+ def github_release_asset_result(asset_messages, success_prefix)
3118
+ failures = asset_messages.reject(&:first).map(&:last)
3119
+ return [false, failures.join("; ")] unless failures.empty?
3120
+
3099
3121
  asset_noun = asset_messages.one? ? "asset" : "assets"
3100
- failed_asset || [true, "already exists with #{asset_messages.length} #{asset_noun} uploaded"]
3122
+ [true, "#{success_prefix} with #{asset_messages.length} #{asset_noun} uploaded"]
3101
3123
  end
3102
3124
 
3103
3125
  def github_release_for_tag(owner:, repo:, token:, tag:)
@@ -3118,6 +3140,42 @@ module Kettle
3118
3140
  path = File.expand_path(path)
3119
3141
  return [false, "asset does not exist: #{path}"] unless File.file?(path)
3120
3142
 
3143
+ attempts = GITHUB_RELEASE_ASSET_UPLOAD_ATTEMPTS
3144
+ attempts.times do |attempt|
3145
+ emit_github_release_event(
3146
+ action: "asset_upload",
3147
+ status: "started",
3148
+ asset: File.basename(path),
3149
+ attempt: attempt + 1,
3150
+ attempts: attempts
3151
+ )
3152
+ result = github_upload_release_asset_once(release_id, owner: owner, repo: repo, token: token, path: path)
3153
+ retrying = !result.first && retryable_github_release_asset_upload_result?(result.last) && attempt < attempts - 1
3154
+ status = if result.first
3155
+ "ok"
3156
+ elsif retrying
3157
+ "retrying"
3158
+ else
3159
+ "failed"
3160
+ end
3161
+ emit_github_release_event(
3162
+ action: "asset_upload",
3163
+ status: status,
3164
+ asset: File.basename(path),
3165
+ attempt: attempt + 1,
3166
+ attempts: attempts,
3167
+ reason: result.first ? nil : result.last
3168
+ )
3169
+ return result unless retrying
3170
+
3171
+ warn("GitHub release asset upload failed for #{File.basename(path)}; retrying #{attempt + 2}/#{attempts}.")
3172
+ sleep(GITHUB_RELEASE_ASSET_UPLOAD_RETRY_DELAY_SECONDS)
3173
+ end
3174
+ rescue => e
3175
+ [false, "asset #{File.basename(path)}: #{e.class}: #{e.message}"]
3176
+ end
3177
+
3178
+ def github_upload_release_asset_once(release_id, owner:, repo:, token:, path:)
3121
3179
  uri = URI("https://uploads.github.com/repos/#{owner}/#{repo}/releases/#{release_id}/assets")
3122
3180
  uri.query = URI.encode_www_form(name: File.basename(path))
3123
3181
  req = Net::HTTP::Post.new(uri)
@@ -3130,20 +3188,39 @@ module Kettle
3130
3188
  return [true, File.basename(path)] if res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPCreated)
3131
3189
 
3132
3190
  [false, "asset #{File.basename(path)}: HTTP #{res.code}: #{res.body}"]
3133
- rescue => e
3191
+ rescue OpenSSL::SSL::SSLError, IOError, SystemCallError, Timeout::Error => e
3134
3192
  [false, "asset #{File.basename(path)}: #{e.class}: #{e.message}"]
3135
3193
  end
3136
3194
 
3195
+ def retryable_github_release_asset_upload_result?(message)
3196
+ message.match?(/(?:OpenSSL::SSL::SSLError|IOError|Errno::|(?:Net::)?(?:Open|Read)Timeout|HTTP (?:429|5\d\d))/)
3197
+ end
3198
+
3199
+ def emit_github_release_event(action:, status:, tag: nil, asset: nil, assets: nil, attempt: nil, attempts: nil, reason: nil)
3200
+ Kettle::Ndjson.emit_event(
3201
+ @event_recorder,
3202
+ "github_release",
3203
+ action: action,
3204
+ phase: "release",
3205
+ status: status,
3206
+ tag: tag,
3207
+ asset: asset,
3208
+ assets: assets,
3209
+ attempt: attempt,
3210
+ attempts: attempts,
3211
+ reason: reason,
3212
+ mark: release_progress_mark(status)
3213
+ )
3214
+ end
3215
+
3137
3216
  def github_update_release(owner:, repo:, token:, tag:, title:, body:)
3138
- lookup = URI("https://api.github.com/repos/#{owner}/#{repo}/releases/tags/#{tag}")
3139
- get = Net::HTTP::Get.new(lookup)
3140
- get["Accept"] = "application/vnd.github+json"
3141
- get["Authorization"] = "token #{token}"
3142
- get["User-Agent"] = "kettle-dev-release-cli"
3143
- release = Net::HTTP.start(lookup.host, lookup.port, use_ssl: true) { |http| http.request(get) }
3144
- return [false, "release #{tag} was not found (HTTP #{release.code})"] unless release.is_a?(Net::HTTPSuccess)
3145
-
3146
- id = JSON.parse(release.body).fetch("id")
3217
+ release, error = github_release_for_tag(owner: owner, repo: repo, token: token, tag: tag)
3218
+ return [false, error] unless release
3219
+
3220
+ github_update_release_by_id(release.fetch("id"), owner: owner, repo: repo, token: token, title: title, body: body)
3221
+ end
3222
+
3223
+ def github_update_release_by_id(id, owner:, repo:, token:, title:, body:)
3147
3224
  uri = URI("https://api.github.com/repos/#{owner}/#{repo}/releases/#{id}")
3148
3225
  request = Net::HTTP::Patch.new(uri)
3149
3226
  request["Accept"] = "application/vnd.github+json"
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "3.0.25"
8
+ VERSION = "3.0.27"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
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: 3.0.25
4
+ version: 3.0.27
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.14
83
+ version: 0.1.15
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.14
93
+ version: 0.1.15
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: kettle-rb
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -400,10 +400,10 @@ licenses:
400
400
  - AGPL-3.0-only
401
401
  metadata:
402
402
  homepage_uri: https://kettle-dev.galtzo.com
403
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.0.25
404
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.0.25/CHANGELOG.md
403
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.0.27
404
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.0.27/CHANGELOG.md
405
405
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
406
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.0.25
406
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.0.27
407
407
  funding_uri: https://github.com/sponsors/pboling
408
408
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
409
409
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file