kettle-dev 1.0.1 → 1.0.2
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/.devcontainer/devcontainer.json +26 -0
- data/.envrc +42 -0
- data/.git-hooks/commit-msg +41 -0
- data/.git-hooks/commit-subjects-goalie.txt +8 -0
- data/.git-hooks/footer-template.erb.txt +16 -0
- data/.git-hooks/prepare-commit-msg +20 -0
- data/.github/FUNDING.yml +13 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ancient.yml +80 -0
- data/.github/workflows/auto-assign.yml +21 -0
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/coverage.yml +130 -0
- data/.github/workflows/current.yml +88 -0
- data/.github/workflows/dependency-review.yml +20 -0
- data/.github/workflows/discord-notifier.yml +38 -0
- data/.github/workflows/heads.yml +87 -0
- data/.github/workflows/jruby.yml +79 -0
- data/.github/workflows/legacy.yml +70 -0
- data/.github/workflows/locked_deps.yml +88 -0
- data/.github/workflows/opencollective.yml +40 -0
- data/.github/workflows/style.yml +67 -0
- data/.github/workflows/supported.yml +85 -0
- data/.github/workflows/truffle.yml +78 -0
- data/.github/workflows/unlocked_deps.yml +87 -0
- data/.github/workflows/unsupported.yml +78 -0
- data/.gitignore +48 -0
- data/.gitlab-ci.yml +45 -0
- data/.junie/guidelines-rbs.md +49 -0
- data/.junie/guidelines.md +132 -0
- data/.opencollective.yml +3 -0
- data/.qlty/qlty.toml +79 -0
- data/.rspec +8 -0
- data/.rubocop.yml +13 -0
- data/.simplecov +7 -0
- data/.tool-versions +1 -0
- data/.yard_gfm_support.rb +22 -0
- data/.yardopts +11 -0
- data/Appraisal.root.gemfile +12 -0
- data/Appraisals +120 -0
- data/CHANGELOG.md +12 -1
- data/Gemfile +32 -0
- data/Rakefile +99 -0
- data/checksums/kettle-dev-1.0.2.gem.sha256 +1 -0
- data/checksums/kettle-dev-1.0.2.gem.sha512 +1 -0
- data/gemfiles/modular/coverage.gemfile +6 -0
- data/gemfiles/modular/documentation.gemfile +11 -0
- data/gemfiles/modular/style.gemfile +16 -0
- data/lib/kettle/dev/rakelib/appraisal.rake +40 -0
- data/lib/kettle/dev/rakelib/bench.rake +58 -0
- data/lib/kettle/dev/rakelib/bundle_audit.rake +18 -0
- data/lib/kettle/dev/rakelib/ci.rake +348 -0
- data/lib/kettle/dev/rakelib/install.rake +304 -0
- data/lib/kettle/dev/rakelib/reek.rake +34 -0
- data/lib/kettle/dev/rakelib/require_bench.rake +7 -0
- data/lib/kettle/dev/rakelib/rubocop_gradual.rake +9 -0
- data/lib/kettle/dev/rakelib/spec_test.rake +42 -0
- data/lib/kettle/dev/rakelib/template.rake +413 -0
- data/lib/kettle/dev/rakelib/yard.rake +33 -0
- data/lib/kettle/dev/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +63 -4
- metadata.gz.sig +3 -2
data/Gemfile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" }
|
7
|
+
|
8
|
+
#### IMPORTANT #######################################################
|
9
|
+
# Gemfile is for local development ONLY; Gemfile is NOT loaded in CI #
|
10
|
+
####################################################### IMPORTANT ####
|
11
|
+
|
12
|
+
# Include dependencies from <gem name>.gemspec
|
13
|
+
gemspec
|
14
|
+
|
15
|
+
platform :mri do
|
16
|
+
# Debugging - Ensure ENV["DEBUG"] == "true" to use debuggers within spec suite
|
17
|
+
# Use binding.break, binding.b, or debugger in code
|
18
|
+
gem "debug", ">= 1.0.0" # ruby >= 2.7
|
19
|
+
gem "gem_bench", "~> 2.0", ">= 2.0.5"
|
20
|
+
|
21
|
+
# Dev Console - Binding.pry - Irb replacement
|
22
|
+
gem "pry", "~> 0.14" # ruby >= 2.0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Code Coverage
|
26
|
+
eval_gemfile "gemfiles/modular/coverage.gemfile"
|
27
|
+
|
28
|
+
# Linting
|
29
|
+
eval_gemfile "gemfiles/modular/style.gemfile"
|
30
|
+
|
31
|
+
# Documentation
|
32
|
+
eval_gemfile "gemfiles/modular/documentation.gemfile"
|
data/Rakefile
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Galtzo FLOSS Rakefile v1.0.11 - 2025-08-19
|
4
|
+
# Ruby 2.3 (Safe Navigation) or higher required
|
5
|
+
#
|
6
|
+
# CHANGELOG
|
7
|
+
# v1.0.0 - initial release w/ support for rspec, minitest, rubocop, reek, yard, and stone_checksums
|
8
|
+
# v1.0.1 - fix test / spec tasks running 2x
|
9
|
+
# v1.0.2 - fix duplicate task warning from RuboCop
|
10
|
+
# v1.0.3 - add bench tasks to run mini benchmarks (add scripts to /benchmarks)
|
11
|
+
# v1.0.4 - add support for floss_funding:install
|
12
|
+
# v1.0.5 - add support for halting in Rake tasks with binding.b (from debug gem)
|
13
|
+
# v1.0.6 - add RBS files and checksums to YARD-generated docs site
|
14
|
+
# v1.0.7 - works with vanilla ruby, non-gem, bundler-managed, projects
|
15
|
+
# v1.0.8 - improved Dir globs, add back and document rbconfig dependency
|
16
|
+
# v1.0.9 - add appraisal:update task to update Appraisal gemfiles and autocorrect with RuboCop Gradual
|
17
|
+
# v1.0.10 - add ci:act to run GHA workflows locally, and get status of remote workflows
|
18
|
+
# v1.0.11 - ci:act workflows are populated entirely dynamically, based on existing files
|
19
|
+
#
|
20
|
+
# MIT License (see License.txt)
|
21
|
+
#
|
22
|
+
# Copyright (c) 2025 Peter H. Boling (galtzo.com)
|
23
|
+
#
|
24
|
+
# Expected to work in any project that uses Bundler.
|
25
|
+
#
|
26
|
+
# Sets up tasks for appraisal, floss_funding, rspec, minitest, rubocop, reek, yard, and stone_checksums.
|
27
|
+
#
|
28
|
+
# rake appraisal:update # Update Appraisal gemfiles and run RuboCop Gradual autocorrect
|
29
|
+
# rake bench # Run all benchmarks (alias for bench:run)
|
30
|
+
# rake bench:list # List available benchmark scripts
|
31
|
+
# rake bench:run # Run all benchmark scripts (skips on CI)
|
32
|
+
# rake build # Build gitmoji-regex-1.0.2.gem into the pkg directory
|
33
|
+
# rake build:checksum # Generate SHA512 checksum of gitmoji-regex-1.0.2.gem into the checksums directory
|
34
|
+
# rake build:generate_checksums # Generate both SHA256 & SHA512 checksums into the checksums directory, and git...
|
35
|
+
# rake bundle:audit:check # Checks the Gemfile.lock for insecure dependencies
|
36
|
+
# rake bundle:audit:update # Updates the bundler-audit vulnerability database
|
37
|
+
# rake ci:act[opt] # Run 'act' with a selected workflow
|
38
|
+
# rake clean # Remove any temporary products
|
39
|
+
# rake clobber # Remove any generated files
|
40
|
+
# rake coverage # Run specs w/ coverage and open results in browser
|
41
|
+
# rake floss_funding:install # (stub) floss_funding is unavailable
|
42
|
+
# rake install # Build and install gitmoji-regex-1.0.2.gem into system gems
|
43
|
+
# rake install:local # Build and install gitmoji-regex-1.0.2.gem into system gems without network ac...
|
44
|
+
# rake reek # Check for code smells
|
45
|
+
# rake reek:update # Run reek and store the output into the REEK file
|
46
|
+
# rake release[remote] # Create tag v1.0.2 and build and push gitmoji-regex-1.0.2.gem to rubygems.org
|
47
|
+
# rake rubocop # alias rubocop task to rubocop_gradual
|
48
|
+
# rake rubocop_gradual # Run RuboCop Gradual
|
49
|
+
# rake rubocop_gradual:autocorrect # Run RuboCop Gradual with autocorrect (only when it's safe)
|
50
|
+
# rake rubocop_gradual:autocorrect_all # Run RuboCop Gradual with autocorrect (safe and unsafe)
|
51
|
+
# rake rubocop_gradual:check # Run RuboCop Gradual to check the lock file
|
52
|
+
# rake rubocop_gradual:force_update # Run RuboCop Gradual to force update the lock file
|
53
|
+
# rake spec # Run RSpec code examples
|
54
|
+
# rake test # Run tests
|
55
|
+
# rake yard # Generate YARD Documentation
|
56
|
+
|
57
|
+
# External gems
|
58
|
+
require "bundler/gem_tasks" if !Dir[File.join(__dir__, "*.gemspec")].empty?
|
59
|
+
|
60
|
+
# Detect if the invoked task is spec/test to avoid eagerly requiring the library,
|
61
|
+
# which would load code before SimpleCov can start (when running `rake spec`).
|
62
|
+
invoked_tasks = Rake.application.top_level_tasks
|
63
|
+
running_specs = invoked_tasks.any? { |t| t == "spec" || t == "test" || t == "coverage" }
|
64
|
+
|
65
|
+
if running_specs
|
66
|
+
# Define minimal rspec tasks locally to keep coverage accurate
|
67
|
+
begin
|
68
|
+
require "rspec/core/rake_task"
|
69
|
+
desc("Run RSpec code examples")
|
70
|
+
RSpec::Core::RakeTask.new(:spec)
|
71
|
+
desc("Run tests")
|
72
|
+
task(test: :spec)
|
73
|
+
rescue LoadError
|
74
|
+
# If rspec isn't available, let it fail when the task is invoked
|
75
|
+
end
|
76
|
+
else
|
77
|
+
require "kettle/dev"
|
78
|
+
|
79
|
+
# Define a base default task early so other files can enhance it.
|
80
|
+
desc "Default tasks aggregator"
|
81
|
+
task :default do
|
82
|
+
puts "Default task complete."
|
83
|
+
end
|
84
|
+
|
85
|
+
Kettle::Dev.install_tasks
|
86
|
+
|
87
|
+
### RELEASE TASKS
|
88
|
+
# Setup stone_checksums
|
89
|
+
begin
|
90
|
+
require "stone_checksums"
|
91
|
+
|
92
|
+
GemChecksums.install_tasks
|
93
|
+
rescue LoadError
|
94
|
+
desc("(stub) build:generate_checksums is unavailable")
|
95
|
+
task("build:generate_checksums") do
|
96
|
+
warn("NOTE: stone_checksums isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
2338f9fc4e14a03c39c6509d11fdcfad85faaa7615d855703cb511bc9f95351c
|
@@ -0,0 +1 @@
|
|
1
|
+
ccc6a5c3cd36a8c40d78458166adfbd7ad452b62055579ee4333089a56313170bf97d0aca1f8c3d4f15287bb3d396cfbfcce2174665feb7a77f0b6b03ac8096c
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Documentation
|
4
|
+
gem "kramdown", "~> 2.5", ">= 2.5.1" # Ruby >= 2.5
|
5
|
+
gem "kramdown-parser-gfm", "~> 1.1" # Ruby >= 2.3
|
6
|
+
gem "yard", "~> 0.9", ">= 0.9.37", require: false
|
7
|
+
gem "yard-junk", "~> 0.0", ">= 0.0.10", github: "pboling/yard-junk", branch: "next", require: false
|
8
|
+
gem "yard-relative_markdown_links", "~> 0.5.0"
|
9
|
+
|
10
|
+
# Std Lib extractions
|
11
|
+
gem "rdoc", "~> 6.11"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# We run rubocop on the latest version of Ruby,
|
4
|
+
# but in support of the oldest supported version of Ruby
|
5
|
+
|
6
|
+
gem "reek", "~> 6.5"
|
7
|
+
# gem "rubocop", "~> 1.73", ">= 1.73.2" # constrained by standard
|
8
|
+
gem "rubocop-lts", "~> 10.1", ">= 10.1.1" # Linting that targets compatibility with each specific minor version of Ruby
|
9
|
+
gem "rubocop-ruby2_3", "~> 2.0", ">= 2.0.5"
|
10
|
+
gem "rubocop-packaging", "~> 0.6", ">= 0.6.0"
|
11
|
+
gem "rubocop-rspec", "~> 3.6"
|
12
|
+
gem "standard", ">= 1.50"
|
13
|
+
gem "rubocop-on-rbs", "~> 1.8" # ruby >= 3.1.0
|
14
|
+
|
15
|
+
# Std Lib extractions
|
16
|
+
gem "benchmark", "~> 0.4", ">= 0.4.1" # Removed from Std Lib in Ruby 3.5
|
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler"
|
3
|
+
rescue LoadError
|
4
|
+
warn("[kettle-dev][appraisal.rake] failed to load bundler") if Kettle::Dev::DEBUGGING
|
5
|
+
# ok, might still work
|
6
|
+
end
|
7
|
+
|
8
|
+
# --- Appraisals (dev-only) ---
|
9
|
+
begin
|
10
|
+
require "appraisal/task"
|
11
|
+
|
12
|
+
desc("Update Appraisal gemfiles and run RuboCop Gradual autocorrect")
|
13
|
+
task("appraisal:update") do
|
14
|
+
bundle = Gem.bindir ? File.join(Gem.bindir, "bundle") : "bundle"
|
15
|
+
|
16
|
+
run_in_unbundled = proc do
|
17
|
+
env = {"BUNDLE_GEMFILE" => "Appraisal.root.gemfile"}
|
18
|
+
|
19
|
+
# 1) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle
|
20
|
+
ok = system(env, bundle)
|
21
|
+
abort("appraisal:update failed: bundler install under Appraisal.root.gemfile") unless ok
|
22
|
+
|
23
|
+
# 2) BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal update
|
24
|
+
ok = system(env, bundle, "exec", "appraisal", "update")
|
25
|
+
abort("appraisal:update failed: bundle exec appraisal update") unless ok
|
26
|
+
|
27
|
+
# 3) bundle exec rake rubocop_gradual:autocorrect
|
28
|
+
ok = system(bundle, "exec", "rake", "rubocop_gradual:autocorrect")
|
29
|
+
abort("appraisal:update failed: rubocop_gradual:autocorrect") unless ok
|
30
|
+
end
|
31
|
+
|
32
|
+
if defined?(Bundler)
|
33
|
+
Bundler.with_unbundled_env(&run_in_unbundled)
|
34
|
+
else
|
35
|
+
run_in_unbundled.call
|
36
|
+
end
|
37
|
+
end
|
38
|
+
rescue LoadError
|
39
|
+
warn("[kettle-dev][appraisal.rake] failed to load appraisal/tasks") if Kettle::Dev::DEBUGGING
|
40
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "rbconfig" if !Dir[File.join(__dir__, "benchmarks")].empty? # Used by `rake bench:run`
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler"
|
5
|
+
rescue LoadError
|
6
|
+
warn("[kettle-dev][bench.rake] failed to load bundler") if Kettle::Dev::DEBUGGING
|
7
|
+
# ok, might still work
|
8
|
+
end
|
9
|
+
|
10
|
+
# --- Benchmarks (dev-only) ---
|
11
|
+
namespace :bench do
|
12
|
+
desc "List available benchmark scripts"
|
13
|
+
task :list do
|
14
|
+
bench_files = Dir[File.join(__dir__, "benchmarks", "*.rb")].sort
|
15
|
+
if bench_files.empty?
|
16
|
+
puts "No benchmark scripts found under benchmarks/."
|
17
|
+
else
|
18
|
+
bench_files.each { |f| puts File.basename(f) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Run all benchmark scripts (skips on CI)"
|
23
|
+
task :run do
|
24
|
+
if ENV.fetch("CI", "false").casecmp("true").zero?
|
25
|
+
puts "Benchmarks are disabled on CI. Skipping."
|
26
|
+
next
|
27
|
+
end
|
28
|
+
|
29
|
+
ruby = RbConfig.ruby
|
30
|
+
bundle = Gem.bindir ? File.join(Gem.bindir, "bundle") : "bundle"
|
31
|
+
bench_files = Dir[File.join(__dir__, "benchmarks", "*.rb")].sort
|
32
|
+
if bench_files.empty?
|
33
|
+
puts "No benchmark scripts found under benchmarks/."
|
34
|
+
next
|
35
|
+
end
|
36
|
+
|
37
|
+
use_bundler = ENV.fetch("BENCH_BUNDLER", "0") == "1"
|
38
|
+
|
39
|
+
bench_files.each do |script|
|
40
|
+
puts "\n=== Running: #{File.basename(script)} ==="
|
41
|
+
if use_bundler
|
42
|
+
cmd = [bundle, "exec", ruby, "-Ilib", script]
|
43
|
+
system(*cmd) || abort("Benchmark failed: #{script}")
|
44
|
+
elsif defined?(Bundler)
|
45
|
+
# Run benchmarks without Bundler to reduce overhead and better reflect plain ruby -Ilib
|
46
|
+
Bundler.with_unbundled_env do
|
47
|
+
system(ruby, "-Ilib", script) || abort("Benchmark failed: #{script}")
|
48
|
+
end
|
49
|
+
else
|
50
|
+
# If Bundler isn't available, just run directly
|
51
|
+
system(ruby, "-Ilib", script) || abort("Benchmark failed: #{script}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "Run all benchmarks (alias for bench:run)"
|
58
|
+
task bench: "bench:run"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Setup Bundle Audit
|
2
|
+
begin
|
3
|
+
require "bundler/audit/task"
|
4
|
+
|
5
|
+
Bundler::Audit::Task.new
|
6
|
+
Kettle::Dev.register_default("bundle:audit:update")
|
7
|
+
Kettle::Dev.register_default("bundle:audit")
|
8
|
+
rescue LoadError
|
9
|
+
warn("[kettle-dev][bundle_audit.rake] failed to load bundle/audit/task") if Kettle::Dev::DEBUGGING
|
10
|
+
desc("(stub) bundle:audit is unavailable")
|
11
|
+
task("bundle:audit") do
|
12
|
+
warn("NOTE: bundler-audit isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
|
13
|
+
end
|
14
|
+
desc("(stub) bundle:audit:update is unavailable")
|
15
|
+
task("bundle:audit:update") do
|
16
|
+
warn("NOTE: bundler-audit isn't installed, or is disabled for #{RUBY_VERSION} in the current environment")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
# --- CI helpers ---
|
2
|
+
namespace :ci do
|
3
|
+
# rubocop:disable ThreadSafety/NewThread
|
4
|
+
desc "Run 'act' with a selected workflow. Usage: rake ci:act[loc] (short code = first 3 letters of filename, e.g., 'loc' => locked_deps.yml), rake ci:act[locked_deps], rake ci:act[locked_deps.yml], or rake ci:act (then choose)"
|
5
|
+
task :act, [:opt] do |_t, args|
|
6
|
+
require "io/console"
|
7
|
+
require "open3"
|
8
|
+
require "net/http"
|
9
|
+
require "json"
|
10
|
+
require "uri"
|
11
|
+
require "kettle/dev/ci_helpers"
|
12
|
+
|
13
|
+
# Build mapping dynamically from workflow files; short code = first three letters of filename.
|
14
|
+
# Collisions are resolved by first-come wins via ||= as requested.
|
15
|
+
mapping = {}
|
16
|
+
|
17
|
+
# Normalize provided option. Accept either short code or the exact yml/yaml filename
|
18
|
+
choice = args[:opt]&.strip
|
19
|
+
root_dir = Kettle::Dev::CIHelpers.project_root
|
20
|
+
workflows_dir = File.join(root_dir, ".github", "workflows")
|
21
|
+
|
22
|
+
# Determine actual workflow files present, and prepare dynamic additions excluding specified files.
|
23
|
+
existing_files = if Dir.exist?(workflows_dir)
|
24
|
+
Dir[File.join(workflows_dir, "*.yml")] + Dir[File.join(workflows_dir, "*.yaml")]
|
25
|
+
else
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
existing_basenames = existing_files.map { |p| File.basename(p) }
|
29
|
+
|
30
|
+
# Build short-code mapping (first 3 chars of filename stem), excluding some maintenance workflows.
|
31
|
+
exclusions = Kettle::Dev::CIHelpers.exclusions
|
32
|
+
candidate_files = existing_basenames.uniq - exclusions
|
33
|
+
candidate_files.sort.each do |fname|
|
34
|
+
stem = fname.sub(/\.(ya?ml)\z/, "")
|
35
|
+
code = stem[0, 3].to_s.downcase
|
36
|
+
next if code.empty?
|
37
|
+
mapping[code] ||= fname # first-come wins on collisions
|
38
|
+
end
|
39
|
+
|
40
|
+
# Any remaining candidates that didn't get a unique shortcode are treated as dynamic (number-only) options
|
41
|
+
dynamic_files = candidate_files - mapping.values
|
42
|
+
|
43
|
+
# For internal status tracking and rendering, we use a display_code_for hash.
|
44
|
+
# For mapped (short-code) entries, display_code is the short code.
|
45
|
+
# For dynamic entries, display_code is empty string, but we key statuses by a unique code = the filename.
|
46
|
+
display_code_for = {}
|
47
|
+
mapping.keys.each { |k| display_code_for[k] = k }
|
48
|
+
dynamic_files.each { |f| display_code_for[f] = "" }
|
49
|
+
|
50
|
+
# Helpers
|
51
|
+
get_branch = proc do
|
52
|
+
out, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
|
53
|
+
status.success? ? out.strip : nil
|
54
|
+
end
|
55
|
+
|
56
|
+
get_origin = proc do
|
57
|
+
out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
|
58
|
+
next nil unless status.success?
|
59
|
+
url = out.strip
|
60
|
+
# Support ssh and https URLs
|
61
|
+
if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
|
62
|
+
[$1, $2.sub(/\.git\z/, "")]
|
63
|
+
elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
|
64
|
+
[$1, $2.sub(/\.git\z/, "")]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
status_emoji = proc do |status, conclusion|
|
69
|
+
case status
|
70
|
+
when "queued"
|
71
|
+
"⏳️"
|
72
|
+
when "in_progress"
|
73
|
+
"👟"
|
74
|
+
when "completed"
|
75
|
+
(conclusion == "success") ? "✅" : "🍅"
|
76
|
+
else
|
77
|
+
"⏳️"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
fetch_and_print_status = proc do |workflow_file|
|
82
|
+
branch = get_branch.call
|
83
|
+
org_repo = get_origin.call
|
84
|
+
unless branch && org_repo
|
85
|
+
puts "GHA status: (skipped; missing git branch or remote)"
|
86
|
+
next
|
87
|
+
end
|
88
|
+
owner, repo = org_repo
|
89
|
+
uri = URI("https://api.github.com/repos/#{owner}/#{repo}/actions/workflows/#{workflow_file}/runs?branch=#{URI.encode_www_form_component(branch)}&per_page=1")
|
90
|
+
req = Net::HTTP::Get.new(uri)
|
91
|
+
req["User-Agent"] = "ci:act rake task"
|
92
|
+
token = ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
|
93
|
+
req["Authorization"] = "token #{token}" if token && !token.empty?
|
94
|
+
|
95
|
+
begin
|
96
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
97
|
+
if res.is_a?(Net::HTTPSuccess)
|
98
|
+
data = JSON.parse(res.body)
|
99
|
+
run = data["workflow_runs"]&.first
|
100
|
+
if run
|
101
|
+
status = run["status"]
|
102
|
+
conclusion = run["conclusion"]
|
103
|
+
emoji = status_emoji.call(status, conclusion)
|
104
|
+
details = [status, conclusion].compact.join("/")
|
105
|
+
puts "Latest GHA (#{branch}) for #{workflow_file}: #{emoji} (#{details})"
|
106
|
+
else
|
107
|
+
puts "Latest GHA (#{branch}) for #{workflow_file}: none"
|
108
|
+
end
|
109
|
+
else
|
110
|
+
puts "GHA status: request failed (#{res.code})"
|
111
|
+
end
|
112
|
+
rescue StandardError => e
|
113
|
+
puts "GHA status: error #{e.class}: #{e.message}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def run_act_for(file_path)
|
118
|
+
# Prefer array form to avoid shell escaping issues
|
119
|
+
ok = system("act", "-W", file_path)
|
120
|
+
abort("ci:act failed: 'act' command not found or exited with failure") unless ok
|
121
|
+
end
|
122
|
+
|
123
|
+
def process_success_response(res, c, f, old = nil, current = nil)
|
124
|
+
data = JSON.parse(res.body)
|
125
|
+
run = data["workflow_runs"]&.first
|
126
|
+
append = (old && current) ? " (update git remote: #{old} → #{current})" : ""
|
127
|
+
if run
|
128
|
+
st = run["status"]
|
129
|
+
con = run["conclusion"]
|
130
|
+
emoji = case st
|
131
|
+
when "queued" then "⏳️"
|
132
|
+
when "in_progress" then "👟"
|
133
|
+
when "completed" then ((con == "success") ? "✅" : "🍅")
|
134
|
+
else "⏳️"
|
135
|
+
end
|
136
|
+
details = [st, con].compact.join("/")
|
137
|
+
[c, f, "#{emoji} (#{details})#{append}"]
|
138
|
+
else
|
139
|
+
[c, f, "none#{append}"]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
if choice && !choice.empty?
|
144
|
+
# If user passed a filename directly (with or without extension), resolve it
|
145
|
+
file = if mapping.key?(choice)
|
146
|
+
mapping.fetch(choice)
|
147
|
+
elsif !!(/\.(yml|yaml)\z/ =~ choice)
|
148
|
+
# Accept either full basename (without ext) or basename with .yml/.yaml
|
149
|
+
choice
|
150
|
+
else
|
151
|
+
cand_yml = File.join(workflows_dir, "#{choice}.yml")
|
152
|
+
cand_yaml = File.join(workflows_dir, "#{choice}.yaml")
|
153
|
+
if File.file?(cand_yml)
|
154
|
+
"#{choice}.yml"
|
155
|
+
elsif File.file?(cand_yaml)
|
156
|
+
"#{choice}.yaml"
|
157
|
+
else
|
158
|
+
# Fall back to .yml for error messaging; will fail below
|
159
|
+
"#{choice}.yml"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
file_path = File.join(workflows_dir, file)
|
163
|
+
unless File.file?(file_path)
|
164
|
+
puts "Unknown option or missing workflow file: #{choice} -> #{file}"
|
165
|
+
puts "Available options:"
|
166
|
+
mapping.each { |k, v| puts " #{k.ljust(3)} => #{v}" }
|
167
|
+
# Also display dynamically discovered files
|
168
|
+
unless dynamic_files.empty?
|
169
|
+
puts " (others) =>"
|
170
|
+
dynamic_files.each { |v| puts " #{v}" }
|
171
|
+
end
|
172
|
+
abort("ci:act aborted")
|
173
|
+
end
|
174
|
+
fetch_and_print_status.call(file)
|
175
|
+
run_act_for(file_path)
|
176
|
+
next
|
177
|
+
end
|
178
|
+
|
179
|
+
# No option provided: interactive menu with live GHA statuses via Threads (no Ractors)
|
180
|
+
require "thread"
|
181
|
+
|
182
|
+
tty = $stdout.tty?
|
183
|
+
# Build options: first the filtered short-code mapping, then dynamic files (no short codes)
|
184
|
+
options = mapping.to_a + dynamic_files.map { |f| [f, f] }
|
185
|
+
|
186
|
+
# Add a Quit choice
|
187
|
+
quit_code = "q"
|
188
|
+
options_with_quit = options + [[quit_code, "(quit)"]]
|
189
|
+
|
190
|
+
idx_by_code = {}
|
191
|
+
options_with_quit.each_with_index { |(k, _v), i| idx_by_code[k] = i }
|
192
|
+
|
193
|
+
# Determine repo context once
|
194
|
+
branch = get_branch.call
|
195
|
+
org = get_origin.call
|
196
|
+
owner, repo = org if org
|
197
|
+
token = ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
|
198
|
+
|
199
|
+
puts "Select a workflow to run with 'act':"
|
200
|
+
|
201
|
+
# Render initial menu with placeholder statuses
|
202
|
+
placeholder = "[…]"
|
203
|
+
options_with_quit.each_with_index do |(k, v), idx|
|
204
|
+
status_col = (k == quit_code) ? "" : placeholder
|
205
|
+
disp = (k == quit_code) ? k : display_code_for[k]
|
206
|
+
line = format("%2d) %-3s => %-20s %s", idx + 1, disp, v, status_col)
|
207
|
+
puts line
|
208
|
+
end
|
209
|
+
|
210
|
+
puts "(Fetching latest GHA status for branch #{branch || "n/a"} — you can type your choice and press Enter)"
|
211
|
+
prompt = "Enter number or code (or 'q' to quit): "
|
212
|
+
print prompt
|
213
|
+
$stdout.flush
|
214
|
+
|
215
|
+
# Thread + Queue to read user input
|
216
|
+
input_q = Queue.new
|
217
|
+
input_thread = Thread.new do
|
218
|
+
line = $stdin.gets&.strip
|
219
|
+
input_q << line
|
220
|
+
end
|
221
|
+
|
222
|
+
# Worker threads to fetch statuses and stream updates as they complete
|
223
|
+
status_q = Queue.new
|
224
|
+
workers = []
|
225
|
+
|
226
|
+
# Capture a monotonic start time to guard against early race with terminal rendering
|
227
|
+
start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
228
|
+
|
229
|
+
options.each do |code, file|
|
230
|
+
workers << Thread.new(code, file, owner, repo, branch, token, start_at) do |c, f, ow, rp, br, tk, st_at|
|
231
|
+
begin
|
232
|
+
# small initial delay if threads finish too quickly, to let the menu/prompt finish rendering
|
233
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
234
|
+
delay = 0.12 - (now - st_at)
|
235
|
+
sleep(delay) if delay && delay > 0
|
236
|
+
|
237
|
+
if ow.nil? || rp.nil? || br.nil?
|
238
|
+
status_q << [c, f, "n/a"]
|
239
|
+
Thread.exit
|
240
|
+
end
|
241
|
+
uri = URI("https://api.github.com/repos/#{ow}/#{rp}/actions/workflows/#{f}/runs?branch=#{URI.encode_www_form_component(br)}&per_page=1")
|
242
|
+
req = Net::HTTP::Get.new(uri)
|
243
|
+
req["User-Agent"] = "ci:act rake task"
|
244
|
+
req["Authorization"] = "token #{tk}" if tk && !tk.empty?
|
245
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
246
|
+
status_q <<
|
247
|
+
if res.is_a?(Net::HTTPSuccess)
|
248
|
+
process_success_response(res, c, f)
|
249
|
+
else
|
250
|
+
[c, f, "fail #{res.code}"]
|
251
|
+
end
|
252
|
+
rescue StandardError
|
253
|
+
status_q << [c, f, "err"]
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Live update loop: either statuses arrive or the user submits input
|
259
|
+
statuses = Hash.new(placeholder)
|
260
|
+
selected = nil
|
261
|
+
|
262
|
+
loop do
|
263
|
+
# Check for user input first (non-blocking)
|
264
|
+
unless input_q.empty?
|
265
|
+
selected = begin
|
266
|
+
input_q.pop(true)
|
267
|
+
rescue
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
break if selected
|
271
|
+
end
|
272
|
+
|
273
|
+
# Drain any available status updates without blocking
|
274
|
+
begin
|
275
|
+
code, file_name, display = status_q.pop(true)
|
276
|
+
statuses[code] = display
|
277
|
+
|
278
|
+
if tty
|
279
|
+
idx = idx_by_code[code]
|
280
|
+
if idx.nil?
|
281
|
+
puts "status #{code}: #{display}"
|
282
|
+
print(prompt)
|
283
|
+
else
|
284
|
+
move_up = options_with_quit.size - idx + 1 # 1 for instruction line + remaining options above last
|
285
|
+
$stdout.print("\e[#{move_up}A\r\e[2K")
|
286
|
+
disp = (code == quit_code) ? code : display_code_for[code]
|
287
|
+
$stdout.print(format("%2d) %-3s => %-20s %s\n", idx + 1, disp, file_name, display))
|
288
|
+
$stdout.print("\e[#{move_up - 1}B\r")
|
289
|
+
$stdout.print(prompt)
|
290
|
+
end
|
291
|
+
$stdout.flush
|
292
|
+
else
|
293
|
+
puts "status #{code}: #{display}"
|
294
|
+
end
|
295
|
+
rescue ThreadError
|
296
|
+
# Queue empty: brief sleep to avoid busy wait
|
297
|
+
sleep(0.05)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Cleanup: kill any still-running threads
|
302
|
+
begin
|
303
|
+
workers.each { |t| t.kill if t&.alive? }
|
304
|
+
rescue StandardError
|
305
|
+
# ignore
|
306
|
+
end
|
307
|
+
begin
|
308
|
+
input_thread.kill if input_thread&.alive?
|
309
|
+
rescue StandardError
|
310
|
+
# ignore
|
311
|
+
end
|
312
|
+
|
313
|
+
input = selected
|
314
|
+
abort("ci:act aborted: no selection") if input.nil? || input.empty?
|
315
|
+
|
316
|
+
# Normalize selection
|
317
|
+
chosen_file = nil
|
318
|
+
if !!(/^\d+$/ =~ input)
|
319
|
+
idx = input.to_i - 1
|
320
|
+
if idx < 0 || idx >= options_with_quit.length
|
321
|
+
abort("ci:act aborted: invalid selection #{input}")
|
322
|
+
end
|
323
|
+
code, val = options_with_quit[idx]
|
324
|
+
if code == quit_code
|
325
|
+
puts "ci:act: quit"
|
326
|
+
next
|
327
|
+
else
|
328
|
+
chosen_file = val
|
329
|
+
end
|
330
|
+
else
|
331
|
+
code = input
|
332
|
+
if ["q", "quit", "exit"].include?(code.downcase)
|
333
|
+
puts "ci:act: quit"
|
334
|
+
next
|
335
|
+
end
|
336
|
+
chosen_file = mapping[code]
|
337
|
+
abort("ci:act aborted: unknown code '#{code}'") unless chosen_file
|
338
|
+
end
|
339
|
+
|
340
|
+
file_path = File.join(workflows_dir, chosen_file)
|
341
|
+
abort("ci:act aborted: workflow not found: #{file_path}") unless File.file?(file_path)
|
342
|
+
|
343
|
+
# Print status for the chosen workflow (for consistency)
|
344
|
+
fetch_and_print_status.call(chosen_file)
|
345
|
+
run_act_for(file_path)
|
346
|
+
end
|
347
|
+
# rubocop:enable ThreadSafety/NewThread
|
348
|
+
end
|