kettle-dev 2.1.1 → 2.2.0

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: 41074cb0fabc348cfb17e29cfb521661c4492792a7c5e3fab1ff32815f960355
4
- data.tar.gz: d65ce417666381f6cf72ac7f6cdcd6bea0be35c996c820d16765789229408bc9
3
+ metadata.gz: e8a1375214aa3c53580a46da791cc0813ab2303a56bf08d32c3928bb6773425a
4
+ data.tar.gz: 293ad0c152819ca21f1bf9a63cbf4f21078cff641d680ce7b955608c309d5d24
5
5
  SHA512:
6
- metadata.gz: 100dc6197c4a9329d3ae4e483feb7e3c643160a9a446939c7a3d6bb789a0ca8ba6c41f8231971548b4ba91916e55481469d17b6623998046a1cc302bf96e7825
7
- data.tar.gz: cd82a9b3ad27598611f94798fa1187171092017cbf8d28b44ff5fd3c82ed021251ba35053683276ac8dc49a77c33035ec02f3c379a42d79fbabd749d25221bba
6
+ metadata.gz: 24dce616ec2536f3f3afd7dc6b16cffe9442aa7d81f86a25dcc2867166a10c55cc6e1285ea1e91261f4f176fc309435cd8240dca7e8eaf94bbff6efdaa0ea4c2
7
+ data.tar.gz: b4fe9b7736463c18bb8e97f2c91d55257c281502b6ccfb40e51e698b5616141dac2c48d6513262b97ee810fcec1e267c31908a0d02bfbe606d21427a0684cd0e
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,70 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.2.0] - 2026-06-08
34
+
35
+ - TAG: [v2.2.0][2.2.0t]
36
+ - COVERAGE: 91.51% -- 3710/4054 lines in 28 files
37
+ - BRANCH COVERAGE: 72.90% -- 1458/2000 branches in 28 files
38
+ - 67.51% documented
39
+
40
+ ### Added
41
+
42
+ - `kettle-pre-release` now runs `kettle-gha-sha-pins --check` before release
43
+ checks, failing with an outdated-actions summary and recommended
44
+ `kettle-gha-sha-pins --write --upgrade patch` command when workflow actions
45
+ need updated SHA pins.
46
+ - `kettle-release` now runs `kettle-pre-release` as a full-release gate before
47
+ release setup starts.
48
+ - `kettle-bump` now bumps a single gem's version file before `kettle-changelog`
49
+ release prep.
50
+ - `kettle-gha-sha-pins` now shows discovery, workflow scan, and action-resolution
51
+ progress on STDERR for human runs while keeping JSON output quiet by default.
52
+ - `kettle-gha-sha-pins` now times action resolution, caches one resolution plan
53
+ per action repository, and uses GitHub REST tag-ref lookups to avoid resolving
54
+ every release tag through individual commit requests.
55
+
56
+ - `kettle-gha-sha-pins` now keeps a 24-hour persistent action release cache at
57
+ the XDG state location and supports `--refresh-cache` to bypass stale entries
58
+ while preserving cached discoveries for other projects and actions.
59
+
60
+ ### Fixed
61
+
62
+ - Minitest-only projects now keep `rake test` on the generated
63
+ `Rake::TestTask` path instead of synthesizing a `kettle-test` RSpec task when
64
+ tests live directly under `test/`.
65
+
66
+ - `kettle-release start_step=10` now waits for GitHub Actions runs for the
67
+ local HEAD SHA to appear before evaluating results, avoiding stale failures
68
+ from the previous branch run.
69
+ - Removed the duplicate `cgi` declaration from the `head` appraisal so
70
+ `kettle-release` can regenerate Appraisal gemfiles on Ruby 4.
71
+
72
+ - `kettle-gha-sha-pins` now treats version-equivalent but unresolved action
73
+ refs as invalid and converts them to release SHAs instead of accepting
74
+ stripped tag names such as `6.0.2` when only `v6.0.2` exists.
75
+ - `kettle-gha-sha-pins --write` now refreshes adjacent release-version comments
76
+ when the pinned SHA is current but the comment is stale.
77
+ - `kettle-gha-sha-pins` now uses `GH_TOKEN` or `gh auth token` as a fallback
78
+ when `GITHUB_TOKEN` is not set, avoiding false clean reports after
79
+ unauthenticated GitHub API rate limits are exhausted.
80
+ - Coverage workflow uploads now pin `codecov/codecov-action` and
81
+ `coverallsapp/github-action` to resolvable release SHAs.
82
+
83
+ - Ruby 2.4-2.6 CI appraisals now include `backports`, and Ruby 3.0-3.1
84
+ appraisals include `prism`, so `kettle-bump` and GitHub Action SHA pin specs
85
+ run on the supported legacy matrix.
86
+
87
+ - Legacy CI appraisals now skip `kettle-bump` specs when Prism is unavailable,
88
+ and the Ruby 3.2 appraisal includes `prism` so the specs run where supported.
89
+
90
+ ### Changed
91
+
92
+ - Retemplated project metadata with the latest `kettle-jem` stack, refreshing
93
+ README and security-policy generated details for the current release line.
94
+
95
+ - Runtime dependency `kettle-test` now requires 2.0.4 or newer.
96
+
33
97
  ## [2.1.1] - 2026-06-06
34
98
 
35
99
  - TAG: [v2.1.1][2.1.1t]
@@ -1913,7 +1977,9 @@ Please file a bug if you notice a violation of semantic versioning.
1913
1977
  - Selecting will run the selected workflow via `act`
1914
1978
  - This may move to its own gem in the future.
1915
1979
 
1916
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v2.1.1...HEAD
1980
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v2.2.0...HEAD
1981
+ [2.2.0]: https://github.com/kettle-rb/kettle-dev/compare/v2.1.1...v2.2.0
1982
+ [2.2.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v2.2.0
1917
1983
  [2.1.1]: https://github.com/kettle-rb/kettle-dev/compare/v2.1.0...v2.1.1
1918
1984
  [2.1.1t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v2.1.1
1919
1985
  [2.1.0]: https://github.com/kettle-rb/kettle-dev/compare/v2.0.8...v2.1.0
data/README.md CHANGED
@@ -303,6 +303,8 @@ Common local workflows:
303
303
  - `bundle exec rake appraisal:generate` regenerates Appraisal gemfiles.
304
304
  - `bundle exec rake appraisal:update` updates Appraisal locks and applies gradual RuboCop autocorrect.
305
305
  - `bundle exec rake appraisal:reset` removes Appraisal lockfiles below `gemfiles/`.
306
+ - `kettle-bump patch` bumps the current gem's patch version before running
307
+ `kettle-changelog`.
306
308
 
307
309
  GitHub Actions local runner helper:
308
310
 
@@ -373,6 +375,7 @@ What it does:
373
375
 
374
376
  - Script: `exe/kettle-release` (run as `kettle-release`)
375
377
  - Purpose: guided release helper that:
378
+ - Runs `kettle-pre-release` before the numbered release steps on full releases, aborting before release setup if any pre-release gate fails.
376
379
  - Runs sanity checks (`bin/setup`, `bin/rake`), confirms version/changelog, optionally updates Appraisals, regenerates docs via `bin/rake yard`, commits “🔖 Prepare release vX.Y.Z”.
377
380
  - Optionally runs your CI locally with `act` before any push:
378
381
  - Enable with env: `K_RELEASE_LOCAL_CI="true"` (run automatically) or `K_RELEASE_LOCAL_CI="ask"` (prompt \[Y/n\]).
@@ -403,12 +406,29 @@ What it does:
403
406
  19. Push tags to remotes (final)
404
407
  - Examples:
405
408
  - After intermittent CI failure, restart from monitoring: `bundle exec kettle-release start_step=10`
409
+ - After fixing a failed pre-release gate, rerun from the top: `bundle exec kettle-release`
406
410
  - Tips:
407
411
  - The commit message helper `exe/kettle-commit-msg` prefers project-local `.git-hooks` (then falls back to `~/.git-hooks`).
408
412
  - The goalie file `commit-subjects-goalie.txt` controls when a footer is appended; customize `footer-template.erb.txt` as you like.
409
413
 
410
414
  ### Changelog generator
411
415
 
416
+ - Script: `exe/kettle-bump` (run as `kettle-bump`)
417
+ - Purpose: Bumps the current single gem's `lib/**/version.rb` before changelog
418
+ preparation. It accepts an exact version or `major`, `minor`, or `patch`.
419
+ - Usage:
420
+ - `kettle-bump patch`
421
+ - `kettle-bump 1.2.4 --from 1.2.3`
422
+ - `kettle-bump minor --dry-run`
423
+ - `kettle-bump patch --check`
424
+ - Behavior:
425
+ - Writes by default; use `--dry-run` to preview or `--check` to fail when a
426
+ bump would change files.
427
+ - Updates literal `spec.version = "..."` assignments in the gemspec when
428
+ they match the current version. Dynamic gemspec versions are left alone.
429
+ - Uses the same `K_CHANGELOG_VERSION_FILE` override as `kettle-changelog`
430
+ when a project needs to point at a specific version file.
431
+
412
432
  - Script: `exe/kettle-changelog` (run as `kettle-changelog`)
413
433
  - Purpose: Generates a new CHANGELOG.md section for the current version read from `lib/**/version.rb`, moves notes from the Unreleased section, and updates comparison links.
414
434
  - Prerequisites:
@@ -424,6 +444,27 @@ What it does:
424
444
 
425
445
  ### Pre-release checks
426
446
 
447
+ - Script: `exe/kettle-gha-sha-pins` (run as `kettle-gha-sha-pins`)
448
+ - Purpose: Validate and optionally update GitHub Actions `uses:` refs to pinned
449
+ SHAs and current allowed release versions.
450
+ - Usage:
451
+ - `kettle-gha-sha-pins`
452
+ - `kettle-gha-sha-pins --check`
453
+ - `kettle-gha-sha-pins --write --upgrade patch`
454
+ - Behavior:
455
+ - Human output shows discovery, workflow scan, and action-resolution progress
456
+ on STDERR, including per-action timing via `ruby-progressbar`, then prints
457
+ the final report on STDOUT.
458
+ - Action metadata is resolved with the GitHub REST API and cached per
459
+ `owner/repo` action so duplicate uses of the same action reuse one
460
+ resolution plan.
461
+ - The outdated summary reports newer releases even when `--upgrade patch` or
462
+ `--upgrade minor` limits the write target to a safer release line.
463
+ - JSON output keeps progress disabled by default so STDOUT remains parseable.
464
+ Use `--progress` to force progress or `--no-progress` to suppress it.
465
+ - `--check` exits non-zero when workflow action pins are stale or mutable and
466
+ prints a recommended `kettle-gha-sha-pins --write --upgrade patch` command.
467
+
427
468
  - Script: `exe/kettle-pre-release` (run as `kettle-pre-release`)
428
469
  - Purpose: Run a suite of pre-release validations to catch avoidable mistakes (resumable by check number).
429
470
  - Usage:
@@ -432,7 +473,9 @@ What it does:
432
473
  - Options:
433
474
  - `--check-num N` Start from check number N (default: 1)
434
475
  - Checks:
435
- - 1) Validate that all image URLs referenced by Markdown files resolve (HTTP HEAD)
476
+ - 1) Validate GitHub Actions workflow action refs with `kettle-gha-sha-pins --check`; if pins are stale, it prints an outdated-actions summary, exits non-zero, and recommends `kettle-gha-sha-pins --write --upgrade patch`.
477
+ - 2) Normalize Markdown image URLs.
478
+ - 3) Validate that all image URLs referenced by Markdown files resolve (HTTP HEAD).
436
479
 
437
480
  ### Commit message helper (git hook)
438
481
 
@@ -811,7 +854,7 @@ Thanks for RTFM. ☺️
811
854
  [📌gitmoji]: https://gitmoji.dev
812
855
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
813
856
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
814
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.209-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
857
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.054-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
815
858
  [🔐security]: https://github.com/kettle-rb/kettle-dev/blob/main/SECURITY.md
816
859
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
817
860
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -839,7 +882,7 @@ Thanks for RTFM. ☺️
839
882
  | Package | kettle-dev |
840
883
  | Description | 🍲 Kettle::Dev is a meta tool from kettle-rb to streamline development and testing. Acts as a shim dependency, pulling in many other dependencies, to give you OOTB productivity with a RubyGem, or Ruby app project. Configures a complete set of Rake tasks, for all the libraries is brings in, so they arrive ready to go. Fund overlooked open source projects - bottom of stack, dev/test dependencies: floss-funding.dev |
841
884
  | Homepage | https://github.com/kettle-rb/kettle-dev |
842
- | Source | https://github.com/kettle-rb/kettle-dev/tree/v2.1.0 |
885
+ | Source | https://github.com/kettle-rb/kettle-dev/tree/v2.2.0 |
843
886
  | License | `AGPL-3.0-only` |
844
887
  | Funding | https://github.com/sponsors/pboling, https://issuehunt.io/u/pboling, https://ko-fi.com/pboling, https://liberapay.com/pboling/donate, https://opencollective.com/kettle-rb, https://patreon.com/galtzo, https://polar.sh/pboling, https://thanks.dev/u/gh/pboling, https://tidelift.com/funding/github/rubygems/kettle-dev, https://www.buymeacoffee.com/pboling |
845
888
  <!-- kettle-jem:metadata:end -->
data/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |----------|-----------|
7
- | 2.1.latest | ✅ |
7
+ | 2.2.latest | ✅ |
8
8
 
9
9
  ## Security contact information
10
10
 
data/exe/kettle-bump ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # vim: set syntax=ruby
5
+
6
+ # kettle-bump: Bump the current gem version before running kettle-changelog.
7
+
8
+ $stdout.sync = true
9
+ $stderr.sync = true
10
+
11
+ begin
12
+ require "rubygems"
13
+ rescue LoadError
14
+ # Older Rubies always have rubygems; continue anyway
15
+ end
16
+
17
+ script_basename = File.basename(__FILE__)
18
+
19
+ begin
20
+ require "kettle/dev"
21
+ puts "== #{script_basename} v#{Kettle::Dev::Version::VERSION} =="
22
+ rescue LoadError => e
23
+ warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
24
+ warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
25
+ exit(1)
26
+ end
27
+
28
+ begin
29
+ exit(Kettle::Dev::BumpCLI.new(ARGV).run!)
30
+ rescue LoadError => e
31
+ warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
32
+ warn(e.backtrace.join("\n")) if ENV["DEBUG"]
33
+ exit(1)
34
+ rescue SystemExit => e
35
+ warn("#{script_basename}: exited (status=#{e.status}, msg=#{e.message})") if e.status != 0
36
+ raise
37
+ rescue => e
38
+ warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
39
+ warn(e.backtrace.join("\n")) if ENV["DEBUG"]
40
+ exit(1)
41
+ end
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # vim: set syntax=ruby
5
+
6
+ $stdout.sync = true
7
+ $stderr.sync = true
8
+
9
+ # Do not rely on Bundler; allow running in repos that do not depend on kettle-dev
10
+ begin
11
+ require "rubygems"
12
+ rescue LoadError
13
+ # Older Rubies always have rubygems; continue anyway
14
+ end
15
+
16
+ script_basename = File.basename(__FILE__)
17
+
18
+ begin
19
+ require "kettle/dev"
20
+ rescue LoadError
21
+ repo_lib = File.expand_path("../lib", __dir__)
22
+ $LOAD_PATH.unshift(repo_lib) unless $LOAD_PATH.include?(repo_lib)
23
+ begin
24
+ require "kettle/dev"
25
+ rescue LoadError => e
26
+ warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
27
+ warn("Hint: Install the kettle-dev gem (`gem install kettle-dev`) or add it to your Gemfile. Bundler is not required for this script.")
28
+ exit(1)
29
+ end
30
+ end
31
+
32
+ begin
33
+ cli = Kettle::Dev::GhaShaPinsCLI.new(ARGV)
34
+ status = cli.run!
35
+ Kettle::Dev::ExitAdapter.exit(status)
36
+ rescue => e
37
+ warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
38
+ warn(e.backtrace.join("\n"))
39
+ Kettle::Dev::ExitAdapter.exit(1)
40
+ end
@@ -5,7 +5,7 @@
5
5
 
6
6
  # kettle-pre-release: Run pre-release checks to catch avoidable mistakes.
7
7
  # - Structured as a sequence of checks that can be resumed via check_num
8
- # - Initial check: validate all image URLs referenced by Markdown files resolve (HTTP HEAD)
8
+ # - Checks GitHub Actions SHA pins and validates Markdown image links.
9
9
 
10
10
  require "optparse"
11
11
 
@@ -45,7 +45,9 @@ parser = OptionParser.new do |opts|
45
45
  puts opts
46
46
  puts
47
47
  puts "Checks:"
48
- puts " 1) Validate Markdown image links (HTTP HEAD)"
48
+ puts " 1) Validate GitHub Actions SHA pins"
49
+ puts " 2) Normalize Markdown image URLs"
50
+ puts " 3) Validate Markdown image links (HTTP HEAD)"
49
51
  exit(0)
50
52
  end
51
53
  end
data/exe/kettle-release CHANGED
@@ -47,6 +47,10 @@ if ARGV.include?("-h") || ARGV.include?("--help")
47
47
 
48
48
  Automates the release flow for a Ruby gem in the host project.
49
49
 
50
+ Full releases run `kettle-pre-release` before step 1 and abort if any
51
+ pre-release gate fails. Resume with start_step > 1 only after the gate has
52
+ passed or after intentionally handling the failure.
53
+
50
54
  Start steps (use start_step=<n> to begin at that step):
51
55
  1. Verify Bundler >= 2.7 (always runs; start at 1 to do everything)
52
56
  2. Detect version; RubyGems sanity check; confirm CHANGELOG/version; sync copyright years; update badges/headers
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Kettle
6
+ module Dev
7
+ # CLI for bumping the current project's gem version before changelog prep.
8
+ class BumpCLI
9
+ BUMP_TYPES = %w[major minor patch].freeze
10
+
11
+ def initialize(argv = [], out: $stdout, err: $stderr, root: Kettle::Dev::CIHelpers.project_root)
12
+ @argv = argv.dup
13
+ @out = out
14
+ @err = err
15
+ @root = root
16
+ end
17
+
18
+ def run!
19
+ options = parse_options
20
+ return 0 if options.fetch(:help)
21
+
22
+ current_version = Kettle::Dev::Versioning.detect_version(root)
23
+ target_version = resolve_target_version(options.fetch(:target), current_version)
24
+ edits = collect_edits(
25
+ current_version: current_version,
26
+ target_version: target_version,
27
+ from_version: options[:from]
28
+ )
29
+ write_edits(edits) if options.fetch(:mode) == :execute
30
+
31
+ out.puts(summary(edits: edits, current_version: current_version, target_version: target_version, mode: options.fetch(:mode)))
32
+ (options.fetch(:mode) == :check && edits.any?) ? 1 : 0
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :argv, :out, :err, :root
38
+
39
+ def parse_options
40
+ options = {mode: :execute, help: false}
41
+ parser = OptionParser.new do |opts|
42
+ opts.banner = "Usage: kettle-bump VERSION|major|minor|patch [options]"
43
+ opts.on("--from VERSION", "Require the current version before bumping") { |value| options[:from] = validate_version(value) }
44
+ opts.on("--check", "Exit non-zero when the bump would change files") { options[:mode] = :check }
45
+ opts.on("--dry-run", "Print planned changes without writing files") { options[:mode] = :dry_run }
46
+ opts.on("--execute", "Write the bump to disk (default)") { options[:mode] = :execute }
47
+ opts.on("-h", "--help", "Show this help") do
48
+ out.puts(opts)
49
+ options[:help] = true
50
+ end
51
+ end
52
+ parser.parse!(argv)
53
+ options[:target] = argv.shift
54
+ raise Kettle::Dev::Error, "kettle-bump requires VERSION, major, minor, or patch" unless options[:target] || options[:help]
55
+ raise Kettle::Dev::Error, "unexpected arguments: #{argv.join(" ")}" unless argv.empty?
56
+
57
+ options
58
+ rescue OptionParser::ParseError => error
59
+ raise Kettle::Dev::Error, error.message
60
+ end
61
+
62
+ def resolve_target_version(target, current_version)
63
+ if BUMP_TYPES.include?(target)
64
+ bumped_version(target, current_version)
65
+ else
66
+ validate_version(target)
67
+ end
68
+ end
69
+
70
+ def bumped_version(type, current_version)
71
+ version = Gem::Version.new(current_version)
72
+ segments = version.segments
73
+ unless segments.all? { |segment| segment.is_a?(Integer) }
74
+ raise Kettle::Dev::Error, "cannot #{type}-bump non-numeric version #{current_version.inspect}"
75
+ end
76
+
77
+ major, minor, patch = (segments + [0, 0, 0])[0, 3]
78
+ case type
79
+ when "major"
80
+ "#{major + 1}.0.0"
81
+ when "minor"
82
+ "#{major}.#{minor + 1}.0"
83
+ when "patch"
84
+ "#{major}.#{minor}.#{patch + 1}"
85
+ end
86
+ end
87
+
88
+ def validate_version(version)
89
+ Gem::Version.new(version).to_s
90
+ rescue ArgumentError => error
91
+ raise Kettle::Dev::Error, "invalid version #{version.inspect}: #{error.message}"
92
+ end
93
+
94
+ def collect_edits(current_version:, target_version:, from_version:)
95
+ if from_version && current_version != from_version
96
+ raise Kettle::Dev::Error, "current version is #{current_version}, not --from #{from_version}"
97
+ end
98
+
99
+ version_file_edits(target_version) + gemspec_version_edits(current_version, target_version)
100
+ end
101
+
102
+ def version_file_edits(target_version)
103
+ Kettle::Dev::Versioning.version_file_candidates(root).filter_map do |path|
104
+ source = File.read(path)
105
+ node = version_string_node(source, path)
106
+ current = node.unescaped
107
+ next if current == target_version
108
+
109
+ replacement = quote_like(node.location.slice, target_version)
110
+ file_edit(path, source, node.location.start_offset, node.location.end_offset, replacement)
111
+ end
112
+ end
113
+
114
+ def gemspec_version_edits(current_version, target_version)
115
+ gemspec_path = gemspec_path_for_bump
116
+ return [] unless gemspec_path
117
+
118
+ source = File.read(gemspec_path)
119
+ parse_result = parse_source(source, gemspec_path)
120
+ each_node(parse_result.value).filter_map do |node|
121
+ next unless node.is_a?(Prism::CallNode) && node.name == :version=
122
+
123
+ version_node = node.arguments&.arguments&.first
124
+ next unless version_node.is_a?(Prism::StringNode)
125
+ next unless version_node.unescaped == current_version
126
+ next if version_node.unescaped == target_version
127
+
128
+ replacement = quote_like(version_node.location.slice, target_version)
129
+ file_edit(gemspec_path, source, version_node.location.start_offset, version_node.location.end_offset, replacement)
130
+ end
131
+ end
132
+
133
+ def gemspec_path_for_bump
134
+ paths = Dir.glob(File.join(root, "*.gemspec")).sort
135
+ return nil if paths.empty?
136
+ raise Kettle::Dev::Error, "multiple gemspecs found; kettle-bump supports single gems only" if paths.length > 1
137
+
138
+ paths.first
139
+ end
140
+
141
+ def version_string_node(source, path)
142
+ parse_result = parse_source(source, path)
143
+ constant = each_node(parse_result.value).find do |node|
144
+ node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
145
+ end
146
+ raise Kettle::Dev::Error, "could not find string VERSION constant in #{path}" unless constant
147
+
148
+ constant.value
149
+ end
150
+
151
+ def parse_source(source, path)
152
+ require_prism
153
+ parse_result = Prism.parse(source)
154
+ raise Kettle::Dev::Error, "could not parse #{path}" unless parse_result.success?
155
+
156
+ parse_result
157
+ end
158
+
159
+ def require_prism
160
+ return if defined?(Prism)
161
+
162
+ require "prism"
163
+ rescue LoadError => error
164
+ raise Kettle::Dev::Error, "kettle-bump requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
165
+ end
166
+
167
+ def each_node(root)
168
+ return enum_for(__method__, root) unless block_given?
169
+
170
+ queue = [root]
171
+ until queue.empty?
172
+ node = queue.shift
173
+ yield node
174
+ queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
175
+ end
176
+ end
177
+
178
+ def file_edit(path, source, start_offset, end_offset, replacement)
179
+ {path: path, source: source, start_offset: start_offset, end_offset: end_offset, replacement: replacement}
180
+ end
181
+
182
+ def quote_like(original, value)
183
+ quote = original.start_with?("'") ? "'" : '"'
184
+ "#{quote}#{value}#{quote}"
185
+ end
186
+
187
+ def write_edits(edits)
188
+ edits.group_by { |edit| edit.fetch(:path) }.each_value do |file_edits|
189
+ source = file_edits.first.fetch(:source)
190
+ file_edits.sort_by { |edit| -edit.fetch(:start_offset) }.each do |edit|
191
+ source[edit.fetch(:start_offset)...edit.fetch(:end_offset)] = edit.fetch(:replacement)
192
+ end
193
+ File.write(file_edits.first.fetch(:path), source)
194
+ end
195
+ end
196
+
197
+ def summary(edits:, current_version:, target_version:, mode:)
198
+ lines = ["kettle-bump: #{current_version} -> #{target_version}"]
199
+ if edits.empty?
200
+ lines << "No version changes needed."
201
+ else
202
+ verb = (mode == :execute) ? "updated" : "would update"
203
+ edits.map { |edit| edit.fetch(:path) }.uniq.each { |path| lines << "#{verb} #{Kettle::Dev.display_path(path)}" }
204
+ end
205
+ lines.join("\n")
206
+ end
207
+ end
208
+ end
209
+ end
@@ -45,6 +45,13 @@ module Kettle
45
45
  status.success? ? out.strip : nil
46
46
  end
47
47
 
48
+ # Current git commit SHA, or nil when unavailable.
49
+ # @return [String, nil]
50
+ def current_head_sha
51
+ out, status = Open3.capture2("git", "rev-parse", "HEAD")
52
+ status.success? ? out.strip : nil
53
+ end
54
+
48
55
  # List workflow YAML basenames under .github/workflows at the given root.
49
56
  # Excludes maintenance workflows defined by {#exclusions}.
50
57
  # @param root [String] project root (defaults to {#project_root})
@@ -81,15 +88,14 @@ module Kettle
81
88
  # @param branch [String, nil] branch to query; defaults to {#current_branch}
82
89
  # @param token [String, nil] OAuth token for higher rate limits; defaults to {#default_token}
83
90
  # @return [Hash{String=>String,Integer}, nil] minimal run info or nil on error/none
84
- def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token)
91
+ def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token, require_head: false, head_sha: nil)
85
92
  return unless owner && repo
86
93
 
87
94
  b = branch || current_branch
88
95
  return unless b
89
96
 
90
97
  # Scope to the exact commit SHA when available to avoid picking up a previous run on the same branch.
91
- sha_out, status = Open3.capture2("git", "rev-parse", "HEAD")
92
- sha = status.success? ? sha_out.strip : nil
98
+ sha = head_sha || current_head_sha
93
99
  base_url = "https://api.github.com/repos/#{owner}/#{repo}/actions/workflows/#{workflow_file}/runs?branch=#{URI.encode_www_form_component(b)}&per_page=5"
94
100
  uri = URI(base_url)
95
101
  req = Net::HTTP::Get.new(uri)
@@ -102,9 +108,10 @@ module Kettle
102
108
  runs = Array(data["workflow_runs"]) || []
103
109
  # Try to match by head_sha first; fall back to first run (branch-scoped) if none matches yet.
104
110
  run = if sha
105
- runs.find { |r| r["head_sha"] == sha } || runs.first
111
+ match = runs.find { |r| r["head_sha"] == sha }
112
+ require_head ? match : (match || runs.first)
106
113
  else
107
- runs.first
114
+ runs.first unless require_head
108
115
  end
109
116
  return unless run
110
117
 
@@ -112,7 +119,8 @@ module Kettle
112
119
  "status" => run["status"],
113
120
  "conclusion" => run["conclusion"],
114
121
  "html_url" => run["html_url"],
115
- "id" => run["id"]
122
+ "id" => run["id"],
123
+ "head_sha" => run["head_sha"]
116
124
  }
117
125
  rescue => e
118
126
  Kettle::Dev.debug_error(e, __method__)
@@ -285,8 +285,13 @@ module Kettle
285
285
  total = workflows.size
286
286
  abort("No GitHub workflows found under .github/workflows; aborting.") if total.zero?
287
287
 
288
+ head_sha = Kettle::Dev::CIHelpers.current_head_sha
289
+ abort("Could not determine local HEAD SHA for GitHub Actions checks.") unless head_sha && !head_sha.empty?
290
+
288
291
  passed = {}
292
+ started = {}
289
293
  puts "Ensuring GitHub Actions workflows pass on #{branch} (#{owner}/#{repo}) via remote '#{gh_remote}'"
294
+ puts "Waiting for GitHub Actions runs to start for HEAD #{head_sha[0, 12]}."
290
295
  pbar = if defined?(ProgressBar)
291
296
  ProgressBar.create(title: "CI", total: total, format: "%t %b %c/%C", length: 30)
292
297
  end
@@ -300,11 +305,15 @@ module Kettle
300
305
  end
301
306
  end
302
307
  sleep((initial_sleep && initial_sleep >= 0) ? initial_sleep : 3)
308
+ start_timeout = github_start_timeout
309
+ poll_interval = github_poll_interval
310
+ start_deadline = monotonic_time + start_timeout
303
311
  idx = 0
304
312
  loop do
305
313
  wf = workflows[idx]
306
- run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch)
314
+ run = Kettle::Dev::CIHelpers.latest_run(owner: owner, repo: repo, workflow_file: wf, branch: branch, require_head: true, head_sha: head_sha)
307
315
  if run
316
+ started[wf] = true
308
317
  if Kettle::Dev::CIHelpers.success?(run)
309
318
  unless passed[wf]
310
319
  passed[wf] = true
@@ -317,9 +326,14 @@ module Kettle
317
326
  end
318
327
  end
319
328
  break if passed.size == total
329
+ if started.size < total && monotonic_time >= start_deadline
330
+ missing = (workflows - started.keys).join(", ")
331
+ puts
332
+ 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}")
333
+ end
320
334
 
321
335
  idx = (idx + 1) % total
322
- sleep(1)
336
+ sleep(poll_interval)
323
337
  end
324
338
  pbar&.finish unless pbar&.finished?
325
339
  puts "\nAll GitHub workflows passing (#{passed.size}/#{total})."
@@ -327,6 +341,31 @@ module Kettle
327
341
  end
328
342
  module_function :monitor_github_internal!
329
343
 
344
+ def monotonic_time
345
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
346
+ end
347
+ module_function :monotonic_time
348
+
349
+ def github_start_timeout
350
+ seconds = begin
351
+ Integer(ENV["K_RELEASE_CI_START_TIMEOUT"])
352
+ rescue
353
+ nil
354
+ end
355
+ (seconds && seconds >= 0) ? seconds : 120
356
+ end
357
+ module_function :github_start_timeout
358
+
359
+ def github_poll_interval
360
+ seconds = begin
361
+ Float(ENV["K_RELEASE_CI_POLL_INTERVAL"])
362
+ rescue
363
+ nil
364
+ end
365
+ (seconds && seconds >= 0) ? seconds : 1
366
+ end
367
+ module_function :github_poll_interval
368
+
330
369
  def monitor_gitlab_internal!(restart_hint:)
331
370
  root = Kettle::Dev::CIHelpers.project_root
332
371
  gitlab_ci = File.exist?(File.join(root, ".gitlab-ci.yml"))