diff --git a/Gemfile b/Gemfile index b01d370..8be53a6 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gemspec path: "gems/tree_haver" gemspec path: "gems/ast-merge" gemspec path: "gems/ast-template" +gemspec path: "gems/kettle-jem" gemspec path: "gems/text-merge" gemspec path: "gems/json-merge" gemspec path: "gems/toml-merge" diff --git a/Gemfile.lock b/Gemfile.lock index 0465199..ad15d41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,8 +7,8 @@ PATH PATH remote: gems/ast-template specs: - ast-template (0.0.0) - ast-merge (= 0.0.0) + ast-template (0.1.0) + ast-merge (= 0.1.0) PATH remote: gems/citrus-toml-merge @@ -20,9 +20,9 @@ PATH PATH remote: gems/commonmarker-merge specs: - commonmarker-merge (0.0.0) + commonmarker-merge (0.1.0) commonmarker (~> 2.2) - markdown-merge (= 0.0.0) + markdown-merge (= 0.1.0) PATH remote: gems/go-merge @@ -38,25 +38,35 @@ PATH ast-merge (= 0.1.0) tree_haver (= 0.1.0) +PATH + remote: gems/kettle-jem + specs: + kettle-jem (0.1.0) + ast-merge (= 0.1.0) + ruby-merge (= 0.1.0) + token-resolver (~> 1.0, >= 1.0.2) + toml-merge (= 0.1.0) + yaml-merge (= 0.1.0) + PATH remote: gems/kramdown-merge specs: - kramdown-merge (0.0.0) + kramdown-merge (0.1.0) kramdown (~> 2.5) - markdown-merge (= 0.0.0) + markdown-merge (= 0.1.0) PATH remote: gems/markdown-merge specs: - markdown-merge (0.0.0) - ast-merge (= 0.0.0) - tree_haver (= 0.0.0) + markdown-merge (0.1.0) + ast-merge (= 0.1.0) + tree_haver (= 0.1.0) PATH remote: gems/markly-merge specs: - markly-merge (0.0.0) - markdown-merge (= 0.0.0) + markly-merge (0.1.0) + markdown-merge (= 0.1.0) markly (~> 0.9) PATH @@ -177,6 +187,7 @@ DEPENDENCIES commonmarker-merge! go-merge! json-merge! + kettle-jem! kramdown-merge! markdown-merge! markly-merge! @@ -196,19 +207,20 @@ DEPENDENCIES CHECKSUMS ast-merge (0.1.0) - ast-template (0.0.0) + ast-template (0.1.0) citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650 citrus-toml-merge (0.1.0) commonmarker (2.8.1-x86_64-linux) sha256=c8f2e903c6ee4e2c7e280aa8b49ef37c53202d388e3a2ee06dfdccc8c6fb634f - commonmarker-merge (0.0.0) + commonmarker-merge (0.1.0) diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 go-merge (0.1.0) json-merge (0.1.0) + kettle-jem (0.1.0) kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa - kramdown-merge (0.0.0) - markdown-merge (0.0.0) + kramdown-merge (0.1.0) + markdown-merge (0.1.0) markly (0.16.0) sha256=6f70d79e385b1efc9e171f74c81628826259039fe6c778e03c3924c71dac5511 - markly-merge (0.0.0) + markly-merge (0.1.0) parslet (2.0.0) sha256=d45130695d39b43d7e6a91f4d2ec66b388a8d822bae38de9b4de9a5fbde1f606 parslet-toml-merge (0.1.0) prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 diff --git a/README.md b/README.md index 3f7544c..def0951 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,22 @@ -# Structured Merge Ruby +# StructuredMerge Ruby -Monorepo for the new Ruby implementation of the Structured Merge library -family. +Ruby implementation of the StructuredMerge contract. -This repository is a fresh implementation aligned to the current -cross-language spec and fixture corpus. The older Ruby gems in this workspace -remain reference material only. +This repository is one of four peer launch implementations: [Go](https://github.com/structuredmerge/structuredmerge-go), [TypeScript](https://github.com/structuredmerge/structuredmerge-typescript), [Rust](https://github.com/structuredmerge/structuredmerge-rust), and [Ruby](https://github.com/structuredmerge/structuredmerge-ruby). The language repos are not separate products. They consume the same public spec and shared fixture corpus so tools can choose the runtime surface that fits their environment. -Initial planned Ruby packages: +Project links: + +- Website: +- Implementations overview: +- Conformance model: +- Specification: +- Shared fixtures: + +## Workspace + +This is a Ruby monorepo for StructuredMerge packages. + +Initial packages: - `tree-haver` - `ast-merge` @@ -15,20 +24,28 @@ Initial planned Ruby packages: - `json-merge` - `toml-merge` - `yaml-merge` -- `typescript-merge` -- `rust-merge` -- `go-merge` -- `ruby-merge` +- source-family packages for TypeScript, Rust, Go, and Ruby cases + +## Conformance + +Integration tests should consume the shared fixture corpus from the sibling `../structuredmerge-fixtures` checkout. A ruleset, fixture, diagnostic shape, or review outcome should mean the same thing whether exercised through Go, TypeScript, Rust, or Ruby. + +Use the spec repository's conformance matrix for the current launch-readiness snapshot: + +- +- ## Development -This repository follows the same slice-by-slice conformance path used by the -TypeScript, Rust, and Go monorepos. +Standard repo tasks are exposed through `mise` and native Ruby tooling. + +Common checks: + +- `mise run check` +- `bundle exec rake` or package-specific tests + +Bundler path gems are the default isolation mechanism inside this monorepo. When this repository needs to consume sibling workspace projects outside the monorepo itself, prefer `nomono`-driven Bundler wiring rather than manual Ruby load-path changes. -Integration tests should consume the shared fixture corpus from the sibling -`../fixtures` repository rather than copying fixture data into this monorepo. +## Status -Bundler path gems are the default isolation mechanism inside this monorepo. -When this repository needs to consume sibling workspace projects outside the -monorepo itself, prefer `nomono`-driven Bundler wiring rather than manual Ruby -load-path changes. +Early implementation work. Public compatibility claims should be tied to shared fixtures and documented conformance status rather than runtime-specific assumptions. diff --git a/gems/ast-merge/lib/ast/merge.rb b/gems/ast-merge/lib/ast/merge.rb index d6a7dd0..fdaa3bb 100644 --- a/gems/ast-merge/lib/ast/merge.rb +++ b/gems/ast-merge/lib/ast/merge.rb @@ -10,6 +10,16 @@ module Merge REVIEW_TRANSPORT_VERSION = 1 STRUCTURED_EDIT_TRANSPORT_VERSION = 1 TEMPLATE_TOKEN_CONFIG = Token::Resolver::Config.new(separators: ["|", ":"]).freeze + COMPACT_RULESET_REQUIRED_DIRECTIVES = %w[format owners match read attach].freeze + COMPACT_RULESET_SINGLETON_DIRECTIVES = %w[format owners match read attach comment_style render].freeze + COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES = %w[capability logical_owner repair surface delegate].freeze + COMPACT_RULESET_READ_VALUES = %w[source_augmented_portable_write native_read_portable_write native_mutation].freeze + COMPACT_RULESET_ATTACH_VALUES = %w[ + layout_only + tracker_layout_merge + augmenter_preferred_tracker_layout + normalize_tracked_layout_merge + ].freeze module_function @@ -28,6 +38,68 @@ def conformance_family_feature_profile_path(manifest, family) entry && deep_dup(entry[:path]) end + def parse_compact_ruleset(source) + ruleset = { directives: [], comments: [] } + diagnostics = [] + seen_directives = {} + seen_repeatable_keys = {} + + source.to_s.split("\n").each_with_index do |raw_line, index| + line_number = index + 1 + line = raw_line.strip + next if line.empty? + + if line.start_with?("#") + ruleset[:comments] << line + next + end + + name, *arguments = line.split(/\s+/) + path = line_number.to_s + unless compact_ruleset_identifier?(name) + diagnostics << compact_ruleset_diagnostic("invalid directive token #{name.inspect}", path) + next + end + unless compact_ruleset_known_directive?(name) + diagnostics << compact_ruleset_diagnostic("unknown directive #{name.inspect}", path) + next + end + if arguments.empty? + diagnostics << compact_ruleset_diagnostic("directive #{name.inspect} requires at least one argument", path) + next + end + + arguments.each do |argument| + next if %w[true false].include?(argument) || compact_ruleset_identifier?(argument) || compact_ruleset_token?(argument) + + diagnostics << compact_ruleset_diagnostic("invalid argument token #{argument.inspect}", path) + end + + if COMPACT_RULESET_SINGLETON_DIRECTIVES.include?(name) && seen_directives.key?(name) + diagnostics << compact_ruleset_diagnostic( + "repeated singleton directive #{name.inspect} first seen on line #{seen_directives.fetch(name)}", + path + ) + end + if COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES.include?(name) + key = [name, arguments.fetch(0)] + diagnostics << compact_ruleset_diagnostic("repeated #{name.inspect} key #{arguments.fetch(0).inspect}", path) if seen_repeatable_keys[key] + seen_repeatable_keys[key] = true + end + diagnostics << compact_ruleset_diagnostic("unknown read value #{arguments.fetch(0).inspect}", path) if name == "read" && !COMPACT_RULESET_READ_VALUES.include?(arguments.fetch(0)) + diagnostics << compact_ruleset_diagnostic("unknown attach value #{arguments.fetch(0).inspect}", path) if name == "attach" && !COMPACT_RULESET_ATTACH_VALUES.include?(arguments.fetch(0)) + + seen_directives[name] = line_number + ruleset[:directives] << { name: name, arguments: arguments, line: line_number } + end + + COMPACT_RULESET_REQUIRED_DIRECTIVES.each do |required| + diagnostics << compact_ruleset_diagnostic("missing required directive #{required.inspect}") unless seen_directives.key?(required) + end + + diagnostics.empty? ? { ok: true, diagnostics: [], analysis: ruleset, policies: [] } : { ok: false, diagnostics: diagnostics, policies: [] } + end + def normalize_template_source_path(path) return path.delete_suffix(".no-osc.example") if path.end_with?(".no-osc.example") return path.delete_suffix(".example") if path.end_with?(".example") @@ -1040,51 +1112,6 @@ def structured_edit_provider_execution_request(request:, provider_family:, provi execution_request end - def content_recipe_execution_request(recipe_name:, recipe_version:, relative_path:, provider_family:, - template_content:, destination_content:, steps:, provider_backend: nil, runtime_context: nil, metadata: nil) - request = { - recipe_name: recipe_name.to_s, - recipe_version: recipe_version.to_s, - relative_path: relative_path.to_s, - provider_family: provider_family.to_s, - template_content: template_content.to_s, - destination_content: destination_content.to_s, - steps: deep_dup(steps) - } - request[:provider_backend] = provider_backend.to_s if provider_backend - request[:runtime_context] = deep_dup(runtime_context) if runtime_context - request[:metadata] = deep_dup(metadata) if metadata - request - end - - def content_recipe_execution_request_envelope(request) - { - kind: "content_recipe_execution_request", - version: STRUCTURED_EDIT_TRANSPORT_VERSION, - request: deep_dup(request) - } - end - - def content_recipe_execution_report(request:, final_content:, changed:, step_reports:, diagnostics:, metadata: nil) - report = { - request: deep_dup(request), - final_content: final_content.to_s, - changed: changed ? true : false, - step_reports: deep_dup(step_reports), - diagnostics: deep_dup(diagnostics) - } - report[:metadata] = deep_dup(metadata) if metadata - report - end - - def content_recipe_execution_report_envelope(report) - { - kind: "content_recipe_execution_report", - version: STRUCTURED_EDIT_TRANSPORT_VERSION, - report: deep_dup(report) - } - end - def structured_edit_provider_execution_request_envelope(execution_request) { kind: "structured_edit_provider_execution_request", @@ -3392,6 +3419,27 @@ def diagnostic(severity, category, message, path: nil, review: nil) end private_class_method :diagnostic + def compact_ruleset_identifier?(value) + value.to_s.match?(/\A[A-Za-z][A-Za-z0-9_.-]*\z/) + end + private_class_method :compact_ruleset_identifier? + + def compact_ruleset_token?(value) + value.to_s.match?(/\A[\x21\x24-\x7e]+\z/) + end + private_class_method :compact_ruleset_token? + + def compact_ruleset_known_directive?(value) + COMPACT_RULESET_SINGLETON_DIRECTIVES.include?(value) || + COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES.include?(value) + end + private_class_method :compact_ruleset_known_directive? + + def compact_ruleset_diagnostic(message, path = nil) + diagnostic("error", "configuration_error", message, path: path) + end + private_class_method :compact_ruleset_diagnostic + def join_comma(values) values.join(", ") end diff --git a/gems/ast-merge/spec/fixtures_integration_spec.rb b/gems/ast-merge/spec/fixtures_integration_spec.rb index d6e82ba..0e095eb 100644 --- a/gems/ast-merge/spec/fixtures_integration_spec.rb +++ b/gems/ast-merge/spec/fixtures_integration_spec.rb @@ -26,6 +26,55 @@ def json_ready(value) described_class.json_ready(value) end + def fixture_deep_dup(value) + Marshal.load(Marshal.dump(value)) + end + + def content_recipe_execution_request(recipe_name:, recipe_version:, relative_path:, provider_family:, + template_content:, destination_content:, steps:, provider_backend: nil, runtime_context: nil, metadata: nil) + request = { + recipe_name: recipe_name.to_s, + recipe_version: recipe_version.to_s, + relative_path: relative_path.to_s, + provider_family: provider_family.to_s, + template_content: template_content.to_s, + destination_content: destination_content.to_s, + steps: fixture_deep_dup(steps) + } + request[:provider_backend] = provider_backend.to_s if provider_backend + request[:runtime_context] = fixture_deep_dup(runtime_context) if runtime_context + request[:metadata] = fixture_deep_dup(metadata) if metadata + request + end + + def content_recipe_execution_request_envelope(request) + { + kind: "content_recipe_execution_request", + version: described_class::STRUCTURED_EDIT_TRANSPORT_VERSION, + request: fixture_deep_dup(request) + } + end + + def content_recipe_execution_report(request:, final_content:, changed:, step_reports:, diagnostics:, metadata: nil) + report = { + request: fixture_deep_dup(request), + final_content: final_content.to_s, + changed: changed ? true : false, + step_reports: fixture_deep_dup(step_reports), + diagnostics: fixture_deep_dup(diagnostics) + } + report[:metadata] = fixture_deep_dup(metadata) if metadata + report + end + + def content_recipe_execution_report_envelope(report) + { + kind: "content_recipe_execution_report", + version: described_class::STRUCTURED_EDIT_TRANSPORT_VERSION, + report: fixture_deep_dup(report) + } + end + def read_relative_file_tree(root) root = root.expand_path root.find.each_with_object({}) do |path, files| @@ -36,6 +85,10 @@ def read_relative_file_tree(root) end end + def ruleset_fixture_paths + fixtures_root.join("rulesets").find.select { |path| path.file? && path.extname == ".smrules" } + end + def repo_temp_dir root = Pathname(__dir__).join("..", "..", "tmp").expand_path root.mkpath @@ -71,6 +124,32 @@ def execute_from(executions) ]).to eq(fixture[:categories]) end + it "parses shared compact ruleset fixtures" do + paths = ruleset_fixture_paths + expect(paths).not_to be_empty + + paths.each do |path| + result = described_class.parse_compact_ruleset(path.read) + expect(result[:ok]).to be(true), "#{path}: #{result[:diagnostics].inspect}" + expect(result.dig(:analysis, :directives)).not_to be_empty + end + end + + it "rejects malformed compact ruleset edges" do + cases = { + "missing-required" => "format json\nowners line_bound_statements\nmatch stable_path\nread native_read_portable_write\n", + "repeated-format" => "format json\nformat yaml\nowners line_bound_statements\nmatch stable_path\nread native_read_portable_write\nattach layout_only\n", + "unknown-read" => "format json\nowners line_bound_statements\nmatch stable_path\nread imaginary\nattach layout_only\n", + "unknown-directive" => "format json\nowners line_bound_statements\nmatch stable_path\nread native_read_portable_write\nattach layout_only\nmystery value\n" + } + + cases.each do |name, source| + result = described_class.parse_compact_ruleset(source) + expect(result[:ok]).to be(false), name + expect(result[:diagnostics]).not_to be_empty + end + end + it "conforms to the shared policy vocabulary and reporting fixtures" do policy_fixture = diagnostics_fixture("policy_vocabulary") reporting_fixture = diagnostics_fixture("policy_reporting") @@ -1118,11 +1197,18 @@ def execute_from(executions) ruby_gemspec_dependency_section_policy_acceptance_fixture = diagnostics_fixture("ruby_gemspec_dependency_section_policy_acceptance") ruby_gemspec_files_policy_acceptance_fixture = diagnostics_fixture("ruby_gemspec_files_policy_acceptance") ruby_gemspec_version_loader_policy_acceptance_fixture = diagnostics_fixture("ruby_gemspec_version_loader_policy_acceptance") - project_facts_runtime_context_fixture = diagnostics_fixture("project_facts_runtime_context") + runtime_facts_context_fixture = diagnostics_fixture("runtime_facts_context") ruby_gemspec_self_dependency_policy_acceptance_fixture = diagnostics_fixture("ruby_gemspec_self_dependency_policy_acceptance") ruby_gemfile_self_dependency_policy_acceptance_fixture = diagnostics_fixture("ruby_gemfile_self_dependency_policy_acceptance") ruby_appraisals_self_dependency_policy_acceptance_fixture = diagnostics_fixture("ruby_appraisals_self_dependency_policy_acceptance") ruby_appraisals_min_ruby_prune_policy_acceptance_fixture = diagnostics_fixture("ruby_appraisals_min_ruby_prune_policy_acceptance") + changelog_unreleased_normalization_acceptance_fixture = diagnostics_fixture("changelog_unreleased_normalization_acceptance") + readme_supplied_metadata_synchronization_acceptance_fixture = diagnostics_fixture("readme_supplied_metadata_synchronization_acceptance") + supplied_markdown_pruning_acceptance_fixture = diagnostics_fixture("supplied_markdown_pruning_acceptance") + supplied_source_selector_deletion_acceptance_fixture = diagnostics_fixture("supplied_source_selector_deletion_acceptance") + supplied_yaml_snippet_synchronization_acceptance_fixture = diagnostics_fixture("supplied_yaml_snippet_synchronization_acceptance") + supplied_managed_text_block_replacement_acceptance_fixture = diagnostics_fixture("supplied_managed_text_block_replacement_acceptance") + supplied_yaml_placeholder_scalar_backfill_acceptance_fixture = diagnostics_fixture("supplied_yaml_placeholder_scalar_backfill_acceptance") structured_edit_callable_destination_request_fixture = diagnostics_fixture("structured_edit_callable_destination_request") structured_edit_parity_selection_semantics_fixture = diagnostics_fixture("structured_edit_parity_selection_semantics") structured_edit_parity_match_semantics_fixture = diagnostics_fixture("structured_edit_parity_match_semantics") @@ -2217,7 +2303,7 @@ def execute_from(executions) ) content_recipe_execution_envelope_fixture[:cases].each do |entry| - request = described_class.content_recipe_execution_request( + request = content_recipe_execution_request( recipe_name: entry.dig(:request_envelope, :request, :recipe_name), recipe_version: entry.dig(:request_envelope, :request, :recipe_version), relative_path: entry.dig(:request_envelope, :request, :relative_path), @@ -2229,11 +2315,11 @@ def execute_from(executions) runtime_context: entry.dig(:request_envelope, :request, :runtime_context), metadata: entry.dig(:request_envelope, :request, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_request_envelope(request))).to eq( + expect(json_ready(content_recipe_execution_request_envelope(request))).to eq( json_ready(entry[:request_envelope]) ) - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2241,13 +2327,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end single_file_readme_heading_section_acceptance_fixture[:cases].each do |entry| - request = described_class.content_recipe_execution_request( + request = content_recipe_execution_request( recipe_name: entry.dig(:request_envelope, :request, :recipe_name), recipe_version: entry.dig(:request_envelope, :request, :recipe_version), relative_path: entry.dig(:request_envelope, :request, :relative_path), @@ -2259,11 +2345,11 @@ def execute_from(executions) runtime_context: entry.dig(:request_envelope, :request, :runtime_context), metadata: entry.dig(:request_envelope, :request, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_request_envelope(request))).to eq( + expect(json_ready(content_recipe_execution_request_envelope(request))).to eq( json_ready(entry[:request_envelope]) ) - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2271,13 +2357,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end native_structured_edit_recipe_steps_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2285,13 +2371,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end ruby_gemfile_signature_merge_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2299,7 +2385,7 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end @@ -2311,7 +2397,7 @@ def execute_from(executions) expect(ruby_gemspec_native_boundary_report_fixture[:wrapper_required_behaviors].map { |entry| entry[:name] }).to include( "dependency_ruby_floor_comment_alignment" ) - request = described_class.content_recipe_execution_request( + request = content_recipe_execution_request( recipe_name: ruby_gemspec_native_boundary_report_fixture.dig(:example_native_recipe, :request, :recipe_name), recipe_version: ruby_gemspec_native_boundary_report_fixture.dig(:example_native_recipe, :request, :recipe_version), relative_path: ruby_gemspec_native_boundary_report_fixture.dig(:example_native_recipe, :request, :relative_path), @@ -2323,12 +2409,12 @@ def execute_from(executions) runtime_context: ruby_gemspec_native_boundary_report_fixture.dig(:example_native_recipe, :request, :runtime_context), metadata: ruby_gemspec_native_boundary_report_fixture.dig(:example_native_recipe, :request, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_request_envelope(request))).to eq( + expect(json_ready(content_recipe_execution_request_envelope(request))).to eq( json_ready(ruby_gemspec_native_boundary_report_fixture[:example_native_recipe]) ) ruby_gemspec_signature_merge_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2336,13 +2422,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end ruby_gemspec_field_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2350,13 +2436,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end ruby_gemspec_dependency_section_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2364,13 +2450,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end ruby_gemspec_files_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2378,13 +2464,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end ruby_gemspec_version_loader_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2392,13 +2478,13 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) end - project_facts_runtime_context_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + runtime_facts_context_fixture[:cases].each do |entry| + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2406,16 +2492,16 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) - expect(entry.dig(:report_envelope, :report, :request, :runtime_context, :project_facts, :schema)).to eq( - "project_facts.v1" + expect(entry.dig(:report_envelope, :report, :request, :runtime_context, :facts, :schema)).to eq( + "runtime_facts.v1" ) end ruby_gemspec_self_dependency_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2423,7 +2509,7 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) if entry[:label] == "delete-active-self-dependencies-preserve-comments" @@ -2433,7 +2519,7 @@ def execute_from(executions) end ruby_gemfile_self_dependency_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2441,18 +2527,24 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) if entry[:label] == "delete-gemfile-self-dependencies-across-nesting" final_content = entry.dig(:report_envelope, :report, :final_content) expect(final_content).not_to include('gem "demo", "~> 1.0"') + expect(final_content).not_to include('path: "../dev/demo"') expect(final_content).to include('# gem "demo", "~> 0"') + expect(final_content).to include('gem "fallback-gem"') + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :operation)).to eq("delete") + end + if entry[:label] == "missing-project-identity-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") end end ruby_appraisals_self_dependency_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2460,18 +2552,23 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) if entry[:label] == "delete-appraisals-self-dependencies" final_content = entry.dig(:report_envelope, :report, :final_content) expect(final_content).not_to include('gem "demo"') expect(final_content).to include('appraise("rails-6")') + expect(final_content).to include('gem "rspec" # Testing') + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :operation)).to eq("delete") + end + if entry[:label] == "missing-project-identity-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") end end ruby_appraisals_min_ruby_prune_policy_acceptance_fixture[:cases].each do |entry| - report = described_class.content_recipe_execution_report( + report = content_recipe_execution_report( request: entry.dig(:report_envelope, :report, :request), final_content: entry.dig(:report_envelope, :report, :final_content), changed: entry.dig(:report_envelope, :report, :changed), @@ -2479,14 +2576,135 @@ def execute_from(executions) diagnostics: entry.dig(:report_envelope, :report, :diagnostics), metadata: entry.dig(:report_envelope, :report, :metadata) ) - expect(json_ready(described_class.content_recipe_execution_report_envelope(report))).to eq( + expect(json_ready(content_recipe_execution_report_envelope(report))).to eq( json_ready(entry[:report_envelope]) ) if entry[:label] == "delete-ruby-appraisals-below-min-ruby" final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).not_to include("ruby-2-3") expect(final_content).not_to include("ruby-2-7") + expect(final_content).not_to include("ruby-3-0") expect(final_content).to include("ruby-3-2") + expect(final_content).to include('appraise "style"') + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :operation)).to eq("delete") + expect(final_content).not_to include("\n\n\n") + end + if entry[:label] == "missing-min-ruby-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + changelog_unreleased_normalization_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "create-unreleased-section-from-supplied-entries" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content.index("## Unreleased")).to be < final_content.index("## 1.2.0") + expect(final_content).to include("- Added native Markdown recipe boundary.") + expect(final_content).to include("- Existing release.") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :operation)).to eq("insert_or_replace_section") + end + if entry[:label] == "missing-entries-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + readme_supplied_metadata_synchronization_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "sync-readme-heading-and-summary-from-supplied-metadata" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).to start_with("# Demo Toolkit\n") + expect(final_content).to include("A deterministic toolkit for structured merges.") + expect(final_content).to include("Destination usage.") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :consumed_context)).to eq("readme_metadata.title") + expect(entry.dig(:report_envelope, :report, :step_reports, 1, :metadata, :consumed_context)).to eq("readme_metadata.summary") + end + if entry[:label] == "missing-readme-metadata-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + supplied_markdown_pruning_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "prune-supplied-table-rows-and-reference-definitions" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).not_to include("Works with JRuby") + expect(final_content).not_to include("[jruby-9.4]:") + expect(final_content).not_to include("[jruby-head]:") + expect(final_content).to include("Works with MRI Ruby") + expect(final_content).to include("[ruby-3.2]:") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :deleted_rows)).to eq(1) + expect(entry.dig(:report_envelope, :report, :step_reports, 1, :metadata, :deleted_reference_definitions)).to eq(2) + end + if entry[:label] == "missing-prune-selectors-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + supplied_source_selector_deletion_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "delete-supplied-structural-owner-ranges" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).not_to include("kettle/scaffold") + expect(final_content).not_to include("task :scaffold") + expect(final_content).to include('require "bundler/gem_tasks"') + expect(final_content).to include("task :spec") expect(final_content).not_to include("\n\n\n") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :deleted_ranges)).to eq(2) + end + if entry[:label] == "missing-delete-selectors-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + supplied_yaml_snippet_synchronization_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "apply-supplied-sections-and-scalar-pins" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).to include("concurrency:") + expect(final_content).to include("permissions:") + expect(final_content).to include("actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd") + expect(final_content).to include("ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92") + expect(final_content).not_to include("actions/checkout@v3") + expect(final_content).not_to include("ruby/setup-ruby@v1") + expect(final_content).to include("gemfiles/current.gemfile") + expect(final_content).to include('ruby-version: ${{ matrix.ruby }}') + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :updated_sections)).to eq(2) + expect(entry.dig(:report_envelope, :report, :step_reports, 1, :metadata, :updated_scalars)).to eq(2) + end + if entry[:label] == "missing-yaml-updates-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + supplied_managed_text_block_replacement_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "replace-existing-managed-text-block" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).to include('gem "debug", "~> 1.9"') + expect(final_content).to include('gem "irb", "~> 1.15"') + expect(final_content).not_to include("old-debug") + expect(final_content).to include('gem "rake"') + expect(final_content).to include('gem "rspec"') + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :replaced_blocks)).to eq(1) + end + if entry[:label] == "append-missing-managed-text-block" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).to include("# <>") + expect(final_content).to include("# (no shunted dependencies)") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :appended_blocks)).to eq(1) + end + if entry[:label] == "missing-managed-block-updates-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") + end + end + + supplied_yaml_placeholder_scalar_backfill_acceptance_fixture[:cases].each do |entry| + if entry[:label] == "backfill-placeholder-and-blank-scalars" + final_content = entry.dig(:report_envelope, :report, :final_content) + expect(final_content).to include('name: "demo-toolkit"') + expect(final_content).to include("namespace: 'Demo::Toolkit'") + expect(final_content).to include('homepage: "https://example.invalid/existing"') + expect(final_content).to include("# ENV: KJ_GEM_NAME") + expect(final_content).to include("# keep concrete value") + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :updated_scalars)).to eq(2) + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :metadata, :preserved_scalars)).to eq(1) + end + if entry[:label] == "missing-yaml-scalar-backfills-fails-closed" + expect(entry.dig(:report_envelope, :report, :step_reports, 0, :status)).to eq("failed") end end diff --git a/gems/ast-template/lib/ast/template/version.rb b/gems/ast-template/lib/ast/template/version.rb index aab6b9a..3b4bad5 100644 --- a/gems/ast-template/lib/ast/template/version.rb +++ b/gems/ast-template/lib/ast/template/version.rb @@ -2,6 +2,6 @@ module Ast module Template - VERSION = "0.0.0" + VERSION = "0.1.0" end end diff --git a/gems/commonmarker-merge/lib/commonmarker/merge/version.rb b/gems/commonmarker-merge/lib/commonmarker/merge/version.rb index 976007c..8df1b1f 100644 --- a/gems/commonmarker-merge/lib/commonmarker/merge/version.rb +++ b/gems/commonmarker-merge/lib/commonmarker/merge/version.rb @@ -2,6 +2,6 @@ module Commonmarker module Merge - VERSION = "0.0.0" + VERSION = "0.1.0" end end diff --git a/gems/json-merge/lib/json/merge.rb b/gems/json-merge/lib/json/merge.rb index 6180676..64b1806 100644 --- a/gems/json-merge/lib/json/merge.rb +++ b/gems/json-merge/lib/json/merge.rb @@ -186,7 +186,7 @@ def collect_json_owners(value, path = "") def merge_json_values(template, destination) if template.is_a?(Hash) && destination.is_a?(Hash) - (template.keys | destination.keys).sort.each_with_object({}) do |key, merged| + ordered_merge_keys(template, destination).each_with_object({}) do |key, merged| if !template.key?(key) merged[key] = destination[key] elsif !destination.key?(key) @@ -201,6 +201,11 @@ def merge_json_values(template, destination) end private_class_method :merge_json_values + def ordered_merge_keys(template, destination) + template.keys + destination.keys.reject { |key| template.key?(key) } + end + private_class_method :ordered_merge_keys + def detect_trailing_comma(source) state = scanner_state source.each_char.with_index do |char, index| diff --git a/gems/kettle-jem/kettle-jem.gemspec b/gems/kettle-jem/kettle-jem.gemspec new file mode 100644 index 0000000..153619b --- /dev/null +++ b/gems/kettle-jem/kettle-jem.gemspec @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "lib/kettle/jem/version" + +Gem::Specification.new do |spec| + spec.name = "kettle-jem" + spec.version = Kettle::Jem::VERSION + spec.authors = ["Structured Merge Contributors"] + spec.email = ["opensource@structuredmerge.dev"] + + spec.summary = "RubyGems package templating wrapper for Structured Merge" + spec.description = "RubyGems-focused recipe-pack wrapper that shapes package facts into ast-merge transport." + spec.homepage = "https://github.com/structuredmerge/structuredmerge-ruby" + spec.licenses = ["AGPL-3.0-only", "PolyForm-Small-Business-1.0.0"] + spec.required_ruby_version = ">= 4.0.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + + root = __dir__ + spec.files = Dir.chdir(root) do + Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |path| File.directory?(path) } + end + spec.require_paths = ["lib"] + + spec.add_dependency "ast-merge", "= #{Kettle::Jem::VERSION}" + spec.add_dependency "ruby-merge", "= #{Kettle::Jem::VERSION}" + spec.add_dependency "token-resolver", "~> 1.0", ">= 1.0.2" + spec.add_dependency "toml-merge", "= #{Kettle::Jem::VERSION}" + spec.add_dependency "yaml-merge", "= #{Kettle::Jem::VERSION}" +end diff --git a/gems/kettle-jem/lib/kettle-jem.rb b/gems/kettle-jem/lib/kettle-jem.rb new file mode 100644 index 0000000..0081fd1 --- /dev/null +++ b/gems/kettle-jem/lib/kettle-jem.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "kettle/jem" diff --git a/gems/kettle-jem/lib/kettle/jem.rb b/gems/kettle-jem/lib/kettle/jem.rb new file mode 100644 index 0000000..c922ca5 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem.rb @@ -0,0 +1,2494 @@ +# frozen_string_literal: true + +require "fileutils" +require "find" +require "ruby/merge" +require "token/resolver" +require "toml-merge" +require "yaml" +require "yaml/merge" +require "ast/merge" +require_relative "jem/version" + +module Kettle + module Jem + PACKAGE_NAME = "kettle-jem" + CONTENT_RECIPE_TRANSPORT_VERSION = Ast::Merge::STRUCTURED_EDIT_TRANSPORT_VERSION + MANAGED_BLOCK_OPEN = "# <> do not edit below this line" + MANAGED_BLOCK_CLOSE = "# <>" + OBSOLETE_GITHUB_WORKFLOWS = %w[ancient.yml legacy.yml supported.yml unsupported.yml main.yml hoary.yml].freeze + OPENCOLLECTIVE_DISABLED_FILES = %w[.opencollective.yml .github/workflows/opencollective.yml].freeze + FILE_DELETION_PRIMITIVES = %w[supplied_obsolete_file_deletion supplied_disabled_opencollective_file_deletion].freeze + PACKAGED_TEMPLATE_ROOT = File.expand_path("jem/templates", __dir__) + SUPPORTED_TEMPLATE_STRATEGIES = %i[merge accept_template keep_destination raw_copy].freeze + SUPPORTED_TEMPLATE_FILE_TYPES = %i[ruby gemfile appraisals gemspec rakefile yaml toml markdown text].freeze + RUBY_TEMPLATE_BASENAMES = %w[Gemfile Rakefile Appraisals Appraisal.root.gemfile .simplecov].freeze + RUBY_TEMPLATE_SUFFIXES = %w[.gemspec .gemfile].freeze + RUBY_TEMPLATE_EXTENSIONS = %w[.rb .rake].freeze + TEMPLATE_TOKEN_CONFIG = Token::Resolver::Config.new(separators: ["|", ":"]).freeze + EMPTY_TEMPLATE_TOKENS = %w[KJ|COPYRIGHT_PREFIX KJ|MIN_DIVERGENCE_THRESHOLD].freeze + README_TOP_LOGO_MODE_DEFAULT = "org_and_project" + README_TOP_LOGO_MODES = %w[org project org_and_project].freeze + README_DEFAULT_PRESERVE_SECTIONS = ["synopsis", "configuration", "basic usage"].freeze + README_DEFAULT_PRESERVE_PATTERNS = ["note:*"].freeze + README_SECTION_ALIASES = { + "summary" => "synopsis", + "usage" => "basic usage", + "configuration options" => "configuration", + "setup" => "basic usage", + }.freeze + README_STATIC_TOP_LOGO_ROW = "[![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang]" + README_STATIC_TOP_LOGO_REFS = [ + "[🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg", + "[🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN", + "[🖼️ruby-lang-i]: https://logos.galtzo.com/assets/images/ruby-lang/avatar-192px.svg", + "[🖼️ruby-lang]: https://www.ruby-lang.org/", + ].join("\n").freeze + RUBOCOP_VERSION_MAP = [ + [Gem::Version.new("1.8"), "~> 0.1"], + [Gem::Version.new("1.9"), "~> 2.0"], + [Gem::Version.new("2.0"), "~> 4.0"], + [Gem::Version.new("2.1"), "~> 6.0"], + [Gem::Version.new("2.2"), "~> 8.0"], + [Gem::Version.new("2.3"), "~> 10.0"], + [Gem::Version.new("2.4"), "~> 12.0"], + [Gem::Version.new("2.5"), "~> 14.0"], + [Gem::Version.new("2.6"), "~> 16.0"], + [Gem::Version.new("2.7"), "~> 18.0"], + [Gem::Version.new("3.0"), "~> 20.0"], + [Gem::Version.new("3.1"), "~> 22.0"], + [Gem::Version.new("3.2"), "~> 24.0"], + [Gem::Version.new("3.3"), "~> 26.0"], + [Gem::Version.new("3.4"), "~> 28.0"], + ].freeze + FORGE_USER_ENV_KEYS = { + gh_user: "KJ_GH_USER", + gl_user: "KJ_GL_USER", + cb_user: "KJ_CB_USER", + sh_user: "KJ_SH_USER", + }.freeze + FUNDING_TOKEN_ENV_KEYS = { + patreon: "KJ_FUNDING_PATREON", + kofi: "KJ_FUNDING_KOFI", + paypal: "KJ_FUNDING_PAYPAL", + buymeacoffee: "KJ_FUNDING_BUYMEACOFFEE", + polar: "KJ_FUNDING_POLAR", + liberapay: "KJ_FUNDING_LIBERAPAY", + issuehunt: "KJ_FUNDING_ISSUEHUNT", + }.freeze + SOCIAL_TOKEN_ENV_KEYS = { + mastodon: "KJ_SOCIAL_MASTODON", + bluesky: "KJ_SOCIAL_BLUESKY", + linktree: "KJ_SOCIAL_LINKTREE", + devto: "KJ_SOCIAL_DEVTO", + }.freeze + APACHE_LICENSE_COMPAT_CATEGORIES = { + "Apache-2.0" => :a, + "MIT" => :a, + "AGPL-3.0-only" => :x, + "PolyForm-Noncommercial-1.0.0" => :x, + "PolyForm-Small-Business-1.0.0" => :x, + "LicenseRef-Big-Time-Public-License" => :x, + }.freeze + APACHE_LICENSE_COMPAT_BADGE_DATA = { + a: { + alt: "Apache license compatibility: Category A", + label: "Apache_Compatible:_Category_A", + message: "\u2713", + color: "259D6C", + ref: "https://www.apache.org/legal/resolved.html#category-a", + }, + b: { + alt: "Apache license compatibility: Category B", + label: "Apache_Maybe_Compatible:_Category_B", + message: "?", + color: "D9A407", + ref: "https://www.apache.org/legal/resolved.html#category-b", + }, + x: { + alt: "Apache license compatibility: Category X", + label: "Apache_Incompatible:_Category_X", + message: "\u2717", + color: "C0392B", + ref: "https://www.apache.org/legal/resolved.html#category-x", + }, + unknown: { + alt: "Apache license compatibility: Unknown", + label: "Apache_Compatibility", + message: "Unknown", + color: "6C757D", + ref: "https://www.apache.org/legal/resolved.html", + }, + }.freeze + + module_function + + def discover_facts(project_root, env: ENV) + gemspec_path = Dir.glob(File.join(project_root, "*.gemspec")).sort.first + raise ArgumentError, "no gemspec found in #{project_root}" unless gemspec_path + + gemspec = File.read(gemspec_path) + name = extract_gemspec_assignment(gemspec, "spec.name") || File.basename(gemspec_path, ".gemspec") + homepage_url = extract_gemspec_assignment(gemspec, "spec.homepage") + metadata_source_url = extract_metadata_value(gemspec, "source_code_uri") + source_url = concrete_github_url(metadata_source_url) || concrete_github_url(homepage_url) || metadata_source_url || homepage_url + + kettle_config = kettle_jem_config(project_root) + author = author_facts(gemspec, kettle_config, env) + license = license_facts(kettle_config, extract_gemspec_array(gemspec, "spec.licenses"), author_email: author[:email]) + project_runtime = project_runtime_facts( + kettle_config, + env, + package_name: name, + source_url: source_url, + author_domain: author[:domain], + min_ruby: extract_gemspec_assignment(gemspec, "spec.required_ruby_version"), + version: extract_gemspec_assignment(gemspec, "spec.version") + ) + facts = { + package: compact_hash( + ecosystem: "rubygems", + name: name, + slug: name, + description: extract_gemspec_assignment(gemspec, "spec.description") || + extract_gemspec_assignment(gemspec, "spec.summary"), + homepage_url: homepage_url, + source_url: source_url, + license_expression: license[:expression], + ), + rubygems: compact_hash( + gemspec_path: File.basename(gemspec_path), + namespace: classify_namespace(name), + min_ruby: extract_gemspec_assignment(gemspec, "spec.required_ruby_version"), + ), + } + bootstrap = kettle_config_bootstrap_facts(project_root, env) + facts[:kettle_config_bootstrap] = bootstrap if bootstrap + facts[:author] = author unless author.empty? + forge = forge_facts(kettle_config, env) + facts[:forge] = forge unless forge.empty? + social = social_facts(kettle_config, env) + facts[:social] = social unless social.empty? + opencollective_policy = opencollective_policy(kettle_config, env) + opencollective_disabled = opencollective_policy.fetch(:disabled) + open_collective_org = opencollective_org(project_root, env, opencollective_disabled: opencollective_disabled) + funding = compact_hash( + urls: funding_urls( + project_root, + gemspec, + name, + opencollective_disabled: opencollective_disabled, + open_collective_org: open_collective_org && open_collective_org.fetch(:org) + ) + ) + funding_tokens = funding_platform_token_facts(kettle_config, env) + funding[:platform_tokens] = funding_tokens unless funding_tokens.empty? + funding[:open_collective_disabled] = true if opencollective_disabled + funding[:open_collective_disabled_source] = opencollective_policy[:source] if opencollective_disabled + if open_collective_org + funding[:open_collective_org] = open_collective_org.fetch(:org) + funding[:open_collective_org_source] = open_collective_org.fetch(:source) + end + open_collective_files = opencollective_disabled ? opencollective_disabled_files(project_root) : [] + funding[:open_collective_files] = open_collective_files unless open_collective_files.empty? + facts[:funding] = funding unless funding.empty? + facts[:ci] = { + provider: "github_actions", + default_branch: "main", + ruby_versions: github_actions_ruby_versions(facts.fetch(:rubygems).fetch(:min_ruby, nil)), + obsolete_workflows: github_actions_obsolete_workflows(project_root), + custom_workflows: github_actions_custom_workflows(project_root, opencollective_disabled: opencollective_disabled), + } + coverage_config = github_actions_coverage_config(kettle_config) + facts[:ci][:coverage] = coverage_config unless coverage_config.empty? + framework_matrix = github_actions_framework_matrix(kettle_config) + facts[:ci][:framework_matrix] = framework_matrix unless framework_matrix.empty? + template_facts = {} + template_preferences = template_source_preferences(project_root, kettle_config, opencollective_disabled: opencollective_disabled) + template_facts[:source_preferences] = template_preferences unless template_preferences.empty? + unless template_preferences.empty? + facts[:license] = license unless license.empty? + facts[:project_runtime] = project_runtime unless project_runtime.empty? + readme_logo = readme_logo_facts(kettle_config, package_name: name, github_org: project_runtime[:github_org]) + facts[:readme_logo] = readme_logo unless readme_logo.empty? + template_tokens = template_tokens(facts, funding) + template_facts[:tokens] = template_tokens unless template_tokens.empty? + end + facts[:templates] = template_facts unless template_facts.empty? + facts + end + + def recipe_pack(facts) + recipes = [ + recipe_entry("readme_metadata", "README.md", "markdown", "supplied_readme_metadata_synchronization", facts: %w[package funding readme]), + recipe_entry("changelog_unreleased", "CHANGELOG.md", "markdown", "changelog_unreleased_normalization", facts: %w[package changelog]), + recipe_entry("generated_block_sync", "gemfiles/modular/shunted.gemfile", "text", "supplied_managed_text_block_replacement", facts: %w[package generated_blocks]), + recipe_entry( + "github_funding_yml", + ".github/FUNDING.yml", + "yaml", + "supplied_github_funding_yaml_synchronization", + facts: %w[package funding] + ), + recipe_entry( + "github_actions_ci", + ".github/workflows/ci.yml", + "yaml", + "supplied_github_actions_workflow_synchronization", + facts: %w[package rubygems ci] + ), + ] + if facts[:kettle_config_bootstrap] + recipes.unshift(kettle_config_bootstrap_recipe(facts.fetch(:kettle_config_bootstrap))) + end + if facts.dig(:ci, :framework_matrix) + recipes << recipe_entry( + "github_actions_framework_ci", + ".github/workflows/framework-ci.yml", + "yaml", + "supplied_github_actions_framework_workflow_synchronization", + facts: %w[package rubygems ci] + ) + end + if facts.dig(:ci, :coverage) + recipes << recipe_entry( + "github_actions_coverage_ci", + ".github/workflows/coverage.yml", + "yaml", + "supplied_github_actions_coverage_workflow_synchronization", + facts: %w[package rubygems ci] + ) + end + facts.dig(:ci, :obsolete_workflows).to_a.each do |workflow_path| + recipes << recipe_entry( + "github_actions_obsolete_workflow_cleanup_#{workflow_recipe_slug(workflow_path)}", + workflow_path, + "file", + "supplied_obsolete_file_deletion", + facts: %w[ci] + ) + end + facts.dig(:funding, :open_collective_files).to_a.each do |relative_path| + recipes << recipe_entry( + "opencollective_disabled_file_cleanup_#{workflow_recipe_slug(relative_path)}", + relative_path, + "file", + "supplied_disabled_opencollective_file_deletion", + facts: %w[funding] + ) + end + facts.dig(:ci, :custom_workflows).to_a.each do |workflow_path| + recipes << recipe_entry( + "github_actions_workflow_snippets_#{workflow_recipe_slug(workflow_path)}", + workflow_path, + "yaml", + "supplied_github_actions_workflow_snippet_merge", + facts: %w[ci] + ) + end + facts.dig(:templates, :source_preferences).to_a.each do |preference| + apply_template = preference.fetch(:apply, false) + recipe = recipe_entry( + "#{apply_template ? "template_source_application" : "template_source_preference"}_#{workflow_recipe_slug(preference.fetch(:target_path))}", + preference.fetch(:target_path), + "file", + apply_template ? "supplied_template_source_application" : "supplied_template_source_preference", + facts: %w[templates funding] + ) + recipe[:template_preference] = preference + recipe[:template_tokens] = facts.dig(:templates, :tokens) if facts.dig(:templates, :tokens) + recipes << recipe + end + recipes << recipe_entry( + "rakefile_scaffold_cleanup", + "Rakefile", + "generic_ast", + "supplied_source_selector_deletion", + provider_backend: "generic_structural_owners", + facts: %w[rubygems rakefile], + selectors: %w[rakefile_scaffold] + ) + + { + name: "kettle-jem-core", + version: 1, + ecosystem: "rubygems", + recipes: recipes, + } + end + + def plan_project(project_root, env: ENV) + facts = discover_facts(project_root, env: env) + pack = recipe_pack(facts) + files = read_project_files(project_root, pack) + recipe_reports = pack.fetch(:recipes).map do |recipe| + execute_recipe(project_root: project_root, recipe: recipe, facts: facts, files: files) + end + changed_files = recipe_reports.filter_map { |report| report[:relative_path] if report[:changed] }.sort + + { + mode: "plan", + ready: true, + facts: facts, + recipe_pack: pack, + recipe_reports: recipe_reports, + changed_files: changed_files, + diagnostics: recipe_reports.flat_map { |report| report[:diagnostics] }, + } + end + + def apply_project(project_root, env: ENV) + report = plan_project(project_root, env: env).merge(mode: "apply") + report.fetch(:recipe_reports).each do |recipe_report| + next unless recipe_report[:changed] + + path = File.join(project_root, recipe_report.fetch(:relative_path)) + if recipe_report.dig(:metadata, :delete_file) + FileUtils.rm_f(path) + else + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, recipe_report.fetch(:final_content)) + end + end + report + end + + def content_recipe_execution_request(recipe_name:, recipe_version:, relative_path:, provider_family:, + template_content:, destination_content:, steps:, provider_backend: nil, runtime_context: nil, metadata: nil) + compact_hash( + recipe_name: recipe_name.to_s, + recipe_version: recipe_version.to_s, + relative_path: relative_path.to_s, + provider_family: provider_family.to_s, + provider_backend: provider_backend&.to_s, + template_content: template_content.to_s, + destination_content: destination_content.to_s, + steps: deep_dup(steps), + runtime_context: deep_dup(runtime_context || {}), + metadata: deep_dup(metadata || {}), + ) + end + + def content_recipe_execution_request_envelope(request) + { + kind: "content_recipe_execution_request", + version: CONTENT_RECIPE_TRANSPORT_VERSION, + request: deep_dup(request), + } + end + + def content_recipe_execution_report(request:, final_content:, changed:, step_reports:, diagnostics:, metadata: nil) + compact_hash( + request: deep_dup(request), + final_content: final_content.to_s, + changed: changed ? true : false, + step_reports: deep_dup(step_reports), + diagnostics: deep_dup(diagnostics), + metadata: deep_dup(metadata || {}), + ) + end + + def content_recipe_execution_report_envelope(report) + { + kind: "content_recipe_execution_report", + version: CONTENT_RECIPE_TRANSPORT_VERSION, + report: deep_dup(report), + } + end + + def synchronize_readme(content, facts) + package = facts.fetch(:package) + lines = content.to_s.split("\n", -1) + heading = "# #{package.fetch(:name)}" + h1_index = lines.index { |line| line.start_with?("# ") } + unless h1_index + lines.unshift(heading, "") + end + replace_markdown_managed_block(lines.join("\n"), "kettle-jem:metadata", readme_metadata_block(facts)) + end + + def normalize_changelog(content, facts) + text = content.to_s + title = "# Changelog" + text = "#{title}\n\n#{text}" unless text.lines.first&.start_with?("# ") + return ensure_trailing_newline(text) if text.match?(/^##\s+\[?Unreleased\]?/i) + + lines = text.split("\n", -1) + insert_at = lines.index { |line| line.start_with?("## ") } || lines.length + section = [ + "## [Unreleased]", + "", + "### Added", + "", + "### Changed", + "", + "### Fixed", + "", + ] + lines.insert(insert_at, *section) + ensure_trailing_newline(lines.join("\n").gsub(/\n{3,}/, "\n\n")) + end + + def synchronize_managed_block(content, facts) + replacement = [ + MANAGED_BLOCK_OPEN, + "# package: #{facts.fetch(:package).fetch(:name)}", + "# generated by kettle-jem vNext", + MANAGED_BLOCK_CLOSE, + "", + ].join("\n") + replace_text_managed_block(content.to_s, replacement) + end + + def execute_recipe(project_root:, recipe:, facts:, files:) + relative_path = recipe.fetch(:target_path) + original = files.fetch(relative_path, "") + deletion = recipe.fetch(:name) == "rakefile_scaffold_cleanup" ? delete_rakefile_scaffold(original) : nil + final = case recipe.fetch(:name) + when "readme_metadata" + synchronize_readme(original, facts) + when "changelog_unreleased" + normalize_changelog(original, facts) + when "generated_block_sync" + synchronize_managed_block(original, facts) + when "github_funding_yml" + synchronize_github_funding_yml(original, facts) + when "github_actions_ci" + synchronize_github_actions_ci(original, facts) + when "github_actions_framework_ci" + synchronize_github_actions_framework_ci(original, facts) + when "github_actions_coverage_ci" + synchronize_github_actions_coverage_ci(original, facts) + when /\Agithub_actions_obsolete_workflow_cleanup_/ + "" + when /\Aopencollective_disabled_file_cleanup_/ + "" + when /\Agithub_actions_workflow_snippets_/ + synchronize_github_actions_workflow_snippets(original) + when "kettle_config_bootstrap" + apply_kettle_config_bootstrap(project_root, recipe) + when /\Atemplate_source_preference_/ + original + when /\Atemplate_source_application_/ + apply_template_source(project_root, recipe, original) + when "rakefile_scaffold_cleanup" + deletion.fetch(:content) + else + original + end + + template_content = recipe_template_content(project_root, recipe) + request = content_recipe_execution_request( + recipe_name: recipe.fetch(:primitive), + recipe_version: "1", + relative_path: relative_path, + provider_family: recipe.fetch(:provider_family), + provider_backend: recipe[:provider_backend], + template_content: template_content, + destination_content: original, + steps: [content_recipe_step(recipe)], + runtime_context: recipe_runtime_context(recipe, facts, deletion), + metadata: { packaging_recipe: recipe.fetch(:name), project_root: project_root.to_s }, + ) + changed = delete_file_recipe?(recipe) || final != original + step_report = content_recipe_step_report(recipe: recipe, request: request, original: original, final: final, changed: changed, deletion: deletion) + report = content_recipe_execution_report( + request: request, + final_content: final, + changed: changed, + step_reports: [step_report], + diagnostics: [], + metadata: recipe_report_metadata(recipe), + ) + + { + recipe_name: recipe.fetch(:name), + relative_path: relative_path, + changed: changed, + request_envelope: content_recipe_execution_request_envelope(request), + report_envelope: content_recipe_execution_report_envelope(report), + final_content: final, + metadata: recipe_report_metadata(recipe), + diagnostics: [], + } + end + + def content_recipe_step(recipe) + step = { + step_id: recipe.fetch(:name), + step_kind: recipe.fetch(:primitive), + name: recipe.fetch(:name), + provider_family: recipe.fetch(:provider_family), + metadata: { target_path: recipe.fetch(:target_path) }, + } + step[:provider_backend] = recipe[:provider_backend] if recipe[:provider_backend] + if recipe.fetch(:primitive) == "supplied_source_selector_deletion" + step[:step_kind] = "native_policy" + step[:policy] = { + policy_kind: "delete_supplied_structural_owners", + required_context: "delete_selectors", + operation: "delete", + selector_family: "structural_owner_range", + normalize_blank_lines: true, + } + end + step + end + + def content_recipe_step_report(recipe:, request:, original:, final:, changed:, deletion: nil) + operation_profile = Ast::Merge.structured_edit_operation_profile( + operation_kind: recipe.fetch(:primitive), + known_operation_kind: true, + source_requirement: "destination_content", + destination_requirement: "relative_path", + replacement_source: "runtime_context", + captures_source_text: false, + supports_if_missing: true, + operation_family: "kettle-jem", + ) + result = Ast::Merge.structured_edit_result( + operation_kind: recipe.fetch(:primitive), + updated_content: final, + changed: changed, + operation_profile: operation_profile, + ) + application = Ast::Merge.structured_edit_application(request: request, result: result) + { + step_id: recipe.fetch(:name), + step_kind: recipe.fetch(:primitive), + status: changed ? "applied" : "unchanged", + changed: changed, + input_content: original, + output_content: final, + application: application, + diagnostics: [], + metadata: step_report_metadata(recipe, deletion), + } + end + + def read_project_files(project_root, pack) + pack.fetch(:recipes).to_h do |recipe| + relative_path = recipe.fetch(:target_path) + path = File.join(project_root, relative_path) + [relative_path, File.exist?(path) ? File.read(path) : ""] + end + end + + def recipe_template_content(project_root, recipe) + return "" unless %w[ + supplied_kettle_config_bootstrap + supplied_template_source_preference + supplied_template_source_application + ].include?(recipe.fetch(:primitive)) + + preference = recipe.fetch(:template_preference) + path = File.join( + preference.fetch(:source_root_path, project_root), + preference.fetch(:source_relative_path, preference.fetch(:selected_source)) + ) + File.read(path) + end + + def apply_template_source(project_root, recipe, original) + strategy = recipe.dig(:template_preference, :strategy).to_s + return original if strategy == "keep_destination" + + content = recipe_template_content(project_root, recipe) + return content if strategy == "raw_copy" + + resolved = resolve_template_tokens( + content, + recipe.fetch(:template_tokens, {}), + scan_unresolved: unresolved_template_scan?(recipe) + ) + rescue ArgumentError => e + raise ArgumentError, "#{recipe.fetch(:target_path)}: #{e.message}" + else + if recipe.fetch(:target_path) == "README.md" && (strategy.empty? || strategy == "merge") + return merge_readme_template( + template_content: resolved, + destination_content: original, + preserve_config: recipe.dig(:template_preference, :readme_preserve_config) || {} + ) + end + return merge_config_template_source(recipe, resolved, original) if strategy.empty? || strategy == "merge" + + resolved + end + + def merge_config_template_source(recipe, template_content, destination_content) + file_type = template_file_type(recipe) + return template_content if destination_content.to_s.strip.empty? + return destination_content if destination_content == template_content + + case file_type + when :gemspec + return merge_gemspec_template_source(template_content, destination_content) + when :ruby, :gemfile, :appraisals, :rakefile + merge_result = Ruby::Merge.merge_ruby( + template_content, + destination_content, + "ruby", + merge_template_requires: file_type == :rakefile + ) + when :yaml + merge_result = Yaml::Merge.merge_yaml(template_content, destination_content, "yaml") + when :toml + merge_result = Toml::Merge.merge_toml(template_content, destination_content, "toml") + else + return template_content + end + return merge_result.fetch(:output) if merge_result[:ok] + + diagnostics = merge_result.fetch(:diagnostics, []) + message = diagnostics.map { |diagnostic| diagnostic[:message] || diagnostic["message"] }.compact.join("; ") + raise ArgumentError, "failed to merge #{file_type} template #{recipe.fetch(:target_path)}: #{message}" + end + + def merge_gemspec_template_source(template_content, destination_content) + replacements = gemspec_preserved_assignments(destination_content) + return template_content if replacements.empty? + + replacements.reduce(template_content.dup) do |content, (field, source_line)| + pattern = /^(\s*spec\.#{Regexp.escape(field)}\s*=\s*).*$/ + content.match?(pattern) ? content.sub(pattern, source_line.rstrip) : content + end + end + + def gemspec_preserved_assignments(source) + %w[ + name + authors + email + summary + description + homepage + licenses + required_ruby_version + executables + ].each_with_object({}) do |field, assignments| + line = source.to_s.lines.find { |candidate| candidate.match?(/^\s*spec\.#{Regexp.escape(field)}\s*=/) } + next unless line + next if line.include?("TODO:") + + assignments[field] = line + end + end + + def template_file_type(recipe) + configured = recipe.dig(:template_preference, :file_type).to_s + return configured.to_sym unless configured.empty? + + relative_path = recipe.fetch(:target_path).to_s + basename = File.basename(relative_path) + extension = File.extname(relative_path).downcase + return :gemfile if basename == "Gemfile" || basename.end_with?(".gemfile") + return :appraisals if basename.start_with?("Appraisals") || basename == "Appraisal.root.gemfile" + return :gemspec if basename.end_with?(".gemspec") + return :rakefile if basename == "Rakefile" || extension == ".rake" + return :ruby if RUBY_TEMPLATE_BASENAMES.include?(basename) || + RUBY_TEMPLATE_SUFFIXES.any? { |suffix| basename.end_with?(suffix) } || + RUBY_TEMPLATE_EXTENSIONS.include?(extension) + return :yaml if extension.match?(/\A\.ya?ml\z/) || File.basename(relative_path).casecmp("citation.cff").zero? + return :toml if extension == ".toml" + return :markdown if extension.match?(/\A\.md(?:own)?\z/) + + :text + end + + def apply_kettle_config_bootstrap(project_root, recipe) + content = recipe_template_content(project_root, recipe) + tokens = stringify_template_tokens(recipe.fetch(:template_tokens, {})) + content.gsub("{KJ|MIN_DIVERGENCE_THRESHOLD}", tokens.fetch("KJ|MIN_DIVERGENCE_THRESHOLD", "")) + end + + def recipe_report_metadata(recipe) + metadata = { packaging_recipe: recipe.fetch(:name) } + metadata[:delete_file] = true if delete_file_recipe?(recipe) + metadata[:template_source_preference] = deep_dup(recipe[:template_preference]) if recipe[:template_preference] + metadata[:template_tokens] = deep_dup(recipe[:template_tokens]) if recipe[:template_tokens] + metadata[:bootstrap_file] = true if recipe.fetch(:primitive) == "supplied_kettle_config_bootstrap" + metadata + end + + def recipe_entry(name, target_path, provider_family, primitive, facts:, provider_backend: nil, selectors: []) + { + name: name, + target_path: target_path, + provider_family: provider_family, + provider_backend: provider_backend, + primitive: primitive, + facts: facts, + selectors: selectors, + } + end + + def recipe_runtime_context(recipe, facts, deletion) + context = deep_dup(facts) + if recipe.fetch(:primitive) == "supplied_source_selector_deletion" && deletion + context[:delete_selectors] = deletion.fetch(:delete_selectors) + end + context[:template_source_preference] = deep_dup(recipe[:template_preference]) if recipe[:template_preference] + context[:template_tokens] = deep_dup(recipe[:template_tokens]) if recipe[:template_tokens] + context + end + + def step_report_metadata(recipe, deletion) + metadata = { target_path: recipe.fetch(:target_path) } + if recipe.fetch(:primitive) == "supplied_obsolete_file_deletion" + metadata.merge!( + policy_kind: "delete_obsolete_file", + operation: "delete", + deleted_file: recipe.fetch(:target_path), + ) + end + if recipe.fetch(:primitive) == "supplied_disabled_opencollective_file_deletion" + metadata.merge!( + policy_kind: "delete_disabled_opencollective_file", + operation: "delete", + deleted_file: recipe.fetch(:target_path), + ) + end + if recipe.fetch(:primitive) == "supplied_template_source_preference" + metadata.merge!( + policy_kind: "select_template_source", + operation: "select", + template_source_preference: deep_dup(recipe.fetch(:template_preference)), + ) + metadata[:template_tokens] = deep_dup(recipe[:template_tokens]) if recipe[:template_tokens] + end + if recipe.fetch(:primitive) == "supplied_template_source_application" + metadata.merge!( + policy_kind: "apply_template_source", + operation: "replace", + template_source_preference: deep_dup(recipe.fetch(:template_preference)), + ) + metadata[:template_tokens] = deep_dup(recipe[:template_tokens]) if recipe[:template_tokens] + end + if recipe.fetch(:primitive) == "supplied_kettle_config_bootstrap" + metadata.merge!( + policy_kind: "bootstrap_kettle_config", + operation: "create", + template_source_preference: deep_dup(recipe.fetch(:template_preference)), + ) + metadata[:template_tokens] = deep_dup(recipe[:template_tokens]) if recipe[:template_tokens] + end + return metadata unless deletion + + metadata.merge( + policy_kind: "delete_supplied_structural_owners", + operation: "delete", + consumed_context: "delete_selectors", + deleted_ranges: deletion.fetch(:delete_selectors).length, + deleted_selector_ids: deletion.fetch(:delete_selectors).map { |selector| selector.fetch(:selector_id) }, + ) + end + + def extract_gemspec_assignment(source, field) + match = source.match(/#{Regexp.escape(field)}\s*=\s*["']([^"']*)["']/) + match && match[1] + end + + def extract_gemspec_array(source, field) + match = source.match(/#{Regexp.escape(field)}\s*=\s*\[([^\]]*)\]/m) + return [] unless match + + match[1].scan(/["']([^"']+)["']/).flatten + end + + def extract_metadata_value(source, key) + match = source.match(/spec\.metadata\[\s*["']#{Regexp.escape(key)}["']\s*\]\s*=\s*["']([^"']*)["']/) + match && match[1] + end + + def funding_urls(project_root, gemspec_source, package_name, opencollective_disabled: false, open_collective_org: nil) + urls = [extract_metadata_value(gemspec_source, "funding_uri")] + path = File.join(project_root, ".github", "FUNDING.yml") + urls.concat(github_funding_urls(path, opencollective_disabled: opencollective_disabled)) if File.exist?(path) + urls << github_funding_platform_urls("open_collective", [open_collective_org]).first unless opencollective_disabled + urls << github_funding_platform_urls("tidelift", ["rubygems/#{package_name}"]).first + + urls.compact.uniq.sort + end + + def github_funding_urls(path, opencollective_disabled: false) + funding = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false) || {} + return [] unless funding.is_a?(Hash) + + funding.flat_map do |platform, value| + next [] if opencollective_disabled && platform.to_s == "open_collective" + + github_funding_platform_urls(platform.to_s, Array(value).compact) + end + end + + def github_funding_platform_urls(platform, values) + values.filter_map do |value| + handle = value.to_s.strip.delete_prefix("@") + next if handle.empty? + + case platform + when "buy_me_a_coffee" + "https://www.buymeacoffee.com/#{handle}" + when "custom" + handle if handle.match?(%r{\Ahttps?://}) + when "github" + "https://github.com/sponsors/#{handle}" + when "issuehunt" + "https://issuehunt.io/u/#{handle}" + when "ko_fi" + "https://ko-fi.com/#{handle}" + when "liberapay" + "https://liberapay.com/#{handle}/donate" + when "open_collective" + "https://opencollective.com/#{handle}" + when "patreon" + "https://patreon.com/#{handle}" + when "polar" + "https://polar.sh/#{handle}" + when "thanks_dev" + "https://thanks.dev/#{handle}" + when "tidelift" + "https://tidelift.com/funding/github/#{handle}" + end + end + end + + def github_actions_ruby_versions(min_ruby) + floor = min_ruby.to_s[/\d+\.\d+/] || "3.1" + candidates = %w[3.1 3.2 3.3 3.4] + selected = candidates.select { |version| Gem::Version.new(version) >= Gem::Version.new(floor) } + selected.empty? ? [floor] : selected + end + + def github_actions_custom_workflows(project_root, opencollective_disabled: false) + workflow_root = File.join(project_root, ".github", "workflows") + return [] unless Dir.exist?(workflow_root) + + Dir.glob(File.join(workflow_root, "*.{yml,yaml}")).filter_map do |path| + relative_path = path.delete_prefix("#{project_root}/") + next if opencollective_disabled && opencollective_disabled_file?(relative_path) + next if generated_or_obsolete_github_workflow?(relative_path) + + relative_path + end.sort + end + + def github_actions_obsolete_workflows(project_root) + workflow_root = File.join(project_root, ".github", "workflows") + OBSOLETE_GITHUB_WORKFLOWS.filter_map do |workflow| + relative_path = ".github/workflows/#{workflow}" + path = File.join(workflow_root, workflow) + relative_path if File.exist?(path) + end.sort + end + + def generated_or_obsolete_github_workflow?(relative_path) + return true if %w[.github/workflows/ci.yml .github/workflows/coverage.yml .github/workflows/framework-ci.yml].include?(relative_path) + + OBSOLETE_GITHUB_WORKFLOWS.include?(File.basename(relative_path)) + end + + def opencollective_disabled_files(project_root) + OPENCOLLECTIVE_DISABLED_FILES.select do |relative_path| + File.exist?(File.join(project_root, relative_path)) + end + end + + def opencollective_disabled_file?(relative_path) + OPENCOLLECTIVE_DISABLED_FILES.include?(relative_path.to_s) + end + + def delete_file_recipe?(recipe) + FILE_DELETION_PRIMITIVES.include?(recipe.fetch(:primitive)) + end + + def workflow_recipe_slug(workflow_path) + workflow_path.gsub(/[^a-zA-Z0-9]+/, "_").gsub(/\A_+|_+\z/, "") + end + + def kettle_jem_config(project_root) + path = File.join(project_root, ".kettle-jem.yml") + return {} unless File.exist?(path) + + config = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false) || {} + config.is_a?(Hash) ? config : {} + end + + def opencollective_disabled?(config, env: ENV) + opencollective_policy(config, env).fetch(:disabled) + end + + def opencollective_policy(config, env) + funding = config["funding"] + if funding.is_a?(Hash) && funding.key?("open_collective") + config_value = funding["open_collective"] + return { + disabled: falsey_config?(config_value), + source: "config.funding.open_collective", + value: config_value.to_s, + } + end + + env_falsey = opencollective_falsey_env(env) + return { disabled: true, source: "env.#{env_falsey.fetch(:key)}", value: env_falsey.fetch(:value).to_s } if env_falsey + + { disabled: false } + end + + def opencollective_falsey_env(env) + %w[OPENCOLLECTIVE_HANDLE FUNDING_ORG].each do |key| + value = env[key] + return { key: key, value: value } if falsey_config?(value) + end + nil + end + + def opencollective_org(project_root, env, opencollective_disabled: false) + return nil if opencollective_disabled + + env_org = opencollective_org_env(env) + return env_org if env_org + + opencollective_org_file(project_root) + end + + def opencollective_org_env(env) + %w[OPENCOLLECTIVE_HANDLE FUNDING_ORG].each do |key| + value = env[key].to_s.strip + next if value.empty? || falsey_config?(value) + + return { org: value, source: "env.#{key}" } + end + nil + end + + def opencollective_org_file(project_root) + path = File.join(project_root, ".opencollective.yml") + return nil unless File.exist?(path) + + config = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false) || {} + return nil unless config.is_a?(Hash) + + org = config.fetch("collective", config["org"]).to_s.strip + return nil if org.empty? + + { org: org, source: ".opencollective.yml" } + end + + def template_tokens(facts, funding) + package = facts.fetch(:package) + rubygems = facts.fetch(:rubygems) + tokens = { + "KJ|GEM_NAME" => package.fetch(:name).to_s, + "KJ|GEM_NAME_PATH" => package.fetch(:name).to_s.tr("-", "/"), + "KJ|GEM_SHIELD" => shield_token(package.fetch(:name).to_s), + "KJ|GEM_MAJOR" => gem_major_token(facts.fetch(:project_runtime, {})[:version]), + "KJ|GH_ORG" => facts.fetch(:project_runtime, {})[:github_org].to_s, + "KJ|NAMESPACE" => rubygems.fetch(:namespace).to_s, + "KJ|NAMESPACE_SHIELD" => shield_token(rubygems.fetch(:namespace).to_s), + "KJ|MIN_RUBY" => minimum_ruby_token(rubygems[:min_ruby]), + "KJ|MIN_DEV_RUBY" => minimum_dev_ruby_token(rubygems[:min_ruby]), + }.merge( + rubocop_template_tokens(rubygems[:min_ruby]) + ).merge( + author_template_tokens(facts.fetch(:author, {})) + ).merge( + forge_template_tokens(facts.fetch(:forge, {})) + ).merge( + funding_template_tokens(funding) + ).merge( + social_template_tokens(facts.fetch(:social, {})) + ).merge( + license_template_tokens(facts.fetch(:license, {})) + ).merge( + project_runtime_template_tokens(facts.fetch(:project_runtime, {})) + ).merge( + readme_logo_template_tokens(facts.fetch(:readme_logo, {})) + ) + org = funding[:open_collective_org].to_s + tokens["KJ|OPENCOLLECTIVE_ORG"] = org unless org.empty? + + tokens.reject { |key, value| value.empty? && !EMPTY_TEMPLATE_TOKENS.include?(key) } + end + + def minimum_ruby_token(requirement) + requirement.to_s[/\d+(?:\.\d+){1,2}/].to_s + end + + def minimum_dev_ruby_token(requirement) + min_ruby = minimum_ruby_token(requirement) + return "" if min_ruby.empty? + + [Gem::Version.new(min_ruby), Gem::Version.new("2.3")].max.to_s + rescue ArgumentError + "2.3" + end + + def gem_major_token(version) + Gem::Version.new(version.to_s).segments.first.to_s + rescue ArgumentError + "0" + end + + def author_facts(gemspec_source, config, env) + token_config = token_config_values(config) + author_config = token_config["author"].is_a?(Hash) ? token_config["author"] : {} + derived_name = extract_gemspec_array(gemspec_source, "spec.authors").first + derived_email = extract_gemspec_array(gemspec_source, "spec.email").first + name = preferred_template_token_value(derived_name, author_config["name"], env, "KJ_AUTHOR_NAME").to_s + email = preferred_template_token_value(derived_email, author_config["email"], env, "KJ_AUTHOR_EMAIL").to_s + given_names = preferred_template_token_value(author_given_names(name), author_config["given_names"], env, "KJ_AUTHOR_GIVEN_NAMES") + family_names = preferred_template_token_value(author_family_names(name), author_config["family_names"], env, "KJ_AUTHOR_FAMILY_NAMES") + domain = preferred_template_token_value(email.split("@", 2)[1], author_config["domain"], env, "KJ_AUTHOR_DOMAIN") + orcid = preferred_template_token_value(nil, author_config["orcid"], env, "KJ_AUTHOR_ORCID") + compact_hash( + name: name, + given_names: given_names.to_s, + family_names: family_names.to_s, + email: email, + domain: domain.to_s, + orcid: orcid.to_s + ) + end + + def token_config_values(config) + raw = config.is_a?(Hash) ? config["tokens"] : nil + raw.is_a?(Hash) ? raw : {} + end + + def preferred_template_token_value(derived_value, config_value, env, env_key) + env_clean = env[env_key].to_s.strip + return env_clean if present_template_token_value?(env_clean) + + config_clean = config_value.to_s.strip + return config_clean if present_template_token_value?(config_clean) + return unless present_template_token_value?(derived_value) + + derived_value.to_s.strip + end + + def present_template_token_value?(value) + clean = value.to_s.strip + !clean.empty? && !token_placeholder?(clean) + end + + def token_placeholder?(value) + value.to_s.strip.match?(%r{\A\{KJ\|[A-Z][A-Z0-9_:]*\}\z}) + end + + def author_template_tokens(author) + { + "KJ|AUTHOR:NAME" => author[:name].to_s, + "KJ|AUTHOR:GIVEN_NAMES" => author[:given_names].to_s, + "KJ|AUTHOR:FAMILY_NAMES" => author[:family_names].to_s, + "KJ|AUTHOR:EMAIL" => author[:email].to_s, + "KJ|AUTHOR:DOMAIN" => author[:domain].to_s, + "KJ|AUTHOR:ORCID" => author[:orcid].to_s, + } + end + + def forge_facts(config, env) + token_config = token_config_values(config) + forge_config = token_config["forge"].is_a?(Hash) ? token_config["forge"] : {} + compact_hash( + gh_user: forge_user_value(forge_config, env, :gh_user).to_s, + gl_user: forge_user_value(forge_config, env, :gl_user).to_s, + cb_user: forge_user_value(forge_config, env, :cb_user).to_s, + sh_user: forge_user_value(forge_config, env, :sh_user).to_s + ) + end + + def forge_user_value(forge_config, env, key) + preferred_template_token_value(nil, forge_config[key.to_s], env, FORGE_USER_ENV_KEYS.fetch(key)) + end + + def forge_template_tokens(forge) + { + "KJ|GH:USER" => forge[:gh_user].to_s, + "KJ|GL:USER" => forge[:gl_user].to_s, + "KJ|CB:USER" => forge[:cb_user].to_s, + "KJ|SH:USER" => forge[:sh_user].to_s, + } + end + + def funding_platform_token_facts(config, env) + token_config = token_config_values(config) + funding_config = token_config["funding"].is_a?(Hash) ? token_config["funding"] : {} + compact_hash( + patreon: funding_platform_token_value(funding_config, env, :patreon).to_s, + kofi: funding_platform_token_value(funding_config, env, :kofi).to_s, + paypal: funding_platform_token_value(funding_config, env, :paypal).to_s, + buymeacoffee: funding_platform_token_value(funding_config, env, :buymeacoffee).to_s, + polar: funding_platform_token_value(funding_config, env, :polar).to_s, + liberapay: funding_platform_token_value(funding_config, env, :liberapay).to_s, + issuehunt: funding_platform_token_value(funding_config, env, :issuehunt).to_s + ) + end + + def funding_platform_token_value(funding_config, env, key) + preferred_template_token_value(nil, funding_config[key.to_s], env, FUNDING_TOKEN_ENV_KEYS.fetch(key)) + end + + def funding_template_tokens(funding) + platform_tokens = funding.fetch(:platform_tokens, {}) + { + "KJ|FUNDING:PATREON" => platform_tokens[:patreon].to_s, + "KJ|FUNDING:KOFI" => platform_tokens[:kofi].to_s, + "KJ|FUNDING:PAYPAL" => platform_tokens[:paypal].to_s, + "KJ|FUNDING:BUYMEACOFFEE" => platform_tokens[:buymeacoffee].to_s, + "KJ|FUNDING:POLAR" => platform_tokens[:polar].to_s, + "KJ|FUNDING:LIBERAPAY" => platform_tokens[:liberapay].to_s, + "KJ|FUNDING:ISSUEHUNT" => platform_tokens[:issuehunt].to_s, + } + end + + def social_facts(config, env) + token_config = token_config_values(config) + social_config = token_config["social"].is_a?(Hash) ? token_config["social"] : {} + compact_hash( + mastodon: social_token_value(social_config, env, :mastodon).to_s, + bluesky: social_token_value(social_config, env, :bluesky).to_s, + linktree: social_token_value(social_config, env, :linktree).to_s, + devto: social_token_value(social_config, env, :devto).to_s + ) + end + + def social_token_value(social_config, env, key) + preferred_template_token_value(nil, social_config[key.to_s], env, SOCIAL_TOKEN_ENV_KEYS.fetch(key)) + end + + def social_template_tokens(social) + { + "KJ|SOCIAL:MASTODON" => social[:mastodon].to_s, + "KJ|SOCIAL:BLUESKY" => social[:bluesky].to_s, + "KJ|SOCIAL:LINKTREE" => social[:linktree].to_s, + "KJ|SOCIAL:DEVTO" => social[:devto].to_s, + } + end + + def project_runtime_facts(config, env, package_name:, source_url:, author_domain:, min_ruby:, version:) + run_timestamp = Time.now + compact_hash( + freeze_token: config.dig("defaults", "freeze_token").to_s.empty? ? "kettle-jem" : config.dig("defaults", "freeze_token").to_s, + kettle_jem_version: VERSION, + template_run_date: run_timestamp.strftime("%Y-%m-%d"), + template_run_year: run_timestamp.year.to_s, + kettle_dev_gem: "kettle-dev", + yard_host: "#{package_name.to_s.tr("_", "-")}.#{author_domain.to_s.empty? ? "example.com" : author_domain}", + project_emoji: preferred_template_token_value(nil, config["project_emoji"], env, "KJ_PROJECT_EMOJI").to_s, + min_divergence_threshold: preferred_template_token_value(nil, config["min_divergence_threshold"], env, "KJ_MIN_DIVERGENCE_THRESHOLD").to_s, + min_dev_ruby: minimum_dev_ruby_token(min_ruby), + version: version.to_s, + github_org: github_org_from_url(source_url).to_s + ) + end + + def project_runtime_template_tokens(project_runtime) + { + "KJ|FREEZE_TOKEN" => project_runtime[:freeze_token].to_s, + "KJ|KETTLE_JEM_VERSION" => project_runtime[:kettle_jem_version].to_s, + "KJ|TEMPLATE_RUN_DATE" => project_runtime[:template_run_date].to_s, + "KJ|TEMPLATE_RUN_YEAR" => project_runtime[:template_run_year].to_s, + "KJ|KETTLE_DEV_GEM" => project_runtime[:kettle_dev_gem].to_s, + "KJ|YARD_HOST" => project_runtime[:yard_host].to_s, + "KJ|PROJECT_EMOJI" => project_runtime[:project_emoji].to_s, + "KJ|MIN_DIVERGENCE_THRESHOLD" => project_runtime[:min_divergence_threshold].to_s, + } + end + + def shield_token(value) + value.to_s.gsub("-", "--").gsub("_", "__").gsub("::", "%3A%3A").tr(" ", "_") + end + + def github_org_from_url(url) + match = url.to_s.match(%r{\Ahttps?://github\.com/([^/]+)/}) + match && match[1] + end + + def concrete_github_url(url) + github_org_from_url(url) ? url.to_s : nil + end + + def readme_logo_facts(config, package_name:, github_org:) + entries = readme_top_logo_entries(readme_top_logo_mode(config), org: github_org.to_s, gem_name: package_name.to_s) + compact_hash( + top_logo_mode: readme_top_logo_mode(config), + top_logo_row: [README_STATIC_TOP_LOGO_ROW, readme_top_logo_row(entries)].reject(&:empty?).join(" "), + top_logo_refs: [README_STATIC_TOP_LOGO_REFS, readme_top_logo_refs(entries)].reject(&:empty?).join("\n") + ) + end + + def readme_top_logo_mode(config) + raw_config = config.is_a?(Hash) ? config["readme"] : nil + readme_config = raw_config.is_a?(Hash) ? raw_config : {} + normalized = readme_config["top_logo_mode"].to_s.strip.downcase.tr("-", "_") + return README_TOP_LOGO_MODE_DEFAULT if normalized.empty? + return normalized if README_TOP_LOGO_MODES.include?(normalized) + + README_TOP_LOGO_MODE_DEFAULT + end + + def readme_top_logo_entries(mode, org:, gem_name:) + return [] if org.empty? + + entries = [] + if mode == "org" || mode == "org_and_project" + entries << { + label: org, + image_ref: "#{org}-i", + link_ref: org, + image_url: "https://logos.galtzo.com/assets/images/#{org}/avatar-192px.svg", + href: "https://github.com/#{org}", + } + end + if mode == "project" || mode == "org_and_project" + entries << { + label: gem_name, + image_ref: "#{gem_name}-i", + link_ref: gem_name, + image_url: "https://logos.galtzo.com/assets/images/#{org}/#{gem_name}/avatar-192px.svg", + href: "https://github.com/#{org}/#{gem_name}", + } + end + entries.uniq { |entry| [entry[:image_ref], entry[:link_ref], entry[:image_url], entry[:href]] } + end + + def readme_top_logo_row(entries) + entries.map do |entry| + "[![#{entry[:label]} Logo by Aboling0, CC BY-SA 4.0][🖼️#{entry[:image_ref]}]][🖼️#{entry[:link_ref]}]" + end.join(" ") + end + + def readme_top_logo_refs(entries) + entries.flat_map do |entry| + [ + "[🖼️#{entry[:image_ref]}]: #{entry[:image_url]}", + "[🖼️#{entry[:link_ref]}]: #{entry[:href]}", + ] + end.join("\n") + end + + def readme_logo_template_tokens(readme_logo) + { + "KJ|README:TOP_LOGO_ROW" => readme_logo[:top_logo_row].to_s, + "KJ|README:TOP_LOGO_REFS" => readme_logo[:top_logo_refs].to_s, + } + end + + def rubocop_template_tokens(min_ruby) + constraint, gem_name = rubocop_tokens_for(min_ruby_version(min_ruby)) + { + "KJ|RUBOCOP_LTS_CONSTRAINT" => constraint, + "KJ|RUBOCOP_RUBY_GEM" => gem_name, + } + end + + def rubocop_tokens_for(min_ruby) + fallback = RUBOCOP_VERSION_MAP.first + selected = nil + RUBOCOP_VERSION_MAP.reverse_each do |minimum, constraint| + next unless min_ruby && min_ruby >= minimum + + selected = [minimum, constraint] + break + end + selected ||= fallback + [selected[1], "rubocop-ruby#{selected[0].segments.join("_")}"] + end + + def min_ruby_version(requirement) + token = minimum_ruby_token(requirement) + return nil if token.empty? + + Gem::Version.new(token) + rescue ArgumentError + nil + end + + def license_facts(config, gemspec_licenses, author_email: nil) + licenses = resolved_licenses(config, gemspec_licenses) + primary = licenses.first + compat_category = license_compat_category(licenses) + compact_hash( + spdx: licenses, + expression: licenses.join(" OR "), + primary_spdx: primary, + license_md_content: license_md_content(licenses, author_email: author_email), + readme_license_intro: readme_license_intro(licenses, author_email: author_email), + readme_license_badge: license_badge(primary), + readme_license_compat_badge: license_compat_badge(compat_category), + readme_license_refs: readme_license_refs(primary, compat_category), + copyright_prefix: polyform_licenses?(licenses) ? "Required Notice: " : "" + ) + end + + def resolved_licenses(config, gemspec_licenses) + config_licenses = config.is_a?(Hash) ? config["licenses"] : nil + licenses = Array(config_licenses).map { |license| license.to_s.strip }.reject(&:empty?) + return licenses unless licenses.empty? + + licenses = Array(gemspec_licenses).map { |license| license.to_s.strip }.reject(&:empty?) + licenses.empty? ? ["MIT"] : licenses + end + + def license_template_tokens(license) + { + "KJ|LICENSE_MD_CONTENT" => license[:license_md_content].to_s, + "KJ|README:LICENSE_INTRO" => license[:readme_license_intro].to_s, + "KJ|LICENSE:PRIMARY_SPDX" => license[:primary_spdx].to_s, + "KJ|README:LICENSE_BADGE" => license[:readme_license_badge].to_s, + "KJ|README:LICENSE_COMPAT_BADGE" => license[:readme_license_compat_badge].to_s, + "KJ|README:LICENSE_REFS" => license[:readme_license_refs].to_s, + "KJ|COPYRIGHT_PREFIX" => license[:copyright_prefix].to_s, + } + end + + def license_md_content(licenses, author_email: nil) + content = <<~MARKDOWN.chomp + # License + + This project is made available under the following license#{"s" if licenses.size > 1}. + Choose the option that best fits your use case: + + #{licenses.map { |license| "- #{license_link(license)}" }.join("\n")} + MARKDOWN + guide_table = license_use_case_guide_table(licenses, author_email: author_email) + content += "\n\n## Use-case guide\n\n#{guide_table}" if guide_table + content += "\n\n#{license_contact_line(author_email, context: :license_md)}" if non_mit_licenses?(licenses) + content + end + + def readme_license_intro(licenses, author_email: nil) + return mit_readme_license_intro if licenses == ["MIT"] + + intro = "The gem is available under the following license#{"s" if licenses.size > 1}: " \ + "#{licenses.map { |license| license_link(license) }.join(", ")}.\n" \ + "See [LICENSE.md][#{paperclip_ref(:license)}] for details." + intro += "\n\n#{license_contact_line(author_email, context: :readme)}" if non_mit_licenses?(licenses) + guide_table = license_use_case_guide_table(licenses, author_email: author_email) + intro += "\n\n### License use-case guide\n\n#{guide_table}" if guide_table + intro + end + + def mit_readme_license_intro + "The gem is available as open source under the terms of\n" \ + "the #{license_link("MIT")} #{license_badge("MIT")}." + end + + def license_contact_line(author_email, context:) + if author_email.to_s.empty? + return "If none of the above licenses fit your use case, please contact the project maintainer to discuss a custom commercial license." if context == :license_md + + "If none of the available licenses suit your use case, please contact the project maintainer to discuss a custom commercial license." + elsif context == :license_md + "If none of the above licenses fit your use case, please [contact us](mailto:#{author_email}) to discuss a custom commercial license." + else + "If none of the available licenses suit your use case, please [contact us](mailto:#{author_email}) to discuss a custom commercial license." + end + end + + def readme_license_refs(primary, compat_category) + [ + "[#{paperclip_ref(:copyright_notice_explainer)}]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year", + "[#{paperclip_ref(:license)}]: LICENSE.md", + "[#{paperclip_ref(:license_ref)}]: #{license_badge_ref(primary)}", + "[#{paperclip_ref(:license_img)}]: #{license_badge_img(primary)}", + "[#{paperclip_ref(:license_compat)}]: #{license_compat_ref(compat_category)}", + "[#{paperclip_ref(:license_compat_img)}]: #{license_compat_img(compat_category)}", + ].join("\n") + end + + def spdx_basename(spdx_id) + spdx_id.to_s.sub(/\ALicenseRef-/, "") + end + + def license_link(spdx_id) + base = spdx_basename(spdx_id) + "[#{base}](#{base}.md)" + end + + def license_badge(spdx_id) + base = spdx_basename(spdx_id) + "[![License: #{base}][#{paperclip_ref(:license_img)}]][#{paperclip_ref(:license_ref)}]" + end + + def license_badge_ref(spdx_id) + "#{spdx_basename(spdx_id)}.md" + end + + def license_badge_img(spdx_id) + base = spdx_basename(spdx_id).gsub("-", "--").gsub("_", "__").tr(" ", "_") + "https://img.shields.io/badge/License-#{base}-259D6C.svg" + end + + def license_compat_category(licenses) + categories = Array(licenses).filter_map { |license| APACHE_LICENSE_COMPAT_CATEGORIES[license.to_s] }.uniq + return :a if categories.include?(:a) + return :b if categories.include?(:b) + return :x if categories.any? && categories.all?(:x) + + :unknown + end + + def license_compat_badge(category) + data = APACHE_LICENSE_COMPAT_BADGE_DATA.fetch(category) + "[![#{data.fetch(:alt)}][#{paperclip_ref(:license_compat_img)}]][#{paperclip_ref(:license_compat)}]" + end + + def license_compat_ref(category) + APACHE_LICENSE_COMPAT_BADGE_DATA.fetch(category).fetch(:ref) + end + + def license_compat_img(category) + data = APACHE_LICENSE_COMPAT_BADGE_DATA.fetch(category) + "https://img.shields.io/badge/#{data.fetch(:label)}-#{data.fetch(:message)}-#{data.fetch(:color)}.svg?style=flat&logo=Apache" + end + + def polyform_licenses?(licenses) + licenses.any? { |license| license.to_s.start_with?("PolyForm-") } + end + + def non_mit_licenses?(licenses) + licenses.any? { |license| license != "MIT" } + end + + def license_use_case_guide_table(licenses, author_email: nil) + has_floss_oss = licenses.include?("MIT") || licenses.include?("AGPL-3.0-only") + has_polyform = licenses.include?("PolyForm-Noncommercial-1.0.0") || licenses.include?("PolyForm-Small-Business-1.0.0") + has_big_time = licenses.include?("LicenseRef-Big-Time-Public-License") + return unless has_floss_oss && has_polyform && has_big_time + + rows = license_use_case_rows(licenses, author_email: author_email) + return if rows.empty? + + "| Use case | License |\n|---|---|\n" + + rows.map { |use_case, license| "| #{use_case} | #{license} |" }.join("\n") + end + + def license_use_case_rows(licenses, author_email: nil) + rows = [] + rows << ["FLOSS (free and open source)", license_link("MIT")] if licenses.include?("MIT") + rows << ["Copy-left open source", license_link("AGPL-3.0-only")] if licenses.include?("AGPL-3.0-only") + noncommercial_links = %w[PolyForm-Noncommercial-1.0.0 PolyForm-Small-Business-1.0.0 LicenseRef-Big-Time-Public-License] + .select { |license| licenses.include?(license) } + .map { |license| license_link(license) } + rows << ["Non-commercial (research, education, personal use)", noncommercial_links.join(" or ")] unless noncommercial_links.empty? + small_business_links = %w[PolyForm-Small-Business-1.0.0 LicenseRef-Big-Time-Public-License] + .select { |license| licenses.include?(license) } + .map { |license| license_link(license) } + rows << ["Small business commercial", small_business_links.join(" or ")] unless small_business_links.empty? + rows << ["Larger business commercial", large_business_license_cell(author_email)] if licenses.include?("LicenseRef-Big-Time-Public-License") + rows + end + + def large_business_license_cell(author_email) + cell = license_link("LicenseRef-Big-Time-Public-License") + if author_email.to_s.empty? + "#{cell} or contact us for a custom license" + else + "#{cell} or [contact us](mailto:#{author_email}) for a custom license" + end + end + + def paperclip_ref(name) + { + copyright_notice_explainer: "\u{1F4C4}copyright-notice-explainer", + license: "\u{1F4C4}license", + license_ref: "\u{1F4C4}license-ref", + license_img: "\u{1F4C4}license-img", + license_compat: "\u{1F4C4}license-compat", + license_compat_img: "\u{1F4C4}license-compat-img", + }.fetch(name) + end + + def author_given_names(name) + parts = name.to_s.strip.split(/\s+/) + return "" if parts.size < 2 + + parts[0...-1].join(" ") + end + + def author_family_names(name) + parts = name.to_s.strip.split(/\s+/) + return "" if parts.size < 2 + + parts[-1] + end + + def resolve_template_tokens(content, tokens, scan_unresolved: true) + resolver = Token::Resolver::Resolve.new(on_missing: :keep) + document = Token::Resolver::Document.new(content.to_s, config: TEMPLATE_TOKEN_CONFIG) + resolved = resolver.resolve(document, stringify_template_tokens(tokens)) + return resolved unless scan_unresolved + + unresolved = Token::Resolver::Document.new(resolved, config: TEMPLATE_TOKEN_CONFIG).token_keys.grep(/\AKJ\|/).sort + return resolved if unresolved.empty? + + raise ArgumentError, "unresolved kettle-jem template tokens: #{unresolved.map { |token| "{#{token}}" }.join(", ")}" + end + + def unresolved_template_scan?(recipe) + return false if recipe.fetch(:target_path).to_s == ".kettle-jem.yml" + return false if recipe.dig(:template_preference, :skip_unresolved_scan) + + true + end + + def stringify_template_tokens(tokens) + tokens.to_h.transform_keys(&:to_s).transform_values(&:to_s) + end + + def falsey_config?(value) + %w[false no 0].include?(value.to_s.strip.downcase) + end + + def merge_readme_template(template_content:, destination_content:, preserve_config: {}) + return template_content if destination_content.to_s.strip.empty? + + preserved = preserve_readme_sections(template_content, destination_content, preserve_config) + preserve_readme_h1(preserved, destination_content) + end + + def preserve_readme_sections(template_content, destination_content, preserve_config) + template_sections = markdown_sections(template_content) + destination_sections = markdown_sections(destination_content) + destination_lookup = destination_sections.to_h { |section| [section.fetch(:base), section] } + preserve_targets = readme_preserve_targets(template_sections, destination_lookup, preserve_config) + return template_content if preserve_targets.empty? + + lines = template_content.split("\n", -1) + template_sections.reverse_each do |section| + next unless preserve_targets.include?(section.fetch(:base)) + + destination_section = destination_lookup[section.fetch(:base)] || + aliased_readme_destination_section(section.fetch(:base), destination_lookup, preserve_config) + next unless destination_section + + replacement = "#{section.fetch(:heading)}\n#{destination_section.fetch(:body)}".split("\n", -1) + lines[section.fetch(:start)..section.fetch(:end)] = replacement + end + lines.join("\n") + end + + def preserve_readme_h1(merged_content, destination_content) + merged_h1 = markdown_sections(merged_content).find { |section| section.fetch(:level) == 1 } + destination_h1 = markdown_sections(destination_content).find { |section| section.fetch(:level) == 1 } + return merged_content unless merged_h1 && destination_h1 + return merged_content if semantic_readme_heading(destination_h1.fetch(:heading_text)) == semantic_readme_heading(merged_h1.fetch(:heading_text)) + + lines = merged_content.split("\n", -1) + lines[merged_h1.fetch(:start)] = destination_h1.fetch(:heading) + lines.join("\n") + end + + def markdown_sections(content) + lines = content.to_s.split("\n", -1) + headings = [] + in_fence = false + fence_marker = nil + lines.each_with_index do |line, index| + stripped = line.lstrip + if in_fence + if stripped.match?(/\A#{Regexp.escape(fence_marker)}\s*\z/) + in_fence = false + fence_marker = nil + end + next + end + if (fence = stripped.match(/\A(`{3,}|~{3,})/)) + in_fence = true + fence_marker = fence[1] + next + end + next unless (heading = line.match(/\A(\#{1,6})\s+(.+?)\s*#*\s*\z/)) + + headings << { + start: index, + level: heading[1].length, + heading: line, + heading_text: heading[2], + base: normalize_readme_heading(heading[2]), + } + end + + headings.each_with_index.map do |heading, index| + following = headings[(index + 1)..].to_a.find { |candidate| candidate.fetch(:level) <= heading.fetch(:level) } + branch_end = following ? following.fetch(:start) - 1 : lines.length - 1 + body = (lines[(heading.fetch(:start) + 1)..branch_end] || []).join("\n") + heading.merge(end: branch_end, body: body) + end + end + + def readme_preserve_targets(template_sections, destination_lookup, preserve_config) + sections = Array(preserve_config[:sections]).map { |section| normalize_readme_heading(section) } + sections = README_DEFAULT_PRESERVE_SECTIONS.dup if sections.empty? + patterns = Array(preserve_config[:patterns]).map { |pattern| pattern.to_s.strip.downcase } + patterns = README_DEFAULT_PRESERVE_PATTERNS.dup if patterns.empty? + aliases = preserve_config[:aliases] || README_SECTION_ALIASES + targets = sections.dup + template_sections.each do |section| + base = section.fetch(:base) + targets << base if patterns.any? { |pattern| File.fnmatch?(pattern, base, File::FNM_PATHNAME) } + end + aliases.each do |from, to| + targets << to if destination_lookup.key?(from) && targets.include?(to) + end + targets.uniq + end + + def aliased_readme_destination_section(template_base, destination_lookup, preserve_config) + aliases = preserve_config[:aliases] || README_SECTION_ALIASES + aliases.each do |from, to| + return destination_lookup[from] if to == template_base && destination_lookup.key?(from) + end + nil + end + + def readme_preserve_config(config) + readme = config["readme"] + return {} unless readme.is_a?(Hash) + + result = {} + result[:sections] = Array(readme["preserve_sections"]) if readme.key?("preserve_sections") + result[:patterns] = Array(readme["preserve_patterns"]) if readme.key?("preserve_patterns") + if readme["section_aliases"].is_a?(Hash) + result[:aliases] = README_SECTION_ALIASES.merge( + readme["section_aliases"].transform_keys { |key| normalize_readme_heading(key) } + .transform_values { |value| normalize_readme_heading(value) } + ) + end + result + end + + def normalize_readme_heading(text) + strip_readme_heading_adornment(text).strip.downcase + end + + def semantic_readme_heading(text) + normalize_readme_heading(text) + end + + def strip_readme_heading_adornment(text) + text.to_s.sub(/\A(?:\d\uFE0F?\u20E3|[^[:alnum:][:space:]])+[ \t]*/u, "") + end + + def template_source_preferences(project_root, config, opencollective_disabled: false) + templates = config["templates"] + return [] unless templates.is_a?(Hash) + + root = template_root(project_root, templates) + entries = template_entries(project_root, root, templates) + return [] if entries.empty? + + apply_templates = templates["apply"] == true + entries.filter_map do |entry| + template_source_preference( + project_root, + root, + entry, + config, + opencollective_disabled: opencollective_disabled, + apply_templates: apply_templates + ) + end + end + + def template_entries(project_root, root, templates) + return templates["entries"] if templates["entries"].is_a?(Array) + return [] if templates.key?("entries") + + template_inventory_entries(project_root, root.fetch(:path)) + end + + def template_inventory_entries(project_root, template_root_path) + logical_paths = [] + Find.find(template_root_path) do |path| + next if File.directory?(path) + + relative_path = path.delete_prefix("#{template_root_path}/") + logical_path = relative_path + .sub(/\.no-osc\.example\z/, "") + .sub(/\.example\z/, "") + logical_paths << logical_path unless logical_path.empty? + end + + logical_paths.uniq.sort.map do |logical_path| + target_path = template_inventory_target_path(project_root, logical_path) + if target_path == logical_path + logical_path + else + { "source" => logical_path, "target" => target_path } + end + end + end + + def template_inventory_target_path(project_root, logical_path) + return ".env.local.example" if logical_path == ".env.local" + + if logical_path.end_with?(".gemspec") + existing_gemspec = Dir.glob(File.join(project_root, "*.gemspec")).sort.first + return File.basename(existing_gemspec) if existing_gemspec + end + + logical_path + end + + def kettle_config_bootstrap_facts(project_root, env) + return if File.exist?(File.join(project_root, ".kettle-jem.yml")) + + selected_source = preferred_template_source(PACKAGED_TEMPLATE_ROOT, ".kettle-jem.yml") + return unless selected_source + + { + template_preference: { + target_path: ".kettle-jem.yml", + configured_source: ".kettle-jem.yml", + selected_source: selected_source, + source_relative_path: selected_source, + source_root: "packaged", + source_root_path: PACKAGED_TEMPLATE_ROOT, + selection_reason: template_source_selection_reason(".kettle-jem.yml", selected_source), + apply: true, + }, + min_divergence_threshold: preferred_template_token_value(nil, nil, env, "KJ_MIN_DIVERGENCE_THRESHOLD").to_s, + } + end + + def kettle_config_bootstrap_recipe(bootstrap) + recipe = recipe_entry( + "kettle_config_bootstrap", + ".kettle-jem.yml", + "yaml", + "supplied_kettle_config_bootstrap", + facts: %w[kettle_config_bootstrap] + ) + recipe[:template_preference] = bootstrap.fetch(:template_preference) + recipe[:template_tokens] = { + "KJ|MIN_DIVERGENCE_THRESHOLD" => bootstrap.fetch(:min_divergence_threshold).to_s, + } + recipe + end + + def template_source_preference(project_root, template_root, entry, config, opencollective_disabled: false, apply_templates: false) + source_path, target_path = template_entry_paths(entry) + return nil if source_path.to_s.empty? || target_path.to_s.empty? + + selected_source = preferred_template_source(template_root.fetch(:path), source_path, opencollective_disabled: opencollective_disabled) + return nil unless selected_source + + strategy_config = template_strategy_config(config, target_path) + preference = { + target_path: target_path, + configured_source: source_path, + selected_source: template_source_display_path(template_root, selected_source), + selection_reason: template_source_selection_reason(source_path, template_source_display_path(template_root, selected_source)), + apply: template_entry_apply?(entry, apply_templates), + } + preference[:strategy] = strategy_config.fetch(:strategy).to_s if strategy_config + preference[:file_type] = strategy_config.fetch(:file_type).to_s if strategy_config&.key?(:file_type) + preserve_config = readme_preserve_config(config) + preference[:readme_preserve_config] = preserve_config if target_path == "README.md" && !preserve_config.empty? + if template_root.fetch(:kind) == "packaged" + preference[:source_relative_path] = selected_source + preference[:source_root] = template_root.fetch(:kind) + preference[:source_root_path] = template_root.fetch(:path) + end + preference + end + + def template_strategy_config(config, target_path) + template_file_strategy_config(config, target_path) || template_pattern_strategy_config(config, target_path) + end + + def template_file_strategy_config(config, target_path) + files = config["files"] + return unless files.is_a?(Hash) + + current = files + target_path.to_s.delete_prefix("./").split("/").each do |part| + return unless current.is_a?(Hash) && current.key?(part) + + current = current[part] + end + return unless current.is_a?(Hash) && current.key?("strategy") + + template_strategy_entry(config, nil, current) + end + + def template_pattern_strategy_config(config, target_path) + patterns = config["patterns"] + return unless patterns.is_a?(Array) + + match = patterns.find do |entry| + entry.is_a?(Hash) && + File.fnmatch?(entry["path"].to_s, target_path.to_s, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH) + end + return unless match + + template_strategy_entry(config, match["path"].to_s, match) + end + + def template_strategy_entry(config, path, entry) + strategy = entry["strategy"].to_s.strip.downcase.to_sym + raise ArgumentError, "unknown kettle-jem template strategy: #{entry["strategy"]}" unless SUPPORTED_TEMPLATE_STRATEGIES.include?(strategy) + + result = { strategy: strategy } + result[:path] = path if path + result[:skip_unresolved_scan] = true if entry["skip_unresolved_scan"] + if entry.key?("file_type") + file_type = entry["file_type"].to_s.strip.downcase.tr("-", "_").to_sym + raise ArgumentError, "unknown kettle-jem template file_type: #{entry["file_type"]}" unless SUPPORTED_TEMPLATE_FILE_TYPES.include?(file_type) + + result[:file_type] = file_type + end + if strategy == :merge + defaults = config["defaults"].is_a?(Hash) ? config["defaults"] : {} + result[:preference] = (entry.key?("preference") ? entry["preference"] : defaults["preference"]).to_s if entry.key?("preference") || defaults.key?("preference") + if entry.key?("add_template_only_nodes") || defaults.key?("add_template_only_nodes") + result[:add_template_only_nodes] = entry.key?("add_template_only_nodes") ? entry["add_template_only_nodes"] : defaults["add_template_only_nodes"] + end + result[:freeze_token] = (entry.key?("freeze_token") ? entry["freeze_token"] : defaults["freeze_token"]).to_s if entry.key?("freeze_token") || defaults.key?("freeze_token") + end + result + end + + def template_root(project_root, templates) + configured_root = templates["root"].to_s + if configured_root.empty? + local_root = File.join(project_root, "template") + return { kind: "project", path: local_root, display_prefix: "template" } if Dir.exist?(local_root) + + return { kind: "packaged", path: PACKAGED_TEMPLATE_ROOT } + end + + return { kind: "packaged", path: PACKAGED_TEMPLATE_ROOT } if configured_root == "packaged" + + path = configured_root.start_with?("/") ? configured_root : File.join(project_root, configured_root) + { kind: "project", path: path, display_prefix: configured_root } + end + + def template_source_display_path(template_root, selected_source) + prefix = template_root[:display_prefix].to_s + return selected_source if prefix.empty? + + File.join(prefix, selected_source) + end + + def template_entry_paths(entry) + if entry.is_a?(Hash) + source_path = entry.fetch("source", entry["target"]).to_s + target_path = entry.fetch("target", source_path.sub(/\.example\z/, "")).to_s + [source_path, target_path] + else + source_path = entry.to_s + [source_path, source_path.sub(/\.example\z/, "")] + end + end + + def template_entry_apply?(entry, apply_templates) + return entry["apply"] == true if entry.is_a?(Hash) && entry.key?("apply") + + apply_templates + end + + def preferred_template_source(template_root, configured_source, opencollective_disabled: false) + base = configured_source.sub(/\.example\z/, "") + candidates = [] + candidates << "#{base}.no-osc.example" if opencollective_disabled + candidates << "#{base}.example" + candidates << configured_source + candidates.find { |relative_path| File.exist?(File.join(template_root, relative_path)) } + end + + def template_source_selection_reason(configured_source, selected_source) + if selected_source.end_with?(".no-osc.example") + "opencollective_disabled_no_osc_variant" + elsif selected_source.end_with?(".example") + "default_example_variant" + elsif selected_source == configured_source + "configured_source" + else + "fallback_source" + end + end + + def github_actions_framework_matrix(config) + workflows = config["workflows"] + return {} unless workflows.is_a?(Hash) && workflows["preset"].to_s.strip.downcase == "framework" + + raw = workflows["framework_matrix"] + return {} unless raw.is_a?(Hash) + + dimension = raw["dimension"].to_s.strip + versions = raw["versions"] + pattern = raw["gemfile_pattern"].to_s.strip + return {} unless !dimension.empty? && versions.is_a?(Array) && !versions.empty? && !pattern.empty? + + normalized_versions = versions.map { |version| version.to_s.strip }.reject(&:empty?) + return {} if normalized_versions.empty? + + { + dimension: dimension, + versions: normalized_versions, + gemfile_pattern: pattern, + include: normalized_versions.map do |version| + gemfile = expand_framework_gemfile_pattern(pattern, version) + { framework_version: version, gemfile: framework_gemfile_path(gemfile) } + end, + } + end + + def github_actions_coverage_config(config) + workflows = config["workflows"] + return {} unless workflows.is_a?(Hash) + + raw = workflows["coverage"] + enabled = raw == true || (raw.is_a?(Hash) && raw.fetch("enabled", false) == true) + return {} unless enabled + + raw = {} unless raw.is_a?(Hash) + { + enabled: true, + command: raw.fetch("command", "rake test").to_s, + appraisal: raw.fetch("appraisal", "coverage").to_s, + } + end + + def expand_framework_gemfile_pattern(pattern, version) + replacement = if pattern.include?("_{version}") || pattern.include?("{version}_") + version.tr(".", "_") + else + version + end + pattern.gsub("{version}", replacement) + end + + def framework_gemfile_path(gemfile) + gemfile.include?("/") ? gemfile : "gemfiles/#{gemfile}" + end + + def classify_namespace(name) + name.to_s.split(/[-_]/).map { |part| part[0].to_s.upcase + part[1..].to_s }.join("::") + end + + def readme_metadata_block(facts) + package = facts.fetch(:package) + funding_urls = facts.fetch(:funding, {}).fetch(:urls, []) + rows = [ + ["Package", package[:name]], + ["Description", package[:description]], + ["Homepage", package[:homepage_url]], + ["Source", package[:source_url]], + ["License", package[:license_expression]], + ["Funding", funding_urls.join(", ")], + ].reject { |(_, value)| value.to_s.empty? } + + [ + "", + "| Field | Value |", + "|---|---|", + *rows.map { |field, value| "| #{field} | #{value} |" }, + "", + ].join("\n") + end + + def synchronize_github_funding_yml(content, facts) + funding = YAML.safe_load(content.to_s, permitted_classes: [], aliases: false) || {} + funding = {} unless funding.is_a?(Hash) + funding = funding.each_with_object({}) do |(key, value), memo| + next if value.nil? || (value.respond_to?(:empty?) && value.empty?) + + memo[key.to_s] = value + end + funding.delete("open_collective") if facts.fetch(:funding, {})[:open_collective_disabled] + funding["tidelift"] ||= "rubygems/#{facts.fetch(:package).fetch(:name)}" + YAML.dump(funding).sub(/\A---\n?/, "") + end + + def delete_rakefile_scaffold(content) + selectors = rakefile_scaffold_delete_selectors(content) + { + content: delete_line_ranges(content.to_s, selectors), + delete_selectors: selectors, + } + end + + def rakefile_scaffold_delete_selectors(content) + lines = content.to_s.lines + selectors = [] + lines.each_with_index do |line, index| + case line + when /\A\s*require\s+["']bundler\/gem_tasks["']\s*(?:#.*)?\n?\z/ + selectors << rakefile_selector( + "rakefile_scaffold_require_bundler_gem_tasks", + index + 1, + index + 1, + "wrapper_selected_scaffold_require" + ) + when /\A\s*require\s+["']rspec\/core\/rake_task["']\s*(?:#.*)?\n?\z/ + selectors << rakefile_selector( + "rakefile_scaffold_require_rspec_core_rake_task", + index + 1, + index + 1, + "wrapper_selected_scaffold_require" + ) + when /\A\s*require\s+["']rubocop\/rake_task["']\s*(?:#.*)?\n?\z/ + selectors << rakefile_selector( + "rakefile_scaffold_require_rubocop_rake_task", + index + 1, + index + 1, + "wrapper_selected_scaffold_require" + ) + when /\A\s*RSpec::Core::RakeTask\.new\b/ + selectors << rakefile_selector("rakefile_scaffold_rspec_task", index + 1, index + 1, + "wrapper_selected_scaffold_task") + when /\A\s*RuboCop::RakeTask\.new\b/ + selectors << rakefile_selector("rakefile_scaffold_rubocop_task", index + 1, index + 1, + "wrapper_selected_scaffold_task") + end + end + selectors.concat(rakefile_task_block_selectors(lines)) + selectors.sort_by { |selector| [selector.fetch(:start_line), selector.fetch(:end_line)] } + end + + def rakefile_task_block_selectors(lines) + selectors = [] + index = 0 + while index < lines.length + line = lines[index] + if line.match?(/\A\s*task\s+default:/) || line.match?(/\A\s*task\s+:default\b/) + unless rakefile_template_default_task?(lines, index) + end_index = rakefile_block_end(lines, index) + selectors << rakefile_selector("rakefile_scaffold_task_default", index + 1, end_index + 1, + "wrapper_selected_scaffold_task") + index = end_index + 1 + next + end + end + index += 1 + end + selectors + end + + def rakefile_template_default_task?(lines, task_index) + cursor = task_index - 1 + cursor -= 1 while cursor >= 0 && lines[cursor].strip.empty? + return false unless cursor >= 0 + + lines[cursor].strip == 'desc "Default tasks aggregator"' + end + + def rakefile_block_end(lines, start_index) + return start_index unless lines[start_index].match?(/\bdo\b/) + + depth = 0 + (start_index...lines.length).each do |index| + stripped = lines[index].strip + depth += 1 if stripped.match?(/\bdo\b/) + return index if depth.positive? && stripped == "end" && (depth -= 1).zero? + return index if depth.zero? && index > start_index && !stripped.empty? + end + lines.length - 1 + end + + def rakefile_selector(selector_id, start_line, end_line, reason) + { + selector_id: selector_id, + selector_family: "structural_owner_range", + start_line: start_line, + end_line: end_line, + reason: reason, + } + end + + def delete_line_ranges(content, selectors) + lines = content.lines + selectors.sort_by { |selector| -selector.fetch(:start_line) }.each do |selector| + start_index = selector.fetch(:start_line) - 1 + end_index = selector.fetch(:end_line) - 1 + lines.slice!(start_index..end_index) + end + lines.join.gsub(/\n{3,}/, "\n\n") + end + + def synchronize_github_actions_ci(_content, facts) + package = facts.fetch(:package) + ci = facts.fetch(:ci) + ruby_versions = ci.fetch(:ruby_versions) + ruby_matrix = ruby_versions.map { |version| " - \"#{version}\"" }.join("\n") + + <<~YAML + name: CI + + permissions: + contents: read + + on: + push: + branches: + - "#{ci.fetch(:default_branch)}" + - "*-stable" + tags: + - "!*" # Do not execute on tags + pull_request: + branches: + - "*" + workflow_dispatch: + + concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + + jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }} + runs-on: ubuntu-latest + continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} + strategy: + fail-fast: false + matrix: + ruby: + #{ruby_matrix} + rubygems: + - default + bundler: + - default + + steps: + - name: Checkout #{package.fetch(:name)} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + + - name: Tests + run: bundle exec rake + YAML + end + + def synchronize_github_actions_framework_ci(_content, facts) + ci = facts.fetch(:ci) + framework_matrix = ci.fetch(:framework_matrix) + ruby_matrix = ci.fetch(:ruby_versions).map { |version| " - \"#{version}\"" }.join("\n") + include_matrix = framework_matrix.fetch(:include).map do |entry| + [ + " - framework_version: \"#{entry.fetch(:framework_version)}\"", + " gemfile: \"#{entry.fetch(:gemfile)}\"", + ].join("\n") + end.join("\n") + dimension = framework_matrix.fetch(:dimension) + label = dimension.split(/[-_]/).map { |part| part[0].to_s.upcase + part[1..].to_s }.join(" ") + + <<~YAML + name: #{label} CI + + permissions: + contents: read + + on: + push: + branches: + - "#{ci.fetch(:default_branch)}" + - "*-stable" + tags: + - "!*" # Do not execute on tags + pull_request: + branches: + - "*" + workflow_dispatch: + + concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + + jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.framework_version }} + runs-on: ubuntu-latest + continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} + strategy: + fail-fast: false + matrix: + ruby: + #{ruby_matrix} + rubygems: + - default + bundler: + - default + include: + #{include_matrix} + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + + - name: Tests for ${{ matrix.ruby }}@${{ matrix.framework_version }} + run: bundle exec rake test + YAML + end + + def synchronize_github_actions_coverage_ci(_content, facts) + ci = facts.fetch(:ci) + coverage = ci.fetch(:coverage) + <<~YAML + name: Test Coverage + + permissions: + contents: read + pull-requests: write + id-token: write + + env: + K_SOUP_COV_MIN_BRANCH: 100 + K_SOUP_COV_MIN_LINE: 100 + K_SOUP_COV_MIN_HARD: true + K_SOUP_COV_FORMATTERS: "xml,rcov,lcov,tty" + K_SOUP_COV_DO: true + K_SOUP_COV_MULTI_FORMATTERS: true + K_SOUP_COV_COMMAND_NAME: "Test Coverage" + + on: + push: + branches: + - "#{ci.fetch(:default_branch)}" + - "*-stable" + tags: + - "!*" # Do not execute on tags + pull_request: + branches: + - "*" + workflow_dispatch: + + concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + + jobs: + coverage: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Code Coverage on ${{ matrix.ruby }}@current + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + - ruby: "ruby" + appraisal: "#{coverage.fetch(:appraisal)}" + exec_cmd: "#{coverage.fetch(:command)}" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }}@current via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} + #{github_actions_coverage_steps} + YAML + end + + def synchronize_github_actions_workflow_snippets(content) + updated = ensure_workflow_top_level_section( + content.to_s, + "permissions", + "permissions:\n contents: read\n\n", + before: "on" + ) + updated = ensure_workflow_top_level_section( + updated, + "concurrency", + "concurrency:\n group: \"${{ github.workflow }}-${{ github.ref }}\"\n cancel-in-progress: true\n\n", + before: "jobs" + ) + updated = append_github_actions_coverage_steps(updated) if github_actions_coverage_enabled?(updated) + update_github_actions_pins(updated) + end + + def github_actions_coverage_enabled?(content) + content.match?(/K_SOUP_COV_DO:\s*["']?true["']?/) + end + + def append_github_actions_coverage_steps(content) + return content if content.include?("Upload coverage to Coveralls") || content.include?("Upload coverage to CodeCov") + + lines = content.lines + steps_index = lines.index { |line| line.match?(/^ steps:\s*$/) } + return content unless steps_index + + insert_index = lines.length + ((steps_index + 1)...lines.length).each do |index| + line = lines[index] + next if line.strip.empty? + next unless line.match?(/^\S|^ \S|^ \S/) && !line.match?(/^ /) + + insert_index = index + break + end + lines.insert(insert_index, github_actions_coverage_steps) + lines.join + end + + def github_actions_coverage_steps + <<~YAML.lines.map { |line| line.strip.empty? ? line : " #{line}" }.join + - name: Upload coverage to Coveralls + if: ${{ !env.ACT }} + uses: coverallsapp/github-action@0a51d2e0b5417d06e4ecceb534aec87defc53926 # main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Upload coverage to QLTY + if: ${{ !env.ACT }} + uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 + with: + token: ${{secrets.QLTY_COVERAGE_TOKEN}} + files: coverage/.resultset.json + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Upload coverage to CodeCov + if: ${{ !env.ACT }} + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + with: + use_oidc: true + fail_ci_if_error: false + files: coverage/lcov.info,coverage/coverage.xml + verbose: true + + - name: Code Coverage Summary Report + if: ${{ !env.ACT && github.event_name == 'pull_request' }} + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '100 100' + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + if: ${{ !env.ACT && github.event_name == 'pull_request' }} + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} + YAML + end + + def ensure_workflow_top_level_section(content, key, section, before:) + return content if content.match?(/^#{Regexp.escape(key)}:/) + + lines = content.lines + index = lines.index { |line| line.match?(/^#{Regexp.escape(before)}:/) } + if index + prepared_section = index.zero? || lines[index - 1].strip.empty? ? section : "\n#{section}" + lines.insert(index, prepared_section) + else + lines << "\n" unless lines.empty? || lines.last == "\n" + lines << section + end + lines.join + end + + def update_github_actions_pins(content) + github_actions_step_pins.reduce(content) do |updated, (action_prefix, pinned_value)| + updated.gsub(/^(\s*(?:-\s*)?uses:\s*)#{Regexp.escape(action_prefix)}@\S+(?:\s+#.*)?$/) do + "#{$1}#{pinned_value}" + end + end + end + + def github_actions_step_pins + { + "actions/checkout" => "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2", + "ruby/setup-ruby" => "ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0", + "coverallsapp/github-action" => "coverallsapp/github-action@0a51d2e0b5417d06e4ecceb534aec87defc53926 # main", + "qltysh/qlty-action/coverage" => "qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0", + "codecov/codecov-action" => "codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0", + "irongut/CodeCoverageSummary" => "irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0", + "marocchino/sticky-pull-request-comment" => "marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4", + } + end + + def replace_markdown_managed_block(content, marker, replacement) + open = "" + close = "" + replace_between_markers(content, open, close, replacement) do + [content.rstrip, "", replacement, ""].join("\n") + end + end + + def replace_text_managed_block(content, replacement) + replace_between_markers(content, MANAGED_BLOCK_OPEN, MANAGED_BLOCK_CLOSE, replacement) do + [content.rstrip, replacement].reject(&:empty?).join("\n") + end + end + + def replace_between_markers(content, open_marker, close_marker, replacement) + open_index = content.index(open_marker) + close_index = content.index(close_marker) + return yield unless open_index && close_index && close_index >= open_index + + close_end = close_index + close_marker.length + close_end += 1 if content[close_end] == "\n" + "#{content[0...open_index]}#{replacement}\n#{content[close_end..]}" + end + + def ensure_trailing_newline(text) + text.end_with?("\n") ? text : "#{text}\n" + end + + def compact_hash(hash) + hash.reject { |_key, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) } + end + + def deep_dup(value) + Marshal.load(Marshal.dump(value)) + end + end +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.aiignore.example b/gems/kettle-jem/lib/kettle/jem/templates/.aiignore.example new file mode 100644 index 0000000..df6bd8b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.aiignore.example @@ -0,0 +1,19 @@ +# An .aiignore file follows the same syntax as a .gitignore file. +# .gitignore documentation: https://git-scm.com/docs/gitignore + +# you can ignore files +.DS_Store +*.log +*.tmp + +# or folders +.devcontainer/ +.qlty/ +.yardoc/ +dist/ +build/ +out/ +coverage/ +docs/ +pkg/ +results/ diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.config/mise/env.sh b/gems/kettle-jem/lib/kettle/jem/templates/.config/mise/env.sh new file mode 100644 index 0000000..f7b41f1 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.config/mise/env.sh @@ -0,0 +1,26 @@ +# shellcheck shell=bash + +prepend_unique_path_value() { + local value="$1" + local current="$2" + + if [[ -z "$current" ]]; then + printf '%s' "$value" + elif [[ ":$current:" == *":$value:"* ]]; then + printf '%s' "$current" + else + printf '%s:%s' "$value" "$current" + fi +} + +# Preserve existing values while prepending the project defaults needed by TreeHaver. +_tree_sitter_runtime_lib="${TREE_SITTER_RUNTIME_LIB:-/home/linuxbrew/.linuxbrew/Cellar/tree-sitter/0.26.6/lib/libtree-sitter.so}" +_tree_sitter_runtime_dir="$(dirname "${_tree_sitter_runtime_lib}")" +_tree_sitter_java_jars_dir="${TREE_SITTER_JAVA_JARS_DIR:-}" + +if [[ -n "${_tree_sitter_java_jars_dir}" ]]; then + _tree_sitter_java_jar="${_tree_sitter_java_jars_dir}/jtreesitter-0.26.0.jar" + export CLASSPATH="$(prepend_unique_path_value "${_tree_sitter_java_jar}" "${CLASSPATH:-}")" +fi + +export LD_LIBRARY_PATH="$(prepend_unique_path_value "${_tree_sitter_runtime_dir}" "${LD_LIBRARY_PATH:-}")" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/devcontainer-feature.json.example b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/devcontainer-feature.json.example new file mode 100644 index 0000000..bf4d0e4 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/devcontainer-feature.json.example @@ -0,0 +1,9 @@ +{ + "name": "Apt Install Packages", + "id": "apt-install", + "version": "1.0.0", + "description": "More packages are needed", + "install": { + "script": "install.sh" + } +} \ No newline at end of file diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/install.sh.example b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/install.sh.example new file mode 100644 index 0000000..5c7ef2b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/apt-install/install.sh.example @@ -0,0 +1,19 @@ +#!/bin/sh +set -e # Exit on error + +# Install basic development dependencies for Ruby & JRuby projects +apt-get update -y +apt-get install -y direnv default-jdk git zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev + +# Support for PostgreSQL (commented out by default) +# apt-get install -y postgresql libpq-dev + +# NOTE: Tree-sitter setup is NOT done here because the workspace is not mounted yet +# during the devcontainer build phase. Tree-sitter setup happens in postCreateCommand +# after the workspace is mounted. See devcontainer.json for details. +# This gem needs ALL grammars for top-level merging tool (handled by setup-tree-sitter.sh). + +echo "Basic apt packages installed. Tree-sitter will be set up after workspace mount." + +# Adds the direnv setup script to ~/.bashrc file (at the end) +echo 'eval "$(direnv hook bash)"' >> ~/.bashrc diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/devcontainer.json.example b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/devcontainer.json.example new file mode 100644 index 0000000..9fd8254 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/devcontainer.json.example @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ruby +{ + "name": "Ruby", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/ruby:1-3-bookworm", + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "./apt-install": {} + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // First setup tree-sitter (auto-detects sudo requirement), then run bundle as the regular user + // Use ${containerWorkspaceFolder} to get the actual workspace path in the container + "postCreateCommand": "bash ${containerWorkspaceFolder}/.devcontainer/scripts/setup-tree-sitter.sh --workspace=${containerWorkspaceFolder} && bundle update --bundler", + // Configure tool-specific properties. + "customizations": { + "jetbrains": { + "backend": "RubyMine" + } + } + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/scripts/setup-tree-sitter.sh.example b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/scripts/setup-tree-sitter.sh.example new file mode 100755 index 0000000..a269696 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.devcontainer/scripts/setup-tree-sitter.sh.example @@ -0,0 +1,210 @@ +#!/bin/bash +set -e + +# Setup script for tree-sitter dependencies (Ubuntu/Debian) +# Works for both GitHub Actions and devcontainer environments +# +# Dual-Environment Design: +# - GitHub Actions: Runs as non-root user, auto-detects need for sudo +# - Devcontainer: Can run as root (apt-install feature) or non-root (postCreateCommand) +# - Auto-detection: Checks if running as root (id -u = 0), uses sudo if non-root +# +# Grammar building is delegated to tsdl (https://github.com/stackmystack/tsdl). +# Configure grammars and versions in parsers.toml at the project root. +# +# Options: +# --sudo: Force use of sudo (optional, auto-detected by default) +# --cli: Install tree-sitter-cli via npm (optional) +# --build: Build and install the tree-sitter C runtime from source when distro packages are missing (optional) +# --tsdl-version VERSION: Pin tsdl release version (default: v2.0.0) +# --workspace PATH: Workspace root path for informational/debugging purposes only + +SUDO="" +INSTALL_CLI=false +BUILD_FROM_SOURCE=false +TSDL_VERSION="v2.0.0" +WORKSPACE_ROOT="/workspaces/${PWD##*/}" + +while [[ $# -gt 0 ]]; do + case $1 in + --sudo) SUDO="sudo"; shift ;; + --cli) INSTALL_CLI=true; shift ;; + --build) BUILD_FROM_SOURCE=true; shift ;; + --tsdl-version) TSDL_VERSION="$2"; shift 2 ;; + --tsdl-version=*) TSDL_VERSION="${1#*=}"; shift ;; + --workspace) WORKSPACE_ROOT="$2"; shift 2 ;; + --workspace=*) WORKSPACE_ROOT="${1#*=}"; shift ;; + *) echo "Unknown option: $1" >&2; shift ;; + esac +done + +# Auto-detect if we need sudo (running as non-root) +if [ -z "$SUDO" ] && [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" +fi + +echo "Configuration:" +echo " Workspace root: $WORKSPACE_ROOT (informational only)" +echo " Using sudo: $([ -n "$SUDO" ] && echo "yes" || echo "no")" +echo " Install CLI: $INSTALL_CLI" +echo " Build from source: $BUILD_FROM_SOURCE" +echo " tsdl version: $TSDL_VERSION" +echo "" + +have_cmd() { command -v "$1" >/dev/null 2>&1; } + +have_tree_sitter() { + [ -f /usr/include/tree-sitter/api.h ] && return 0 + [ -f /usr/local/include/tree-sitter/api.h ] && return 0 + [ -f /usr/local/include/tree-sitter/lib/include/api.h ] && return 0 + ldconfig -p 2>/dev/null | grep -q libtree-sitter && return 0 || return 1 +} + +install_tree_sitter_from_source() { + echo "[tree-sitter] Building runtime from source..." + tmpdir=$(mktemp -d /tmp/tree-sitter-src-XXXX) + trap 'rm -rf "$tmpdir"' EXIT + git clone --depth 1 https://github.com/tree-sitter/tree-sitter.git "$tmpdir" || return 1 + pushd "$tmpdir" >/dev/null || return 1 + if ! make; then + echo "[tree-sitter] ERROR: 'make' failed" >&2 + popd >/dev/null + return 1 + fi + $SUDO mkdir -p /usr/local/include/tree-sitter + $SUDO cp -r lib/include/* /usr/local/include/tree-sitter/ || true + $SUDO cp -a lib/libtree-sitter.* /usr/local/lib/ 2>/dev/null || true + have_cmd ldconfig && $SUDO ldconfig || true + popd >/dev/null + echo "[tree-sitter] Runtime installed to /usr/local." + return 0 +} + +install_tsdl() { + if have_cmd tsdl; then + echo "[tsdl] Already installed: $(tsdl --version)" + return 0 + fi + + echo "[tsdl] Installing tsdl ${TSDL_VERSION}..." + local arch + arch="$(uname -m)" + case "$arch" in + x86_64) arch="x64" ;; + aarch64) arch="arm64" ;; + armv7l) arch="arm" ;; + i686) arch="x86" ;; + *) echo "[tsdl] ERROR: Unsupported architecture: $arch" >&2; return 1 ;; + esac + + local os + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$os" in + linux) os="linux" ;; + darwin) os="macos" ;; + *) echo "[tsdl] ERROR: Unsupported OS: $os" >&2; return 1 ;; + esac + + local url="https://github.com/stackmystack/tsdl/releases/download/${TSDL_VERSION}/tsdl-${os}-${arch}.gz" + local tmpbin + tmpbin=$(mktemp /tmp/tsdl-XXXX) + + if ! wget -q "$url" -O "${tmpbin}.gz"; then + echo "[tsdl] ERROR: Failed to download from $url" >&2 + return 1 + fi + gunzip -f "${tmpbin}.gz" + chmod +x "$tmpbin" + $SUDO mv "$tmpbin" /usr/local/bin/tsdl + echo "[tsdl] Installed: $(tsdl --version)" +} + +# --- 1. System dependencies --- +echo "Installing system dependencies..." +$SUDO apt-get update -y +if ! $SUDO apt-get install -y \ + build-essential \ + pkg-config \ + $( [ "$BUILD_FROM_SOURCE" = false ] && echo "libtree-sitter-dev" ) \ + wget \ + gcc \ + g++ \ + make \ + zlib1g-dev \ + libssl-dev \ + libreadline-dev \ + libyaml-dev \ + libxml2-dev \ + libxslt1-dev \ + libcurl4-openssl-dev \ + software-properties-common \ + libffi-dev; then + echo "ERROR: apt-get failed to install required packages." >&2 + exit 1 +fi + +# --- 2. Tree-sitter runtime --- +if [ "$BUILD_FROM_SOURCE" = true ]; then + echo "[tree-sitter] --build specified; building runtime from source." +fi + +if ! have_tree_sitter; then + if [ "$BUILD_FROM_SOURCE" = true ]; then + if ! install_tree_sitter_from_source; then + echo "[tree-sitter] ERROR: Failed to build runtime. Aborting." >&2 + exit 1 + fi + else + echo "[tree-sitter] ERROR: Runtime (headers/libs) not found." >&2 + echo "Install libtree-sitter-dev or re-run with --build." >&2 + exit 1 + fi +fi + +# --- 3. tree-sitter-cli (optional) --- +if [ "$INSTALL_CLI" = true ]; then + echo "Installing tree-sitter-cli via npm..." + $SUDO npm install -g tree-sitter-cli +else + echo "Skipping tree-sitter-cli (use --cli to install)" +fi + +# --- 4. Install tsdl and build grammars --- +install_tsdl + +echo "" +echo "Building tree-sitter grammars via tsdl..." +# Use parsers.toml from the project root if it exists, otherwise build defaults. +# tsdl automatically reads parsers.toml in the current directory. +if [ -f parsers.toml ]; then + echo "[tsdl] Using parsers.toml config" + $SUDO tsdl build --out-dir /usr/local/lib --progress plain +else + echo "[tsdl] No parsers.toml found; building default grammars: toml json bash rbs" + $SUDO tsdl build toml json bash rbs --out-dir /usr/local/lib --progress plain +fi + +$SUDO ldconfig || echo "WARNING: ldconfig failed" >&2 + +echo "" +echo "tree-sitter setup complete!" +echo "" +echo "Detected library paths:" + +if [ -f /usr/lib/x86_64-linux-gnu/libtree-sitter.so.0 ]; then + echo " TREE_SITTER_RUNTIME_LIB=/usr/lib/x86_64-linux-gnu/libtree-sitter.so.0" +elif [ -f /usr/lib/x86_64-linux-gnu/libtree-sitter.so ]; then + echo " TREE_SITTER_RUNTIME_LIB=/usr/lib/x86_64-linux-gnu/libtree-sitter.so" +elif [ -f /usr/lib/libtree-sitter.so.0 ]; then + echo " TREE_SITTER_RUNTIME_LIB=/usr/lib/libtree-sitter.so.0" +elif [ -f /usr/lib/libtree-sitter.so ]; then + echo " TREE_SITTER_RUNTIME_LIB=/usr/lib/libtree-sitter.so" +else + echo " WARNING: Could not find libtree-sitter runtime library!" +fi + +echo "" +echo "Grammar libraries:" +for lib in /usr/local/lib/libtree-sitter-*.so; do + [ -f "$lib" ] && echo " $lib" +done diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.env.local.example b/gems/kettle-jem/lib/kettle/jem/templates/.env.local.example new file mode 100644 index 0000000..742ae19 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.env.local.example @@ -0,0 +1,65 @@ +# +# DO NOT EDIT THIS FILE +# +# COPY THIS FILE TO .env.local +# +# That file is ignored by .gitignore. This file is not. +# mise loads .env.local via dotenvy, so use KEY=value lines (not shell `export` statements). +# +DEBUG=false # do not allow byebug statements (override in .env.local) +FLOSS_FUNDING_DEBUG=false # extra logging to help diagnose issues (override in .env.local) +AUTOGEN_FIXTURE_CLEANUP=false # autogenerated gem fixture cleanup after every RSpec run +GIT_HOOK_FOOTER_APPEND=false +GIT_HOOK_FOOTER_APPEND_DEBUG=false +GIT_HOOK_FOOTER_SENTINEL="⚡️ A message from a fellow meat-based-AI" + +# Tokens used by ci:act and CI helpers for reading workflow/pipeline status via APIs +# GitHub (either GITHUB_TOKEN or GH_TOKEN will be used; fine-grained recommended) +# - Scope/permissions: For fine-grained tokens, grant repository access (Read) and Actions: Read +# - For classic tokens, public repos need no scopes; private repos typically require repo +GITHUB_TOKEN="" +# Alternatively: +# GH_TOKEN="" + +# GitLab (either GITLAB_TOKEN or GL_TOKEN will be used) +# - Scope: read_api is sufficient to read pipelines +GITLAB_TOKEN="" +# Alternatively: +# GL_TOKEN="" + +# If this gem does not have an open source collective uncomment and set these to false. +# OPENCOLLECTIVE_HANDLE=false +# FUNDING_ORG=false + +# ── Kettle-Jem Token Replacements ────────────────────────────────────────────── +# Used by `apply_common_replacements` during templating. +# Set these to your own values. Blank/unset values leave the token unresolved. + +# ── Forge Users ──────────────────────────────────────────────────────────────── +# KJ_GH_USER= +# KJ_GL_USER= +# KJ_CB_USER= +# KJ_SH_USER= + +# ── Author Identity ─────────────────────────────────────────────────────────── +# KJ_AUTHOR_NAME= +# KJ_AUTHOR_GIVEN_NAMES= +# KJ_AUTHOR_FAMILY_NAMES= +# KJ_AUTHOR_EMAIL= +# KJ_AUTHOR_ORCID= +# KJ_AUTHOR_DOMAIN= + +# ── Funding Platforms ───────────────────────────────────────────────────────── +# KJ_FUNDING_PATREON= +# KJ_FUNDING_KOFI= +# KJ_FUNDING_PAYPAL= +# KJ_FUNDING_BUYMEACOFFEE= +# KJ_FUNDING_POLAR= +# KJ_FUNDING_LIBERAPAY= +# KJ_FUNDING_ISSUEHUNT= + +# ── Social / Community ──────────────────────────────────────────────────────── +# KJ_SOCIAL_MASTODON= +# KJ_SOCIAL_BLUESKY= +# KJ_SOCIAL_LINKTREE= +# KJ_SOCIAL_DEVTO= diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.envrc.example b/gems/kettle-jem/lib/kettle/jem/templates/.envrc.example new file mode 100644 index 0000000..2de3ea8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.envrc.example @@ -0,0 +1,4 @@ +# This project uses mise.toml for environment management. +# Install/activate mise, or run commands with: +# mise exec -C /path/to/project -- +true diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.envrc.no-osc.example b/gems/kettle-jem/lib/kettle/jem/templates/.envrc.no-osc.example new file mode 100644 index 0000000..2de3ea8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.envrc.no-osc.example @@ -0,0 +1,4 @@ +# This project uses mise.toml for environment management. +# Install/activate mise, or run commands with: +# mise exec -C /path/to/project -- +true diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.gemrc.example b/gems/kettle-jem/lib/kettle/jem/templates/.gemrc.example new file mode 100644 index 0000000..154cd47 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.gemrc.example @@ -0,0 +1 @@ +gem: --no-document diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-msg.example b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-msg.example new file mode 100755 index 0000000..cb458d2 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-msg.example @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +# vim: set syntax=ruby + +# kettle-jem:freeze +# To retain chunks of comments & code during kettle-jem templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# kettle-jem will then preserve content between those markers across template runs. +# kettle-jem:unfreeze + +# Do not rely on Bundler; allow running outside a Bundler context +begin + require "rubygems" +rescue LoadError + # continue +end + +begin + # External gems + require "gitmoji/regex" + + full_text = File.read(ARGV[0]) + # Is the first character a GitMoji? + gitmoji_index = full_text =~ Gitmoji::Regex::REGEX + if gitmoji_index == 0 + exit(0) + else + denied = <<~EOM + Oh snap, think again... + + ______ _______ ___ _______ _______ _______ _______ ______ __ + | _ | | | | || || || || || | | | + | | || | ___| | || ___|| ||_ _|| ___|| _ || | + | |_||_ | |___ | || |___ | | | | | |___ | | | || | + | __ || ___| ___| || ___|| _| | | | ___|| |_| ||__| + | | | || |___ | || |___ | |_ | | | |___ | | __ + |___| |_||_______||_______||_______||_______| |___| |_______||______| |__| + + + Did you forget to add a relevant gitmoji? (see https://gitmoji.dev/ for tools) + In this project, a Gitmoji must be the first grapheme of the commit message. + What's a grapheme? + A symbol rendered to be visually identifiable as a single character, but which may be composed of multiple Unicode code points) + Must match: #{Gitmoji::Regex::REGEX} + #{"Found a gitmoji at character index #{gitmoji_index}... not good enough.\n" if gitmoji_index} + Example: git commit -m "✨ My excellent new feature" + + EOM + puts denied + exit(1) + end +rescue LoadError => e + failure = <<~EOM + gitmoji-regex gem not found: #{e.class}: #{e.message}. + Skipping gitmoji check and allowing commit to proceed. + Recommendation: add 'gitmoji-regex' to your development dependencies to enable this check. + + EOM + warn(failure) + exit(0) +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-subjects-goalie.txt.example b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-subjects-goalie.txt.example new file mode 100644 index 0000000..54b905a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/commit-subjects-goalie.txt.example @@ -0,0 +1,8 @@ +🔖 Prepare release v +🔒️ Checksums for v + +# Lines beginning with # are ignored. +# This file is read by .git-hooks/prepare-commit-msg in each project. +# Each line of this file will be matched against the commit subject using `starts_with?`. +# If any `starts_with?` match the project script bin/prepare-commit-msg will run. +# 🔒️ Checksums for v is the standard commit message by stone_checksums. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/footer-template.erb.txt.example b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/footer-template.erb.txt.example new file mode 100644 index 0000000..daa8fbf --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/footer-template.erb.txt.example @@ -0,0 +1,16 @@ +⚡️ A message from a fellow meat-based-AI ⚡️ +- [❤️] Finely-crafted open-source tools like <%= @gem_name %> (& many more) require time and effort. +- [❤️] Though I adore my work, it lacks financial sustainability. +- [❤️] Please, help me continue enhancing your tools by becoming a sponsor: + - [💲] https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate + - [💲] https://github.com/sponsors/{KJ|GH:USER} + +<% if ENV["GIT_HOOK_FOOTER_APPEND_DEBUG"] == "true" %> + @pwd = <%= @pwd %> + @gemspecs = <%= @gemspecs %> + @spec = <%= @spec %> + @gemspec_path = <%= @gemspec_path %> + @gem_name <%= @gem_name %> + @spec_name <%= @spec_name %> + @content <%= @content %> +<% end %> diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/prepare-commit-msg.example b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/prepare-commit-msg.example new file mode 100644 index 0000000..91df8d0 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.git-hooks/prepare-commit-msg.example @@ -0,0 +1,9 @@ +#!/bin/sh + +# Fail on error and unset variables +set -eu + +# Run the generated wrapper directly so hook execution does not depend on shell activation. +# We are not using direnv exec here because mise and direnv can result in conflicting PATH settings: +# See: https://mise.jdx.dev/direnv.html +exec "kettle-commit-msg" "$@" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/.codecov.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/.codecov.yml.example new file mode 100644 index 0000000..085c1d1 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/.codecov.yml.example @@ -0,0 +1,14 @@ +codecov: + bot: "autobolt" + max_report_age: 24 + disable_default_path_fixes: no + require_ci_to_pass: yes + notify: + after_n_builds: 2 + wait_for_ci: yes +coverage: + status: + project: + default: + target: 100% + threshold: 1% diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.example new file mode 100644 index 0000000..2442e04 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.example @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +buy_me_a_coffee: "{KJ|FUNDING:BUYMEACOFFEE}" +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +github: ["{KJ|GH:USER}"] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +issuehunt: "{KJ|FUNDING:ISSUEHUNT}" # Replace with a single IssueHunt username +ko_fi: "{KJ|FUNDING:KOFI}" # Replace with a single Ko-fi username +liberapay: "{KJ|FUNDING:LIBERAPAY}" # Replace with a single Liberapay username +open_collective: "{KJ|OPENCOLLECTIVE_ORG}" +patreon: "{KJ|FUNDING:PATREON}" # Replace with a single Patreon username +polar: "{KJ|FUNDING:POLAR}" +thanks_dev: u/gh/{KJ|GH:USER} +tidelift: rubygems/{KJ|GEM_NAME} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.no-osc.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.no-osc.example new file mode 100644 index 0000000..d6014dd --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/FUNDING.yml.no-osc.example @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +buy_me_a_coffee: "{KJ|FUNDING:BUYMEACOFFEE}" +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +github: ["{KJ|GH:USER}"] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +issuehunt: "{KJ|FUNDING:ISSUEHUNT}" # Replace with a single IssueHunt username +ko_fi: "{KJ|FUNDING:KOFI}" # Replace with a single Ko-fi username +liberapay: "{KJ|FUNDING:LIBERAPAY}" # Replace with a single Liberapay username +open_collective: # Replace with a single Open Collective slug e.g., orgs/cloud-foundry +patreon: "{KJ|FUNDING:PATREON}" # Replace with a single Patreon username +polar: "{KJ|FUNDING:POLAR}" +thanks_dev: u/gh/{KJ|GH:USER} +tidelift: rubygems/{KJ|GEM_NAME} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/copilot_instructions.md.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/copilot_instructions.md.example new file mode 100644 index 0000000..3f2a7ef --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/copilot_instructions.md.example @@ -0,0 +1 @@ +See AGENTS.md for important information on tool use and project context. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/dependabot.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/dependabot.yml.example new file mode 100644 index 0000000..956aa5a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/dependabot.yml.example @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + ignore: + - dependency-name: "rubocop-lts" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/auto-assign.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/auto-assign.yml.example new file mode 100644 index 0000000..97d786e --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/auto-assign.yml.example @@ -0,0 +1,21 @@ +name: Auto Assign +on: + issues: + types: [opened] + pull_request: + types: [opened] +jobs: + run: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'Auto-assign issue' + uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + assignees: "{KJ|GH:USER}" + abortIfPreviousAssignees: true + allowSelfAssign: true + numOfAssignee: 1 \ No newline at end of file diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/codeql-analysis.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/codeql-analysis.yml.example new file mode 100644 index 0000000..d7b1a4a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/codeql-analysis.yml.example @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, '*-stable' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, '*-stable' ] + schedule: + - cron: '35 1 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/coverage.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/coverage.yml.example new file mode 100644 index 0000000..b9ecbe3 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/coverage.yml.example @@ -0,0 +1,133 @@ +name: Test Coverage + +permissions: + contents: read + pull-requests: write + id-token: write + +env: + K_SOUP_COV_MIN_BRANCH: 100 + K_SOUP_COV_MIN_LINE: 100 + K_SOUP_COV_MIN_HARD: true + K_SOUP_COV_FORMATTERS: "xml,rcov,lcov,tty" + K_SOUP_COV_DO: true + K_SOUP_COV_MULTI_FORMATTERS: true + K_SOUP_COV_COMMAND_NAME: "Test Coverage" + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + coverage: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Code Coverage on ${{ matrix.ruby }}@current + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Coverage + - ruby: "ruby" + appraisal: "coverage" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }}@current via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} + + # Do SaaS coverage uploads first + - name: Upload coverage to Coveralls + if: ${{ !env.ACT }} + uses: coverallsapp/github-action@0a51d2e0b5417d06e4ecceb534aec87defc53926 # main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Upload coverage to QLTY + if: ${{ !env.ACT }} + uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 + with: + token: ${{secrets.QLTY_COVERAGE_TOKEN}} + files: coverage/.resultset.json + continue-on-error: ${{ matrix.experimental != 'false' }} + + # Build will fail here if coverage upload fails + # which will hopefully be noticed for the lack of code coverage comments + - name: Upload coverage to CodeCov + if: ${{ !env.ACT }} + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + with: + use_oidc: true + fail_ci_if_error: false # optional (default = false) + files: coverage/lcov.info,coverage/coverage.xml + verbose: true # optional (default = false) + + # Then PR comments + - name: Code Coverage Summary Report + if: ${{ !env.ACT && github.event_name == 'pull_request' }} + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '100 100' + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + if: ${{ !env.ACT && github.event_name == 'pull_request' }} + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/current.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/current.yml.example new file mode 100644 index 0000000..05580b5 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/current.yml.example @@ -0,0 +1,74 @@ +# Targets the evergreen latest release of ruby +name: Current MRI + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 4.0 + - ruby: "ruby" + appraisal: "current" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dep-heads.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dep-heads.yml.example new file mode 100644 index 0000000..7c38d96 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dep-heads.yml.example @@ -0,0 +1,96 @@ +# Targets the evergreen latest release of ruby, truffleruby, and jruby +# and tests against the HEAD of runtime dependencies +name: Runtime Deps @ HEAD + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: true + matrix: + include: + # Ruby 3.4 + - ruby: "ruby" + appraisal: "dep-heads" + exec_cmd: "rake spec" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # truffleruby-24.1 (targets Ruby 3.3 compatibility) + - ruby: "truffleruby" + appraisal: "dep-heads" + exec_cmd: "rake spec" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + # jruby-10.0 (targets Ruby 3.4 compatibility) + - ruby: "jruby" + appraisal: "dep-heads" + exec_cmd: "rake spec" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && (!env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby'))) }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dependency-review.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dependency-review.yml.example new file mode 100644 index 0000000..102d69a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/dependency-review.yml.example @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/discord-notifier.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/discord-notifier.yml.example new file mode 100644 index 0000000..de846d7 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/discord-notifier.yml.example @@ -0,0 +1,39 @@ +name: Discord Notify + +on: + check_run: + types: [completed] + discussion: + types: [ created ] + discussion_comment: + types: [ created ] + fork: + gollum: + issues: + types: [ opened ] + issue_comment: + types: [ created ] + pull_request: + types: [ opened, reopened, closed ] + release: + types: [ published ] + watch: + types: [ started ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + notify: + if: false + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Actions Status Discord + uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + username: GitHub Actions diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/framework-ci.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/framework-ci.yml.example new file mode 100644 index 0000000..4c7a5d7 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/framework-ci.yml.example @@ -0,0 +1,67 @@ +# Framework matrix CI workflow template. +# Generated by kettle-jem when workflows.preset is "framework". +# The matrix entries are populated from .kettle-jem.yml framework_matrix config. +name: Framework CI + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.framework_version }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} + strategy: + fail-fast: false + matrix: + ruby: + - "3.1" + - "3.2" + - "3.3" + - "3.4" + framework_version: [] + gemfile: [] + rubygems: + - default + bundler: + - default + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + + - name: Tests for ${{ matrix.ruby }}@${{ matrix.framework_version }} + run: bundle exec rake test diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/heads.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/heads.yml.example new file mode 100644 index 0000000..f4507be --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/heads.yml.example @@ -0,0 +1,95 @@ +name: Heads + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: true + matrix: + include: + # NOTE: Heads use default rubygems / bundler; their defaults are custom, unreleased, and from the future! + # ruby-head + - ruby: "ruby-head" + appraisal: "head" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + # truffleruby-head + - ruby: "truffleruby-head" + appraisal: "head" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + # jruby-head + - ruby: "jruby-head" + appraisal: "head" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && (!env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby'))) }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT || (! startsWith(matrix.ruby, 'jruby') && !startsWith(matrix.ruby, 'truffleruby')) }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.1.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.1.yml.example new file mode 100644 index 0000000..413d643 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.1.yml.example @@ -0,0 +1,77 @@ +name: JRuby 9.1 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # jruby-9.1 (targets Ruby 2.3 compatibility) + - ruby: "jruby-9.1" + appraisal: "ruby-2-3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.2.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.2.yml.example new file mode 100644 index 0000000..7b8da59 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.2.yml.example @@ -0,0 +1,77 @@ +name: JRuby 9.2 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # jruby-9.2 (targets Ruby 2.5 compatibility) + - ruby: "jruby-9.2" + appraisal: "ruby-2-5" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.3.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.3.yml.example new file mode 100644 index 0000000..3f772be --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.3.yml.example @@ -0,0 +1,77 @@ +name: JRuby 9.3 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # jruby-9.3 (targets Ruby 2.6 compatibility) + - ruby: "jruby-9.3" + appraisal: "ruby-2-6" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.4.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.4.yml.example new file mode 100644 index 0000000..bd48eb8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby-9.4.yml.example @@ -0,0 +1,77 @@ +name: JRuby 9.4 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # jruby-9.4 (targets Ruby 3.1 compatibility) + - ruby: "jruby-9.4" + appraisal: "ruby-3-1" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby.yml.example new file mode 100644 index 0000000..458dd84 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/jruby.yml.example @@ -0,0 +1,77 @@ +name: Current JRuby + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # jruby-10.0 (targets Ruby 3.4 compatibility) + - ruby: "jruby" + appraisal: "current" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/license-eye.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/license-eye.yml.example new file mode 100644 index 0000000..eda9c10 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/license-eye.yml.example @@ -0,0 +1,40 @@ +name: Apache SkyWalking Eyes + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + license-check: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check Dependencies' License + uses: apache/skywalking-eyes/dependency@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1 # v0.8.0 + with: + config: .licenserc.yaml + # Ruby packages declared as dependencies in gemspecs or Gemfiles are + # typically consumed as binaries; enable weak-compatibility + # so permissive and weak-copyleft combinations are treated as compatible. + flags: --weak-compatible diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/locked_deps.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/locked_deps.yml.example new file mode 100644 index 0000000..068de9e --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/locked_deps.yml.example @@ -0,0 +1,99 @@ +--- +# Lock/Unlock Deps Pattern +# +# Two often conflicting goals resolved! +# +# - unlocked_deps.yml +# - All runtime & dev dependencies, but does not have a `gemfiles/*.gemfile.lock` committed +# - Uses an Appraisal2 "unlocked_deps" gemfile, and the current MRI Ruby release +# - Know when new dependency releases will break local dev with unlocked dependencies +# - Broken workflow indicates that new releases of dependencies may not work +# +# - locked_deps.yml +# - All runtime & dev dependencies, and has a `Gemfile.lock` committed +# - Uses the project's main Gemfile, and the current MRI Ruby release +# - Matches what contributors and maintainers use locally for development +# - Broken workflow indicates that a new contributor will have a bad time +# +name: Deps Locked + +permissions: + contents: read + +env: + # Running coverage, but not validating minimum coverage, + # because it would be redundant with the coverage workflow. + # Also we can validate all output formats without breaking CodeCov, + # since we aren't submitting these reports anywhere. + K_SOUP_COV_MIN_BRANCH: 10 + K_SOUP_COV_MIN_LINE: 40 + K_SOUP_COV_MIN_HARD: false + K_SOUP_COV_FORMATTERS: "html,xml,rcov,lcov,json,tty" + K_SOUP_COV_DO: true + K_SOUP_COV_MULTI_FORMATTERS: true + K_SOUP_COV_COMMAND_NAME: "Test Coverage" + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Default rake task w/ main Gemfile.lock ${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + # Ruby + - ruby: "ruby" + exec_cmd: "rake" + rubygems: latest + bundler: latest + experimental: false + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Checks the kitchen sink via ${{ matrix.exec_cmd }} + run: bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/opencollective.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/opencollective.yml.example new file mode 100644 index 0000000..b66cf04 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/opencollective.yml.example @@ -0,0 +1,40 @@ +name: Open Collective Backers + +on: + schedule: + # Run once a week on Sunday at 12:00 AM UTC + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-backers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ruby + rubygems: default + bundler: default + bundler-cache: true + + - name: README Update + env: + # Keep GITHUB_TOKEN for any tools/scripts expecting it, mapped to the same secret + GITHUB_TOKEN: ${{ secrets.README_UPDATER_TOKEN }} + README_UPDATER_TOKEN: ${{ secrets.README_UPDATER_TOKEN }} + REPO: ${{ github.repository }} + run: | + git config user.name 'autobolt' + git config user.email 'autobots@9thbit.net' + # Use the configured token for authenticated pushes + git remote set-url origin "https://x-access-token:${README_UPDATER_TOKEN}@github.com/${REPO}.git" + bin/kettle-readme-backers + # Push back to the same branch/ref that triggered the workflow (default branch for schedule) + git push origin HEAD diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.3.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.3.yml.example new file mode 100644 index 0000000..ead2a15 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.3.yml.example @@ -0,0 +1,67 @@ +name: Ruby 2.3 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 2.3 + - ruby: "ruby-2.3" + appraisal: "ruby-2-3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.4.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.4.yml.example new file mode 100644 index 0000000..feeb87a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.4.yml.example @@ -0,0 +1,67 @@ +name: Ruby 2.4 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 2.4 + - ruby: "ruby-2.4" + appraisal: "ruby-2-4" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.5.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.5.yml.example new file mode 100644 index 0000000..2ae913b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.5.yml.example @@ -0,0 +1,67 @@ +name: Ruby 2.5 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 2.5 + - ruby: "ruby-2.5" + appraisal: "ruby-2-5" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: "3.3.27" + bundler: "2.3.27" + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.6.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.6.yml.example new file mode 100644 index 0000000..a6607d2 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.6.yml.example @@ -0,0 +1,67 @@ +name: Ruby 2.6 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 2.6 + - ruby: "ruby-2.6" + appraisal: "ruby-2-6" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: '3.4.22' + bundler: '2.4.22' + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.7.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.7.yml.example new file mode 100644 index 0000000..4a6e021 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-2.7.yml.example @@ -0,0 +1,67 @@ +name: Ruby 2.7 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 2.7 + - ruby: "ruby-2.7" + appraisal: "ruby-2-7" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: '3.4.22' + bundler: '2.4.22' + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.0.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.0.yml.example new file mode 100644 index 0000000..69041d0 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.0.yml.example @@ -0,0 +1,72 @@ +name: Ruby 3.0 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.0 + - ruby: "ruby-3.0" + appraisal: "ruby-3-0" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: '3.5.23' + bundler: '2.5.23' + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.1.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.1.yml.example new file mode 100644 index 0000000..23175a3 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.1.yml.example @@ -0,0 +1,70 @@ +name: Ruby 3.1 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.1 + - ruby: "ruby-3.1" + appraisal: "ruby-3-1" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: '3.6.9' + bundler: '2.6.9' + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.2.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.2.yml.example new file mode 100644 index 0000000..d6333e2 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.2.yml.example @@ -0,0 +1,67 @@ +name: Ruby 3.2 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.2 + - ruby: "ruby-3.2" + appraisal: "ruby-3-2" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} ${{ matrix.appraisal }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.3.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.3.yml.example new file mode 100644 index 0000000..a27530c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.3.yml.example @@ -0,0 +1,67 @@ +name: Ruby 3.3 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.3 + - ruby: "ruby-3.3" + appraisal: "ruby-3-3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} ${{ matrix.appraisal }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.4.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.4.yml.example new file mode 100644 index 0000000..da856d9 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/ruby-3.4.yml.example @@ -0,0 +1,67 @@ +name: Ruby 3.4 + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.4 + - ruby: "ruby-3.4" + appraisal: "ruby-3-4" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + continue-on-error: true + + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} ${{ matrix.appraisal }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/style.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/style.yml.example new file mode 100644 index 0000000..3d9e0a9 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/style.yml.example @@ -0,0 +1,74 @@ +name: Style + +permissions: + contents: read + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + rubocop: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Style on ${{ matrix.ruby }}@current + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Style + - ruby: "ruby" + appraisal: "style" + exec_cmd: "rake rubocop_gradual:check" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Run ${{ matrix.appraisal }} checks via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} + + - name: Validate RBS Types + run: bundle exec appraisal ${{ matrix.appraisal }} bin/rbs validate diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/templating.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/templating.yml.example new file mode 100644 index 0000000..89665ee --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/templating.yml.example @@ -0,0 +1,77 @@ +# Targets the evergreen latest release of ruby +name: Templating + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Latest Ruby + - ruby: "ruby" + appraisal: "templating" + exec_cmd: "rake kettle:jem:selftest" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install tree-sitter library + uses: kettle-rb/ts-grammar-action@4b0c04d11ed5b85c67c0c60c6ecb590e81748ccb # v1.0.1 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Templating self-test for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffle.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffle.yml.example new file mode 100644 index 0000000..a23f09b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffle.yml.example @@ -0,0 +1,78 @@ +name: Current TruffleRuby + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-33.0 (targets Ruby 3.3 compatibility) + - ruby: "truffleruby" + appraisal: "current" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-22.3.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-22.3.yml.example new file mode 100644 index 0000000..8b99204 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-22.3.yml.example @@ -0,0 +1,78 @@ +name: TruffleRuby 22.3 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-22.3 (targets Ruby 3.0 compatibility) + - ruby: "truffleruby-22.3" + appraisal: "ruby-3-0" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.0.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.0.yml.example new file mode 100644 index 0000000..1c67db3 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.0.yml.example @@ -0,0 +1,78 @@ +name: TruffleRuby 23.0 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-23.0 (targets Ruby 3.0 compatibility) + - ruby: "truffleruby-23.0" + appraisal: "ruby-3-0" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.1.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.1.yml.example new file mode 100644 index 0000000..f1ef9cb --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-23.1.yml.example @@ -0,0 +1,78 @@ +name: TruffleRuby 23.1 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-23.1 (targets Ruby 3.2 compatibility) + - ruby: "truffleruby-23.1" + appraisal: "ruby-3-2" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-24.2.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-24.2.yml.example new file mode 100644 index 0000000..253574d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-24.2.yml.example @@ -0,0 +1,78 @@ +name: TruffleRuby 24.2 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-24.2 (targets Ruby 3.3 compatibility) + - ruby: "truffleruby-24.2" + appraisal: "ruby-3-3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-25.0.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-25.0.yml.example new file mode 100644 index 0000000..0417736 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/truffleruby-25.0.yml.example @@ -0,0 +1,78 @@ +name: TruffleRuby 25.0 + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Specs ${{ matrix.ruby }}@${{ matrix.appraisal }} + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # NOTE: truffleruby does not support upgrading rubygems. + # truffleruby-25.0 (targets Ruby 3.3 compatibility) + - ruby: "truffleruby-25.0" + appraisal: "ruby-3-3" + exec_cmd: "rake test" + gemfile: "Appraisal.root" + rubygems: default + bundler: default + + steps: + - name: Checkout + if: ${{ !env.ACT }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Ruby & RubyGems + if: ${{ !env.ACT }} + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }} + if: ${{ !env.ACT }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/unlocked_deps.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/unlocked_deps.yml.example new file mode 100644 index 0000000..a7bf257 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.github/workflows/unlocked_deps.yml.example @@ -0,0 +1,93 @@ +--- +# Lock/Unlock Deps Pattern +# +# Two often conflicting goals resolved! +# +# - unlocked_deps.yml +# - All runtime & dev dependencies, but does not have a `gemfiles/*.gemfile.lock` committed +# - Uses an Appraisal2 "unlocked_deps" gemfile, and the current MRI Ruby release +# - Know when new dependency releases will break local dev with unlocked dependencies +# - Broken workflow indicates that new releases of dependencies may not work +# +# - locked_deps.yml +# - All runtime & dev dependencies, and has a `Gemfile.lock` committed +# - Uses the project's main Gemfile, and the current MRI Ruby release +# - Matches what contributors and maintainers use locally for development +# - Broken workflow indicates that a new contributor will have a bad time +# +name: Deps Unlocked + +permissions: + contents: read + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + - '*-stable' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + name: Default rake task w/ unlocked deps ${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby + - ruby: "ruby" + appraisal: "unlocked_deps" + exec_cmd: "rake" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install tree-sitter library + uses: kettle-rb/ts-grammar-action@4b0c04d11ed5b85c67c0c60c6ecb590e81748ccb # v1.0.1 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: true + + - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt1 + run: bundle exec appraisal ${{ matrix.appraisal }} install + # Continue to the next step on failure + continue-on-error: true + + # Effectively an automatic retry of the previous step. + - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}" + id: bundleAppraisalAttempt2 + # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip. + if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' }} + run: bundle exec appraisal ${{ matrix.appraisal }} install + + - name: Run ${{ matrix.exec_cmd }} on ${{ matrix.ruby }}@${{ matrix.appraisal }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.gitignore.example b/gems/kettle-jem/lib/kettle/jem/templates/.gitignore.example new file mode 100755 index 0000000..7f02050 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.gitignore.example @@ -0,0 +1,52 @@ +# Build Artifacts +/pkg/ +/tmp/* +!/tmp/.gitignore +*.gem + +# Bundler +/vendor/bundle/ +/.bundle/ +/gemfiles/*.lock +/gemfiles/.bundle/ +/gemfiles/.bundle/config +/gemfiles/vendor/ +Appraisal.*.gemfile.lock + +# Specs +.rspec_status +/coverage/ +/spec/reports/ +/results/ +.output.txt + +# Documentation +/.yardoc/ +/_yardoc/ +/rdoc/ +/doc/ + +# Ruby Version Managers (RVM, rbenv, etc) +# Ignored because we currently use mise.toml +.rvmrc +.ruby-version +.ruby-gemset +.tool-versions + +# Benchmarking +/measurement/ + +# Debugger detritus +.byebug_history + +# Local environment overrides (KEY=value, loaded by mise via dotenvy) +.env.local + +# OS Detritus +.DS_Store + +# Editors +*~ + +# Sentinels +.floss_funding.*.lock diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.gitlab-ci.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.gitlab-ci.yml.example new file mode 100644 index 0000000..f5e3127 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.gitlab-ci.yml.example @@ -0,0 +1,134 @@ +# You can override the included template(s) by including variable overrides +# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings +# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings +# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings +# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings +# Note that environment variables can be set in several places +# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence +#stages: +# - test +#sast: +# stage: test +#include: +# - template: Security/SAST.gitlab-ci.yml + +default: + image: "ruby:${RUBY_VERSION}" + +variables: + BUNDLE_INSTALL_FLAGS: "--quiet --jobs=$(nproc) --retry=3" + BUNDLE_FROZEN: "false" # No lockfile! + BUNDLE_GEMFILE: Appraisal.root.gemfile + K_SOUP_COV_DEBUG: true + K_SOUP_COV_DO: true + K_SOUP_COV_HARD: true + K_SOUP_COV_MIN_BRANCH: 74 + K_SOUP_COV_MIN_LINE: 90 + K_SOUP_COV_VERBOSE: true + K_SOUP_COV_FORMATTERS: "tty" + K_SOUP_COV_MULTI_FORMATTERS: true + K_SOUP_COV_COMMAND_NAME: "RSpec Coverage" + +workflow: + rules: + # For merge requests, create a pipeline. + - if: '$CI_MERGE_REQUEST_IID' + # For the ` main ` branch, create a pipeline (this includes on schedules, pushes, merges, etc.). + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + # For tags, create a pipeline. + - if: '$CI_COMMIT_TAG' + +.test_template-current: &test_definition-current + stage: test + script: + # || true so we don't fail here, because it'll probably work even if the gem update fails + - gem update --silent --system > /dev/null 2>&1 || true + - mkdir -p vendor/bundle + - bundle config set path 'vendor/bundle' + - chmod +t -R vendor/bundle + - chmod o-w -R vendor/bundle + # Setup appraisal2 + - bundle install + # Bundle a specific appraisal + - bundle exec appraisal unlocked_deps bundle install + # Light smoke test + - bundle exec appraisal unlocked_deps bin/rake --tasks + # Run tests, skipping those that won't work in CI + - > + bundle exec appraisal unlocked_deps \ + bin/rspec spec \ + --tag \~ci_skip \ + --format progress \ + --format RspecJunitFormatter + cache: + key: ${CI_JOB_IMAGE} + paths: + - vendor/ruby + +.test_template-legacy: &test_definition-legacy + stage: test + script: + # RUBYGEMS_VERSION because we support EOL Ruby still... + # || true so we don't fail here, because it'll probably work even if the gem update fails + - gem install rubygems-update -v ${RUBYGEMS_VERSION} || true + # Actually updates both RubyGems and Bundler! + - update_rubygems + - mkdir -p vendor/bundle + - bundle config set path 'vendor/bundle' + - chmod +t -R vendor/bundle + - chmod o-w -R vendor/bundle + # Setup appraisal2 + - bundle install + # Bundle a specific appraisal + - bundle exec appraisal ${APPRAISAL} bundle install + # Light smoke test + - bundle exec appraisal ${APPRAISAL} bin/rake --tasks + # Run tests, skipping those that won't work in CI + - > + bundle exec appraisal ${APPRAISAL} \ + bin/rspec spec \ + --tag \~ci_skip \ + --format progress \ + --format RspecJunitFormatter + cache: + key: ${CI_JOB_IMAGE} + paths: + - vendor/ruby + +ruby-current: + variables: + K_SOUP_COV_DO: true + <<: *test_definition-current + parallel: + matrix: + - RUBY_VERSION: ["3.2", "3.3", "3.4"] + +ruby-ruby3_1: + variables: + RUBYGEMS_VERSION: "3.6.9" + APPRAISAL: ruby-3-1 + K_SOUP_COV_DO: false + <<: *test_definition-legacy + parallel: + matrix: + - RUBY_VERSION: ["3.1"] + +ruby-ruby3_0: + variables: + RUBYGEMS_VERSION: "3.5.23" + APPRAISAL: ruby-3-0 + K_SOUP_COV_DO: false + <<: *test_definition-legacy + parallel: + matrix: + - RUBY_VERSION: ["3.0"] + +ruby-ruby2_7: + variables: + RUBYGEMS_VERSION: "3.4.22" + APPRAISAL: ruby-2-7 + K_SOUP_COV_DO: false + <<: *test_definition-legacy + parallel: + matrix: + - RUBY_VERSION: ["2.7"] diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.kettle-jem.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.kettle-jem.yml.example new file mode 100644 index 0000000..71dcbf7 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.kettle-jem.yml.example @@ -0,0 +1,337 @@ +# kettle-jem configuration file +# +# This file is both configuration and field guide for templated gems. +# In most projects this comment block is the first place to look when you +# want to understand what kettle-jem will merge, what it will copy as-is, +# and which values you may want to customize. +# +# High-level structure: +# defaults - default merge behavior for files that use strategy: merge +# tokens - values for {KJ|...} placeholders used across template files +# patterns - glob-based overrides for files/directories +# files - exact per-file overrides using a nested directory tree +# +# Default behavior: +# If a file is NOT mentioned under files: and does NOT match a patterns: +# entry, kettle-jem will merge it using the format-aware merge gem for that +# file type. +# +# Strategies: +# merge: +# Resolve tokens, then merge template + destination using the appropriate +# AST-aware/text-aware merger for the file type. +# accept_template: +# Resolve tokens, then write the template result without merging. +# If the file is missing in the destination, it will be created from the template. +# keep_destination: +# Ignore this file during templating. +# No merge. No replacement. No creation when the destination file is missing. +# Use this when you want to permanently exclude a file from the template. +# raw_copy: +# Copy bytes exactly as they appear in the template. +# No token resolution. No merge. Use for immutable/binary assets such as certs. +# +# Merge options (used only with strategy: merge): +# preference: +# template - template wins when both sides define the same node/value +# destination - destination wins when both sides define the same node/value +# add_template_only_nodes: +# true - add nodes/entries that exist only in the template +# false - do not add template-only nodes +# freeze_token: +# Marker name used for frozen sections (default: kettle-jem) +# file_type: +# Optional explicit merge-engine hint for extensionless or ambiguous files. +# Supported values: ruby, gemfile, appraisals, gemspec, rakefile, +# yaml, markdown, bash, tool_versions, text, json, jsonc, toml, +# dotenv, rbs +# max_recursion_depth: +# Limit for recursive merging when a merger supports it +# +# Other per-file options (apply to any strategy): +# skip_unresolved_scan: +# true - do not scan this file for unresolved {KJ|...} tokens after writing. +# Use when a file legitimately contains {KJ|...} text as documentation +# or example content rather than as a template placeholder. +# +# Token values: +# Precedence is: +# 1) ENV variables +# 2) tokens: values in this file +# 3) safe automatic derivation (author fields only, where possible) +# +# Important merge note for tokens in THIS file: +# Existing destination values are respected. +# That means: +# - a real value you already set here stays in place on future runs +# - an empty string also counts as an explicit destination value +# If you want kettle-jem to re-seed a missing value on a later run, +# delete that key instead of leaving it blank. +# +# Safe automatic derivations currently implemented: +# AUTHOR:NAME <- first author from the gemspec +# AUTHOR:EMAIL <- first email from the gemspec +# AUTHOR:DOMAIN <- domain part of AUTHOR:EMAIL +# AUTHOR:GIVEN_NAMES <- AUTHOR:NAME minus the last word +# AUTHOR:FAMILY_NAMES <- last word of AUTHOR:NAME +# +# Everything else should be set explicitly here or via ENV. + +# This project's identifying emoji. +# Used as the leading emoji in the README H1 heading and in the gemspec +# summary and description. Every gem should have its own unique emoji. +# +# Set this to a single emoji that represents your gem. Examples: 🪙 🔑 🛠️ 🔮 +# This key is REQUIRED. kettle-jem will abort if it is absent. +# ENV override: KJ_PROJECT_EMOJI +project_emoji: "" + +# Engines this project supports. +# List the Ruby engines that your gem targets. kettle-jem uses this to decide +# which CI workflows, README compatibility rows, and badge references to +# include. Engines not listed here are pruned from generated files. +# +# Supported values: ruby, jruby, truffleruby +# Default (when key is absent): all three engines. +engines: + - ruby + - jruby + - truffleruby + +# Framework matrix workflows. +# When your gem tests against multiple versions of a framework (Rails, +# ActiveRecord, ActiveSupport, etc.), this section configures how +# kettle-jem generates the CI matrix strategy and gemfile references. +# +# preset: controls overall workflow strategy +# modern — Ruby-only matrix (default when absent) +# framework — Ruby × framework-version matrix +# legacy-compat — supported / unsupported / ancient workflow split +# +# framework_matrix (only used when preset: framework): +# dimension — framework name used in the matrix key and display +# (e.g. activerecord, rails, actionmailer) +# versions — ordered list of versions to test against +# gemfile_pattern — pattern for gemfile paths; {version} is replaced +# with each version string (dots replaced by underscores +# when the pattern contains an underscore placeholder). +# Resolved relative to gemfiles/ directory. +# Examples: +# "ar-{version}.x" → gemfiles/ar-7.0.x +# "rails_{version}" → gemfiles/rails_7_0.gemfile +# Use this when you need a simple 2D matrix: Ruby × one framework gem/version axis. +# For deeper or more complex matrices that are better modeled as Appraisals entries, +# prefer kettle-jem-appraisals instead. +# +# workflows: +# preset: framework +# framework_matrix: +# dimension: activerecord +# versions: +# - "7.0" +# - "7.1" +# - "7.2" +# - "8.0" +# gemfile_pattern: "ar-{version}.x" + +# README top logo mode. +# Controls whether the generated README header includes the GitHub org logo, +# the project logo, or both after the shared Galtzo and ruby-lang logos. +# Supported values: org, project, org_and_project +# Default (when key is absent): org_and_project +readme: + top_logo_mode: org + # Sections to preserve from the destination README during template merging. + # These section headings (case-insensitive) will keep the destination body + # instead of being overwritten by the template. Default when absent: + # - synopsis + # - configuration + # - basic usage + # + # Built-in aliases (destination → template): + # summary → synopsis, usage → basic usage, + # configuration options → configuration, setup → basic usage + # + # preserve_sections: + # - synopsis + # - configuration + # - basic usage + # - usage + # - summary + # + # Glob patterns for additional sections to preserve (e.g. Note:* headings). + # Default when absent: ["note:*"] + # + # preserve_patterns: + # - "note:*" + # - "setup*" + # + # Custom section aliases (destination heading → canonical template heading). + # Merged with built-in aliases above. + # + # section_aliases: + # howto: basic usage + # options: configuration + +# SPDX license identifiers for this project. +# List one or more licenses that apply to the gem. Users may use the software +# under the terms of any one license listed here (OR semantics, not AND). +# +# Accepted values (initial supported set): +# MIT - MIT License (open source, permissive) +# AGPL-3.0-only - GNU Affero General Public License v3 +# PolyForm-Noncommercial-1.0.0 - PolyForm Noncommercial License 1.0.0 +# PolyForm-Small-Business-1.0.0 - PolyForm Small Business License 1.0.0 +# LicenseRef-Big-Time-Public-License - Big Time Public License 2.0.2 +# +# Defaults to spec.licenses from the gemspec when this key is absent. +# +# Template behaviour: +# - Each listed license is copied as .md into the project. +# - A LICENSE.md index file is generated referencing all chosen licenses. +# - Any non-MIT license adds a "contact for commercial license" prompt. +# - When MIT (or AGPL), a PolyForm variant, AND LicenseRef-Big-Time-Public-License +# are all present, a use-case guide table is added to README and LICENSE.md. +licenses: + - MIT + +# Machine / automation accounts to exclude from generated copyright output. +# +# When kettle-jem collects copyright information via `git blame`, it filters +# out known GitHub/GitLab bot accounts automatically. Use this list to also +# exclude internal automation accounts that are not bots in the git sense +# (i.e. they do not follow the "[bot]" naming convention). +# +# Each entry is matched case-insensitively against the commit author name AND +# the commit author email, so you can list either form. +# +# Default (when key is absent or empty): ["autobolt"] +machine_users: + - autobolt + +# Self-test / templating CI threshold. +# Set to a number from 0 to 100 to fail `rake kettle:jem:selftest` once +# divergence exceeds that percent. Leave blank to report only. +# Divergence is currently measured as the percent of produced files that would +# change or be added during templating (100 - unchanged-file score). +# ENV override: KJ_MIN_DIVERGENCE_THRESHOLD +min_divergence_threshold: {KJ|MIN_DIVERGENCE_THRESHOLD} + +# Optional kettle-jem plugins. +# Each entry is a gem name. During bootstrap, kettle-jem ensures each plugin is +# added as a development dependency so it is available after bin/setup. +# During bundled templating, kettle-jem requires the plugin and calls +# `register_kettle_jem_plugin(registrar)` on its top-level handle. +# +# Phase callback names currently available: +# - config_sync +# - dev_container +# - github_workflows +# - quality_config +# - modular_gemfiles +# - spec_helper +# - environment_templates +# - remaining_files +# - git_hooks +# - license_files +# - duplicate_check +# +# Phase guarantees: +# - All plugin callbacks run during the bundled templating phase, after +# bin/setup and bundle install have completed. +# - kettle-jem, kettle-dev, and any plugin gems listed here are loadable. +# - kettle-jem's parser / merger runtime is available to callbacks. +# - `remaining_files` runs after the destination Rakefile, gemspec, README, +# and most template-managed files already exist on disk. +# - `duplicate_check` runs last, after all template writes are complete. +plugins: + - kettle-drift + +# Default merge options +defaults: + preference: "template" + add_template_only_nodes: true + freeze_token: "kettle-jem" + +# Token replacement values. +# +# General rules: +# - Empty strings are treated as unset. +# - Use the bare identifier/slug/handle expected by the inline comment. +# - Do NOT paste full URLs unless the comment explicitly says to. +# +# Tip: +# The author fields in a newly created destination config are normally seeded +# from the gemspec via safe derivation. After that, destination values win. +tokens: + forge: + gh_user: "pboling" # GitHub username only, no @, no URL. Used for GitHub Sponsors and profile links. ENV: KJ_GH_USER + gl_user: "pboling" # GitLab username only, no @, no URL. Used for profile links. ENV: KJ_GL_USER + cb_user: "pboling" # Codeberg username only, no @, no URL. Used for profile links. ENV: KJ_CB_USER + sh_user: "galtzo" # SourceHut username only, no leading ~, no URL. Used as https://sr.ht/~/. ENV: KJ_SH_USER + + author: + name: "Peter H. Boling" # Full display name. Example: Ada Lovelace. ENV: KJ_AUTHOR_NAME. Auto-seeded from gemspec authors.first + given_names: "Peter H." # Given/personal names only. Example: Ada. ENV: KJ_AUTHOR_GIVEN_NAMES. Auto-seeded when AUTHOR:NAME can be split + family_names: "Boling" # Family/surname only. Example: Lovelace. ENV: KJ_AUTHOR_FAMILY_NAMES. Auto-seeded when AUTHOR:NAME can be split + email: "floss@galtzo.com" # Primary public email address. Example: floss@galtzo.com. ENV: KJ_AUTHOR_EMAIL. Auto-seeded from gemspec email.first + domain: "galtzo.com" # Bare domain only, no scheme, no email. Example: galtzo.com. ENV: KJ_AUTHOR_DOMAIN. Auto-seeded from AUTHOR:EMAIL + orcid: "0009-0008-8519-441X" # ORCID identifier only, not the full URL. Example: 0000-0001-2345-6789. ENV: KJ_AUTHOR_ORCID + + funding: + patreon: "galtzo" # Patreon account slug only. Used as https://patreon.com/. ENV: KJ_FUNDING_PATREON + kofi: "pboling" # Ko-fi handle/slug only. Used as https://ko-fi.com/. ENV: KJ_FUNDING_KOFI + paypal: "peterboling" # PayPal.Me slug only. Used as https://www.paypal.com/paypalme/. ENV: KJ_FUNDING_PAYPAL + buymeacoffee: "pboling" # Buy Me a Coffee slug only. Used as https://www.buymeacoffee.com/. ENV: KJ_FUNDING_BUYMEACOFFEE + polar: "pboling" # Polar handle/slug only. Used as https://polar.sh/. ENV: KJ_FUNDING_POLAR + liberapay: "pboling" # Liberapay account slug only. Used as https://liberapay.com//donate. ENV: KJ_FUNDING_LIBERAPAY + issuehunt: "pboling" # IssueHunt identifier/handle only, not a URL. ENV: KJ_FUNDING_ISSUEHUNT + + social: + mastodon: "galtzo" # Local handle only for the instance assumed by the template link. Current template uses https://ruby.social/@. ENV: KJ_SOCIAL_MASTODON + bluesky: "galtzo.com" # Full Bluesky handle. Example: peterboling.dev or alice.bsky.social. Used as https://bsky.app/profile/. ENV: KJ_SOCIAL_BLUESKY + linktree: "galtzo" # Linktree username only. Used as https://linktr.ee/. ENV: KJ_SOCIAL_LINKTREE + devto: "galtzo" # DEV Community username only. Used as https://dev.to/. ENV: KJ_SOCIAL_DEVTO + +# Glob patterns evaluated in order (first match wins) +patterns: + - path: "certs/**" + strategy: raw_copy + +# Per-file configuration (nested directory structure) +# Only files that need overrides belong here. Everything else defaults to merge. +files: + AGENTS.md: + strategy: accept_template + .github: + copilot_instructions.md: + strategy: accept_template + gemfiles: + modular: + templating.gemfile: + strategy: accept_template + templating_local.gemfile: + strategy: accept_template + +# To override specific files, add entries like: +# +# files: +# README.md: +# strategy: accept_template +# +# SECURITY.md: +# strategy: keep_destination +# +# certs: +# pboling.pem: +# strategy: raw_copy +# +# .git-hooks: +# commit-msg: +# strategy: merge +# file_type: ruby +# +# Rakefile: +# strategy: merge +# preference: destination +# add_template_only_nodes: false diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.licenserc.yaml.example b/gems/kettle-jem/lib/kettle/jem/templates/.licenserc.yaml.example new file mode 100644 index 0000000..c169084 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.licenserc.yaml.example @@ -0,0 +1,7 @@ +header: + license: + spdx-id: "{KJ|LICENSE:PRIMARY_SPDX}" + +dependency: + files: + - Gemfile.lock diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.opencollective.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.opencollective.yml.example new file mode 100644 index 0000000..7122583 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.opencollective.yml.example @@ -0,0 +1,3 @@ +collective: "{KJ|OPENCOLLECTIVE_ORG}" +readme-backers-commit-subject: "💸 Thanks 🙏 to our new backers 🎒 and subscribers 📜" +readme-osc-tag: "OPENCOLLECTIVE" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.qlty/qlty.toml.example b/gems/kettle-jem/lib/kettle/jem/templates/.qlty/qlty.toml.example new file mode 100644 index 0000000..9ae9cae --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.qlty/qlty.toml.example @@ -0,0 +1,79 @@ +# For a guide to configuration, visit https://qlty.sh/d/config +# Or for a full reference, visit https://qlty.sh/d/qlty-toml +config_version = "0" + +exclude_patterns = [ + "*_min.*", + "*-min.*", + "*.min.*", + "**/.yarn/**", + "**/*.d.ts", + "**/assets/**", + "**/bin/**", + "**/bower_components/**", + "**/build/**", + "**/cache/**", + "**/config/**", + "**/.devcontainer", + "**/db/**", + "**/deps/**", + "**/dist/**", + "**/doc/**", + "**/docs/**", + "**/extern/**", + "**/external/**", + "**/generated/**", + "**/Godeps/**", + "**/gradlew/**", + "**/mvnw/**", + "**/node_modules/**", + "**/protos/**", + "**/seed/**", + "**/target/**", + "**/templates/**", + "**/testdata/**", + "**/vendor/**", + ".github/workflows/codeql-analysis.yml" +] + +test_patterns = [ + "**/test/**", + "**/spec/**", + "**/*.test.*", + "**/*.spec.*", + "**/*_test.*", + "**/*_spec.*", + "**/test_*.*", + "**/spec_*.*", +] + +[smells] +mode = "comment" + +[smells.boolean_logic] +threshold = 4 +enabled = true + +[smells.file_complexity] +threshold = 55 +enabled = false + +[smells.return_statements] +threshold = 4 +enabled = true + +[smells.nested_control_flow] +threshold = 4 +enabled = true + +[smells.function_parameters] +threshold = 4 +enabled = true + +[smells.function_complexity] +threshold = 5 +enabled = true + +[smells.duplication] +enabled = true +threshold = 20 \ No newline at end of file diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.rspec.example b/gems/kettle-jem/lib/kettle/jem/templates/.rspec.example new file mode 100644 index 0000000..a43744c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.rspec.example @@ -0,0 +1,9 @@ +--format progress +--color +--order random +--require spec_helper +--warnings +--format html +--out results/test_results.html +--format RspecJunitFormatter +--out results/test_results.xml diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.example b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.example new file mode 100644 index 0000000..00dd72e --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.example @@ -0,0 +1 @@ +--config .rubocop.yml diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.yml.example new file mode 100644 index 0000000..4452c70 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop.yml.example @@ -0,0 +1,35 @@ +inherit_gem: + rubocop-lts: config/rubygem_rspec.yml + +inherit_from: + - .rubocop_rspec.yml + +plugins: rubocop-on-rbs + +# This tells RuboCop to add the exclusions in this file +# to the default exclusions (and those from other inherited files), +# rather than overwriting them. +inherit_mode: + merge: + - Exclude + +AllCops: + Exclude: + - certs/**/* + - checksums/**/* + - docs/**/* + - examples/**/* + - fixtures/**/* + - gemfiles/*.gemfile + - pkg/**/* + - results/**/* + - spec/fixtures/**/* + - tmp/**/* + - vendor/**/* + - AGENTS.md + +RBS: + Enabled: true + +Layout/IndentationConsistency: + Exclude: ['*.md'] diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.rubocop_rspec.yml.example b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop_rspec.yml.example new file mode 100644 index 0000000..fd9bd5c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.rubocop_rspec.yml.example @@ -0,0 +1,55 @@ +RSpec/MultipleExpectations: + Enabled: false + +RSpec/NamedSubject: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/VerifiedDoubles: + Enabled: false + +RSpec/MessageSpies: + Enabled: false + +RSpec/InstanceVariable: + Enabled: false + +RSpec/NestedGroups: + Enabled: false + +RSpec/ExpectInHook: + Enabled: false + +RSpec/DescribeClass: + Exclude: + - 'spec/examples/*' + - 'spec/integration/*' + - 'spec/system/*' + - 'spec/e2e/*' + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/SpecFilePathSuffix: + Exclude: + - 'spec/README.md' + +RSpec/NoExpectationExample: + Exclude: + - 'spec/README.md' + +RSpec/MultipleDescribes: + Exclude: + - 'spec/README.md' + +RSpec/RepeatedExampleGroupDescription: + Exclude: + - 'spec/README.md' + +RSpec/Output: + Exclude: + - 'spec/support/**/*' + - 'spec/config/**/*' + - 'spec/fixtures/**/*' diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.simplecov.example b/gems/kettle-jem/lib/kettle/jem/templates/.simplecov.example new file mode 100644 index 0000000..01d4920 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.simplecov.example @@ -0,0 +1,18 @@ +# {KJ|FREEZE_TOKEN}:freeze +# To retain chunks of comments & code during {KJ|GEM_NAME} templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# {KJ|GEM_NAME} will then preserve content between those markers across template runs. +# {KJ|FREEZE_TOKEN}:unfreeze + +require "kettle/soup/cover/config" + +# Minimum coverage thresholds are set by kettle-soup-cover. +# They are controlled by ENV variables loaded by `mise` from `mise.toml` +# (with optional machine-local overrides in `.env.local`). +# If the values for minimum coverage need to change, they should be changed both there, +# and in 2 places in .github/workflows/coverage.yml. +SimpleCov.start do + track_files "lib/**/*.rb" + track_files "lib/**/*.rake" + track_files "exe/*.rb" +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.yardignore.example b/gems/kettle-jem/lib/kettle/jem/templates/.yardignore.example new file mode 100644 index 0000000..ade54b7 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.yardignore.example @@ -0,0 +1,13 @@ +# Ignore built gem artifacts and package dir +bin/**/* +certs/**/* +checksums/**/* +gemfiles/**/* +pkg/**/* +spec/**/* +spec*/**/* +test/**/* +tmp/**/* +*.gem +# Also ignore yardoc cache +.yardoc/ diff --git a/gems/kettle-jem/lib/kettle/jem/templates/.yardopts.example b/gems/kettle-jem/lib/kettle/jem/templates/.yardopts.example new file mode 100644 index 0000000..aca5ddc --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/.yardopts.example @@ -0,0 +1,15 @@ +--plugin timekeeper +--plugin fence +-e yard/fence/hoist.rb +--plugin yaml +--plugin junk +--plugin relative_markdown_links +--readme tmp/yard-fence/README.md +--charset utf-8 +--markup markdown +--markup-provider kramdown +--output docs +'lib/**/*.rb' +- +'tmp/yard-fence/*.md' +'tmp/yard-fence/*.txt' diff --git a/gems/kettle-jem/lib/kettle/jem/templates/AGENTS.md.example b/gems/kettle-jem/lib/kettle/jem/templates/AGENTS.md.example new file mode 100644 index 0000000..1cd6e1b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/AGENTS.md.example @@ -0,0 +1,276 @@ +# AGENTS.md - Development Guide + +## 🎯 Project Overview + +This project is a **RubyGem** managed with the [kettle-rb](https://github.com/kettle-rb) toolchain. + +**Minimum Supported Ruby**: See the gemspec `required_ruby_version` constraint. +**Local Development Ruby**: See `mise.toml` for the version used in local development (typically the latest stable Ruby). + +### Modular Gemfile Architecture + +Gemfiles are split into modular components under `gemfiles/modular/`. Each component handles a specific concern (coverage, style, debug, etc.). The main `Gemfile` loads these modular components via `eval_gemfile`. +Gemfiles in the project, including modular ones, can utilize a `*_local.gemfile` counterpart pattern enabled via an ENV flag. This uses `nomono` to load sibling gems in the same workspace. + +## ⚠️ AI Agent Terminal Limitations + +### Use `mise` for Project Environment + +**CRITICAL**: The canonical project environment lives in `mise.toml`, with local overrides in `.env.local` loaded via `dotenvy`. + +⚠️ **Watch for trust prompts**: After editing `mise.toml` or `.env.local`, `mise` may require trust to be refreshed before commands can load the project environment. Until that trust step is handled, commands can appear hung or produce no output, which can look like terminal access is broken. + +**Recovery rule**: If a `mise exec` command goes silent or appears hung, assume `mise trust` is the first thing to check. Recover by running: + +```bash +mise trust -C /path/to/project +mise exec -C /path/to/project -- bundle exec kettle-test +``` + +Do this before spending time on unrelated debugging; in this workspace pattern, silent `mise` commands are usually a trust problem first. + +✅ **CORRECT** — Run self-contained commands with `mise exec`: + +```bash +mise exec -C /path/to/project -- bundle exec kettle-test +``` + +✅ **CORRECT** — If you need shell syntax first, load the environment in the same command: + +```bash +eval "$(mise env -C /path/to/project -s bash)" && bundle exec kettle-test +``` + +❌ **WRONG** — Do not rely on a previous command changing directories: + +```bash +cd /path/to/project +bundle exec rspec +``` + +❌ **WRONG** — A chained `cd` does not give directory-change hooks time to update the environment: + +```bash +cd /path/to/project && bundle exec rspec +``` + +### Prefer Internal Tools Over Terminal + +✅ **PREFERRED** — Use internal tools: + +- `grep_search` instead of `grep` command +- `file_search` instead of `find` command +- `read_file` instead of `cat` command +- `list_dir` instead of `ls` command +- `replace_string_in_file` or `create_file` instead of `sed` / manual editing + +❌ **AVOID** when possible: + +- `run_in_terminal` for information gathering + +Only use terminal for: + +- Running tests (`bundle exec kettle-test`) +- Installing dependencies (`bundle install`) +- Simple commands that do not require much shell escaping +- Running scripts (prefer writing a script over a complicated command with shell escaping) + +When you do run tests, keep the full output visible so you can inspect failures completely. + +## 🏗️ Architecture + +### Toolchain Dependencies + +This gem is part of the **kettle-rb** ecosystem. Key development tools: + +| Tool | Purpose | +|------|---------| +| `kettle-dev` | Development dependency: Rake tasks, release tooling, CI helpers | +| `kettle-test` | Test infrastructure: RSpec helpers, stubbed_env, timecop | +| `kettle-jem` | Template management and gem scaffolding | + +### Executables (from kettle-dev) + +| Executable | Purpose | +|-----------|---------| +| `kettle-release` | Full gem release workflow | +| `kettle-pre-release` | Pre-release validation | +| `kettle-changelog` | Changelog generation | +| `kettle-dvcs` | DVCS (git) workflow automation | +| `kettle-commit-msg` | Commit message validation | +| `kettle-check-eof` | EOF newline validation | + +## 📁 Project Structure + +``` +lib/ +├── / # Main library code +│ └── version.rb # Version constant (managed by kettle-release) +spec/ +├── fixtures/ # Test fixture files (NOT auto-loaded) +├── support/ +│ ├── classes/ # Helper classes for specs +│ └── shared_contexts/ # Shared RSpec contexts +├── spec_helper.rb # RSpec configuration (loaded by .rspec) +gemfiles/ +├── modular/ # Modular Gemfile components +│ ├── coverage.gemfile # SimpleCov dependencies +│ ├── debug.gemfile # Debugging tools +│ ├── documentation.gemfile # YARD/documentation +│ ├── optional.gemfile # Optional dependencies +│ ├── rspec.gemfile # RSpec testing +│ ├── style.gemfile # RuboCop/linting +│ └── x_std_libs.gemfile # Extracted stdlib gems +├── ruby_*.gemfile # Per-Ruby-version Appraisal Gemfiles +└── Appraisal.root.gemfile # Root Gemfile for Appraisal builds +.git-hooks/ +├── commit-msg # Commit message validation hook +├── prepare-commit-msg # Commit message preparation +├── commit-subjects-goalie.txt # Commit subject prefix filters +└── footer-template.erb.txt # Commit footer ERB template +``` + +## 🔧 Development Workflows + +### Running Commands + +Always make commands self-contained. Use `mise exec -C /home/pboling/src/kettle-rb/prism-merge -- ...` so the command gets the project environment in the same invocation. +If the command is complicated write a script in local tmp/ and then run the script. + +### Running Tests + +**Always run specs via `kettle-test`** (provided by `kettle-test`). It runs `bundle exec rspec`, +captures all output to `tmp/kettle-test/rspec-TIMESTAMP.log`, and prints a structured highlight block: +timing, seed, pass/fail count, failing examples, and SimpleCov coverage percentages. + +Full suite: + +```bash +mise exec -C /path/to/project -- bundle exec kettle-test +``` + +For single file, targeted, or partial spec runs the coverage threshold **must** be disabled. +Use the `K_SOUP_COV_MIN_HARD=false` environment variable to disable hard failure: + +```bash +mise exec -C /path/to/project -- env K_SOUP_COV_MIN_HARD=false bundle exec kettle-test spec/path/to/spec.rb +``` + +### Template Management (kettle-jem) + +Run the kettle-jem templater to sync project files with the latest template: + +```bash +# Standard run (quiet, non-interactive — the default) +mise exec -C /path/to/project -- bundle exec rake kettle:jem:install + +# Verbose output (see per-file detail) +mise exec -C /path/to/project -- env KETTLE_JEM_VERBOSE=true bundle exec rake kettle:jem:install + +# Interactive mode (prompt before each change) +mise exec -C /path/to/project -- bundle exec rake kettle:jem:install force=false +``` + +**Current defaults** (no flags needed): +- **quiet=true** — only phase summary lines shown; use `--verbose` or `KETTLE_JEM_VERBOSE=true` to opt out +- **force=true** — non-interactive; use `--interactive` or `force=false` to opt out +- **allowed=true** — env file changes auto-accepted; set `allowed=false` to require review + +### Building & Installing Locally + +To test local code changes across sibling repos, rebuild and reinstall the gem: + +```bash +cd /path/to/gem && rm -rf *.gem && SKIP_GEM_SIGNING=true gem build *.gemspec && gem install --force *.gem +``` + +- `SKIP_GEM_SIGNING=true` bypasses the PEM passphrase prompt for signed gemspecs. +- `--force` overwrites the currently installed version. +- Always rebuild **and** reinstall before verifying cross-repo behaviour. + +### Coverage Reports + +```bash +mise exec -C /path/to/project -- bin/rake coverage +mise exec -C /path/to/project -- bin/kettle-soup-cover -d +``` + +**Key ENV variables** (set in `mise.toml`, with local overrides in `.env.local`): +- `K_SOUP_COV_DO=true` – Enable coverage +- `K_SOUP_COV_MIN_LINE` – Line coverage threshold +- `K_SOUP_COV_MIN_BRANCH` – Branch coverage threshold +- `K_SOUP_COV_MIN_HARD=true` – Fail if thresholds not met + +### Code Quality + +```bash +mise exec -C /path/to/project -- bundle exec rake reek +mise exec -C /path/to/project -- bundle exec rubocop-gradual +``` + +### Releasing + +```bash +bin/kettle-pre-release # Validate everything before release +bin/kettle-release # Full release workflow +``` + +## 📝 Project Conventions + +### Freeze Block Preservation + +Template updates preserve custom code wrapped in freeze blocks: + +```ruby +# kettle-jem:freeze +# ... custom code preserved across template runs ... +# kettle-jem:unfreeze +``` + +### Modular Gemfile Architecture + +Gemfiles are split into modular components under `gemfiles/modular/`. Each component handles a specific concern (coverage, style, debug, etc.). The main `Gemfile` loads these modular components via `eval_gemfile`. + +### Forward Compatibility with `**options` + +**CRITICAL**: All constructors and public API methods that accept keyword arguments MUST include `**options` as the final parameter for forward compatibility. + +## 🧪 Testing Patterns + +### Test Infrastructure + +- Uses `kettle-test` for RSpec helpers (stubbed_env, block_is_expected, silent_stream, timecop) +- Uses `Dir.mktmpdir` for isolated filesystem tests +- Spec helper is loaded by `.rspec` — never add `require "spec_helper"` to spec files + +### Environment Variable Helpers + +```ruby +before do + stub_env("MY_ENV_VAR" => "value") +end + +before do + hide_env("HOME", "USER") +end +``` + +### Dependency Tags + +Use dependency tags to conditionally skip tests when optional dependencies are not available: + +```ruby +RSpec.describe SomeClass, :prism_merge do + # Skipped if prism-merge is not available +end +``` + +## 🚫 Common Pitfalls + +1. **NEVER pipe test output through `head`/`tail`** — Run tests without truncation so you can inspect the full output. +2. **README.md is mostly auto-generated by kettle-jem** — Only the following sections may be edited by hand or by agents: + - `## 🌻 Synopsis` + - `## ⚙️ Configuration` + - `## 🔧 Basic Usage` + + All other sections (badges, installation, FLOSS funding, security, contributing, versioning, license, etc.) are managed by the kettle-jem template and will be overwritten on the next templating run. Do not edit them. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/AGPL-3.0-only.md.example b/gems/kettle-jem/lib/kettle/jem/templates/AGPL-3.0-only.md.example new file mode 100644 index 0000000..0c97efd --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/AGPL-3.0-only.md.example @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/gems/kettle-jem/lib/kettle/jem/templates/Appraisal.root.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/Appraisal.root.gemfile.example new file mode 100755 index 0000000..359db51 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/Appraisal.root.gemfile.example @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# kettle-jem:freeze +# To retain chunks of comments & code during kettle-jem templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# kettle-jem will then preserve content between those markers across template runs. +# kettle-jem:unfreeze + +source "https://gem.coop" + +# Appraisal Root Gemfile is for running appraisal to generate the Appraisal Gemfiles +# in gemfiles/*gemfile. +# On CI, we use it for the Appraisal-based builds. +# We do not load the standard Gemfile, as it is tailored for local development. + +gemspec diff --git a/gems/kettle-jem/lib/kettle/jem/templates/Appraisals.example b/gems/kettle-jem/lib/kettle/jem/templates/Appraisals.example new file mode 100644 index 0000000..bd2485c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/Appraisals.example @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +# {KJ|FREEZE_TOKEN}:freeze +# To retain chunks of comments & code during {KJ|GEM_NAME} templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# {KJ|GEM_NAME} will then preserve content between those markers across template runs. +# {KJ|FREEZE_TOKEN}:unfreeze + +# HOW TO UPDATE APPRAISALS (will run rubocop_gradual's autocorrect afterward): +# bin/rake appraisal:update + +# Lock/Unlock Deps Pattern +# +# Two often conflicting goals resolved! +# +# - unlocked_deps.yml +# - All runtime & dev dependencies, but does not have a `gemfiles/*.gemfile.lock` committed +# - Uses an Appraisal2 "unlocked_deps" gemfile, and the current MRI Ruby release +# - Know when new dependency releases will break local dev with unlocked dependencies +# - Broken workflow indicates that new releases of dependencies may not work +# +# - locked_deps.yml +# - All runtime & dev dependencies, and has a `Gemfile.lock` committed +# - Uses the project's main Gemfile, and the current MRI Ruby release +# - Matches what contributors and maintainers use locally for development +# - Broken workflow indicates that a new contributor will have a bad time +# +appraise "unlocked_deps" do + # Seems to be an undeclared dependency of yard. + # /opt/hostedtoolcache/Ruby/4.0.0/x64/lib/ruby/gems/4.0.0/gems/yard-0.9.38/lib/yard/parser/ruby/legacy/irb/slex.rb:13: warning: irb/notifier is found in irb, which is not part of the default gems since Ruby 4.0.0. + # You can add irb to your Gemfile or gemspec to fix this error. + # rake aborted! + # LoadError: cannot load such file -- irb/notifier (LoadError) + # /opt/hostedtoolcache/Ruby/4.0.0/x64/bin/bundle:25:in '
' + # But it won't install on TruffleRuby, so it can't be part of modular gemfiles used there: + # An error occurred while installing psych (5.3.1), and Bundler cannot continue. + # + # In ruby_3_2.gemfile: + # irb was resolved to 1.16.0, which depends on + # rdoc was resolved to 7.0.3, which depends on + # psych + gem "irb", "~> 1.17" # ruby >= 2.7 + + eval_gemfile "modular/coverage.gemfile" + eval_gemfile "modular/documentation.gemfile" + eval_gemfile "modular/optional.gemfile" + eval_gemfile "modular/style.gemfile" + eval_gemfile "modular/x_std_libs.gemfile" +end + +# Used for head (nightly) releases of ruby, truffleruby, and jruby. +# Split into discrete appraisals if one of them needs a dependency locked discretely. +appraise "head" do + # Why is gem "cgi" here? See: https://github.com/vcr/vcr/issues/1057 + # gem "cgi", ">= 0.5" + eval_gemfile "modular/x_std_libs.gemfile" +end + +# Used for current releases of ruby, truffleruby, and jruby. +# Split into discrete appraisals if one of them needs a dependency locked discretely. +appraise "current" do + eval_gemfile "modular/x_std_libs.gemfile" +end + +# Test current Rubies against head versions of runtime dependencies +appraise "dep-heads" do + eval_gemfile "modular/runtime_heads.gemfile" +end + +appraise "ruby-2-3" do + eval_gemfile "modular/x_std_libs/r2.3/libs.gemfile" +end + +appraise "ruby-2-4" do + eval_gemfile "modular/x_std_libs/r2.4/libs.gemfile" +end + +appraise "ruby-2-5" do + eval_gemfile "modular/x_std_libs/r2.6/libs.gemfile" +end + +appraise "ruby-2-6" do + eval_gemfile "modular/x_std_libs/r2.6/libs.gemfile" +end + +appraise "ruby-2-7" do + eval_gemfile "modular/x_std_libs/r2/libs.gemfile" +end + +appraise "ruby-3-0" do + eval_gemfile "modular/x_std_libs/r3.1/libs.gemfile" +end + +appraise "ruby-3-1" do + eval_gemfile "modular/x_std_libs/r3.1/libs.gemfile" +end + +appraise "ruby-3-2" do + eval_gemfile "modular/x_std_libs/r3/libs.gemfile" +end + +appraise "ruby-3-3" do + eval_gemfile "modular/x_std_libs/r3/libs.gemfile" +end + +appraise "ruby-3-4" do + eval_gemfile "modular/x_std_libs/r3/libs.gemfile" +end + +# Only run security audit on the latest version of Ruby +appraise "audit" do + eval_gemfile "modular/x_std_libs.gemfile" +end + +# Only run coverage on the latest version of Ruby +appraise "coverage" do + eval_gemfile "modular/coverage.gemfile" + eval_gemfile "modular/optional.gemfile" + eval_gemfile "modular/x_std_libs.gemfile" +end + +# Only run linter on the latest version of Ruby (but, in support of oldest supported Ruby version) +appraise "style" do + eval_gemfile "modular/style.gemfile" + eval_gemfile "modular/x_std_libs.gemfile" +end + +appraise "templating" do + eval_gemfile "modular/templating.gemfile" + eval_gemfile "modular/x_std_libs.gemfile" +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/Big-Time-Public-License.md.example b/gems/kettle-jem/lib/kettle/jem/templates/Big-Time-Public-License.md.example new file mode 100644 index 0000000..8dba3d8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/Big-Time-Public-License.md.example @@ -0,0 +1,99 @@ +# Big Time Public License + +Version 2.0.2 + + + +## Purpose + +These terms let you use and share this software for noncommercial purposes and in small business for free, while also guaranteeing that paid licenses for big businesses will be available on fair, reasonable, and nondiscriminatory terms. + +## Acceptance + +In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. + +## Noncommercial Purposes + +You may use the software for any noncommercial purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, count as use for noncommercial purposes. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution counts as use for noncommercial purposes, regardless of the source of funding or obligations resulting from the funding. + +## Small Business + +You may use the software for the benefit of your company if it meets all these criteria: + +1. had fewer than 20 total individuals working as employees and independent contractors at all times during the last tax year + +2. earned less than $1,000,000 total revenue in the last tax year + +3. received less than $1,000,000 total debt, equity, and other investment in the last five tax years, counting investment in predecessor companies that reorganized into, merged with, or spun out your company + +All dollar figures are United States dollars as of 2019. Adjust for them inflation according to the United States Bureau of Labor Statistics' consumer price index for all urban consumers, United States city average, for all items, not seasonally adjusted, with 1982–1984=100 reference base. + +## Big Business + +You may use the software for the benefit of your company: + +1. for 128 days after your company stops qualifying under [Small Business](#small-business) + +2. indefinitely, if the licensor or their legal successor does not offer fair, reasonable, and nondiscriminatory terms for a commercial license for the software within 32 days of [written request](#how-to-request) and negotiate in good faith to conclude a deal + +## How to Request + +If this software includes an address for the licensor or an agent of the licensor in a standard place, such as in documentation, software package metadata, or an "about" page or screen, try to request a fair commercial license at that address. If this package includes both online and offline addresses, try online before offline. If you can't deliver a request that way, or this software doesn't include any addressees, spend one hour online researching an address, recording all your searches and inquiries as you go, and try any addresses that you find. If you can't find any addresses, or if those addresses also fail, that counts as failure to offer a fair commercial license by the licensor under [Big Business](#big-business). + +## Fair, Reasonable, and Nondiscriminatory Terms + +Fair, reasonable, and nondiscriminatory terms may license the software perpetually or for a term, and may or may not cover new versions of the software. If the licensor advertises license terms and a pricing structure for generally available commercial licenses, the licensor proposes license terms and a price as advertised, and a customer not affiliated with the licensor has bought a commercial license for the software on substantially equivalent terms in the past year, the proposal is fair, reasonable, and nondiscriminatory. + +## Copyright License + +The licensor grants you a copyright license to do everything with the software that would otherwise infringe the licensor's copyright in it for any purpose allowed by these terms. + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Patent License + +The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Fair Use + +You may have "fair use" rights for the software under the law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. + +**You** refers to the individual or entity agreeing to these terms. + +**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the software under these terms. + +**Use** means anything you do with the software requiring one of your licenses. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/CHANGELOG.md.example b/gems/kettle-jem/lib/kettle/jem/templates/CHANGELOG.md.example new file mode 100644 index 0000000..9810eef --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/CHANGELOG.md.example @@ -0,0 +1,47 @@ +# Changelog + +[![SemVer 2.0.0][📌semver-img]][📌semver] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog][📗keep-changelog], +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and [yes][📌major-versions-not-sacred], platform and engine support are part of the [public API][📌semver-breaking]. +Please file a bug if you notice a violation of semantic versioning. + +[📌semver]: https://semver.org/spec/v2.0.0.html +[📌semver-img]: https://img.shields.io/badge/semver-2.0.0-FFDD67.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat + +## [Unreleased] +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security + +## [1.0.1] - 2025-08-24 +- TAG: [v1.0.1][1.0.1t] +- COVERAGE: 100.00% -- 130/130 lines in 7 files +- BRANCH COVERAGE: 96.00% -- 48/50 branches in 7 files +- 100% documented +### Fixed +- bugfix: oopsie + +## [1.0.0] - 2025-08-24 +- TAG: [v1.0.0][1.0.0t] +- COVERAGE: 100.00% -- 130/130 lines in 7 files +- BRANCH COVERAGE: 96.00% -- 48/50 branches in 7 files +- 100% documented +### Added +- Initial release + +[Unreleased]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/compare/v1.0.0...HEAD +[1.0.0]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/compare/a427c302df09cfe4253a7c8d400333f9a4c1a208...v1.0.0 +[1.0.0t]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/tags/v1.0.0 +[1.0.1]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/compare/v1.0.0...v1.0.1 +[1.0.1t]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/tags/v1.0.1 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/CITATION.cff.example b/gems/kettle-jem/lib/kettle/jem/templates/CITATION.cff.example new file mode 100644 index 0000000..3ca783b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/CITATION.cff.example @@ -0,0 +1,20 @@ +cff-version: 1.2.0 +title: "{KJ|GEM_NAME}" +message: >- + If you use this work and you want to cite it, + then you can use the metadata from this file. +type: software +authors: + - given-names: "{KJ|AUTHOR:GIVEN_NAMES}" + family-names: "{KJ|AUTHOR:FAMILY_NAMES}" + email: "{KJ|AUTHOR:EMAIL}" + affiliation: "{KJ|AUTHOR:DOMAIN}" + orcid: 'https://orcid.org/{KJ|AUTHOR:ORCID}' +identifiers: + - type: url + value: 'https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}' + description: "{KJ|GEM_NAME}" +repository-code: 'https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}' +abstract: >- + {KJ|GEM_NAME} +license: See license file diff --git a/gems/kettle-jem/lib/kettle/jem/templates/CODE_OF_CONDUCT.md.example b/gems/kettle-jem/lib/kettle/jem/templates/CODE_OF_CONDUCT.md.example new file mode 100644 index 0000000..7ad4c15 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/CODE_OF_CONDUCT.md.example @@ -0,0 +1,134 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations +[🚂maint-contact]: http://www.railsbling.com/contact +[🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red diff --git a/gems/kettle-jem/lib/kettle/jem/templates/CONTRIBUTING.md.example b/gems/kettle-jem/lib/kettle/jem/templates/CONTRIBUTING.md.example new file mode 100644 index 0000000..6298dcf --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/CONTRIBUTING.md.example @@ -0,0 +1,235 @@ +# Contributing + +Bug reports and pull requests are welcome on [CodeBerg][📜src-cb], [GitLab][📜src-gl], or [GitHub][📜src-gh]. +This project should be a safe, welcoming space for collaboration, so contributors agree to adhere to +the [code of conduct][🤝conduct]. + +To submit a patch, please fork the project, create a patch with tests, and send a pull request. + +Remember to [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] if you make changes. + +## Developer Certificate of Origin + +In order to protect users of this project, we require all contributors to comply with the +[Developer Certificate of Origin](https://developercertificate.org/). +This ensures that all contributions are properly licensed and attributed. + +## Help out! + +Take a look at the open issues and pull requests, or use the gem and find something to improve. + +Follow these instructions: + +1. Join the Discord: [![Live Chat on Discord][✉️discord-invite-img]][✉️discord-invite] +2. Fork the repository +3. Create your feature branch (`git checkout -b my-new-feature`) +4. Make some fixes. +5. Commit your changes (`git commit -am 'Added some feature'`) +6. Push to the branch (`git push origin my-new-feature`) +7. Make sure to add tests for it. This is important, so it doesn't break in a future release. +8. Create new Pull Request. +9. Announce it in the channel for this org in the [Discord][✉️discord-invite]! + +## Executables vs Rake tasks + +Executables shipped by dependencies, such as {KJ|KETTLE_DEV_GEM}, and stone_checksums, are available +after running `bin/setup`. These include: + +- gem_checksums +- kettle-changelog +- kettle-commit-msg +- {KJ|KETTLE_DEV_GEM}-setup +- kettle-dvcs +- kettle-pre-release +- kettle-readme-backers +- kettle-release + +There are many Rake tasks available as well. You can see them by running: + +```shell +bin/rake -T +``` + +## Environment Variables for Local Development + +Below are the primary environment variables recognized by stone_checksums (and its integrated tools). Unless otherwise noted, set boolean values to the string "true" to enable. + +General/runtime +- DEBUG: Enable extra internal logging for this library (default: false) +- REQUIRE_BENCH: Enable `require_bench` to profile requires (default: false) +- CI: When set to true, adjusts default rake tasks toward CI behavior + +Coverage (kettle-soup-cover / SimpleCov) +- K_SOUP_COV_DO: Enable coverage collection (default: true in `mise.toml`) +- K_SOUP_COV_FORMATTERS: Comma-separated list of formatters (html, xml, rcov, lcov, json, tty) +- K_SOUP_COV_MIN_LINE: Minimum line coverage threshold (integer, e.g., 100) +- K_SOUP_COV_MIN_BRANCH: Minimum branch coverage threshold (integer, e.g., 100) +- K_SOUP_COV_MIN_HARD: Fail the run if thresholds are not met (true/false) +- K_SOUP_COV_MULTI_FORMATTERS: Enable multiple formatters at once (true/false) +- K_SOUP_COV_OPEN_BIN: Path to browser opener for HTML (empty disables auto-open) +- MAX_ROWS: Limit console output rows for simplecov-console (e.g., 1) + Tip: When running a single spec file locally, you may want `K_SOUP_COV_MIN_HARD=false` to avoid failing thresholds for a partial run. + +GitHub API and CI helpers +- GITHUB_TOKEN or GH_TOKEN: Token used by `ci:act` and release workflow checks to query GitHub Actions status at higher rate limits + +Releasing and signing +- SKIP_GEM_SIGNING: If set, skip gem signing during build/release +- GEM_CERT_USER: Username for selecting your public cert in `certs/.pem` (defaults to $USER) +- SOURCE_DATE_EPOCH: Reproducible build timestamp. + - `kettle-release` will set this automatically for the session. + - Not needed on bundler >= 2.7.0, as reproducible builds have become the default. + +Git hooks and commit message helpers (exe/kettle-commit-msg) +- GIT_HOOK_BRANCH_VALIDATE: Branch name validation mode (e.g., `jira`) or `false` to disable +- GIT_HOOK_FOOTER_APPEND: Append a footer to commit messages when goalie allows (true/false) +- GIT_HOOK_FOOTER_SENTINEL: Required when footer append is enabled — a unique first-line sentinel to prevent duplicates +- GIT_HOOK_FOOTER_APPEND_DEBUG: Extra debug output in the footer template (true/false) + +For a quick starting point, this repository’s `mise.toml` defines the shared defaults, and `.env.local` can override them locally. Copy `.env.local.example` to `.env.local`, use `KEY=value` lines, and either activate `mise` in your shell or run commands through `mise exec -C /path/to/project -- ...`. + +## Appraisals + +From time to time the [appraisal2][🚎appraisal2] gemfiles in `gemfiles/` will need to be updated. +They are created and updated with the commands: + +```console +bin/rake appraisal:update +``` + +If you need to reset all gemfiles/*.gemfile.lock files: + +```console +bin/rake appraisal:reset +``` + +When adding an appraisal to CI, check the [runner tool cache][🏃‍♂️runner-tool-cache] to see which runner to use. + +## Run Tests + +Run tests via `kettle-test` (provided by `kettle-test`). It runs RSpec, writes the full log to +`tmp/kettle-test/rspec-TIMESTAMP.log`, and prints a compact highlight block with timing, seed, +pass/fail count, failing example list, and SimpleCov coverage percentages. + +```console +bundle exec kettle-test +``` + +For targeted runs, disable the hard coverage threshold to avoid false failures: + +```console +K_SOUP_COV_MIN_HARD=false bundle exec kettle-test spec/path/to/spec.rb +``` + +### Spec organization (required) + +- One spec file per class/module. For each class or module under `lib/`, keep all of its unit tests in a single spec file under `spec/` that mirrors the path and file name exactly: `lib/{KJ|GEM_NAME_PATH}/my_class.rb` -> `spec/{KJ|GEM_NAME_PATH}/my_class_spec.rb`. +- Exception: Integration specs that intentionally span multiple classes. Place these under `spec/integration/` (or a clearly named integration folder), and do not directly mirror a single class. Name them after the scenario, not a class. + +## Lint It + +Run all the default tasks, which includes running the gradually autocorrecting linter, `rubocop-gradual`. + +```console +bundle exec rake +``` + +Or just run the linter. + +```console +bundle exec rake rubocop_gradual:autocorrect +``` + +For more detailed information about using RuboCop in this project, please see the [RUBOCOP.md](RUBOCOP.md) guide. This project uses `rubocop_gradual` instead of vanilla RuboCop, which requires specific commands for checking violations. + +### Important: Do not add inline RuboCop disables + +Never add `# rubocop:disable ...` / `# rubocop:enable ...` comments to code or specs (except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). Instead: + +- Prefer configuration-based exclusions when a rule should not apply to certain paths or files (e.g., via `.rubocop.yml`). +- When a violation is temporary, and you plan to fix it later, record it in `.rubocop_gradual.lock` using the gradual workflow: + - `bundle exec rake rubocop_gradual:autocorrect` (preferred) + - `bundle exec rake rubocop_gradual:force_update` (only when you cannot fix the violations immediately) + +As a general rule, fix style issues rather than ignoring them. For example, our specs should follow RSpec conventions like using `described_class` for the class under test. + +## Contributors + +Your picture could be here! + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +Also see GitLab Contributors: [https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main][🚎contributors-gl] + +## For Maintainers + +### One-time, Per-maintainer, Setup + +**IMPORTANT**: To sign a build, +a public key for signing gems will need to be picked up by the line in the +`gemspec` defining the `spec.cert_chain` (check the relevant ENV variables there). +All releases are signed releases. +See: [RubyGems Security Guide][🔒️rubygems-security-guide] + +NOTE: To build without signing the gem set `SKIP_GEM_SIGNING` to any value in the environment. + +### To release a new version: + +#### Automated process + +1. Update version.rb to contain the correct version-to-be-released. +2. Run `bundle exec kettle-changelog`. +3. Run `bundle exec kettle-release`. +4. Stay awake and monitor the release process for any errors, and answer any prompts. + +#### Manual process + +1. Run `bin/setup && bin/rake` as a "test, coverage, & linting" sanity check +2. Update the version number in `version.rb`, and ensure `CHANGELOG.md` reflects changes +3. Run `bin/setup && bin/rake` again as a secondary check, and to update `Gemfile.lock` +4. Run `bin/rake yard` to regenerate the docs site using the canonical docs task +5. Run `git commit -am "🔖 Prepare release v"` to commit the changes +6. Run `git push` to trigger the final CI pipeline before release, and merge PRs + - NOTE: Remember to [check the build][🧪build]. +7. Run `export GIT_TRUNK_BRANCH_NAME="$(git remote show origin | grep 'HEAD branch' | cut -d ' ' -f5)" && echo $GIT_TRUNK_BRANCH_NAME` +8. Run `git checkout $GIT_TRUNK_BRANCH_NAME` +9. Run `git pull origin $GIT_TRUNK_BRANCH_NAME` to ensure latest trunk code +10. Optional for older Bundler (< 2.7.0): Set `SOURCE_DATE_EPOCH` so `rake build` and `rake release` use the same timestamp and generate the same checksums + - If your Bundler is >= 2.7.0, you can skip this; builds are reproducible by default. + - Run `export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH` + - If the echo above has no output, then it didn't work. + - Note: `zsh/datetime` module is needed, if running `zsh`. + - In older versions of `bash` you can use `date +%s` instead, i.e. `export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH` +11. Run `bundle exec rake build` +12. Run `bin/gem_checksums` (more context [1][🔒️rubygems-checksums-pr], [2][🔒️rubygems-guides-pr]) + to create SHA-256 and SHA-512 checksums. This functionality is provided by the `stone_checksums` + [gem][💎stone_checksums]. + - The script automatically commits but does not push the checksums +13. Sanity check the SHA256, comparing with the output from the `bin/gem_checksums` command: + - `sha256sum pkg/-.gem` +14. Run `bundle exec rake release` which will create a git tag for the version, + push git commits and tags, and push the `.gem` file to the gem host configured in the gemspec. + +[📜src-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/ +[📜src-cb]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME} +[📜src-gh]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[🧪build]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions +[🤝conduct]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/blob/main/CODE_OF_CONDUCT.md +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/graphs/contributors +[🚎contributors-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main +[🖐contributors-img]: https://contrib.rocks/image?repo={KJ|GH_ORG}/{KJ|GEM_NAME} +[💎gem-coop]: https://gem.coop +[🔒️rubygems-security-guide]: https://guides.rubygems.org/security/#building-gems +[🔒️rubygems-checksums-pr]: https://github.com/rubygems/rubygems/pull/6022 +[🔒️rubygems-guides-pr]: https://github.com/rubygems/guides/pull/325 +[💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[🚎appraisal2]: https://github.com/appraisal-rb/appraisal2 +[🏃‍♂️runner-tool-cache]: https://github.com/ruby/ruby-builder/releases/tag/toolcache +[✉️discord-invite]: https://discord.gg/3qme4XHNKN diff --git a/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.example b/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.example new file mode 100644 index 0000000..fdfb50a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.example @@ -0,0 +1,74 @@ + + +Official Discord 👉️ [![Live Chat on Discord][✉️discord-invite-img]][✉️discord-invite] + +Many paths lead to being a sponsor or a backer of this project. Are you on such a path? + +[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] + +[![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon] + +[⛳liberapay-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?logo=liberapay&color=a51611&style=flat +[⛳liberapay]: https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate +[🖇osc-backers]: https://opencollective.com/{KJ|OPENCOLLECTIVE_ORG}#backer +[🖇osc-backers-i]: https://opencollective.com/{KJ|OPENCOLLECTIVE_ORG}/backers/badge.svg?style=flat +[🖇osc-sponsors]: https://opencollective.com/{KJ|OPENCOLLECTIVE_ORG}#sponsor +[🖇osc-sponsors-i]: https://opencollective.com/{KJ|OPENCOLLECTIVE_ORG}/sponsors/badge.svg?style=flat +[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}.svg?style=social&logo=github +[🖇sponsor]: https://github.com/sponsors/{KJ|GH:USER} +[🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat +[🖇polar]: https://polar.sh/{KJ|FUNDING:POLAR} +[🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat +[🖇kofi]: https://ko-fi.com/{KJ|FUNDING:KOFI} +[🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat +[🖇patreon]: https://patreon.com/{KJ|FUNDING:PATREON} +[🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat +[🖇buyme]: https://www.buymeacoffee.com/{KJ|FUNDING:BUYMEACOFFEE} +[🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal +[🖇paypal]: https://www.paypal.com/paypalme/{KJ|FUNDING:PAYPAL} +[✉️discord-invite]: https://discord.gg/3qme4XHNKN +[✉️discord-invite-img]: https://img.shields.io/discord/1373797679469170758?style=flat + + + +# 🤑 A request for help + +Maintainers have teeth and need to pay their dentists. +After getting laid off in an RIF in March, and encountering difficulty finding a new one, +I began spending most of my time building open source tools. +I'm hoping to be able to pay for my kids' health insurance this month, +so if you value the work I am doing, I need your support. +Please consider sponsoring me or the project. + +To join the community or get help 👇️ Join the Discord. + +[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] + +To say "thanks!" ☝️ Join the Discord or 👇️ send money. + +[![Sponsor {KJ|GH_ORG}/{KJ|GEM_NAME} on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal] + +# Another Way to Support Open Source Software + +I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats). + +If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`. + +I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look. + +**[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags** + +[⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?style=for-the-badge&logo=liberapay&color=a51611 +[🖇osc-all-img]: https://img.shields.io/opencollective/all/{KJ|OPENCOLLECTIVE_ORG} +[🖇osc-sponsors-img]: https://img.shields.io/opencollective/sponsors/{KJ|OPENCOLLECTIVE_ORG} +[🖇osc-backers-img]: https://img.shields.io/opencollective/backers/{KJ|OPENCOLLECTIVE_ORG} +[🖇osc-all-bottom-img]: https://img.shields.io/opencollective/all/{KJ|OPENCOLLECTIVE_ORG}?style=for-the-badge +[🖇osc-sponsors-bottom-img]: https://img.shields.io/opencollective/sponsors/{KJ|OPENCOLLECTIVE_ORG}?style=for-the-badge +[🖇osc-backers-bottom-img]: https://img.shields.io/opencollective/backers/{KJ|OPENCOLLECTIVE_ORG}?style=for-the-badge +[🖇osc]: https://opencollective.com/{KJ|OPENCOLLECTIVE_ORG} +[🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}-blue?style=for-the-badge&logo=github +[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug={KJ|FUNDING:BUYMEACOFFEE}&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff +[🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A +[🖇floss-funding.dev]: https://floss-funding.dev +[🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding +[✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge diff --git a/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.no-osc.example b/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.no-osc.example new file mode 100644 index 0000000..7e7093a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/FUNDING.md.no-osc.example @@ -0,0 +1,63 @@ + + +Official Discord 👉️ [![Live Chat on Discord][✉️discord-invite-img]][✉️discord-invite] + +Many paths lead to being a sponsor or a backer of this project. Are you on such a path? + +[![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] + +[![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon] + +[⛳liberapay-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?logo=liberapay&color=a51611&style=flat +[⛳liberapay]: https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate +[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}.svg?style=social&logo=github +[🖇sponsor]: https://github.com/sponsors/{KJ|GH:USER} +[🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat +[🖇polar]: https://polar.sh/{KJ|FUNDING:POLAR} +[🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat +[🖇kofi]: https://ko-fi.com/{KJ|FUNDING:KOFI} +[🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat +[🖇patreon]: https://patreon.com/{KJ|FUNDING:PATREON} +[🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat +[🖇buyme]: https://www.buymeacoffee.com/{KJ|FUNDING:BUYMEACOFFEE} +[🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal +[🖇paypal]: https://www.paypal.com/paypalme/{KJ|FUNDING:PAYPAL} +[✉️discord-invite]: https://discord.gg/3qme4XHNKN +[✉️discord-invite-img]: https://img.shields.io/discord/1373797679469170758?style=flat + + + +# 🤑 A request for help + +Maintainers have teeth and need to pay their dentists. +After getting laid off in an RIF in March, and encountering difficulty finding a new one, +I began spending most of my time building open source tools. +I'm hoping to be able to pay for my kids' health insurance this month, +so if you value the work I am doing, I need your support. +Please consider sponsoring me or the project. + +To join the community or get help 👇️ Join the Discord. + +[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] + +To say "thanks!" ☝️ Join the Discord or 👇️ send money. + +[![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal] + +# Another Way to Support Open Source Software + +I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats). + +If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`. + +I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look. + +**[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags** + +[⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?style=for-the-badge&logo=liberapay&color=a51611 +[🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}-blue?style=for-the-badge&logo=github +[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug={KJ|FUNDING:BUYMEACOFFEE}&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff +[🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A +[🖇floss-funding.dev]: https://floss-funding.dev +[🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding +[✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge diff --git a/gems/kettle-jem/lib/kettle/jem/templates/Gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/Gemfile.example new file mode 100644 index 0000000..e1e76ac --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/Gemfile.example @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# {KJ|FREEZE_TOKEN}:freeze +# To retain chunks of comments & code during {KJ|GEM_NAME} templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# {KJ|GEM_NAME} will then preserve content between those markers across template runs. +# {KJ|FREEZE_TOKEN}:unfreeze + +source "https://gem.coop" + +git_source(:codeberg) { |repo_name| "https://codeberg.org/#{repo_name}" } +git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" } + +#### IMPORTANT ####################################################### +# Gemfile is for local development ONLY; Gemfile is NOT loaded in CI # +####################################################### IMPORTANT #### + +# Include dependencies from {KJ|GEM_NAME}.gemspec +gemspec + +# Templating (env-switched: KETTLE_RB_DEV=true for local paths) +eval_gemfile "gemfiles/modular/templating.gemfile" + +# Debugging +eval_gemfile "gemfiles/modular/debug.gemfile" + +# Code Coverage (env-switched: KETTLE_RB_DEV=true for local paths) +eval_gemfile "gemfiles/modular/coverage.gemfile" + +# Linting +eval_gemfile "gemfiles/modular/style.gemfile" + +# Documentation +eval_gemfile "gemfiles/modular/documentation.gemfile" + +# Optional +eval_gemfile "gemfiles/modular/optional.gemfile" + +### Std Lib Extracted Gems +eval_gemfile "gemfiles/modular/x_std_libs.gemfile" + +# See unlocked_deps appraisal for more details on irb inclusion +gem "irb", "~> 1.17" # ruby >= 2.7 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/LICENSE.md.example b/gems/kettle-jem/lib/kettle/jem/templates/LICENSE.md.example new file mode 100644 index 0000000..ad301bb --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/LICENSE.md.example @@ -0,0 +1,3 @@ +{KJ|LICENSE_MD_CONTENT} + +{KJ|COPYRIGHT_PREFIX}Copyright (c) {KJ|TEMPLATE_RUN_YEAR} {KJ|AUTHOR:GIVEN_NAMES} {KJ|AUTHOR:FAMILY_NAMES} diff --git a/gems/kettle-jem/lib/kettle/jem/templates/MIT.md.example b/gems/kettle-jem/lib/kettle/jem/templates/MIT.md.example new file mode 100644 index 0000000..6d5d66d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/MIT.md.example @@ -0,0 +1,21 @@ +The MIT License (MIT) + +See [LICENSE.md](LICENSE.md) for the copyright notice. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Noncommercial-1.0.0.md.example b/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Noncommercial-1.0.0.md.example new file mode 100644 index 0000000..1a71cb6 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Noncommercial-1.0.0.md.example @@ -0,0 +1,131 @@ +# PolyForm Noncommercial License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for +the benefit of public knowledge, personal study, private +entertainment, hobby projects, amateur pursuits, or religious +observance, without any anticipated commercial application, +is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, +public research organization, public safety or health +organization, environmental protection organization, +or government institution is use for a permitted purpose +regardless of the source of funding or obligations resulting +from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. **Control** +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Small-Business-1.0.0.md.example b/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Small-Business-1.0.0.md.example new file mode 100644 index 0000000..5b5790e --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/PolyForm-Small-Business-1.0.0.md.example @@ -0,0 +1,121 @@ +# PolyForm Small Business License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## Small Business + +Use of the software for the benefit of your company is use for +a permitted purpose if your company has fewer than 100 total +individuals working as employees and independent contractors, +and less than 1,000,000 USD (2019) total revenue in the prior +tax year. Adjust this revenue threshold for inflation according +to the United States Bureau of Labor Statistics' consumer price +index for all urban consumers, U.S. city average, for all items, +not seasonally adjusted, with 1982–1984=100 reference base. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. **Control** +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/README.md.example b/gems/kettle-jem/lib/kettle/jem/templates/README.md.example new file mode 100644 index 0000000..a88ea6d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/README.md.example @@ -0,0 +1,555 @@ +{KJ|README:TOP_LOGO_ROW} + +{KJ|README:TOP_LOGO_REFS} + +# {KJ|PROJECT_EMOJI} {KJ|NAMESPACE} + +[![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] {KJ|README:LICENSE_BADGE} [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf] + +`if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord]. + +--- + +`if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job. + +[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi] + +
+ 👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️ + +I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo). + +
+ +## 🌻 Synopsis + + +## 💡 Info you can shake a stick at + +| Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] | +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Works with JRuby | [![JRuby 9.1 Compat][💎jruby-9.1i]][🚎jruby-9.1-wf] [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf]
[![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf] | +| Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.0 Compat][💎truby-23.0i]][🚎truby-23.0-wf] [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf]
[![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf] | +| Works with MRI Ruby 4 | [![Ruby 4.0 Compat][💎ruby-4.0i]][🚎11-c-wf] [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf] | +| Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf] | +| Works with MRI Ruby 2 | ![Ruby 2.0 Compat][💎ruby-2.0i] ![Ruby 2.1 Compat][💎ruby-2.1i] ![Ruby 2.2 Compat][💎ruby-2.2i]
[![Ruby 2.3 Compat][💎ruby-2.3i]][🚎ruby-2.3-wf] [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf] | +| Works with MRI Ruby 1 | ![Ruby 1.8 Compat][💎ruby-1.8i] ![Ruby 1.9 Compat][💎ruby-1.9i] | +| Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] | +| Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ!][🧮kloc-img]][🧮kloc] | +| Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] | +| Compliance | {KJ|README:LICENSE_BADGE} {KJ|README:LICENSE_COMPAT_BADGE} [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] | +| Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] | +| Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] | +| `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] | + +### Compatibility + +Compatible with MRI Ruby {KJ|MIN_RUBY}+, and concordant releases of JRuby, and TruffleRuby. + +| 🚚 _Amazing_ test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚 | +|------------------------------------------------|--------------------------------------------------------| +| 👟 Check it out! | ✨ [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ | + +### Federated DVCS + +
+ Find this repo on federated forges (Coming soon!) + +| Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions | +|-------------------------------------------------|-----------------------------------------------------------------------|---------------------------|--------------------------|---------------------------|--------------------------|------------------------------| +| 🧪 [{KJ|GH_ORG}/{KJ|GEM_NAME} on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix | ➖ | +| 🧊 [{KJ|GH_ORG}/{KJ|GEM_NAME} on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix | ➖ | +| 🐙 [{KJ|GH_ORG}/{KJ|GEM_NAME} on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix | [💚][gh-discussions] | +| 🎮️ [Discord Server][✉️discord-invite] | [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] | [Let's][✉️discord-invite] | [talk][✉️discord-invite] | [about][✉️discord-invite] | [this][✉️discord-invite] | [library!][✉️discord-invite] | + +
+ +[gh-discussions]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/discussions + +### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/{KJ|GEM_NAME})](https://tidelift.com/subscription/pkg/rubygems-{KJ|GEM_NAME}?utm_source=rubygems-{KJ|GEM_NAME}&utm_medium=referral&utm_campaign=readme) + +Available as part of the Tidelift Subscription. + +
+ Need enterprise-level guarantees? + +The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. + +[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift] + +- 💡Subscribe for support guarantees covering _all_ your FLOSS dependencies +- 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar] +- 💡Tidelift pays maintainers to maintain the software you depend on!
📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers + +Alternatively: + +- [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] +- [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] +- [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] + +
+ +## ✨ Installation + +Install the gem and add to the application's Gemfile by executing: + +```console +bundle add {KJ|GEM_NAME} +``` + +If bundler is not being used to manage dependencies, install the gem by executing: + +```console +gem install {KJ|GEM_NAME} +``` + +### 🔒 Secure Installation + +
+ For Medium or High Security Installations + +This gem is cryptographically signed and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by +[stone_checksums][💎stone_checksums]. Be sure the gem you install hasn’t been tampered with +by following the instructions below. + +Add my public key (if you haven’t already; key expires 2045-04-29) as a trusted certificate: + +```console +gem cert --add <(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem) +``` + +You only need to do that once. Then proceed to install with: + +```console +gem install {KJ|GEM_NAME} -P HighSecurity +``` + +The `HighSecurity` trust profile will verify signed gems, and not allow the installation of unsigned dependencies. + +If you want to up your security game full-time: + +```console +bundle config set --global trust-policy MediumSecurity +``` + +`MediumSecurity` instead of `HighSecurity` is necessary if not all the gems you use are signed. + +NOTE: Be prepared to track down certs for signed gems and add them the same way you added mine. + +
+ +## ⚙️ Configuration + + +## 🔧 Basic Usage + + +## 🦷 FLOSS Funding + +While {KJ|GH_ORG} tools are free software and will always be, the project would benefit immensely from some funding. +Raising a monthly budget of... "dollars" would make the project more sustainable. + +We welcome both individual and corporate sponsors! We also offer a +wide array of funding channels to account for your preferences +(although currently [Open Collective][🖇osc] is our preferred funding platform). + +**If you're working in a company that's making significant use of {KJ|GH_ORG} tools we'd +appreciate it if you suggest to your company to become a {KJ|GH_ORG} sponsor.** + +You can support the development of {KJ|GH_ORG} tools via +[GitHub Sponsors][🖇sponsor], +[Liberapay][⛳liberapay], +[PayPal][🖇paypal], +[Open Collective][🖇osc] +and [Tidelift][🏙️entsup-tidelift]. + +| 📍 NOTE | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| If doing a sponsorship in the form of donation is problematic for your company
from an accounting standpoint, we'd recommend the use of Tidelift,
where you can get a support-like subscription instead. | + +### Open Collective for Individuals + +Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/{KJ|GH_ORG}#backer)] + +NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically. + + +No backers yet. Be the first! + + +### Open Collective for Organizations + +Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/{KJ|GH_ORG}#sponsor)] + +NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically. + + +No sponsors yet. Be the first! + + +[kettle-readme-backers]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/blob/main/exe/kettle-readme-backers + +### Another way to support open-source + +I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats). + +If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`. + +I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look. + +**[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags** + +[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon] + +## 🔐 Security + +See [SECURITY.md][🔐security]. + +## 🤝 Contributing + +If you need some ideas of where to help, you could work on adding more code coverage, +or if it is already 💯 (see [below](#code-coverage)) check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], +or use the gem and think about how it could be better. + +We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it. + +See [CONTRIBUTING.md][🤝contributing] for more detailed instructions. + +### 🚀 Release Instructions + +See [CONTRIBUTING.md][🤝contributing]. + +### Code Coverage + +[![Coverage Graph][🏀codecov-g]][🏀codecov] + +[![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] + +[![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] + +### 🪇 Code of Conduct + +Everyone interacting with this project's codebases, issue trackers, +chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct]. + +## 🌈 Contributors + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +Also see GitLab Contributors: [https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main][🚎contributors-gl] + +
+ ⭐️ Star History + + + + + + Star History Chart + + + +
+ +## 📌 Versioning + +This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver]. +Violations of this scheme should be reported as bugs. +Specifically, if a minor or patch version is released that breaks backward compatibility, +a new version should be immediately released that restores compatibility. +Breaking changes to the public API will only be introduced with new major versions. + +> dropping support for a platform is both obviously and objectively a breaking change
+>—Jordan Harband ([@ljharb](https://github.com/ljharb), maintainer of SemVer) [in SemVer issue 716][📌semver-breaking] + +I understand that policy doesn't work universally ("exceptions to every rule!"), +but it is the policy here. +As such, in many cases it is good to specify a dependency on this library using +the [Pessimistic Version Constraint][📌pvc] with two digits of precision. + +For example: + +```ruby +spec.add_dependency("{KJ|GEM_NAME}", "~> {KJ|GEM_MAJOR}.0") +``` + +
+📌 Is "Platform Support" part of the public API? More details inside. + +SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms +is a *breaking change* to an API, and for that reason the bike shedding is endless. + +To get a better understanding of how SemVer is intended to work over a project's lifetime, +read this article from the creator of SemVer: + +- ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred] + +
+ +See [CHANGELOG.md][📌changelog] for a list of releases. + +## 📄 License + +{KJ|README:LICENSE_INTRO} + +### © Copyright + +See [LICENSE.md][📄license] for the official copyright notice. + +## 🤑 A request for help + +Maintainers have teeth and need to pay their dentists. +After getting laid off in an RIF in March, and encountering difficulty finding a new one, +I began spending most of my time building open source tools. +I'm hoping to be able to pay for my kids' health insurance this month, +so if you value the work I am doing, I need your support. +Please consider sponsoring me or the project. + +To join the community or get help 👇️ Join the Discord. + +[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] + +To say "thanks!" ☝️ Join the Discord or 👇️ send money. + +[![Sponsor {KJ|GH_ORG}/{KJ|GEM_NAME} on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal] + +### Please give the project a star ⭐ ♥. + +Thanks for RTFM. ☺️ + +[⛳liberapay-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?logo=liberapay&color=a51611&style=flat +[⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?style=for-the-badge&logo=liberapay&color=a51611 +[⛳liberapay]: https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate +[🖇osc-all-img]: https://img.shields.io/opencollective/all/{KJ|GH_ORG} +[🖇osc-sponsors-img]: https://img.shields.io/opencollective/sponsors/{KJ|GH_ORG} +[🖇osc-backers-img]: https://img.shields.io/opencollective/backers/{KJ|GH_ORG} +[🖇osc-backers]: https://opencollective.com/{KJ|GH_ORG}#backer +[🖇osc-backers-i]: https://opencollective.com/{KJ|GH_ORG}/backers/badge.svg?style=flat +[🖇osc-sponsors]: https://opencollective.com/{KJ|GH_ORG}#sponsor +[🖇osc-sponsors-i]: https://opencollective.com/{KJ|GH_ORG}/sponsors/badge.svg?style=flat +[🖇osc-all-bottom-img]: https://img.shields.io/opencollective/all/{KJ|GH_ORG}?style=for-the-badge +[🖇osc-sponsors-bottom-img]: https://img.shields.io/opencollective/sponsors/{KJ|GH_ORG}?style=for-the-badge +[🖇osc-backers-bottom-img]: https://img.shields.io/opencollective/backers/{KJ|GH_ORG}?style=for-the-badge +[🖇osc]: https://opencollective.com/{KJ|GH_ORG} +[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}.svg?style=social&logo=github +[🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}-blue?style=for-the-badge&logo=github +[🖇sponsor]: https://github.com/sponsors/{KJ|GH:USER} +[🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat +[🖇polar]: https://polar.sh/{KJ|FUNDING:POLAR} +[🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat +[🖇kofi]: https://ko-fi.com/{KJ|FUNDING:KOFI} +[🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat +[🖇patreon]: https://patreon.com/{KJ|FUNDING:PATREON} +[🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat +[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug={KJ|FUNDING:BUYMEACOFFEE}&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff +[🖇buyme]: https://www.buymeacoffee.com/{KJ|FUNDING:BUYMEACOFFEE} +[🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal +[🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A +[🖇paypal]: https://www.paypal.com/paypalme/{KJ|FUNDING:PAYPAL} +[🖇floss-funding.dev]: https://floss-funding.dev +[🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding +[✉️discord-invite]: https://discord.gg/3qme4XHNKN +[✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord +[✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white +[✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends + +[✇bundle-group-pattern]: https://gist.github.com/pboling/4564780 +[⛳️gem-namespace]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[⛳️namespace-img]: https://img.shields.io/badge/namespace-{KJ|NAMESPACE}-3C2D2D.svg?style=square&logo=ruby&logoColor=white +[⛳️gem-name]: https://bestgems.org/gems/{KJ|GEM_NAME} +[⛳️name-img]: https://img.shields.io/badge/name-{KJ|GEM_SHIELD}-3C2D2D.svg?style=square&logo=rubygems&logoColor=red +[⛳️tag-img]: https://img.shields.io/github/tag/{KJ|GH_ORG}/{KJ|GEM_NAME}.svg +[⛳️tag]: http://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/releases +[🚂maint-blog]: http://www.railsbling.com/tags/{KJ|GEM_NAME} +[🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange +[🚂maint-contact]: http://www.railsbling.com/contact +[🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red +[💖🖇linkedin]: http://www.linkedin.com/in/peterboling +[💖🖇linkedin-img]: https://img.shields.io/badge/LinkedIn-Profile-0B66C2?style=flat&logo=newjapanprowrestling +[💖✌️wellfound]: https://wellfound.com/u/peter-boling +[💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound +[💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling +[💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase +[💖🐘ruby-mast]: https://ruby.social/@{KJ|SOCIAL:MASTODON} +[💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@{KJ|SOCIAL:MASTODON} +[💖🦋bluesky]: https://bsky.app/profile/{KJ|SOCIAL:BLUESKY} +[💖🦋bluesky-img]: https://img.shields.io/badge/@{KJ|SOCIAL:BLUESKY}-0285FF?style=flat&logo=bluesky&logoColor=white +[💖🌳linktree]: https://linktr.ee/{KJ|SOCIAL:LINKTREE} +[💖🌳linktree-img]: https://img.shields.io/badge/{KJ|SOCIAL:LINKTREE}-purple?style=flat&logo=linktree +[💖💁🏼‍♂️devto]: https://dev.to/{KJ|SOCIAL:DEVTO} +[💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white +[💖💁🏼‍♂️aboutme]: https://about.me/peter.boling +[💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white +[💖🧊berg]: https://codeberg.org/{KJ|CB:USER} +[💖🐙hub]: https://github.org/{KJ|GH:USER} +[💖🛖hut]: https://sr.ht/~{KJ|SH:USER}/ +[💖🧪lab]: https://gitlab.com/{KJ|GL:USER} +[👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share +[👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white +[👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github +[👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white +[🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-{KJ|GEM_NAME}?utm_source=rubygems-{KJ|GEM_NAME}&utm_medium=referral&utm_campaign=readme +[🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white +[🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar +[💁🏼‍♂️peterboling]: http://www.peterboling.com +[🚂railsbling]: http://www.railsbling.com +[📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange +[📜src-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/ +[📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue +[📜src-cb]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME} +[📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=Github&logoColor=green +[📜src-gh]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white +[📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white +[📜gl-wiki]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/wikis/home +[📜gh-wiki]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/wiki +[📜gl-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white +[📜gh-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=github&logoColor=white +[👽dl-rank]: https://bestgems.org/gems/{KJ|GEM_NAME} +[👽dl-ranki]: https://img.shields.io/gem/rd/{KJ|GEM_NAME}.svg +[👽oss-help]: https://www.codetriage.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[👽oss-helpi]: https://www.codetriage.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/badges/users.svg +[👽version]: https://bestgems.org/gems/{KJ|GEM_NAME} +[👽versioni]: https://img.shields.io/gem/v/{KJ|GEM_NAME}.svg +[🏀qlty-mnt]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME} +[🏀qlty-mnti]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/maintainability.svg +[🏀qlty-cov]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/metrics/code?sort=coverageRating +[🏀qlty-covi]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/coverage.svg +[🏀codecov]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME} +[🏀codecovi]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME}/graph/badge.svg +[🏀coveralls]: https://coveralls.io/github/{KJ|GH_ORG}/{KJ|GEM_NAME}?branch=main +[🏀coveralls-img]: https://coveralls.io/repos/github/{KJ|GH_ORG}/{KJ|GEM_NAME}/badge.svg?branch=main +[🖐codeQL]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/security/code-scanning +[🖐codeQL-img]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/codeql-analysis.yml/badge.svg +[🚎ruby-2.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.3.yml +[🚎ruby-2.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.3.yml/badge.svg +[🚎ruby-2.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.4.yml +[🚎ruby-2.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.4.yml/badge.svg +[🚎ruby-2.5-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.5.yml +[🚎ruby-2.5-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.5.yml/badge.svg +[🚎ruby-2.6-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.6.yml +[🚎ruby-2.6-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.6.yml/badge.svg +[🚎ruby-2.7-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.7.yml +[🚎ruby-2.7-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.7.yml/badge.svg +[🚎ruby-3.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.0.yml +[🚎ruby-3.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.0.yml/badge.svg +[🚎ruby-3.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.1.yml +[🚎ruby-3.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.1.yml/badge.svg +[🚎ruby-3.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.2.yml +[🚎ruby-3.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.2.yml/badge.svg +[🚎ruby-3.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.3.yml +[🚎ruby-3.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.3.yml/badge.svg +[🚎ruby-3.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.4.yml +[🚎ruby-3.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.4.yml/badge.svg +[🚎jruby-9.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.1.yml +[🚎jruby-9.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.1.yml/badge.svg +[🚎jruby-9.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.2.yml +[🚎jruby-9.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.2.yml/badge.svg +[🚎jruby-9.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.3.yml +[🚎jruby-9.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.3.yml/badge.svg +[🚎jruby-9.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.4.yml +[🚎jruby-9.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.4.yml/badge.svg +[🚎truby-22.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-22.3.yml +[🚎truby-22.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-22.3.yml/badge.svg +[🚎truby-23.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.0.yml +[🚎truby-23.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.0.yml/badge.svg +[🚎truby-23.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.1.yml +[🚎truby-23.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.1.yml/badge.svg +[🚎truby-24.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-24.2.yml +[🚎truby-24.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-24.2.yml/badge.svg +[🚎truby-25.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-25.0.yml +[🚎truby-25.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-25.0.yml/badge.svg +[🚎2-cov-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/coverage.yml +[🚎2-cov-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/coverage.yml/badge.svg +[🚎3-hd-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/heads.yml +[🚎3-hd-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/heads.yml/badge.svg +[🚎5-st-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/style.yml +[🚎5-st-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/style.yml/badge.svg +[🚎9-t-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffle.yml +[🚎9-t-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffle.yml/badge.svg +[🚎10-j-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby.yml +[🚎10-j-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby.yml/badge.svg +[🚎11-c-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/current.yml +[🚎11-c-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/current.yml/badge.svg +[🚎12-crh-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/dep-heads.yml +[🚎12-crh-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/dep-heads.yml/badge.svg +[🚎13-🔒️-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/locked_deps.yml +[🚎13-🔒️-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/locked_deps.yml/badge.svg +[🚎14-🔓️-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/unlocked_deps.yml +[🚎14-🔓️-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/unlocked_deps.yml/badge.svg +[🚎15-🪪-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/license-eye.yml +[🚎15-🪪-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/license-eye.yml/badge.svg +[💎ruby-1.8i]: https://img.shields.io/badge/Ruby-1.8_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-1.9i]: https://img.shields.io/badge/Ruby-1.9_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.0i]: https://img.shields.io/badge/Ruby-2.0_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.1i]: https://img.shields.io/badge/Ruby-2.1_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.2i]: https://img.shields.io/badge/Ruby-2.2_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.3i]: https://img.shields.io/badge/Ruby-2.3-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.4i]: https://img.shields.io/badge/Ruby-2.4-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.5i]: https://img.shields.io/badge/Ruby-2.5-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.6i]: https://img.shields.io/badge/Ruby-2.6-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.7i]: https://img.shields.io/badge/Ruby-2.7-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.0i]: https://img.shields.io/badge/Ruby-3.0-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.1i]: https://img.shields.io/badge/Ruby-3.1-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.4i]: https://img.shields.io/badge/Ruby-3.4-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-4.0i]: https://img.shields.io/badge/Ruby-4.0-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green +[💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue +[💎truby-22.3i]: https://img.shields.io/badge/Truffle_Ruby-22.3-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-23.0i]: https://img.shields.io/badge/Truffle_Ruby-23.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-24.2i]: https://img.shields.io/badge/Truffle_Ruby-24.2-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-25.0i]: https://img.shields.io/badge/Truffle_Ruby-25.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green +[💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue +[💎jruby-9.1i]: https://img.shields.io/badge/JRuby-9.1-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.2i]: https://img.shields.io/badge/JRuby-9.2-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.3i]: https://img.shields.io/badge/JRuby-9.3-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.4i]: https://img.shields.io/badge/JRuby-9.4-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-c-i]: https://img.shields.io/badge/JRuby-current-FBE742?style=for-the-badge&logo=ruby&logoColor=green +[💎jruby-headi]: https://img.shields.io/badge/JRuby-HEAD-FBE742?style=for-the-badge&logo=ruby&logoColor=blue +[🤝gh-issues]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/issues +[🤝gh-pulls]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/pulls +[🤝gl-issues]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/issues +[🤝gl-pulls]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/merge_requests +[🤝cb-issues]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME}/issues +[🤝cb-pulls]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME}/pulls +[🤝cb-donate]: https://donate.codeberg.org/ +[🤝contributing]: CONTRIBUTING.md +[🏀codecov-g]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME}/graphs/tree.svg +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/graphs/contributors +[🖐contributors-img]: https://contrib.rocks/image?repo={KJ|GH_ORG}/{KJ|GEM_NAME} +[🚎contributors-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main +[🪇conduct]: CODE_OF_CONDUCT.md +[🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg +[📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint +[📌semver]: https://semver.org/spec/v2.0.0.html +[📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[📌changelog]: CHANGELOG.md +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat +[📌gitmoji]: https://gitmoji.dev +[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square +[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ +[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.053-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue +[🔐security]: SECURITY.md +[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat +{KJ|README:LICENSE_REFS} +[📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm +[📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat +[🚎yard-current]: http://rubydoc.info/gems/{KJ|GEM_NAME} +[🚎yard-head]: https://{KJ|YARD_HOST} +[💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums +[💎SHA_checksums]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/tree/main/checksums +[💎rlts]: https://github.com/rubocop-lts/rubocop-lts +[💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white +[💎appraisal2]: https://github.com/appraisal-rb/appraisal2 +[💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white +[💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/ diff --git a/gems/kettle-jem/lib/kettle/jem/templates/README.md.no-osc.example b/gems/kettle-jem/lib/kettle/jem/templates/README.md.no-osc.example new file mode 100644 index 0000000..33ceabf --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/README.md.no-osc.example @@ -0,0 +1,523 @@ +{KJ|README:TOP_LOGO_ROW} + +{KJ|README:TOP_LOGO_REFS} + +# {KJ|PROJECT_EMOJI} {KJ|NAMESPACE} + +[![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf] + +`if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord]. + +--- + +`if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job. + +[![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi] + +## 🌻 Synopsis + +
+ 👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️ + +I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo). + +
+ +## 💡 Info you can shake a stick at + +| Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] | +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Works with JRuby | [![JRuby 9.1 Compat][💎jruby-9.1i]][🚎jruby-9.1-wf] [![JRuby 9.2 Compat][💎jruby-9.2i]][🚎jruby-9.2-wf] [![JRuby 9.3 Compat][💎jruby-9.3i]][🚎jruby-9.3-wf]
[![JRuby 9.4 Compat][💎jruby-9.4i]][🚎jruby-9.4-wf] [![JRuby current Compat][💎jruby-c-i]][🚎10-j-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf] | +| Works with Truffle Ruby | [![Truffle Ruby 22.3 Compat][💎truby-22.3i]][🚎truby-22.3-wf] [![Truffle Ruby 23.0 Compat][💎truby-23.0i]][🚎truby-23.0-wf] [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎truby-23.1-wf]
[![Truffle Ruby 24.2 Compat][💎truby-24.2i]][🚎truby-24.2-wf] [![Truffle Ruby 25.0 Compat][💎truby-25.0i]][🚎truby-25.0-wf] [![Truffle Ruby current Compat][💎truby-c-i]][🚎9-t-wf] | +| Works with MRI Ruby 4 | [![Ruby 4.0 Compat][💎ruby-4.0i]][🚎11-c-wf] [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf] | +| Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf] | +| Works with MRI Ruby 2 | ![Ruby 2.0 Compat][💎ruby-2.0i] ![Ruby 2.1 Compat][💎ruby-2.1i] ![Ruby 2.2 Compat][💎ruby-2.2i]
[![Ruby 2.3 Compat][💎ruby-2.3i]][🚎ruby-2.3-wf] [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf] | +| Works with MRI Ruby 1 | ![Ruby 1.8 Compat][💎ruby-1.8i] ![Ruby 1.9 Compat][💎ruby-1.9i] | +| Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] | +| Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ!][🧮kloc-img]][🧮kloc] | +| Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] | +| Compliance | [![License: MIT][📄license-img]][📄license-ref] [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] | +| Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] | +| Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] | +| `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] | + +### Compatibility + +Compatible with MRI Ruby {KJ|MIN_RUBY}+, and concordant releases of JRuby, and TruffleRuby. + + +| 🚚 _Amazing_ test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚 | +|------------------------------------------------|--------------------------------------------------------| +| 👟 Check it out! | ✨ [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ | + +### Federated DVCS + +
+ Find this repo on federated forges (Coming soon!) + +| Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions | +|-------------------------------------------------|-----------------------------------------------------------------------|---------------------------|--------------------------|---------------------------|--------------------------|------------------------------| +| 🧪 [{KJ|GH_ORG}/{KJ|GEM_NAME} on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix | ➖ | +| 🧊 [{KJ|GH_ORG}/{KJ|GEM_NAME} on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix | ➖ | +| 🐙 [{KJ|GH_ORG}/{KJ|GEM_NAME} on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix | [💚][gh-discussions] | +| 🎮️ [Discord Server][✉️discord-invite] | [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] | [Let's][✉️discord-invite] | [talk][✉️discord-invite] | [about][✉️discord-invite] | [this][✉️discord-invite] | [library!][✉️discord-invite] | + +
+ +[gh-discussions]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/discussions + +### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/{KJ|GEM_NAME})](https://tidelift.com/subscription/pkg/rubygems-{KJ|GEM_NAME}?utm_source=rubygems-{KJ|GEM_NAME}&utm_medium=referral&utm_campaign=readme) + +Available as part of the Tidelift Subscription. + +
+ Need enterprise-level guarantees? + +The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. + +[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift] + +- 💡Subscribe for support guarantees covering _all_ your FLOSS dependencies +- 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar] +- 💡Tidelift pays maintainers to maintain the software you depend on!
📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers + +Alternatively: + +- [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] +- [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] +- [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] + +
+ +## ✨ Installation + +Install the gem and add to the application's Gemfile by executing: + +```console +bundle add {KJ|GEM_NAME} +``` + +If bundler is not being used to manage dependencies, install the gem by executing: + +```console +gem install {KJ|GEM_NAME} +``` + +### 🔒 Secure Installation + +
+ For Medium or High Security Installations + +This gem is cryptographically signed and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by +[stone_checksums][💎stone_checksums]. Be sure the gem you install hasn’t been tampered with +by following the instructions below. + +Add my public key (if you haven’t already; key expires 2045-04-29) as a trusted certificate: + +```console +gem cert --add <(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem) +``` + +You only need to do that once. Then proceed to install with: + +```console +gem install {KJ|GEM_NAME} -P HighSecurity +``` + +The `HighSecurity` trust profile will verify signed gems, and not allow the installation of unsigned dependencies. + +If you want to up your security game full-time: + +```console +bundle config set --global trust-policy MediumSecurity +``` + +`MediumSecurity` instead of `HighSecurity` is necessary if not all the gems you use are signed. + +NOTE: Be prepared to track down certs for signed gems and add them the same way you added mine. + +
+ +## ⚙️ Configuration + + +## 🔧 Basic Usage + + +## 🦷 FLOSS Funding + +While {KJ|GH_ORG} tools are free software and will always be, the project would benefit immensely from some funding. +Raising a monthly budget of... "dollars" would make the project more sustainable. + +We welcome both individual and corporate sponsors! We also offer a +wide array of funding channels to account for your preferences. +Currently, [GitHub Sponsors][🖇sponsor], and [Liberapay][⛳liberapay] are our preferred funding platforms. + +**If you're working in a company that's making significant use of {KJ|GH_ORG} tools we'd +appreciate it if you suggest to your company to become a {KJ|GH_ORG} sponsor.** + +You can support the development of {KJ|GH_ORG} tools via +[GitHub Sponsors][🖇sponsor], +[Liberapay][⛳liberapay], +[PayPal][🖇paypal], +and [Tidelift][🏙️entsup-tidelift]. + +| 📍 NOTE | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| If doing a sponsorship in the form of donation is problematic for your company
from an accounting standpoint, we'd recommend the use of Tidelift,
where you can get a support-like subscription instead. | + +### Another way to support open-source + +I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats). + +If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`. + +I’m developing a new library, [floss_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look. + +**[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags** + +[![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon] + +## 🔐 Security + +See [SECURITY.md][🔐security]. + +## 🤝 Contributing + +If you need some ideas of where to help, you could work on adding more code coverage, +or if it is already 💯 (see [below](#code-coverage)) check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], +or use the gem and think about how it could be better. + +We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it. + +See [CONTRIBUTING.md][🤝contributing] for more detailed instructions. + +### 🚀 Release Instructions + +See [CONTRIBUTING.md][🤝contributing]. + +### Code Coverage + +[![Coverage Graph][🏀codecov-g]][🏀codecov] + +[![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] + +[![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] + +### 🪇 Code of Conduct + +Everyone interacting with this project's codebases, issue trackers, +chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct]. + +## 🌈 Contributors + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +Also see GitLab Contributors: [https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main][🚎contributors-gl] + +
+ ⭐️ Star History + + + + + + Star History Chart + + + +
+ +## 📌 Versioning + +This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver]. +Violations of this scheme should be reported as bugs. +Specifically, if a minor or patch version is released that breaks backward compatibility, +a new version should be immediately released that restores compatibility. +Breaking changes to the public API will only be introduced with new major versions. + +> dropping support for a platform is both obviously and objectively a breaking change
+>—Jordan Harband ([@ljharb](https://github.com/ljharb), maintainer of SemVer) [in SemVer issue 716][📌semver-breaking] + +I understand that policy doesn't work universally ("exceptions to every rule!"), +but it is the policy here. +As such, in many cases it is good to specify a dependency on this library using +the [Pessimistic Version Constraint][📌pvc] with two digits of precision. + +For example: + +```ruby +spec.add_dependency("{KJ|GEM_NAME}", "~> {KJ|GEM_MAJOR}.0") +``` + +
+📌 Is "Platform Support" part of the public API? More details inside. + +SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms +is a *breaking change* to an API, and for that reason the bike shedding is endless. + +To get a better understanding of how SemVer is intended to work over a project's lifetime, +read this article from the creator of SemVer: + +- ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred] + +
+ +See [CHANGELOG.md][📌changelog] for a list of releases. + +## 📄 License + +{KJ|README:LICENSE_INTRO} + +### © Copyright + +See [LICENSE.md][📄license] for the official copyright notice. + +## 🤑 A request for help + +Maintainers have teeth and need to pay their dentists. +After getting laid off in an RIF in March, and encountering difficulty finding a new one, +I began spending most of my time building open source tools. +I'm hoping to be able to pay for my kids' health insurance this month, +so if you value the work I am doing, I need your support. +Please consider sponsoring me or the project. + +To join the community or get help 👇️ Join the Discord. + +[![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] + +To say "thanks!" ☝️ Join the Discord or 👇️ send money. + +[![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal] + +### Please give the project a star ⭐ ♥. + +Thanks for RTFM. ☺️ + +[⛳liberapay-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?logo=liberapay&color=a51611&style=flat +[⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/{KJ|FUNDING:LIBERAPAY}.svg?style=for-the-badge&logo=liberapay&color=a51611 +[⛳liberapay]: https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate +[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}.svg?style=social&logo=github +[🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-{KJ|GH:USER}-blue?style=for-the-badge&logo=github +[🖇sponsor]: https://github.com/sponsors/{KJ|GH:USER} +[🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat +[🖇polar]: https://polar.sh/{KJ|FUNDING:POLAR} +[🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat +[🖇kofi]: https://ko-fi.com/{KJ|FUNDING:KOFI} +[🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat +[🖇patreon]: https://patreon.com/{KJ|FUNDING:PATREON} +[🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat +[🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug={KJ|FUNDING:BUYMEACOFFEE}&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff +[🖇buyme]: https://www.buymeacoffee.com/{KJ|FUNDING:BUYMEACOFFEE} +[🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal +[🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A +[🖇paypal]: https://www.paypal.com/paypalme/{KJ|FUNDING:PAYPAL} +[🖇floss-funding.dev]: https://floss-funding.dev +[🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding +[✉️discord-invite]: https://discord.gg/3qme4XHNKN +[✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord +[✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white +[✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends + +[✇bundle-group-pattern]: https://gist.github.com/pboling/4564780 +[⛳️gem-namespace]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[⛳️namespace-img]: https://img.shields.io/badge/namespace-{KJ|NAMESPACE}-3C2D2D.svg?style=square&logo=ruby&logoColor=white +[⛳️gem-name]: https://bestgems.org/gems/{KJ|GEM_NAME} +[⛳️name-img]: https://img.shields.io/badge/name-{KJ|GEM_SHIELD}-3C2D2D.svg?style=square&logo=rubygems&logoColor=red +[⛳️tag-img]: https://img.shields.io/github/tag/{KJ|GH_ORG}/{KJ|GEM_NAME}.svg +[⛳️tag]: http://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/releases +[🚂maint-blog]: http://www.railsbling.com/tags/{KJ|GEM_NAME} +[🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange +[🚂maint-contact]: http://www.railsbling.com/contact +[🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red +[💖🖇linkedin]: http://www.linkedin.com/in/peterboling +[💖🖇linkedin-img]: https://img.shields.io/badge/LinkedIn-Profile-0B66C2?style=flat&logo=newjapanprowrestling +[💖✌️wellfound]: https://wellfound.com/u/peter-boling +[💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound +[💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling +[💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase +[💖🐘ruby-mast]: https://ruby.social/@{KJ|SOCIAL:MASTODON} +[💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@{KJ|SOCIAL:MASTODON} +[💖🦋bluesky]: https://bsky.app/profile/{KJ|SOCIAL:BLUESKY} +[💖🦋bluesky-img]: https://img.shields.io/badge/@{KJ|SOCIAL:BLUESKY}-0285FF?style=flat&logo=bluesky&logoColor=white +[💖🌳linktree]: https://linktr.ee/{KJ|SOCIAL:LINKTREE} +[💖🌳linktree-img]: https://img.shields.io/badge/{KJ|SOCIAL:LINKTREE}-purple?style=flat&logo=linktree +[💖💁🏼‍♂️devto]: https://dev.to/{KJ|SOCIAL:DEVTO} +[💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white +[💖💁🏼‍♂️aboutme]: https://about.me/peter.boling +[💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white +[💖🧊berg]: https://codeberg.org/{KJ|CB:USER} +[💖🐙hub]: https://github.org/{KJ|GH:USER} +[💖🛖hut]: https://sr.ht/~{KJ|SH:USER}/ +[💖🧪lab]: https://gitlab.com/{KJ|GL:USER} +[👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share +[👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white +[👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github +[👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white +[🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-{KJ|GEM_NAME}?utm_source=rubygems-{KJ|GEM_NAME}&utm_medium=referral&utm_campaign=readme +[🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white +[🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar +[💁🏼‍♂️peterboling]: http://www.peterboling.com +[🚂railsbling]: http://www.railsbling.com +[📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange +[📜src-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/ +[📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue +[📜src-cb]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME} +[📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=Github&logoColor=green +[📜src-gh]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white +[📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white +[📜gl-wiki]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/wikis/home +[📜gh-wiki]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/wiki +[📜gl-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white +[📜gh-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=github&logoColor=white +[👽dl-rank]: https://bestgems.org/gems/{KJ|GEM_NAME} +[👽dl-ranki]: https://img.shields.io/gem/rd/{KJ|GEM_NAME}.svg +[👽oss-help]: https://www.codetriage.com/{KJ|GH_ORG}/{KJ|GEM_NAME} +[👽oss-helpi]: https://www.codetriage.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/badges/users.svg +[👽version]: https://bestgems.org/gems/{KJ|GEM_NAME} +[👽versioni]: https://img.shields.io/gem/v/{KJ|GEM_NAME}.svg +[🏀qlty-mnt]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME} +[🏀qlty-mnti]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/maintainability.svg +[🏀qlty-cov]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/metrics/code?sort=coverageRating +[🏀qlty-covi]: https://qlty.sh/gh/{KJ|GH_ORG}/projects/{KJ|GEM_NAME}/coverage.svg +[🏀codecov]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME} +[🏀codecovi]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME}/graph/badge.svg +[🏀coveralls]: https://coveralls.io/github/{KJ|GH_ORG}/{KJ|GEM_NAME}?branch=main +[🏀coveralls-img]: https://coveralls.io/repos/github/{KJ|GH_ORG}/{KJ|GEM_NAME}/badge.svg?branch=main +[🖐codeQL]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/security/code-scanning +[🖐codeQL-img]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/codeql-analysis.yml/badge.svg +[🚎ruby-2.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.3.yml +[🚎ruby-2.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.3.yml/badge.svg +[🚎ruby-2.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.4.yml +[🚎ruby-2.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.4.yml/badge.svg +[🚎ruby-2.5-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.5.yml +[🚎ruby-2.5-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.5.yml/badge.svg +[🚎ruby-2.6-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.6.yml +[🚎ruby-2.6-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.6.yml/badge.svg +[🚎ruby-2.7-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.7.yml +[🚎ruby-2.7-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-2.7.yml/badge.svg +[🚎ruby-3.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.0.yml +[🚎ruby-3.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.0.yml/badge.svg +[🚎ruby-3.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.1.yml +[🚎ruby-3.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.1.yml/badge.svg +[🚎ruby-3.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.2.yml +[🚎ruby-3.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.2.yml/badge.svg +[🚎ruby-3.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.3.yml +[🚎ruby-3.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.3.yml/badge.svg +[🚎ruby-3.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.4.yml +[🚎ruby-3.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/ruby-3.4.yml/badge.svg +[🚎jruby-9.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.1.yml +[🚎jruby-9.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.1.yml/badge.svg +[🚎jruby-9.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.2.yml +[🚎jruby-9.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.2.yml/badge.svg +[🚎jruby-9.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.3.yml +[🚎jruby-9.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.3.yml/badge.svg +[🚎jruby-9.4-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.4.yml +[🚎jruby-9.4-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby-9.4.yml/badge.svg +[🚎truby-22.3-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-22.3.yml +[🚎truby-22.3-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-22.3.yml/badge.svg +[🚎truby-23.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.0.yml +[🚎truby-23.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.0.yml/badge.svg +[🚎truby-23.1-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.1.yml +[🚎truby-23.1-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-23.1.yml/badge.svg +[🚎truby-24.2-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-24.2.yml +[🚎truby-24.2-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-24.2.yml/badge.svg +[🚎truby-25.0-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-25.0.yml +[🚎truby-25.0-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffleruby-25.0.yml/badge.svg +[🚎2-cov-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/coverage.yml +[🚎2-cov-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/coverage.yml/badge.svg +[🚎3-hd-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/heads.yml +[🚎3-hd-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/heads.yml/badge.svg +[🚎5-st-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/style.yml +[🚎5-st-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/style.yml/badge.svg +[🚎9-t-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffle.yml +[🚎9-t-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/truffle.yml/badge.svg +[🚎10-j-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby.yml +[🚎10-j-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/jruby.yml/badge.svg +[🚎11-c-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/current.yml +[🚎11-c-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/current.yml/badge.svg +[🚎12-crh-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/dep-heads.yml +[🚎12-crh-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/dep-heads.yml/badge.svg +[🚎13-🔒️-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/locked_deps.yml +[🚎13-🔒️-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/locked_deps.yml/badge.svg +[🚎14-🔓️-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/unlocked_deps.yml +[🚎14-🔓️-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/unlocked_deps.yml/badge.svg +[🚎15-🪪-wf]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/license-eye.yml +[🚎15-🪪-wfi]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/actions/workflows/license-eye.yml/badge.svg +[💎ruby-1.8i]: https://img.shields.io/badge/Ruby-1.8_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-1.9i]: https://img.shields.io/badge/Ruby-1.9_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.0i]: https://img.shields.io/badge/Ruby-2.0_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.1i]: https://img.shields.io/badge/Ruby-2.1_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.2i]: https://img.shields.io/badge/Ruby-2.2_(%F0%9F%9A%ABCI)-AABBCC?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.3i]: https://img.shields.io/badge/Ruby-2.3-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.4i]: https://img.shields.io/badge/Ruby-2.4-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.5i]: https://img.shields.io/badge/Ruby-2.5-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.6i]: https://img.shields.io/badge/Ruby-2.6-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-2.7i]: https://img.shields.io/badge/Ruby-2.7-DF00CA?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.0i]: https://img.shields.io/badge/Ruby-3.0-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.1i]: https://img.shields.io/badge/Ruby-3.1-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-3.4i]: https://img.shields.io/badge/Ruby-3.4-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-4.0i]: https://img.shields.io/badge/Ruby-4.0-CC342D?style=for-the-badge&logo=ruby&logoColor=white +[💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green +[💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue +[💎truby-22.3i]: https://img.shields.io/badge/Truffle_Ruby-22.3-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-23.0i]: https://img.shields.io/badge/Truffle_Ruby-23.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-24.2i]: https://img.shields.io/badge/Truffle_Ruby-24.2-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-25.0i]: https://img.shields.io/badge/Truffle_Ruby-25.0-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink +[💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green +[💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue +[💎jruby-9.1i]: https://img.shields.io/badge/JRuby-9.1-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.2i]: https://img.shields.io/badge/JRuby-9.2-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.3i]: https://img.shields.io/badge/JRuby-9.3-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-9.4i]: https://img.shields.io/badge/JRuby-9.4-FBE742?style=for-the-badge&logo=ruby&logoColor=red +[💎jruby-c-i]: https://img.shields.io/badge/JRuby-current-FBE742?style=for-the-badge&logo=ruby&logoColor=green +[💎jruby-headi]: https://img.shields.io/badge/JRuby-HEAD-FBE742?style=for-the-badge&logo=ruby&logoColor=blue +[🤝gh-issues]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/issues +[🤝gh-pulls]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/pulls +[🤝gl-issues]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/issues +[🤝gl-pulls]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/merge_requests +[🤝cb-issues]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME}/issues +[🤝cb-pulls]: https://codeberg.org/{KJ|GH_ORG}/{KJ|GEM_NAME}/pulls +[🤝cb-donate]: https://donate.codeberg.org/ +[🤝contributing]: CONTRIBUTING.md +[🏀codecov-g]: https://codecov.io/gh/{KJ|GH_ORG}/{KJ|GEM_NAME}/graphs/tree.svg +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/graphs/contributors +[🖐contributors-img]: https://contrib.rocks/image?repo={KJ|GH_ORG}/{KJ|GEM_NAME} +[🚎contributors-gl]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/graphs/main +[🪇conduct]: CODE_OF_CONDUCT.md +[🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg +[📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint +[📌semver]: https://semver.org/spec/v2.0.0.html +[📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[📌changelog]: CHANGELOG.md +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat +[📌gitmoji]: https://gitmoji.dev +[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square +[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ +[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.076-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue +[🔐security]: SECURITY.md +[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat +{KJ|README:LICENSE_REFS} +[📄license-compat]: https://dev.to/{KJ|SOCIAL:DEVTO}/how-to-check-license-compatibility-41h0 +[📄license-compat-img]: https://img.shields.io/badge/Apache_Compatible:_Category_A-%E2%9C%93-259D6C.svg?style=flat&logo=Apache +[📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm +[📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat +[🚎yard-current]: http://rubydoc.info/gems/{KJ|GEM_NAME} +[🚎yard-head]: https://{KJ|YARD_HOST} +[💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums +[💎SHA_checksums]: https://gitlab.com/{KJ|GH_ORG}/{KJ|GEM_NAME}/-/tree/main/checksums +[💎rlts]: https://github.com/rubocop-lts/rubocop-lts +[💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white +[💎appraisal2]: https://github.com/appraisal-rb/appraisal2 +[💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white +[💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/ diff --git a/gems/kettle-jem/lib/kettle/jem/templates/REEK b/gems/kettle-jem/lib/kettle/jem/templates/REEK new file mode 100644 index 0000000..e69de29 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/RUBOCOP.md.example b/gems/kettle-jem/lib/kettle/jem/templates/RUBOCOP.md.example new file mode 100644 index 0000000..f15b980 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/RUBOCOP.md.example @@ -0,0 +1,71 @@ +# RuboCop Usage Guide + +## Overview + +A tale of two RuboCop plugin gems. + +### RuboCop Gradual + +This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file. + +### RuboCop LTS + +This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2. +RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more. + +## Checking RuboCop Violations + +To check for RuboCop violations in this project, always use: + +```bash +bundle exec rake rubocop_gradual:check +``` + +**Do not use** the standard RuboCop commands like: +- `bundle exec rubocop` +- `rubocop` + +## Understanding the Lock File + +The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to: + +1. Prevent new violations while gradually fixing existing ones +2. Track progress on code style improvements +3. Ensure CI builds don't fail due to pre-existing violations + +## Common Commands + +- **Check violations** + - `bundle exec rake rubocop_gradual` + - `bundle exec rake rubocop_gradual:check` +- **(Safe) Autocorrect violations, and update lockfile if no new violations** + - `bundle exec rake rubocop_gradual:autocorrect` +- **Force update the lock file (w/o autocorrect) to match violations present in code** + - `bundle exec rake rubocop_gradual:force_update` + +## Workflow + +1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect` + a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task. +2. If there are new violations, either: + - Fix them in your code + - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately) +3. Commit the updated `.rubocop_gradual.lock` file along with your changes + +## Never add inline RuboCop disables + +Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways: + +- Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide. +- Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow: + - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced) + - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately) + +In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test. + +## Benefits of rubocop_gradual + +- Allows incremental adoption of code style rules +- Prevents CI failures due to pre-existing violations +- Provides a clear record of code style debt +- Enables focused efforts on improving code quality over time diff --git a/gems/kettle-jem/lib/kettle/jem/templates/Rakefile.example b/gems/kettle-jem/lib/kettle/jem/templates/Rakefile.example new file mode 100644 index 0000000..88900ae --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/Rakefile.example @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# {KJ|FREEZE_TOKEN}:freeze +# To retain chunks of comments & code during {KJ|GEM_NAME} templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# {KJ|GEM_NAME} will then preserve content between those markers across template runs. +# {KJ|FREEZE_TOKEN}:unfreeze + +# {KJ|GEM_NAME} Rakefile v{KJ|KETTLE_JEM_VERSION} - {KJ|TEMPLATE_RUN_DATE} +# Ruby 2.3 (Safe Navigation) or higher required +# +# See LICENSE.md for license information. +# +# Copyright (c) {KJ|TEMPLATE_RUN_YEAR} {KJ|AUTHOR:NAME} ({KJ|AUTHOR:DOMAIN}) +# +# Expected to work in any project that uses Bundler. +# +# Sets up tasks for appraisal2, floss_funding, kettle-jem, kettle-dev, rspec, minitest, rubocop_gradual, reek, yard, and stone_checksums. +# +# rake appraisal:install # Install Appraisal gemfiles (initial setup... +# rake appraisal:reset # Delete Appraisal lockfiles (gemfiles/*.gemfile.lock) +# rake appraisal:update # Update Appraisal gemfiles and run RuboCop... +# rake bench # Run all benchmarks (alias for bench:run) +# rake bench:list # List available benchmark scripts +# rake bench:run # Run all benchmark scripts (skips on CI) +# rake build:generate_checksums # Generate both SHA256 & SHA512 checksums i... +# rake bundle:audit:check # Checks the Gemfile.lock for insecure depe... +# rake bundle:audit:update # Updates the bundler-audit vulnerability d... +# rake ci:act[opt] # Run 'act' with a selected workflow +# rake coverage # Run specs w/ coverage and open results in... +# rake default # Default tasks aggregator +# rake install # Build and install {KJ|GEM_NAME}-1.0.0.gem in... +# rake install:local # Build and install {KJ|GEM_NAME}-1.0.0.gem in... +# rake kettle:jem:install # Install {KJ|GEM_NAME} GitHub automation and ... +# rake kettle:jem:selftest # Self-test: template {KJ|GEM_NAME} against itse... +# rake kettle:jem:template # Template {KJ|GEM_NAME} files into the curren... +# rake reek # Check for code smells +# rake reek:update # Run reek and store the output into the RE... +# rake release[remote] # Create tag v1.0.0 and build and push kett... +# rake rubocop_gradual # Run RuboCop Gradual +# rake rubocop_gradual:autocorrect # Run RuboCop Gradual with autocorrect (onl... +# rake rubocop_gradual:autocorrect_all # Run RuboCop Gradual with autocorrect (saf... +# rake rubocop_gradual:check # Run RuboCop Gradual to check the lock file +# rake rubocop_gradual:force_update # Run RuboCop Gradual to force update the l... +# rake rubocop_gradual_debug # Run RuboCop Gradual +# rake rubocop_gradual_debug:autocorrect # Run RuboCop Gradual with autocorrect (onl... +# rake rubocop_gradual_debug:autocorrect_all # Run RuboCop Gradual with autocorrect (saf... +# rake rubocop_gradual_debug:check # Run RuboCop Gradual to check the lock file +# rake rubocop_gradual_debug:force_update # Run RuboCop Gradual to force update the l... +# rake spec # Run RSpec code examples +# rake test # Run tests +# rake yard # Generate YARD Documentation +# + +# :nocov: +require "bundler/gem_tasks" if !Dir[File.join(__dir__, "*.gemspec")].empty? +# :nocov: + +# Define a base default task early so other files can enhance it. +desc "Default tasks aggregator" +task :default do + puts "Default task complete." +end + +# External gems that define tasks - add here! +require "kettle/dev" + +### TEMPLATING TASKS +begin + require "kettle/jem" +rescue LoadError + desc("(stub) kettle:jem:selftest is unavailable") + task("kettle:jem:selftest") do + warn("NOTE: kettle-jem isn't installed, or is disabled for #{RUBY_VERSION} in the current environment") + end +end + +### RELEASE TASKS +# Setup stone_checksums +begin + require "stone_checksums" +rescue LoadError + desc("(stub) build:generate_checksums is unavailable") + task("build:generate_checksums") do + warn("NOTE: stone_checksums isn't installed, or is disabled for #{RUBY_VERSION} in the current environment") + end +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/SECURITY.md.example b/gems/kettle-jem/lib/kettle/jem/templates/SECURITY.md.example new file mode 100755 index 0000000..ee1be7d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/SECURITY.md.example @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|----------|-----------| +| 1.latest | ✅ | + +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. + +## Additional Support + +If you are interested in support for versions older than the latest release, +please consider sponsoring the project / maintainer @ https://liberapay.com/{KJ|FUNDING:LIBERAPAY}/donate, +or find other sponsorship links in the [README]. + +[README]: README.md diff --git a/gems/kettle-jem/lib/kettle/jem/templates/bin/setup.example b/gems/kettle-jem/lib/kettle/jem/templates/bin/setup.example new file mode 100644 index 0000000..9b22169 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/bin/setup.example @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +quiet=false +bundle_args=(install) + +for arg in "$@"; do + case "$arg" in + --quiet) + quiet=true + bundle_args+=(--quiet) + ;; + -h|--help) + cat <<'USAGE' +Usage: bin/setup [--quiet] + +Options: + --quiet Pass --quiet to bundle install and suppress shell tracing. +USAGE + exit 0 + ;; + *) + printf 'bin/setup: unknown option: %s\n' "$arg" >&2 + exit 2 + ;; + esac +done + +if [ "$quiet" != "true" ]; then + set -vx +fi + +bundle "${bundle_args[@]}" + +# Do any other automated setup that you need to do here diff --git a/gems/kettle-jem/lib/kettle/jem/templates/certs/pboling.pem.example b/gems/kettle-jem/lib/kettle/jem/templates/certs/pboling.pem.example new file mode 100644 index 0000000..d5c7e8b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/certs/pboling.pem.example @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl +ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW +A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM +DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy +LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA +uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61 +LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5 +mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN +coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV +FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj +yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1 +to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD +qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj +fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ +HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG +A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD +ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9 +wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR +L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm +GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k +kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq +QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA +0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p +DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt +L9nRqA== +-----END CERTIFICATE----- diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gem.gemspec.example b/gems/kettle-jem/lib/kettle/jem/templates/gem.gemspec.example new file mode 100644 index 0000000..130ab29 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gem.gemspec.example @@ -0,0 +1,159 @@ +# coding: utf-8 +# frozen_string_literal: true + +# {KJ|FREEZE_TOKEN}:freeze +# To retain chunks of comments & code during {KJ|GEM_NAME} templating: +# Wrap custom sections with freeze markers (e.g., as above and below this comment chunk). +# {KJ|GEM_NAME} will then preserve content between those markers across template runs. +# {KJ|FREEZE_TOKEN}:unfreeze + +gem_version = + if RUBY_VERSION >= "3.1" # rubocop:disable Gemspec/RubyVersionGlobalsUsage + # Loading Version into an anonymous module allows version.rb to get code coverage from SimpleCov! + # See: https://github.com/simplecov-ruby/simplecov/issues/557#issuecomment-2630782358 + # See: https://github.com/panorama-ed/memo_wise/pull/397 + Module.new.tap { |mod| Kernel.load("#{__dir__}/lib/{KJ|GEM_NAME_PATH}/version.rb", mod) }::{KJ|NAMESPACE}::Version::VERSION + else + # NOTE: Use __FILE__ or __dir__ until removal of Ruby 1.x support + # __dir__ introduced in Ruby 1.9.1 + # lib = File.expand_path("../lib", __FILE__) + lib = File.expand_path("lib", __dir__) + $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + require "{KJ|GEM_NAME_PATH}/version" + {KJ|NAMESPACE}::Version::VERSION + end + +Gem::Specification.new do |spec| + spec.name = "{KJ|GEM_NAME}" + spec.version = gem_version + spec.authors = ["{KJ|AUTHOR:NAME}"] + spec.email = ["{KJ|AUTHOR:EMAIL}"] + + spec.summary = "{KJ|PROJECT_EMOJI} " + spec.description = "{KJ|PROJECT_EMOJI} " + spec.homepage = "https://github.com/{KJ|GH_ORG}/{KJ|GEM_NAME}" + spec.licenses = ["MIT"] + spec.required_ruby_version = ">= 2.3.0" + + # Linux distros often package gems and securely certify them independent + # of the official RubyGem certification process. Allowed via ENV["SKIP_GEM_SIGNING"] + # Ref: https://gitlab.com/ruby-oauth/version_gem/-/issues/3 + # Hence, only enable signing if `SKIP_GEM_SIGNING` is not set in ENV. + # See CONTRIBUTING.md + unless ENV.include?("SKIP_GEM_SIGNING") + user_cert = "certs/#{ENV.fetch("GEM_CERT_USER", ENV["USER"])}.pem" + cert_file_path = File.join(__dir__, user_cert) + cert_chain = cert_file_path.split(",") + cert_chain.select! { |fp| File.exist?(fp) } + if cert_file_path && cert_chain.any? + spec.cert_chain = cert_chain + if $PROGRAM_NAME.end_with?("gem") && ARGV[0] == "build" + spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem") + end + end + end + + spec.metadata["homepage_uri"] = "https://{KJ|YARD_HOST}/" + spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/v#{spec.version}" + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues" + spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + spec.metadata["funding_uri"] = "https://github.com/sponsors/{KJ|GH:USER}" + spec.metadata["wiki_uri"] = "#{spec.homepage}/wiki" + spec.metadata["news_uri"] = "https://www.railsbling.com/tags/#{spec.name}" + spec.metadata["discord_uri"] = "https://discord.gg/3qme4XHNKN" + spec.metadata["rubygems_mfa_required"] = "true" + + # Specify which files are part of the released package. + spec.files = Dir[ + # Code / tasks / data (NOTE: exe/ is specified via spec.bindir and spec.executables below) + "lib/**/*.rb", + "lib/**/*.rake", + # Signatures + "sig/**/*.rbs", + ] + + # Automatically included with gem package, no need to list again in files. + spec.extra_rdoc_files = Dir[ + # Files (alphabetical) + "CHANGELOG.md", + "CITATION.cff", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "FUNDING.md", + "LICENSE.txt", + "README.md", + "RUBOCOP.md", + "SECURITY.md", + ] + spec.rdoc_options += [ + "--title", + "#{spec.name} - #{spec.summary}", + "--main", + "README.md", + "--exclude", + "^sig/", + "--line-numbers", + "--inline-source", + "--quiet", + ] + spec.bindir = "exe" + # Listed files are the relative paths from bindir above. + spec.executables = [] + spec.require_paths = ["lib"] + + # Utilities + spec.add_dependency("version_gem", "~> 1.1", ">= 1.1.9") # ruby >= 2.2.0 + + # NOTE: It is preferable to list development dependencies in the gemspec due to increased + # visibility and discoverability. + # However, development dependencies in gemspec will install on + # all versions of Ruby that will run in CI. + # This gem, and its gemspec runtime dependencies, will install on Ruby down to {KJ|MIN_RUBY}. + # This gem, and its gemspec development dependencies, will install on Ruby down to {KJ|MIN_DEV_RUBY}. + # Thus, dev dependencies in gemspec must have + # + # required_ruby_version ">= {KJ|MIN_DEV_RUBY}" (or lower) + # + # Development dependencies that require strictly newer Ruby versions should be in a "gemfile", + # and preferably a modular one (see gemfiles/modular/*.gemfile). + + # Dev, Test, & Release Tasks + spec.add_development_dependency("{KJ|KETTLE_DEV_GEM}", "~> 2.0") # ruby >= 2.3.0 + + # Security + spec.add_development_dependency("bundler-audit", "~> 0.9.3") # ruby >= 2.0.0 + + # Tasks + spec.add_development_dependency("rake", "~> 13.0") # ruby >= 2.2.0 + + # Debugging + spec.add_development_dependency("require_bench", "~> 1.0", ">= 1.0.4") # ruby >= 2.2.0 + + # Testing + spec.add_development_dependency("appraisal2", "~> 3.0", ">= 3.0.6") # ruby >= 1.8.7, for testing against multiple versions of dependencies + spec.add_development_dependency("kettle-test", "~> 2.0", ">= 2.0.0") # ruby >= 2.3 + + # Releasing + spec.add_development_dependency("ruby-progressbar", "~> 1.13") # ruby >= 0 + spec.add_development_dependency("stone_checksums", "~> 1.0", ">= 1.0.3") # ruby >= 2.2.0 + + # Git integration (optional) + # The 'git' gem is optional; {KJ|GEM_NAME} falls back to shelling out to `git` if it is not present. + # The current release of the git gem depends on activesupport, which makes it too heavy to depend on directly + # spec.add_dependency("git", ">= 1.19.1") # ruby >= 2.3 + + # Development tasks + # The cake is a lie. erb v2.2, the oldest release, was never compatible with Ruby 2.3. + # This means we have no choice but to use the erb that shipped with Ruby 2.3 + # /opt/hostedtoolcache/Ruby/2.3.8/x64/lib/ruby/gems/2.3.0/gems/erb-2.2.2/lib/erb.rb:670:in `prepare_trim_mode': undefined method `match?' for "-":String (NoMethodError) + # spec.add_development_dependency("erb", ">= 2.2") # ruby >= 2.3.0, not SemVer, old rubies get dropped in a patch. + spec.add_development_dependency("gitmoji-regex", "~> 1.0", ">= 1.0.3") # ruby >= 2.3.0 + + # HTTP recording for deterministic specs + # In Ruby 3.5 (HEAD) the CGI library has been pared down, so we also need to depend on gem "cgi" for ruby@head + # This is done in the "head" appraisal. + # See: https://github.com/vcr/vcr/issues/1057 + # spec.add_development_dependency("vcr", ">= 4") # 6.0 claims to support ruby >= 2.3, but fails on ruby 2.4 + # spec.add_development_dependency("webmock", ">= 3") # Last version to support ruby >= 2.3 +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/r4/v0.5.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/r4/v0.5.gemfile.example new file mode 100644 index 0000000..dec6508 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/r4/v0.5.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.1 +gem "benchmark", "~> 0.5", ">= 0.5.0" # Removed from Std Lib in Ruby 4.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/vHEAD.gemfile.example new file mode 100644 index 0000000..e1eb20c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/benchmark/vHEAD.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.1 +gem "benchmark", github: "ruby/benchmark", branch: "master" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage.gemfile.example new file mode 100755 index 0000000..fb3dd08 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage.gemfile.example @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# We run code coverage on the latest version of Ruby only. + +# Set KETTLE_RB_DEV=true for local development with path-based dependencies. +# When false (default / CI), remote released gems are used. +if ENV.fetch("KETTLE_RB_DEV", "false").casecmp("false").zero? + # Coverage (remote/released) + platform :mri do + gem "kettle-soup-cover", "~> 1.0", ">= 1.0.10", require: false + end +else + eval_gemfile "coverage_local.gemfile" +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example new file mode 100644 index 0000000..89b92c5 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Local path overrides for development. +# Loaded by the associated non-local gemfile when KETTLE_RB_DEV != "false". + +require "nomono/bundler" unless defined?(Nomono) + +local_gems = %w[kettle-dev kettle-test kettle-soup-cover turbo_tests2] + +# export VENDORED_GEMS=kettle-dev,kettle-test,kettle-soup-cover,turbo_tests2 +platform :mri do + eval_nomono_gems( + gems: local_gems, + prefix: "KETTLE_RB", + path_env: "KETTLE_RB_DEV", + vendored_gems_env: "VENDORED_GEMS", + vendor_gem_dir_env: "VENDOR_GEM_DIR", + debug_env: "KETTLE_DEV_DEBUG", + ) +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/debug.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/debug.gemfile.example new file mode 100644 index 0000000..4e15b1a --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/debug.gemfile.example @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# To retain during kettle-jem templating: +# kettle-jem:freeze +# # ... your code +# kettle-jem:unfreeze +# + +# Ex-Standard Library gems +# irb is included in main Gemfile (and unlocked_deps Appraisal), so it can't be included here. +# gem "irb", "~> 1.15", ">= 1.15.2" # removed from stdlib in 3.5 + +platform :mri do + # Debugging - Ensure ENV["DEBUG"] == "true" to use debuggers within spec suite + # Use binding.break, binding.b, or debugger in code + gem "debug", ">= 1.1" # ruby >= 2.7 + + # Dev Console - Binding.pry - Irb replacement + # gem "pry", "~> 0.14" # ruby >= 2.0 +end + +gem "gem_bench", "~> 2.0", ">= 2.0.5" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation.gemfile.example new file mode 100755 index 0000000..d2f5eb8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation.gemfile.example @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# To retain during kettle-jem templating: +# kettle-jem:freeze +# # ... your code +# kettle-jem:unfreeze +# + +# Documentation +# Documentation +gem "kramdown", "~> 2.5", ">= 2.5.1", require: false # Ruby >= 2.5 +gem "kramdown-parser-gfm", "~> 1.1", require: false # Ruby >= 2.3 +gem "yaml-converter", "~> 0.1", require: false # Ruby >= 3.2 +gem "yard", "~> 0.9", ">= 0.9.37", require: false +gem "yard-junk", "~> 0.1", ">= 0.1.0", require: false # Ruby >= 3.1 +gem "yard-relative_markdown_links", "~> 0.6", require: false + +if ENV.fetch("KETTLE_RB_DEV", "false").casecmp("false").zero? + gem "yard-fence", "~> 0.8", ">= 0.8.2", require: false # Ruby >= 3.2 + gem "yard-timekeeper", "~> 0.1", require: false + gem "yard-yaml", "~> 0.1", require: false +else + eval_gemfile "documentation_local.gemfile" +end + +# Std Lib extractions +gem "rdoc", "~> 6.11", require: false diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation_local.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation_local.gemfile.example new file mode 100644 index 0000000..aaedfb0 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/documentation_local.gemfile.example @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Local path overrides for development. +# Loaded by the associated non-local gemfile when KETTLE_RB_DEV != "false". + +require "nomono/bundler" unless defined?(Nomono) + +local_gems = %w[yard-fence yard-timekeeper] + +# export VENDORED_GEMS=yard-fence,yard-timekeeper +eval_nomono_gems( + gems: local_gems, + prefix: "KETTLE_RB", + path_env: "KETTLE_RB_DEV", + vendored_gems_env: "VENDORED_GEMS", + vendor_gem_dir_env: "VENDOR_GEM_DIR", + debug_env: "KETTLE_DEV_DEBUG", +) diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.3/default.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.3/default.gemfile.example new file mode 100644 index 0000000..ca868e8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.3/default.gemfile.example @@ -0,0 +1,6 @@ +# The cake is a lie. +# erb v2.2, the oldest release, was never compatible with Ruby 2.3. +# In addition, erb does not follow SemVer, and old rubies get dropped in a patch. +# This means we have no choice but to use the erb that shipped with Ruby 2.3 +# /opt/hostedtoolcache/Ruby/2.3.8/x64/lib/ruby/gems/2.3.0/gems/erb-2.2.2/lib/erb.rb:670:in `prepare_trim_mode': undefined method `match?' for "-":String (NoMethodError) +# gem "erb", ">= 2.2" # ruby >= 2.3.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.6/v2.2.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.6/v2.2.gemfile.example new file mode 100644 index 0000000..7cd8574 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2.6/v2.2.gemfile.example @@ -0,0 +1,3 @@ +# Ruby >= 2.3.0 (claimed, but not true, minimum support is Ruby 2.4) +# Last version supporting Ruby <= 2.6 +gem "erb", "~> 2.2.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2/v3.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2/v3.0.gemfile.example new file mode 100644 index 0000000..c03bd8d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r2/v3.0.gemfile.example @@ -0,0 +1 @@ +gem "erb", "~> 3.0" # ruby >= 2.7.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3.1/v4.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3.1/v4.0.gemfile.example new file mode 100644 index 0000000..2e9046d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3.1/v4.0.gemfile.example @@ -0,0 +1,2 @@ +# last version compatible with Ruby 3.1 +gem "erb", "~> 4.0" # ruby >= 2.7.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3/v5.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3/v5.0.gemfile.example new file mode 100644 index 0000000..97033fa --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r3/v5.0.gemfile.example @@ -0,0 +1 @@ +gem "erb", "~> 5.0" # ruby >= 3.2.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r4/v5.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r4/v5.0.gemfile.example new file mode 100644 index 0000000..97033fa --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/r4/v5.0.gemfile.example @@ -0,0 +1 @@ +gem "erb", "~> 5.0" # ruby >= 3.2.0 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/vHEAD.gemfile.example new file mode 100644 index 0000000..48fee42 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/erb/vHEAD.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 3.2 +gem "erb", github: "ruby/erb", branch: "master" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile.example new file mode 100644 index 0000000..cabf980 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile.example @@ -0,0 +1,3 @@ +# Ruby >= 0 +# Last version supporting Ruby <= 2.4 +gem "mutex_m", "~> 0.1" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2/v0.3.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2/v0.3.gemfile.example new file mode 100644 index 0000000..42e9d9b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r2/v0.3.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.5 +gem "mutex_m", "~> 0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r3/v0.3.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r3/v0.3.gemfile.example new file mode 100644 index 0000000..42e9d9b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r3/v0.3.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.5 +gem "mutex_m", "~> 0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r4/v0.3.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r4/v0.3.gemfile.example new file mode 100644 index 0000000..42e9d9b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/r4/v0.3.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.5 +gem "mutex_m", "~> 0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/vHEAD.gemfile.example new file mode 100644 index 0000000..8af3b6f --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/mutex_m/vHEAD.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.5 (dependency of omniauth) +gem "mutex_m", github: "ruby/mutex_m", branch: "master" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/optional.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/optional.gemfile.example new file mode 100644 index 0000000..ec7063e --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/optional.gemfile.example @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# To retain during {KJ|GEM_NAME} templating: +# {KJ|FREEZE_TOKEN}:freeze +# # ... your code +# {KJ|FREEZE_TOKEN}:unfreeze +# + +# Optional dependencies are not depended on directly, but may be used if present. + +# Required for kettle-pre-release +# URL parsing with Unicode support (falls back to URI if not available) +gem "addressable", ">= 2.8", "< 3" # ruby >= 2.2 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.3/recording.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.3/recording.gemfile.example new file mode 100644 index 0000000..0b7901f --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.3/recording.gemfile.example @@ -0,0 +1,16 @@ +# HTTP recording for deterministic specs +# It seems that somehow just having a newer version of appraisal installed breaks +# Ruby 2.3 and 2.4 even if their bundle specifies an older version, +# and as a result it can only be a dependency in the appraisals. +# Putting it here in recording.gemfile will satisfy the rubies that can load latest vcr. +# | An error occurred while loading spec_helper. +# | Failure/Error: require "vcr" +# | +# | NoMethodError: +# | undefined method `delete_prefix' for "CONTENT_LENGTH":String +# | # ./spec/config/vcr.rb:3:in `require' +# | # ./spec/config/vcr.rb:3:in `' +# | # ./spec/spec_helper.rb:8:in `require_relative' +# | # ./spec/spec_helper.rb:8:in `' +gem "vcr", "< 6" # 6.0.0 appears to have been released accidentally without changing required_ruby_version in the gemspec. +gem "webmock", "~> 3.0" # Last version to support ruby >= 2.3 diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.4/recording.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.4/recording.gemfile.example new file mode 100644 index 0000000..53f8e3d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.4/recording.gemfile.example @@ -0,0 +1,18 @@ +# HTTP recording for deterministic specs +# It seems that somehow just having a newer version of appraisal installed breaks +# Ruby 2.3 and 2.4 even if their bundle specifies an older version, +# and as a result it can only be a dependency in the appraisals. +# Putting it here in recording.gemfile will satisfy the rubies that can load latest vcr. +# | An error occurred while loading spec_helper. +# | Failure/Error: require "vcr" +# | +# | NoMethodError: +# | undefined method `delete_prefix' for "CONTENT_LENGTH":String +# | # ./spec/config/vcr.rb:3:in `require' +# | # ./spec/config/vcr.rb:3:in `' +# | # ./spec/spec_helper.rb:8:in `require_relative' +# | # ./spec/spec_helper.rb:8:in `' +gem "vcr", "< 4" +gem "webmock", ">= 3" + +# For now turning off Ruby 2.4 testing. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.5/recording.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.5/recording.gemfile.example new file mode 100644 index 0000000..506a0c6 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r2.5/recording.gemfile.example @@ -0,0 +1,16 @@ +# HTTP recording for deterministic specs +# It seems that somehow just having a newer version of appraisal installed breaks +# Ruby 2.3 and 2.4 even if their bundle specifies an older version, +# and as a result it can only be a dependency in the appraisals. +# Putting it here in recording.gemfile will satisfy the rubies that can load latest vcr. +# | An error occurred while loading spec_helper. +# | Failure/Error: require "vcr" +# | +# | NoMethodError: +# | undefined method `delete_prefix' for "CONTENT_LENGTH":String +# | # ./spec/config/vcr.rb:3:in `require' +# | # ./spec/config/vcr.rb:3:in `' +# | # ./spec/spec_helper.rb:8:in `require_relative' +# | # ./spec/spec_helper.rb:8:in `' +gem "vcr", ">= 6" +gem "webmock", ">= 3" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r3/recording.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r3/recording.gemfile.example new file mode 100644 index 0000000..506a0c6 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r3/recording.gemfile.example @@ -0,0 +1,16 @@ +# HTTP recording for deterministic specs +# It seems that somehow just having a newer version of appraisal installed breaks +# Ruby 2.3 and 2.4 even if their bundle specifies an older version, +# and as a result it can only be a dependency in the appraisals. +# Putting it here in recording.gemfile will satisfy the rubies that can load latest vcr. +# | An error occurred while loading spec_helper. +# | Failure/Error: require "vcr" +# | +# | NoMethodError: +# | undefined method `delete_prefix' for "CONTENT_LENGTH":String +# | # ./spec/config/vcr.rb:3:in `require' +# | # ./spec/config/vcr.rb:3:in `' +# | # ./spec/spec_helper.rb:8:in `require_relative' +# | # ./spec/spec_helper.rb:8:in `' +gem "vcr", ">= 6" +gem "webmock", ">= 3" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r4/recording.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r4/recording.gemfile.example new file mode 100644 index 0000000..506a0c6 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/r4/recording.gemfile.example @@ -0,0 +1,16 @@ +# HTTP recording for deterministic specs +# It seems that somehow just having a newer version of appraisal installed breaks +# Ruby 2.3 and 2.4 even if their bundle specifies an older version, +# and as a result it can only be a dependency in the appraisals. +# Putting it here in recording.gemfile will satisfy the rubies that can load latest vcr. +# | An error occurred while loading spec_helper. +# | Failure/Error: require "vcr" +# | +# | NoMethodError: +# | undefined method `delete_prefix' for "CONTENT_LENGTH":String +# | # ./spec/config/vcr.rb:3:in `require' +# | # ./spec/config/vcr.rb:3:in `' +# | # ./spec/spec_helper.rb:8:in `require_relative' +# | # ./spec/spec_helper.rb:8:in `' +gem "vcr", ">= 6" +gem "webmock", ">= 3" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/vHEAD.gemfile.example new file mode 100644 index 0000000..9c91207 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/recording/vHEAD.gemfile.example @@ -0,0 +1,2 @@ +gem "vcr", github: "vcr/vcr", branch: "master" +gem "webmock", github: "bblimke/webmock", branch: "master" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/rspec.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/rspec.gemfile.example new file mode 100644 index 0000000..72fdf8b --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/rspec.gemfile.example @@ -0,0 +1,4 @@ +gem "rspec_junit_formatter", + github: "pboling/rspec_junit_formatter", + branch: "main", + ref: "9e6bcaaedd49477b62878ed568dc4ada5cc7cb5e" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/runtime_heads.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/runtime_heads.gemfile.example new file mode 100644 index 0000000..49dc94f --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/runtime_heads.gemfile.example @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# To retain during {KJ|GEM_NAME} templating: +# {KJ|FREEZE_TOKEN}:freeze +# # ... your code +# {KJ|FREEZE_TOKEN}:unfreeze +# + +# Test against HEAD of runtime dependencies so we can proactively file bugs + +# Ruby >= 2.2 +gem "version_gem", github: "ruby-oauth/version_gem", branch: "main" + +eval_gemfile("x_std_libs/vHEAD.gemfile") diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/shunted.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/shunted.gemfile.example new file mode 100644 index 0000000..77b8ad5 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/shunted.gemfile.example @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# To retain during kettle-jem templating: +# kettle-jem:freeze +# # ... your code +# kettle-jem:unfreeze +# + +# Shunted development dependencies. +# +# These gems have been moved here from the gemspec because they require a +# newer Ruby than the project's effective dev floor: +# +# effective_dev_floor = max(gemspec required_ruby_version, 2.3) +# +# They are loaded via the Appraisals "unlocked" group so that CI can still +# test them on the Rubies they support, while older-Ruby CI jobs simply +# skip this gemfile. +# +# This file is regenerated by `rake template` — add custom gems inside a +# kettle-jem:freeze / kettle-jem:unfreeze block to preserve them. diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile.example new file mode 100644 index 0000000..94021cf --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile.example @@ -0,0 +1,4 @@ +# !!WARNING!! +# NOT SEMVER +# Last version to support Ruby <= 2.5 +gem "stringio", ">= 0.0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2/v3.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2/v3.0.gemfile.example new file mode 100644 index 0000000..e85bb18 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r2/v3.0.gemfile.example @@ -0,0 +1,5 @@ +# !!WARNING!! +# NOT SEMVER +# Version 3.0.7 dropped support for Ruby <= 2.7 +# Version 3.0.0 dropped support for Ruby <= 2.4 +gem "stringio", ">= 3.0" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r3/v3.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r3/v3.0.gemfile.example new file mode 100644 index 0000000..e85bb18 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r3/v3.0.gemfile.example @@ -0,0 +1,5 @@ +# !!WARNING!! +# NOT SEMVER +# Version 3.0.7 dropped support for Ruby <= 2.7 +# Version 3.0.0 dropped support for Ruby <= 2.4 +gem "stringio", ">= 3.0" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r4/v3.0.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r4/v3.0.gemfile.example new file mode 100644 index 0000000..e85bb18 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/r4/v3.0.gemfile.example @@ -0,0 +1,5 @@ +# !!WARNING!! +# NOT SEMVER +# Version 3.0.7 dropped support for Ruby <= 2.7 +# Version 3.0.0 dropped support for Ruby <= 2.4 +gem "stringio", ">= 3.0" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/vHEAD.gemfile.example new file mode 100644 index 0000000..5f2a741 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/stringio/vHEAD.gemfile.example @@ -0,0 +1,2 @@ +# Ruby >= 2.5 (dependency of omniauth) +gem "stringio", github: "ruby/stringio", branch: "master" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style.gemfile.example new file mode 100644 index 0000000..89a1272 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style.gemfile.example @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# To retain during {KJ|GEM_NAME} templating: +# {KJ|FREEZE_TOKEN}:freeze +# # ... your code +# {KJ|FREEZE_TOKEN}:unfreeze +# + +# We run rubocop on the latest version of Ruby, +# but in support of the oldest supported version of Ruby + +gem "reek", "~> 6.5" + +platform :mri do + # gem "rubocop", "~> 1.73", ">= 1.73.2" # constrained by standard + gem "rubocop-packaging", "~> 0.6", ">= 0.6.0" + gem "standard", ">= 1.50" + gem "rubocop-on-rbs", "~> 1.8" # ruby >= 3.1.0 + + if ENV.fetch("RUBOCOP_LTS_LOCAL", "false").casecmp("false").zero? + gem "rubocop-lts", "{KJ|RUBOCOP_LTS_CONSTRAINT}" + # "rubocop-lts-rspec" can't add here until other gems are updated. + gem "rubocop-rspec", "~> 3.6" + gem "{KJ|RUBOCOP_RUBY_GEM}" + else + eval_gemfile("style_local.gemfile") + end +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style_local.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style_local.gemfile.example new file mode 100644 index 0000000..bee7451 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/style_local.gemfile.example @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# We run rubocop on the latest version of Ruby, +# but in support of the oldest supported version of Ruby + +# Local path overrides for development. +# Loaded by the associated non-local gemfile when RUBOCOP_LTS_LOCAL != "false". + +require "nomono/bundler" unless defined?(Nomono) + +local_gems = %w[rubocop-lts rubocop-lts-rspec {KJ|RUBOCOP_RUBY_GEM} standard-rubocop-lts] + +# export VENDORED_GEMS=rubocop-lts,rubocop-lts-rspec,{KJ|RUBOCOP_RUBY_GEM},standard-rubocop-lts +platform :mri do + eval_nomono_gems( + gems: local_gems, + prefix: "RUBOCOP_LTS", + path_env: "RUBOCOP_LTS_LOCAL", + vendored_gems_env: "VENDORED_GEMS", + vendor_gem_dir_env: "VENDOR_GEM_DIR", + debug_env: "RUBOCOP_LTS_DEBUG", + root: ["src", "rubocop-lts"], + ) +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating.gemfile.example new file mode 100644 index 0000000..788fa28 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating.gemfile.example @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +remote = ENV.fetch("KETTLE_RB_DEV", "false").casecmp("false").zero? + +# Cross-platform parsers +gem "parslet" +gem "prism", "~> 1.6" +gem "toml" +gem "unparser", "~> 0.8", ">= 0.8.1" + +platform :mri do + # MRI only Parsers: + # kettle-jem currently uses markly instead of commonmarker, but these can easily be swapped. + # gem "commonmarker" + gem "markly", "~> 0.15", ">= 0.15.3" + gem "rbs", ">= 3.10" # ruby >= 3.1.0 + + # MRI Backend + gem "ruby_tree_sitter", + "~> 2.1", + require: false # DO NOT LOAD, because conflicts with FFI + + if remote + # AST parsing for advanced templating. + # Kettle-rb's Merge Recipes (released) + gem "kettle-jem", "~> 1.0" + end +end + +# Set KETTLE_RB_DEV=true (or path) for local development with path-based dependencies. +# When false (default / CI), released gems are used and explicit parser pins are needed. +# Local path overrides — merge gems bring their own parser dependencies +eval_gemfile "templating_local.gemfile" unless remote diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example new file mode 100644 index 0000000..a123162 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Local path overrides for development. +# Loaded by the associated non-local gemfile when KETTLE_RB_DEV != "false". + +require "nomono/bundler" unless defined?(Nomono) + +local_gems = %w[ + tree_haver + ast-merge + bash-merge + kettle-drift + dotenv-merge + json-merge + markdown-merge + prism-merge + psych-merge + rbs-merge + toml-merge + markly-merge + kettle-jem +] + +# export VENDORED_GEMS=bash-merge,dotenv-merge,json-merge,jsonc-merge,kettle-drift,markdown-merge,prism-merge,psych-merge,rbs-merge,toml-merge,markly-merge,kettle-jem +platform :mri do + eval_nomono_gems( + gems: local_gems, + prefix: "KETTLE_RB", + path_env: "KETTLE_RB_DEV", + vendored_gems_env: "VENDORED_GEMS", + vendor_gem_dir_env: "VENDOR_GEM_DIR", + debug_env: "KETTLE_DEV_DEBUG", + ) +end diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs.gemfile.example new file mode 100644 index 0000000..6928708 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs.gemfile.example @@ -0,0 +1,2 @@ +### Std Lib Extracted Gems +eval_gemfile "x_std_libs/r4/libs.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.3/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.3/libs.gemfile.example new file mode 100644 index 0000000..2fee8b6 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.3/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r2.3/default.gemfile" +eval_gemfile "../../mutex_m/r2.4/v0.1.gemfile" +eval_gemfile "../../stringio/r2.4/v0.0.2.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.4/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.4/libs.gemfile.example new file mode 100644 index 0000000..c1bcbd8 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.4/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r2.6/v2.2.gemfile" +eval_gemfile "../../mutex_m/r2.4/v0.1.gemfile" +eval_gemfile "../../stringio/r2.4/v0.0.2.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.6/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.6/libs.gemfile.example new file mode 100644 index 0000000..beac38c --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2.6/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r2.6/v2.2.gemfile" +eval_gemfile "../../mutex_m/r2/v0.3.gemfile" +eval_gemfile "../../stringio/r2/v3.0.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2/libs.gemfile.example new file mode 100644 index 0000000..441c4f0 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r2/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r2/v3.0.gemfile" +eval_gemfile "../../mutex_m/r2/v0.3.gemfile" +eval_gemfile "../../stringio/r2/v3.0.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3.1/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3.1/libs.gemfile.example new file mode 100644 index 0000000..bdab5bd --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3.1/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r3.1/v4.0.gemfile" +eval_gemfile "../../mutex_m/r3/v0.3.gemfile" +eval_gemfile "../../stringio/r3/v3.0.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3/libs.gemfile.example new file mode 100644 index 0000000..c293a3d --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r3/libs.gemfile.example @@ -0,0 +1,3 @@ +eval_gemfile "../../erb/r3/v5.0.gemfile" +eval_gemfile "../../mutex_m/r3/v0.3.gemfile" +eval_gemfile "../../stringio/r3/v3.0.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r4/libs.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r4/libs.gemfile.example new file mode 100644 index 0000000..21f7e99 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/r4/libs.gemfile.example @@ -0,0 +1,4 @@ +eval_gemfile "../../erb/r4/v5.0.gemfile" +eval_gemfile "../../mutex_m/r4/v0.3.gemfile" +eval_gemfile "../../stringio/r4/v3.0.gemfile" +eval_gemfile "../../benchmark/r4/v0.5.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/vHEAD.gemfile.example b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/vHEAD.gemfile.example new file mode 100644 index 0000000..b38c6fa --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/gemfiles/modular/x_std_libs/vHEAD.gemfile.example @@ -0,0 +1,4 @@ +eval_gemfile "../erb/vHEAD.gemfile" +eval_gemfile "../mutex_m/vHEAD.gemfile" +eval_gemfile "../stringio/vHEAD.gemfile" +eval_gemfile "../benchmark/vHEAD.gemfile" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.example b/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.example new file mode 100644 index 0000000..87a7c98 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.example @@ -0,0 +1,27 @@ +# Shared development environment for this gem. +# Local overrides belong in .env.local (loaded via dotenvy through mise). + +[env] +DEBUG = "false" +FLOSS_CFG_FUND_DEBUG = "false" +FLOSS_CFG_FUND_LOGFILE = "tmp/log/debug.log" +FUNDING_ORG = "{KJ|OPENCOLLECTIVE_ORG}" +KETTLE_DEV_DEBUG = "false" +KETTLE_TEST_SILENT = "true" +K_SOUP_COV_COMMAND_NAME = "Test Coverage" +K_SOUP_COV_DO = "true" +K_SOUP_COV_FORMATTERS = "html,xml,rcov,lcov,json,tty" +K_SOUP_COV_MIN_BRANCH = "76" +K_SOUP_COV_MIN_HARD = "true" +K_SOUP_COV_MIN_LINE = "92" +K_SOUP_COV_MULTI_FORMATTERS = "true" +K_SOUP_COV_OPEN_BIN = "" +MAX_ROWS = "1" +OPENCOLLECTIVE_HANDLE = "{KJ|OPENCOLLECTIVE_ORG}" +RUBOCOP_LTS_LOCAL = "false" +_.file = { path = ".env.local", redact = true } +_.path = ["exe", "bin"] +_.source = ".config/mise/env.sh" + +[tools] +ruby = "4.0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.no-osc.example b/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.no-osc.example new file mode 100644 index 0000000..5368c60 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/mise.toml.no-osc.example @@ -0,0 +1,27 @@ +# Shared development environment for this gem. +# Local overrides belong in .env.local (loaded via dotenvy through mise). + +[env] +DEBUG = "false" +FLOSS_CFG_FUND_DEBUG = "false" +FLOSS_CFG_FUND_LOGFILE = "tmp/log/debug.log" +FUNDING_ORG = "false" +KETTLE_DEV_DEBUG = "false" +KETTLE_TEST_SILENT = "true" +K_SOUP_COV_COMMAND_NAME = "Test Coverage" +K_SOUP_COV_DO = "true" +K_SOUP_COV_FORMATTERS = "html,xml,rcov,lcov,json,tty" +K_SOUP_COV_MIN_BRANCH = "76" +K_SOUP_COV_MIN_HARD = "true" +K_SOUP_COV_MIN_LINE = "92" +K_SOUP_COV_MULTI_FORMATTERS = "true" +K_SOUP_COV_OPEN_BIN = "" +MAX_ROWS = "1" +OPENCOLLECTIVE_HANDLE = "false" +RUBOCOP_LTS_LOCAL = "false" +_.file = { path = ".env.local", redact = true } +_.path = ["exe", "bin"] +_.source = ".config/mise/env.sh" + +[tools] +ruby = "4.0.2" diff --git a/gems/kettle-jem/lib/kettle/jem/templates/parsers.toml.example b/gems/kettle-jem/lib/kettle/jem/templates/parsers.toml.example new file mode 100644 index 0000000..9aefac4 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/templates/parsers.toml.example @@ -0,0 +1,19 @@ +# tsdl configuration — tree-sitter grammar versions +# https://github.com/stackmystack/tsdl +# +# Run: tsdl build --out-dir /usr/local/lib +# Or let .devcontainer/scripts/setup-tree-sitter.sh handle it. + +out-dir = "/usr/local/lib" + +[parsers] +json = "v0.24.8" +bash = "v0.25.1" + +[parsers.toml] +ref = "v0.7.0" +from = "https://github.com/tree-sitter-grammars/tree-sitter-toml" + +[parsers.rbs] +ref = "v0.2.2" +from = "https://github.com/joker1007/tree-sitter-rbs" diff --git a/gems/kettle-jem/lib/kettle/jem/version.rb b/gems/kettle-jem/lib/kettle/jem/version.rb new file mode 100644 index 0000000..99c0655 --- /dev/null +++ b/gems/kettle-jem/lib/kettle/jem/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Kettle + module Jem + VERSION = "0.1.0" + end +end diff --git a/gems/kettle-jem/spec/fixtures/thin_slice.json b/gems/kettle-jem/spec/fixtures/thin_slice.json new file mode 100644 index 0000000..20de3c6 --- /dev/null +++ b/gems/kettle-jem/spec/fixtures/thin_slice.json @@ -0,0 +1,98 @@ +{ + "case_id": "kettle-jem-vnext-readme-changelog-managed-block", + "ecosystem": "rubygems", + "inputs": { + "files": { + "example.gemspec": "# frozen_string_literal: true\n\nGem::Specification.new do |spec|\n spec.name = \"example\"\n spec.summary = \"Example gem\"\n spec.description = \"Example gem for kettle-jem\"\n spec.homepage = \"https://example.test\"\n spec.metadata[\"source_code_uri\"] = \"https://github.com/example/example\"\n spec.metadata[\"funding_uri\"] = \"https://github.com/sponsors/example\"\n spec.licenses = [\"MIT\"]\n spec.required_ruby_version = \">= 3.2\"\nend\n", + ".kettle-jem.yml": "workflows:\n preset: framework\n framework_matrix:\n dimension: rails\n versions:\n - \"7.0\"\n - \"7.1\"\n gemfile_pattern: rails_{version}\n", + ".github/FUNDING.yml": "github: [example]\nopen_collective: example\ncustom:\n - https://example.test/fund\n", + ".github/workflows/ancient.yml": "name: Ancient CI\n", + ".github/workflows/custom-ci.yml": "name: Custom CI\nenv:\n K_SOUP_COV_DO: true\non:\n push:\n branches: [main]\njobs:\n test:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n ruby: [\"3.2\", \"3.3\"]\n steps:\n - uses: actions/checkout@v3\n - uses: ruby/setup-ruby@v1\n with:\n ruby-version: \"${{ matrix.ruby }}\"\n", + "README.md": "# Old Name\n\nExisting intro.\n", + "CHANGELOG.md": "# Changelog\n\n## [0.1.0] - 2026-01-01\n\n- Initial release.\n", + "gemfiles/modular/shunted.gemfile": "# frozen_string_literal: true\n\n# local additions stay above\n", + "Rakefile": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:spec)\n\ntask default: :spec\n\n# Keep custom task.\ntask :custom do\n puts \"custom\"\nend\n" + } + }, + "expected": { + "facts": { + "package": { + "ecosystem": "rubygems", + "name": "example", + "slug": "example", + "description": "Example gem for kettle-jem", + "homepage_url": "https://example.test", + "source_url": "https://github.com/example/example", + "license_expression": "MIT" + }, + "rubygems": { + "gemspec_path": "example.gemspec", + "namespace": "Example", + "min_ruby": ">= 3.2" + }, + "funding": { + "urls": [ + "https://example.test/fund", + "https://github.com/sponsors/example", + "https://opencollective.com/example", + "https://tidelift.com/funding/github/rubygems/example" + ] + }, + "ci": { + "provider": "github_actions", + "default_branch": "main", + "ruby_versions": [ + "3.2", + "3.3", + "3.4" + ], + "obsolete_workflows": [ + ".github/workflows/ancient.yml" + ], + "custom_workflows": [ + ".github/workflows/custom-ci.yml" + ], + "framework_matrix": { + "dimension": "rails", + "versions": [ + "7.0", + "7.1" + ], + "gemfile_pattern": "rails_{version}", + "include": [ + { + "framework_version": "7.0", + "gemfile": "gemfiles/rails_7_0" + }, + { + "framework_version": "7.1", + "gemfile": "gemfiles/rails_7_1" + } + ] + } + } + }, + "changed_files": [ + ".github/FUNDING.yml", + ".github/workflows/ancient.yml", + ".github/workflows/ci.yml", + ".github/workflows/custom-ci.yml", + ".github/workflows/framework-ci.yml", + "CHANGELOG.md", + "README.md", + "Rakefile", + "gemfiles/modular/shunted.gemfile" + ], + "files": { + ".github/FUNDING.yml": "github:\n- example\nopen_collective: example\ncustom:\n- https://example.test/fund\ntidelift: rubygems/example\n", + ".github/workflows/ancient.yml": null, + ".github/workflows/framework-ci.yml": "name: Rails CI\n\npermissions:\n contents: read\n\non:\n push:\n branches:\n - \"main\"\n - \"*-stable\"\n tags:\n - \"!*\" # Do not execute on tags\n pull_request:\n branches:\n - \"*\"\n workflow_dispatch:\n\nconcurrency:\n group: \"${{ github.workflow }}-${{ github.ref }}\"\n cancel-in-progress: true\n\njobs:\n test:\n if: \"!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')\"\n name: Specs ${{ matrix.ruby }}@${{ matrix.framework_version }}\n runs-on: ubuntu-latest\n continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}\n env:\n BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}\n strategy:\n fail-fast: false\n matrix:\n ruby:\n - \"3.2\"\n - \"3.3\"\n - \"3.4\"\n rubygems:\n - default\n bundler:\n - default\n include:\n - framework_version: \"7.0\"\n gemfile: \"gemfiles/rails_7_0\"\n - framework_version: \"7.1\"\n gemfile: \"gemfiles/rails_7_1\"\n\n steps:\n - name: Checkout\n uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n - name: Setup Ruby & RubyGems\n uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0\n with:\n ruby-version: \"${{ matrix.ruby }}\"\n rubygems: \"${{ matrix.rubygems }}\"\n bundler: \"${{ matrix.bundler }}\"\n bundler-cache: true\n\n - name: Tests for ${{ matrix.ruby }}@${{ matrix.framework_version }}\n run: bundle exec rake test\n", + ".github/workflows/custom-ci.yml": "name: Custom CI\nenv:\n K_SOUP_COV_DO: true\n\npermissions:\n contents: read\n\non:\n push:\n branches: [main]\n\nconcurrency:\n group: \"${{ github.workflow }}-${{ github.ref }}\"\n cancel-in-progress: true\n\njobs:\n test:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n ruby: [\"3.2\", \"3.3\"]\n steps:\n - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n - uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0\n with:\n ruby-version: \"${{ matrix.ruby }}\"\n - name: Upload coverage to Coveralls\n if: ${{ !env.ACT }}\n uses: coverallsapp/github-action@0a51d2e0b5417d06e4ecceb534aec87defc53926 # main\n with:\n github-token: ${{ secrets.GITHUB_TOKEN }}\n continue-on-error: ${{ matrix.experimental != 'false' }}\n\n - name: Upload coverage to QLTY\n if: ${{ !env.ACT }}\n uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0\n with:\n token: ${{secrets.QLTY_COVERAGE_TOKEN}}\n files: coverage/.resultset.json\n continue-on-error: ${{ matrix.experimental != 'false' }}\n\n - name: Upload coverage to CodeCov\n if: ${{ !env.ACT }}\n uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0\n with:\n use_oidc: true\n fail_ci_if_error: false\n files: coverage/lcov.info,coverage/coverage.xml\n verbose: true\n\n - name: Code Coverage Summary Report\n if: ${{ !env.ACT && github.event_name == 'pull_request' }}\n uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0\n with:\n filename: ./coverage/coverage.xml\n badge: true\n fail_below_min: true\n format: markdown\n hide_branch_rate: false\n hide_complexity: true\n indicators: true\n output: both\n thresholds: '100 100'\n continue-on-error: ${{ matrix.experimental != 'false' }}\n\n - name: Add Coverage PR Comment\n uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4\n if: ${{ !env.ACT && github.event_name == 'pull_request' }}\n with:\n recreate: true\n path: code-coverage-results.md\n continue-on-error: ${{ matrix.experimental != 'false' }}\n", + ".github/workflows/ci.yml": "name: CI\n\npermissions:\n contents: read\n\non:\n push:\n branches:\n - \"main\"\n - \"*-stable\"\n tags:\n - \"!*\" # Do not execute on tags\n pull_request:\n branches:\n - \"*\"\n workflow_dispatch:\n\nconcurrency:\n group: \"${{ github.workflow }}-${{ github.ref }}\"\n cancel-in-progress: true\n\njobs:\n test:\n if: \"!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')\"\n name: Specs ${{ matrix.ruby }}\n runs-on: ubuntu-latest\n continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}\n strategy:\n fail-fast: false\n matrix:\n ruby:\n - \"3.2\"\n - \"3.3\"\n - \"3.4\"\n rubygems:\n - default\n bundler:\n - default\n\n steps:\n - name: Checkout example\n uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n - name: Setup Ruby & RubyGems\n uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1.300.0\n with:\n ruby-version: \"${{ matrix.ruby }}\"\n rubygems: \"${{ matrix.rubygems }}\"\n bundler: \"${{ matrix.bundler }}\"\n bundler-cache: true\n\n - name: Tests\n run: bundle exec rake\n", + "README.md": "# Old Name\n\nExisting intro.\n\n\n| Field | Value |\n|---|---|\n| Package | example |\n| Description | Example gem for kettle-jem |\n| Homepage | https://example.test |\n| Source | https://github.com/example/example |\n| License | MIT |\n| Funding | https://example.test/fund, https://github.com/sponsors/example, https://opencollective.com/example, https://tidelift.com/funding/github/rubygems/example |\n\n", + "CHANGELOG.md": "# Changelog\n\n## [Unreleased]\n\n### Added\n\n### Changed\n\n### Fixed\n\n## [0.1.0] - 2026-01-01\n\n- Initial release.\n", + "Rakefile": "# frozen_string_literal: true\n\n# Keep custom task.\ntask :custom do\n puts \"custom\"\nend\n", + "gemfiles/modular/shunted.gemfile": "# frozen_string_literal: true\n\n# local additions stay above\n# <> do not edit below this line\n# package: example\n# generated by kettle-jem vNext\n# <>\n" + } + } +} diff --git a/gems/kettle-jem/spec/spec_helper.rb b/gems/kettle-jem/spec/spec_helper.rb new file mode 100644 index 0000000..83ba1ce --- /dev/null +++ b/gems/kettle-jem/spec/spec_helper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "json" +require "fileutils" +require "pathname" +require "tmpdir" +require "kettle-jem" + +RSpec.configure do |config| + config.disable_monkey_patching! + config.expect_with(:rspec) do |expectations| + expectations.syntax = :expect + end +end diff --git a/gems/kettle-jem/spec/system/bundle_gem_scaffold_spec.rb b/gems/kettle-jem/spec/system/bundle_gem_scaffold_spec.rb new file mode 100644 index 0000000..34ebea3 --- /dev/null +++ b/gems/kettle-jem/spec/system/bundle_gem_scaffold_spec.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" +require "open3" + +RSpec.describe "bundle gem scaffold + kettle-jem", :system do + let(:sandbox_root) { File.expand_path("../../../tmp/sandbox", __dir__) } + let(:gem_root) { File.join(sandbox_root, "dummy-gem") } + let(:env) do + { + "FUNDING_ORG" => "acme", + "KJ_MIN_DIVERGENCE_THRESHOLD" => "5", + } + end + let(:expected_hidden_directories) do + %w[ + .config + .config/mise + .devcontainer + .devcontainer/apt-install + .devcontainer/scripts + .git-hooks + .github + .github/workflows + .qlty + ] + end + let(:expected_hidden_files) do + %w[ + .config/mise/env.sh + .devcontainer/apt-install/devcontainer-feature.json + .devcontainer/apt-install/install.sh + .devcontainer/devcontainer.json + .devcontainer/scripts/setup-tree-sitter.sh + .git-hooks/commit-msg + .git-hooks/footer-template.erb.txt + .git-hooks/prepare-commit-msg + .github/.codecov.yml + .github/copilot_instructions.md + .github/dependabot.yml + .github/workflows/templating.yml + .qlty/qlty.toml + ] + end + + before do + FileUtils.rm_rf(gem_root) + FileUtils.mkdir_p(sandbox_root) + scaffold_bundle_gem! + normalize_scaffold_gemspec! + end + + after do + FileUtils.rm_rf(gem_root) + end + + def scaffold_bundle_gem! + stdout, stderr, status = Open3.capture3( + "bundle", + "gem", + "dummy-gem", + "--no-git", + "--no-ci", + "--no-mit", + "--no-coc", + "--no-ext", + "--test=rspec", + "--no-changelog", + "--no-linter", + "--no-github-username", + chdir: sandbox_root + ) + expect(status.success?).to be(true), "bundle gem failed\nstdout=#{stdout}\nstderr=#{stderr}" + end + + def normalize_scaffold_gemspec! + path = File.join(gem_root, "dummy-gem.gemspec") + content = File.read(path) + content = content.sub('spec.authors = ["TODO: Write your name"]', 'spec.authors = ["Test User"]') + content = content.sub('spec.email = ["TODO: Write your email address"]', 'spec.email = ["test@example.com"]') + content = content.sub( + 'spec.summary = "TODO: Write a short summary, because RubyGems requires one."', + 'spec.summary = "Dummy gem"' + ) + content = content.sub( + 'spec.description = "TODO: Write a longer description or delete this line."', + 'spec.description = "Dummy gem for kettle-jem system testing."' + ) + content = content.sub( + 'spec.homepage = "TODO: Put your gem\'s website or public repo URL here."', + 'spec.homepage = "https://github.com/acme/dummy-gem"' + ) + content = content.sub( + 'spec.metadata["source_code_uri"] = "TODO: Put your gem\'s public repo URL here."', + 'spec.metadata["source_code_uri"] = "https://github.com/acme/dummy-gem"' + ) + File.write(path, content) + end + + def enable_packaged_templates! + path = File.join(gem_root, ".kettle-jem.yml") + content = File.read(path) + content = content.sub('project_emoji: ""', 'project_emoji: "💎"') + content += <<~YAML + + templates: + root: packaged + apply: true + YAML + File.write(path, content) + end + + def seed_destination_readme! + File.write(File.join(gem_root, "README.md"), <<~MARKDOWN) + # 1️⃣ Dummy::Gem + + ## Synopsis + + Destination synopsis from the scaffolded project. + + ## Usage + + Destination usage from the scaffolded project. + + ## Note: Local + + Destination note from the scaffolded project. + + ## Installation + + Old scaffold installation notes. + MARKDOWN + end + + def seed_destination_dependabot! + FileUtils.mkdir_p(File.join(gem_root, ".github")) + File.write(File.join(gem_root, ".github/dependabot.yml"), <<~YAML) + updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + YAML + end + + it "bootstraps config and applies selected packaged templates to a fresh scaffold" do + bootstrap = Kettle::Jem.apply_project(gem_root, env: env) + bootstrap_report = bootstrap.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "kettle_config_bootstrap" + end + expect(bootstrap_report.fetch(:changed)).to be(true) + expect(File.read(File.join(gem_root, ".kettle-jem.yml"))).to include("# kettle-jem configuration file") + expect(File.read(File.join(gem_root, ".kettle-jem.yml"))).to include("min_divergence_threshold: 5") + expect(bootstrap.fetch(:changed_files)).to include( + ".github/FUNDING.yml", + ".github/workflows/ci.yml", + ".kettle-jem.yml", + "Rakefile" + ) + + enable_packaged_templates! + seed_destination_readme! + seed_destination_dependabot! + + apply = Kettle::Jem.apply_project(gem_root, env: env) + expect(apply.fetch(:changed_files)).to include(".github/dependabot.yml", "Gemfile", "Rakefile", "README.md") + expect(File).to exist(File.join(gem_root, ".github/FUNDING.yml")) + expect(File).to exist(File.join(gem_root, ".github/workflows/ci.yml")) + + readme = File.read(File.join(gem_root, "README.md")) + expect(readme).to include("# 💎 Dummy::Gem") + expect(readme).to include("## 🌻 Synopsis\n\nDestination synopsis from the scaffolded project.") + expect(readme).to include("## 🔧 Basic Usage\n\nDestination usage from the scaffolded project.") + expect(readme).not_to include("Old scaffold installation notes.") + expect(readme).to include("Compatible with MRI Ruby 3.2.0+") + expect(readme).to include("https://github.com/acme/dummy-gem") + + dependabot = YAML.safe_load(File.read(File.join(gem_root, ".github/dependabot.yml"))) + expect(dependabot).to eq( + "updates" => [ + { + "directory" => "/", + "package-ecosystem" => "bundler", + "schedule" => { "interval" => "daily" }, + }, + ], + "version" => 2 + ) + + style_gemfile = File.read(File.join(gem_root, "gemfiles/modular/style.gemfile")) + expect(style_gemfile).to include('gem "rubocop-lts", "~> 24.0"') + expect(style_gemfile).to include('gem "rubocop-ruby3_2"') + + gemfile = File.read(File.join(gem_root, "Gemfile")) + expect(gemfile).to include('source "https://gem.coop"') + expect(gemfile).not_to include('source "https://rubygems.org"') + expect(gemfile.scan(/^gemspec$/).size).to eq(1) + expect(gemfile.scan('eval_gemfile "gemfiles/modular/style.gemfile"').size).to eq(1) + expect(gemfile).to include('gem "irb"') + + gemspec = File.read(File.join(gem_root, "dummy-gem.gemspec")) + expect(gemspec.scan("Gem::Specification.new").size).to eq(1) + expect(gemspec).to include('spec.name = "dummy-gem"') + expect(gemspec).to include('spec.summary = "Dummy gem"') + expect(gemspec).to include('spec.description = "Dummy gem for kettle-jem system testing."') + expect(gemspec).to include('spec.homepage = "https://github.com/acme/dummy-gem"') + expect(gemspec).to include('spec.required_ruby_version = ">= 3.2.0"') + expect(gemspec).to include('spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/v#{spec.version}"') + + rakefile = File.read(File.join(gem_root, "Rakefile")) + expect(rakefile).to include('require "bundler/gem_tasks"') + expect(rakefile).to include('require "kettle/dev"') + expect(rakefile.scan(/^task\s+:default\b/).size).to eq(1) + expect(rakefile).to include('desc "Default tasks aggregator"') + expect(rakefile.index('desc "Default tasks aggregator"')).to be < rakefile.index("task :default do") + expect(rakefile.scan('task("kettle:jem:selftest")').size).to eq(1) + expect(rakefile.scan('task("build:generate_checksums")').size).to eq(1) + + aggregate_failures "hidden packaged template directories" do + expected_hidden_directories.each do |relative_path| + expect(Dir).to exist(File.join(gem_root, relative_path)), "expected #{relative_path} to exist" + end + end + + aggregate_failures "hidden packaged template files" do + expected_hidden_files.each do |relative_path| + expect(File).to exist(File.join(gem_root, relative_path)), "expected #{relative_path} to exist" + end + end + + selected_template_paths = [ + ".github/copilot_instructions.md", + ".github/dependabot.yml", + ".qlty/qlty.toml", + "Gemfile", + "certs/pboling.pem", + "gemfiles/modular/style.gemfile", + ] + before_second_apply = selected_template_paths.to_h do |relative_path| + [relative_path, File.read(File.join(gem_root, relative_path))] + end + + Kettle::Jem.apply_project(gem_root, env: env) + + expect(selected_template_paths.to_h { |relative_path| + [relative_path, File.read(File.join(gem_root, relative_path))] + }).to eq(before_second_apply) + + readme_after_second_apply = File.read(File.join(gem_root, "README.md")) + expect(readme_after_second_apply).to include("# 💎 Dummy::Gem") + expect(readme_after_second_apply).to include("## 🌻 Synopsis\n\nDestination synopsis from the scaffolded project.") + + rakefile_after_second_apply = File.read(File.join(gem_root, "Rakefile")) + expect(rakefile_after_second_apply).to include('require "bundler/gem_tasks"') + expect(rakefile_after_second_apply).to include('require "kettle/dev"') + expect(rakefile_after_second_apply.scan('task("kettle:jem:selftest")').size).to eq(1) + expect(rakefile_after_second_apply.scan('task("build:generate_checksums")').size).to eq(1) + end +end diff --git a/gems/kettle-jem/spec/thin_slice_spec.rb b/gems/kettle-jem/spec/thin_slice_spec.rb new file mode 100644 index 0000000..a4888a4 --- /dev/null +++ b/gems/kettle-jem/spec/thin_slice_spec.rb @@ -0,0 +1,1463 @@ +# frozen_string_literal: true + +require_relative "spec_helper" + +RSpec.describe Kettle::Jem do + def json_ready(value) + JSON.parse(JSON.generate(value), symbolize_names: true) + end + + def write_tree(root, files) + files.each do |relative_path, content| + path = File.join(root, relative_path.to_s) + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, content) + end + end + + def project_files(root, paths) + paths.to_h do |relative_path| + path = File.join(root, relative_path) + [relative_path.to_sym, File.exist?(path) ? File.read(path) : nil] + end + end + + let(:fixture_path) { Pathname(__dir__).join("fixtures/thin_slice.json") } + let(:fixture) { JSON.parse(fixture_path.read, symbolize_names: true) } + let(:contract_path) do + Pathname(__dir__).join("../../../../fixtures/packaging/thin-slice-contract.json").expand_path + end + let(:contract) { JSON.parse(contract_path.read, symbolize_names: true) } + + it "plans and applies the RubyGems thin vertical slice" do + expected_recipe_names = contract.fetch(:canonical_recipes).map { |recipe| recipe.fetch(:name).to_s } + expect(contract.fetch(:validated_ecosystems)).to include(fixture.fetch(:ecosystem)) + expect(fixture.fetch(:expected).fetch(:facts).keys).to include( + *contract.fetch(:required_fact_groups).map(&:to_sym), + contract.fetch(:ecosystem_fact_groups).fetch(:rubygems).to_sym + ) + + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-thin-slice", tmp_root) do |root| + write_tree(root, fixture.fetch(:inputs).fetch(:files)) + + plan = described_class.plan_project(root, env: {}) + expect(json_ready(plan[:facts])).to eq(json_ready(fixture.fetch(:expected).fetch(:facts))) + recipe_names = plan[:recipe_pack][:recipes].map { |recipe| recipe[:name] } + expect(recipe_names.take(expected_recipe_names.length)).to eq(expected_recipe_names) + expect(recipe_names).to include("github_funding_yml") + expect(recipe_names).to include("github_actions_ci") + expect(recipe_names).to include("github_actions_framework_ci") + expect(recipe_names).to include(a_string_starting_with("github_actions_obsolete_workflow_cleanup_")) + expect(recipe_names).to include("rakefile_scaffold_cleanup") + expect(recipe_names).to include(a_string_starting_with("github_actions_workflow_snippets_")) + expect(plan[:changed_files]).to eq(fixture.fetch(:expected).fetch(:changed_files)) + expect(plan[:recipe_reports].map { |report| report[:request_envelope][:kind] }.uniq).to eq( + [contract.fetch(:report_contract).fetch(:request_envelope_kind)] + ) + expect(plan[:recipe_reports].map { |report| report[:report_envelope][:kind] }.uniq).to eq( + [contract.fetch(:report_contract).fetch(:report_envelope_kind)] + ) + rakefile_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "rakefile_scaffold_cleanup" } + expect(rakefile_report.dig(:request_envelope, :request, :runtime_context, :delete_selectors).length).to eq(4) + expect(rakefile_report.dig(:report_envelope, :report, :step_reports, 0, :metadata, :deleted_ranges)).to eq(4) + expect(rakefile_report.fetch(:final_content)).to include("task :custom") + expect(rakefile_report.fetch(:final_content)).not_to include("bundler/gem_tasks") + expect(rakefile_report.fetch(:final_content)).not_to include("RSpec::Core::RakeTask") + ci_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "github_actions_ci" } + expect(ci_report.dig(:request_envelope, :request, :provider_family)).to eq("yaml") + expect(ci_report.fetch(:final_content)).to include("ruby/setup-ruby@") + expect(ci_report.fetch(:final_content)).to include("- \"3.2\"") + funding_yml_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "github_funding_yml" } + expect(funding_yml_report.fetch(:final_content)).to include("tidelift: rubygems/example") + expect(funding_yml_report.fetch(:final_content)).to include("open_collective: example") + framework_ci_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "github_actions_framework_ci" } + expect(framework_ci_report.fetch(:final_content)).to include("name: Rails CI") + expect(framework_ci_report.fetch(:final_content)).to include("gemfiles/rails_7_0") + expect(framework_ci_report.fetch(:final_content)).to include("BUNDLE_GEMFILE") + custom_ci_report = plan[:recipe_reports].find do |report| + report.fetch(:relative_path) == ".github/workflows/custom-ci.yml" + end + expect(custom_ci_report.fetch(:final_content)).to include("permissions:") + expect(custom_ci_report.fetch(:final_content)).to include("concurrency:") + expect(custom_ci_report.fetch(:final_content)).to include("actions/checkout@de0fac2") + expect(custom_ci_report.fetch(:final_content)).to include("ruby/setup-ruby@e65c17") + expect(custom_ci_report.fetch(:final_content)).to include("Upload coverage to Coveralls") + expect(custom_ci_report.fetch(:final_content)).to include("qltysh/qlty-action/coverage@a192421") + expect(custom_ci_report.fetch(:final_content)).to include("Code Coverage Summary Report") + expect(custom_ci_report.fetch(:final_content)).to include("ruby: [\"3.2\", \"3.3\"]") + obsolete_workflow_report = plan[:recipe_reports].find do |report| + report.fetch(:relative_path) == ".github/workflows/ancient.yml" + end + expect(obsolete_workflow_report.fetch(:metadata).fetch(:delete_file)).to be(true) + expect(obsolete_workflow_report.dig(:report_envelope, :report, :step_reports, 0, :metadata, :deleted_file)).to eq( + ".github/workflows/ancient.yml" + ) + + apply = described_class.apply_project(root, env: {}) + expect(apply[:changed_files]).to eq(fixture.fetch(:expected).fetch(:changed_files)) + expect(project_files(root, fixture.fetch(:expected).fetch(:files).keys.map(&:to_s))).to eq(fixture.fetch(:expected).fetch(:files)) + end + end + + it "generates a coverage workflow when configured" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-coverage-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.required_ruby_version = ">= 3.2" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + workflows: + coverage: + enabled: true + appraisal: coverage + command: rake test + YAML + }) + + plan = described_class.plan_project(root, env: {}) + coverage_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "github_actions_coverage_ci" } + expect(coverage_report.dig(:request_envelope, :request, :provider_family)).to eq("yaml") + expect(coverage_report.fetch(:relative_path)).to eq(".github/workflows/coverage.yml") + expect(coverage_report.fetch(:final_content)).to include("name: Test Coverage") + expect(coverage_report.fetch(:final_content)).to include("K_SOUP_COV_DO: true") + expect(coverage_report.fetch(:final_content)).to include("bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }}") + expect(coverage_report.fetch(:final_content)).to include("Upload coverage to CodeCov") + expect(plan[:changed_files]).to include(".github/workflows/coverage.yml") + end + end + + it "removes Open Collective funding when disabled" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-opencollective-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.required_ruby_version = ">= 3.2" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + funding: + open_collective: false + templates: + root: packaged + entries: + - README.md + - source: FUNDING.md.example + target: FUNDING.md + YAML + ".github/FUNDING.yml" => <<~YAML, + github: [example] + open_collective: example + YAML + ".opencollective.yml" => <<~YAML, + collective: example + YAML + ".github/workflows/opencollective.yml" => <<~YAML, + name: Open Collective + on: + workflow_dispatch: + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + YAML + }) + + plan = described_class.plan_project(root, env: {}) + expect(plan.dig(:facts, :funding, :open_collective_disabled)).to be(true) + expect(plan.dig(:facts, :funding, :open_collective_disabled_source)).to eq("config.funding.open_collective") + expect(plan.dig(:facts, :funding, :open_collective_files)).to eq( + [".opencollective.yml", ".github/workflows/opencollective.yml"] + ) + expect(plan.dig(:facts, :funding, :urls)).not_to include("https://opencollective.com/example") + recipe_names = plan[:recipe_pack][:recipes].map { |recipe| recipe.fetch(:name) } + expect(recipe_names).to include("opencollective_disabled_file_cleanup_opencollective_yml") + expect(recipe_names).to include("opencollective_disabled_file_cleanup_github_workflows_opencollective_yml") + expect(recipe_names).not_to include("github_actions_workflow_snippets_github_workflows_opencollective_yml") + expect(plan.dig(:facts, :templates, :source_preferences)).to contain_exactly( + a_hash_including( + target_path: "README.md", + configured_source: "README.md", + selected_source: "README.md.no-osc.example", + source_relative_path: "README.md.no-osc.example", + source_root: "packaged", + selection_reason: "opencollective_disabled_no_osc_variant", + apply: false + ), + a_hash_including( + target_path: "FUNDING.md", + configured_source: "FUNDING.md.example", + selected_source: "FUNDING.md.no-osc.example", + source_relative_path: "FUNDING.md.no-osc.example", + source_root: "packaged", + selection_reason: "opencollective_disabled_no_osc_variant", + apply: false + ) + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_preference_README_md" + end + expect(template_report.fetch(:changed)).to be(false) + expect(template_report.dig(:metadata, :template_source_preference, :selected_source)).to eq( + "README.md.no-osc.example" + ) + expect(template_report.dig(:request_envelope, :request, :runtime_context, :template_source_preference, :selection_reason)).to eq( + "opencollective_disabled_no_osc_variant" + ) + funding_report = plan[:recipe_reports].find { |report| report.fetch(:recipe_name) == "github_funding_yml" } + expect(funding_report.fetch(:final_content)).not_to include("open_collective") + expect(funding_report.fetch(:final_content)).to include("tidelift: rubygems/example") + open_collective_reports = plan[:recipe_reports].select do |report| + report.fetch(:recipe_name).start_with?("opencollective_disabled_file_cleanup_") + end + expect(open_collective_reports.map { |report| report.fetch(:relative_path) }).to eq( + [".opencollective.yml", ".github/workflows/opencollective.yml"] + ) + expect(open_collective_reports).to all(satisfy { |report| report.fetch(:metadata).fetch(:delete_file) == true }) + + apply = described_class.apply_project(root, env: {}) + expect(apply[:changed_files]).to include(".opencollective.yml", ".github/workflows/opencollective.yml") + expect(File).not_to exist(File.join(root, ".opencollective.yml")) + expect(File).not_to exist(File.join(root, ".github/workflows/opencollective.yml")) + end + end + + it "honors falsey Open Collective environment variables when config is absent" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-opencollective-env-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.required_ruby_version = ">= 3.2" + end + RUBY + ".github/FUNDING.yml" => <<~YAML, + github: [example] + open_collective: example + YAML + ".opencollective.yml" => <<~YAML, + collective: example + YAML + }) + + plan = described_class.plan_project(root, env: { "OPENCOLLECTIVE_HANDLE" => "NO" }) + expect(plan.dig(:facts, :funding, :open_collective_disabled)).to be(true) + expect(plan.dig(:facts, :funding, :open_collective_disabled_source)).to eq("env.OPENCOLLECTIVE_HANDLE") + expect(plan.dig(:facts, :funding, :open_collective_org)).to be_nil + expect(plan.dig(:facts, :funding, :urls)).not_to include("https://opencollective.com/example") + expect(plan[:changed_files]).to include(".opencollective.yml") + end + end + + it "lets explicit Open Collective config override falsey environment variables" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-opencollective-config-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + funding: + open_collective: true + YAML + ".github/FUNDING.yml" => <<~YAML, + github: [example] + open_collective: example + YAML + }) + + plan = described_class.plan_project(root, env: { "FUNDING_ORG" => "0" }) + expect(plan.dig(:facts, :funding, :open_collective_disabled)).to be_nil + expect(plan.dig(:facts, :funding, :urls)).to include("https://opencollective.com/example") + end + end + + it "discovers Open Collective org from environment before .opencollective.yml" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-opencollective-env-org-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + entries: + - README.md + YAML + ".opencollective.yml" => <<~YAML, + collective: yaml-org + YAML + "template/README.md.example" => <<~MARKDOWN, + # {KJ|OPENCOLLECTIVE_ORG} + MARKDOWN + }) + + plan = described_class.plan_project(root, env: { "FUNDING_ORG" => "env-org" }) + expect(plan.dig(:facts, :funding, :open_collective_org)).to eq("env-org") + expect(plan.dig(:facts, :funding, :open_collective_org_source)).to eq("env.FUNDING_ORG") + expect(plan.dig(:facts, :funding, :urls)).to include("https://opencollective.com/env-org") + expect(plan.dig(:facts, :templates, :tokens)).to include( + "KJ|GEM_NAME" => "example", + "KJ|GEM_NAME_PATH" => "example", + "KJ|NAMESPACE" => "Example", + "KJ|OPENCOLLECTIVE_ORG" => "env-org" + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_preference_README_md" + end + expect(template_report.dig(:metadata, :template_tokens)).to include("KJ|OPENCOLLECTIVE_ORG" => "env-org") + expect(template_report.dig(:request_envelope, :request, :runtime_context, :template_tokens)).to include( + "KJ|OPENCOLLECTIVE_ORG" => "env-org" + ) + end + end + + it "discovers Open Collective org from .opencollective.yml when env is absent" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-opencollective-yaml-org-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".opencollective.yml" => <<~YAML, + org: yaml-org + YAML + }) + + plan = described_class.plan_project(root, env: {}) + expect(plan.dig(:facts, :funding, :open_collective_org)).to eq("yaml-org") + expect(plan.dig(:facts, :funding, :open_collective_org_source)).to eq(".opencollective.yml") + expect(plan.dig(:facts, :funding, :urls)).to include("https://opencollective.com/yaml-org") + end + end + + it "applies selected template content with projected tokens when configured" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-template-application-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.authors = ["Jane Q Public"] + spec.email = ["jane@example.test"] + spec.required_ruby_version = ">= 3.2" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + files: + README.md: + strategy: accept_template + templates: + root: template + apply: true + entries: + - README.md + YAML + "README.md" => "# old\n", + ".opencollective.yml" => <<~YAML, + collective: yaml-org + YAML + "template/README.md.example" => <<~MARKDOWN, + # {KJ|GEM_NAME} + + Namespace: {KJ|NAMESPACE} + Path: {KJ|GEM_NAME_PATH} + Ruby: {KJ|MIN_RUBY} + Author: {KJ|AUTHOR:NAME} + Given: {KJ|AUTHOR:GIVEN_NAMES} + Family: {KJ|AUTHOR:FAMILY_NAMES} + Email: {KJ|AUTHOR:EMAIL} + Domain: {KJ|AUTHOR:DOMAIN} + Funding: {KJ|OPENCOLLECTIVE_ORG} + MARKDOWN + }) + + plan = described_class.plan_project(root, env: {}) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.fetch(:changed)).to be(true) + expect(template_report.dig(:request_envelope, :request, :template_content)).to include("{KJ|GEM_NAME}") + expect(template_report.fetch(:final_content)).to eq(<<~MARKDOWN) + # example + + Namespace: Example + Path: example + Ruby: 3.2 + Author: Jane Q Public + Given: Jane Q + Family: Public + Email: jane@example.test + Domain: example.test + Funding: yaml-org + MARKDOWN + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|AUTHOR:DOMAIN" => "example.test", + "KJ|AUTHOR:EMAIL" => "jane@example.test", + "KJ|AUTHOR:FAMILY_NAMES" => "Public", + "KJ|AUTHOR:GIVEN_NAMES" => "Jane Q", + "KJ|AUTHOR:NAME" => "Jane Q Public", + "KJ|GEM_NAME" => "example", + "KJ|GEM_NAME_PATH" => "example", + "KJ|MIN_RUBY" => "3.2", + "KJ|NAMESPACE" => "Example", + "KJ|OPENCOLLECTIVE_ORG" => "yaml-org" + ) + + described_class.apply_project(root, env: {}) + expect(File.read(File.join(root, "README.md"))).to eq(template_report.fetch(:final_content)) + end + end + + it "applies packaged template files when no project template root exists" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-packaged-template-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.required_ruby_version = ">= 3.2" + spec.metadata["source_code_uri"] = "https://github.com/acme/example" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + project_emoji: "💎" + tokens: + forge: + gh_user: acme + gl_user: acme + cb_user: acme + sh_user: acme + funding: + patreon: acme + kofi: acme + paypal: acme + buymeacoffee: acme + polar: acme + liberapay: acme + issuehunt: acme + social: + mastodon: "@acme@example.social" + bluesky: acme.example + linktree: acme + devto: acme + templates: + apply: true + entries: + - README.md + YAML + }) + + plan = described_class.plan_project(root, env: {}) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.dig(:metadata, :template_source_preference)).to include( + selected_source: "README.md.example", + source_relative_path: "README.md.example", + source_root: "packaged" + ) + expect(template_report.dig(:metadata, :template_source_preference, :source_root_path)).to end_with( + "lib/kettle/jem/templates" + ) + expect(template_report.dig(:request_envelope, :request, :template_content)).to include("# {KJ|PROJECT_EMOJI} {KJ|NAMESPACE}") + expect(template_report.fetch(:final_content)).to include("# 💎 Example") + expect(template_report.fetch(:final_content)).to include("Compatible with MRI Ruby 3.2+") + expect(template_report.fetch(:final_content)).to include("https://patreon.com/acme") + expect(template_report.fetch(:final_content)).to include("https://github.com/acme/example") + + described_class.apply_project(root, env: {}) + expect(File.read(File.join(root, "README.md"))).to eq(template_report.fetch(:final_content)) + end + end + + it "bootstraps kettle config from packaged reference template when missing" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-config-bootstrap-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + }) + + plan = described_class.plan_project(root, env: { "KJ_MIN_DIVERGENCE_THRESHOLD" => "7" }) + bootstrap_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "kettle_config_bootstrap" + end + expect(bootstrap_report.fetch(:changed)).to be(true) + expect(bootstrap_report.fetch(:relative_path)).to eq(".kettle-jem.yml") + expect(bootstrap_report.dig(:metadata, :bootstrap_file)).to be(true) + expect(bootstrap_report.dig(:metadata, :template_source_preference)).to include( + selected_source: ".kettle-jem.yml.example", + source_relative_path: ".kettle-jem.yml.example", + source_root: "packaged" + ) + expect(bootstrap_report.fetch(:final_content)).to include("# kettle-jem configuration file") + expect(bootstrap_report.fetch(:final_content)).to include("min_divergence_threshold: 7") + expect(bootstrap_report.fetch(:final_content)).to include("# tokens - values for {KJ|...} placeholders used across template files") + + described_class.apply_project(root, env: { "KJ_MIN_DIVERGENCE_THRESHOLD" => "7" }) + expect(File.read(File.join(root, ".kettle-jem.yml"))).to eq(bootstrap_report.fetch(:final_content)) + end + end + + it "classifies template entries with files and patterns strategy config" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-template-strategy-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + patterns: + - path: "certs/**" + strategy: raw_copy + files: + README.md: + strategy: keep_destination + templates: + root: packaged + apply: true + entries: + - README.md + - source: certs/pboling.pem.example + target: certs/pboling.pem + YAML + "README.md" => "# destination\n", + }) + + packaged_cert = File.read(File.join(__dir__, "../lib/kettle/jem/templates/certs/pboling.pem.example")) + plan = described_class.plan_project(root, env: {}) + readme_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + cert_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_certs_pboling_pem" + end + expect(readme_report.fetch(:changed)).to be(false) + expect(readme_report.fetch(:final_content)).to eq("# destination\n") + expect(readme_report.dig(:metadata, :template_source_preference)).to include(strategy: "keep_destination") + expect(cert_report.fetch(:changed)).to be(true) + expect(cert_report.fetch(:final_content)).to eq(packaged_cert) + expect(cert_report.dig(:metadata, :template_source_preference)).to include(strategy: "raw_copy") + end + end + + it "plans packaged template inventory when entries are omitted" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-template-inventory-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + funding: + open_collective: false + patterns: + - path: "certs/**" + strategy: raw_copy + files: + AGENTS.md: + strategy: accept_template + templates: + root: packaged + YAML + }) + + plan = described_class.plan_project(root, env: {}) + preferences = plan.dig(:facts, :templates, :source_preferences) + expect(preferences.size).to be > 100 + + agents = preferences.find { |preference| preference.fetch(:target_path) == "AGENTS.md" } + cert = preferences.find { |preference| preference.fetch(:target_path) == "certs/pboling.pem" } + envrc = preferences.find { |preference| preference.fetch(:target_path) == ".envrc" } + env_local = preferences.find { |preference| preference.fetch(:target_path) == ".env.local.example" } + gemspec = preferences.find { |preference| preference.fetch(:target_path) == "example.gemspec" } + + expect(agents).to include(selected_source: "AGENTS.md.example", strategy: "accept_template") + expect(cert).to include(selected_source: "certs/pboling.pem.example", strategy: "raw_copy") + expect(envrc).to include(selected_source: ".envrc.no-osc.example") + expect(env_local).to include(configured_source: ".env.local", selected_source: ".env.local.example") + expect(gemspec).to include(configured_source: "gem.gemspec", selected_source: "gem.gemspec.example") + end + end + + it "preserves configured README sections during merge template application" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-readme-merge-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + readme: + preserve_sections: + - synopsis + - basic usage + - custom section + preserve_patterns: + - "note:*" + section_aliases: + usage: basic usage + templates: + root: template + apply: true + entries: + - README.md + YAML + "README.md" => <<~MARKDOWN, + # 1️⃣ Example + + ## Synopsis + + Destination synopsis. + + ## Usage + + Destination usage. + + ## Custom Section + + Destination custom. + + ## Note: Local + + Destination note. + + ## Installation + + Old install. + MARKDOWN + "template/README.md.example" => <<~MARKDOWN, + # 💎 Example + + ## 🌻 Synopsis + + Template synopsis. + + ## 🔧 Basic Usage + + Template usage. + + ## Custom Section + + Template custom. + + ## Note: Local + + Template note. + + ## Installation + + Template install. + MARKDOWN + }) + + apply = described_class.apply_project(root, env: {}) + readme_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + final_content = readme_report.fetch(:final_content) + expect(final_content).to include("# 💎 Example") + expect(final_content).to include("## 🌻 Synopsis\n\nDestination synopsis.") + expect(final_content).to include("## 🔧 Basic Usage\n\nDestination usage.") + expect(final_content).to include("## Custom Section\n\nDestination custom.") + expect(final_content).to include("## Note: Local\n\nDestination note.") + expect(final_content).to include("## Installation\n\nTemplate install.") + expect(final_content).not_to include("Template synopsis.") + expect(final_content).not_to include("Template usage.") + expect(final_content).not_to include("Template custom.") + expect(final_content).not_to include("Template note.") + expect(File.read(File.join(root, "README.md"))).to eq(final_content) + end + end + + it "merges YAML and TOML template applications with destination values" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-config-merge-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + files: + config: + explicit.yml: + strategy: merge + file_type: yaml + templates: + root: template + apply: true + entries: + - .github/dependabot.yml + - config/settings.yml + - config/tool.toml + - config/explicit.yml + YAML + ".github/dependabot.yml" => <<~YAML, + updates: + - package-ecosystem: bundler + directory: / + version: 1 + YAML + "config/settings.yml" => <<~YAML, + engines: + - ruby + nested: + value: destination + version: 1 + YAML + "config/tool.toml" => <<~TOML, + title = "destination" + + [settings] + retries = 1 + TOML + "config/explicit.yml" => <<~YAML, + destination_only: keep + nested: + value: destination + YAML + "template/.github/dependabot.yml.example" => <<~YAML, + schedule: + interval: weekly + updates: + - package-ecosystem: github-actions + directory: / + version: 2 + YAML + "template/config/settings.yml.example" => <<~YAML, + engines: + - ruby + - jruby + nested: + template_only: true + value: template + version: 2 + YAML + "template/config/tool.toml.example" => <<~TOML, + title = "template" + + [settings] + retries = 3 + timeout = 30 + TOML + "template/config/explicit.yml.example" => <<~YAML, + nested: + value: template + template_only: true + template_only: added + YAML + }) + + apply = described_class.apply_project(root, env: {}) + dependabot_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_github_dependabot_yml" + end + yaml_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_config_settings_yml" + end + toml_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_config_tool_toml" + end + explicit_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_config_explicit_yml" + end + + expect(YAML.safe_load(dependabot_report.fetch(:final_content))).to eq( + "schedule" => { "interval" => "weekly" }, + "updates" => [ + { + "directory" => "/", + "package-ecosystem" => "bundler", + }, + ], + "version" => 1 + ) + expect(YAML.safe_load(yaml_report.fetch(:final_content))).to eq( + "engines" => ["ruby"], + "nested" => { + "template_only" => true, + "value" => "destination", + }, + "version" => 1 + ) + expect(toml_report.fetch(:final_content)).to eq(<<~TOML) + title = "destination" + + [settings] + retries = 1 + timeout = 30 + TOML + expect(YAML.safe_load(explicit_report.fetch(:final_content))).to eq( + "destination_only" => "keep", + "nested" => { + "template_only" => true, + "value" => "destination", + }, + "template_only" => "added" + ) + expect(explicit_report.dig(:metadata, :template_source_preference)).to include(file_type: "yaml") + end + end + + it "merges Ruby-family template applications with destination declarations and DSL calls" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-ruby-merge-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + apply: true + entries: + - Gemfile + - Rakefile + - lib/example.rb + YAML + "Gemfile" => <<~RUBY, + source "https://rubygems.org" + gem "rspec" + eval_gemfile "gemfiles/modular/style.gemfile" + RUBY + "Rakefile" => <<~RUBY, + desc "Default" + task :default do + puts "destination" + end + RUBY + "lib/example.rb" => <<~RUBY, + require "set" + + class Existing + def keep + :destination + end + end + RUBY + "template/Gemfile.example" => <<~RUBY, + source "https://gem.coop" + gemspec + eval_gemfile "gemfiles/modular/style.gemfile" + gem "rake" + RUBY + "template/Rakefile.example" => <<~RUBY, + desc "Default" + task :default do + puts "template" + end + + desc "CI" + task :ci do + sh "bundle exec rspec" + end + RUBY + "template/lib/example.rb.example" => <<~RUBY, + require "json" + + class Existing + def keep + :template + end + end + + class Added + def call + :template_only + end + end + RUBY + }) + + apply = described_class.apply_project(root, env: {}) + ruby_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_lib_example_rb" + end + gemfile_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_Gemfile" + end + rakefile_report = apply.fetch(:recipe_reports).find do |report| + report.fetch(:recipe_name) == "template_source_application_Rakefile" + end + final_content = ruby_report.fetch(:final_content) + + expect(final_content).to include('require "set"') + expect(final_content).not_to include('require "json"') + expect(final_content).to include("def keep\n :destination\n end") + expect(final_content).to include("class Added") + expect(final_content).to include(":template_only") + expect(File.read(File.join(root, "lib/example.rb"))).to eq(final_content) + + gemfile_content = gemfile_report.fetch(:final_content) + expect(gemfile_content).to include('source "https://gem.coop"') + expect(gemfile_content).to include("gemspec") + expect(gemfile_content.scan('eval_gemfile "gemfiles/modular/style.gemfile"').size).to eq(1) + expect(gemfile_content).to include('gem "rspec"') + expect(gemfile_content).to include('gem "rake"') + + rakefile_content = rakefile_report.fetch(:final_content) + expect(rakefile_content.scan(/task\s+:default/).size).to eq(1) + expect(rakefile_content).to include('puts "destination"') + expect(rakefile_content).to include("task :ci") + end + end + + it "honors author template token config and environment overrides" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-author-token-override-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.authors = ["Jane Q Public"] + spec.email = ["jane@example.test"] + end + RUBY + ".kettle-jem.yml" => <<~YAML, + tokens: + author: + name: Config Person + given_names: Config + family_names: Person + email: config@example.test + domain: config.example.test + orcid: "{KJ|AUTHOR:ORCID}" + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + Author: {KJ|AUTHOR:NAME} + Given: {KJ|AUTHOR:GIVEN_NAMES} + Family: {KJ|AUTHOR:FAMILY_NAMES} + Email: {KJ|AUTHOR:EMAIL} + Domain: {KJ|AUTHOR:DOMAIN} + ORCID: {KJ|AUTHOR:ORCID} + MARKDOWN + }) + + plan = described_class.plan_project( + root, + env: { + "KJ_AUTHOR_NAME" => "Env A Writer", + "KJ_AUTHOR_EMAIL" => "env@example.test", + "KJ_AUTHOR_DOMAIN" => "env.example.test", + "KJ_AUTHOR_ORCID" => "0000-0002-1825-0097", + } + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.fetch(:final_content)).to eq(<<~MARKDOWN) + Author: Env A Writer + Given: Config + Family: Person + Email: env@example.test + Domain: env.example.test + ORCID: 0000-0002-1825-0097 + MARKDOWN + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|AUTHOR:DOMAIN" => "env.example.test", + "KJ|AUTHOR:EMAIL" => "env@example.test", + "KJ|AUTHOR:FAMILY_NAMES" => "Person", + "KJ|AUTHOR:GIVEN_NAMES" => "Config", + "KJ|AUTHOR:NAME" => "Env A Writer", + "KJ|AUTHOR:ORCID" => "0000-0002-1825-0097" + ) + end + end + + it "honors forge user template token config and environment overrides" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-forge-token-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + tokens: + forge: + gh_user: config-gh + gl_user: config-gl + cb_user: "{KJ|CB:USER}" + sh_user: config-sh + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + GitHub: {KJ|GH:USER} + GitLab: {KJ|GL:USER} + Codeberg: {KJ|CB:USER} + SourceHut: {KJ|SH:USER} + MARKDOWN + }) + + plan = described_class.plan_project( + root, + env: { + "KJ_GH_USER" => "env-gh", + "KJ_CB_USER" => "env-cb", + } + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.fetch(:final_content)).to eq(<<~MARKDOWN) + GitHub: env-gh + GitLab: config-gl + Codeberg: env-cb + SourceHut: config-sh + MARKDOWN + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|CB:USER" => "env-cb", + "KJ|GH:USER" => "env-gh", + "KJ|GL:USER" => "config-gl", + "KJ|SH:USER" => "config-sh" + ) + end + end + + it "honors funding platform template token config and environment overrides" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-funding-token-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + tokens: + funding: + patreon: config-patreon + kofi: config-kofi + paypal: "{KJ|FUNDING:PAYPAL}" + buymeacoffee: config-bmac + polar: config-polar + liberapay: config-liberapay + issuehunt: config-issuehunt + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + Patreon: {KJ|FUNDING:PATREON} + Ko-fi: {KJ|FUNDING:KOFI} + PayPal: {KJ|FUNDING:PAYPAL} + BuyMeACoffee: {KJ|FUNDING:BUYMEACOFFEE} + Polar: {KJ|FUNDING:POLAR} + Liberapay: {KJ|FUNDING:LIBERAPAY} + IssueHunt: {KJ|FUNDING:ISSUEHUNT} + MARKDOWN + }) + + plan = described_class.plan_project( + root, + env: { + "KJ_FUNDING_PATREON" => "env-patreon", + "KJ_FUNDING_PAYPAL" => "env-paypal", + } + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.fetch(:final_content)).to eq(<<~MARKDOWN) + Patreon: env-patreon + Ko-fi: config-kofi + PayPal: env-paypal + BuyMeACoffee: config-bmac + Polar: config-polar + Liberapay: config-liberapay + IssueHunt: config-issuehunt + MARKDOWN + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|FUNDING:BUYMEACOFFEE" => "config-bmac", + "KJ|FUNDING:ISSUEHUNT" => "config-issuehunt", + "KJ|FUNDING:KOFI" => "config-kofi", + "KJ|FUNDING:LIBERAPAY" => "config-liberapay", + "KJ|FUNDING:PATREON" => "env-patreon", + "KJ|FUNDING:PAYPAL" => "env-paypal", + "KJ|FUNDING:POLAR" => "config-polar" + ) + end + end + + it "honors social template token config and environment overrides" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-social-token-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + tokens: + social: + mastodon: config-mastodon + bluesky: config-bluesky + linktree: "{KJ|SOCIAL:LINKTREE}" + devto: config-devto + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + Mastodon: {KJ|SOCIAL:MASTODON} + Bluesky: {KJ|SOCIAL:BLUESKY} + Linktree: {KJ|SOCIAL:LINKTREE} + Dev.to: {KJ|SOCIAL:DEVTO} + MARKDOWN + }) + + plan = described_class.plan_project( + root, + env: { + "KJ_SOCIAL_MASTODON" => "env-mastodon", + "KJ_SOCIAL_LINKTREE" => "env-linktree", + } + ) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + expect(template_report.fetch(:final_content)).to eq(<<~MARKDOWN) + Mastodon: env-mastodon + Bluesky: config-bluesky + Linktree: env-linktree + Dev.to: config-devto + MARKDOWN + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|SOCIAL:BLUESKY" => "config-bluesky", + "KJ|SOCIAL:DEVTO" => "config-devto", + "KJ|SOCIAL:LINKTREE" => "env-linktree", + "KJ|SOCIAL:MASTODON" => "env-mastodon" + ) + end + end + + it "projects license template tokens from configured licenses" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-license-token-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.authors = ["Jane Q Public"] + spec.email = ["jane@example.test"] + spec.licenses = ["MIT"] + end + RUBY + ".kettle-jem.yml" => <<~YAML, + licenses: + - AGPL-3.0-only + - PolyForm-Small-Business-1.0.0 + - LicenseRef-Big-Time-Public-License + templates: + root: packaged + apply: true + entries: + - LICENSE.md + YAML + }) + + plan = described_class.plan_project(root, env: {}) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_LICENSE_md" + end + final_content = template_report.fetch(:final_content) + expect(plan.dig(:facts, :license, :spdx)).to eq( + ["AGPL-3.0-only", "PolyForm-Small-Business-1.0.0", "LicenseRef-Big-Time-Public-License"] + ) + expect(plan.dig(:facts, :package, :license_expression)).to eq( + "AGPL-3.0-only OR PolyForm-Small-Business-1.0.0 OR LicenseRef-Big-Time-Public-License" + ) + expect(final_content).to include("[AGPL-3.0-only](AGPL-3.0-only.md)") + expect(final_content).to include("[PolyForm-Small-Business-1.0.0](PolyForm-Small-Business-1.0.0.md)") + expect(final_content).to include("[Big-Time-Public-License](Big-Time-Public-License.md)") + expect(final_content).to include("## Use-case guide") + expect(final_content).to include("Required Notice: Copyright") + expect(final_content).to include("Jane Q Public") + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|COPYRIGHT_PREFIX" => "Required Notice: ", + "KJ|LICENSE:PRIMARY_SPDX" => "AGPL-3.0-only" + ) + expect(template_report.dig(:metadata, :template_tokens, "KJ|LICENSE_MD_CONTENT")).to include( + "This project is made available under the following licenses." + ) + expect(template_report.dig(:metadata, :template_tokens, "KJ|README:LICENSE_REFS")).to include( + "AGPL-3.0-only.md" + ) + end + end + + it "projects project runtime template tokens" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-project-runtime-token-slice", tmp_root) do |root| + write_tree(root, { + "example-gem.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example-gem" + spec.version = "2.4.6" + spec.summary = "Example gem" + spec.authors = ["Jane Q Public"] + spec.email = ["jane@example.test"] + spec.required_ruby_version = ">= 3.2" + spec.metadata["source_code_uri"] = "https://github.com/acme/example-gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + project_emoji: "🫖" + min_divergence_threshold: "{KJ|MIN_DIVERGENCE_THRESHOLD}" + defaults: + freeze_token: custom-freeze + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + Gem shield: {KJ|GEM_SHIELD} + Major: {KJ|GEM_MAJOR} + GitHub org: {KJ|GH_ORG} + Namespace shield: {KJ|NAMESPACE_SHIELD} + Min dev Ruby: {KJ|MIN_DEV_RUBY} + Freeze: {KJ|FREEZE_TOKEN} + Version: {KJ|KETTLE_JEM_VERSION} + Date: {KJ|TEMPLATE_RUN_DATE} + Year: {KJ|TEMPLATE_RUN_YEAR} + Dev gem: {KJ|KETTLE_DEV_GEM} + YARD: {KJ|YARD_HOST} + Emoji: {KJ|PROJECT_EMOJI} + Divergence: {KJ|MIN_DIVERGENCE_THRESHOLD} + MARKDOWN + }) + + plan = described_class.plan_project(root, env: { "KJ_MIN_DIVERGENCE_THRESHOLD" => "12" }) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + final_content = template_report.fetch(:final_content) + expect(final_content).to include("Gem shield: example--gem") + expect(final_content).to include("Major: 2") + expect(final_content).to include("GitHub org: acme") + expect(final_content).to include("Namespace shield: Example%3A%3AGem") + expect(final_content).to include("Min dev Ruby: 3.2") + expect(final_content).to include("Freeze: custom-freeze") + expect(final_content).to include("Version: #{Kettle::Jem::VERSION}") + expect(final_content).to include("Date: #{Time.now.strftime("%Y-%m-%d")}") + expect(final_content).to include("Year: #{Time.now.year}") + expect(final_content).to include("Dev gem: kettle-dev") + expect(final_content).to include("YARD: example-gem.example.test") + expect(final_content).to include("Emoji: 🫖") + expect(final_content).to include("Divergence: 12") + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|FREEZE_TOKEN" => "custom-freeze", + "KJ|GEM_MAJOR" => "2", + "KJ|GEM_SHIELD" => "example--gem", + "KJ|GH_ORG" => "acme", + "KJ|MIN_DEV_RUBY" => "3.2", + "KJ|MIN_DIVERGENCE_THRESHOLD" => "12", + "KJ|NAMESPACE_SHIELD" => "Example%3A%3AGem", + "KJ|PROJECT_EMOJI" => "🫖", + "KJ|YARD_HOST" => "example-gem.example.test" + ) + end + end + + it "projects README top logo template tokens" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-readme-logo-token-slice", tmp_root) do |root| + write_tree(root, { + "example-gem.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example-gem" + spec.summary = "Example gem" + spec.metadata["source_code_uri"] = "https://github.com/acme/example-gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + Row: + {KJ|README:TOP_LOGO_ROW} + Refs: + {KJ|README:TOP_LOGO_REFS} + MARKDOWN + }) + + plan = described_class.plan_project(root, env: {}) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_README_md" + end + final_content = template_report.fetch(:final_content) + expect(final_content).to include("Galtzo FLOSS Logo") + expect(final_content).to include("ruby-lang Logo") + expect(final_content).to include("[![acme Logo by Aboling0, CC BY-SA 4.0][🖼️acme-i]][🖼️acme]") + expect(final_content).to include("[![example-gem Logo by Aboling0, CC BY-SA 4.0][🖼️example-gem-i]][🖼️example-gem]") + expect(final_content).to include("[🖼️acme-i]: https://logos.galtzo.com/assets/images/acme/avatar-192px.svg") + expect(final_content).to include("[🖼️example-gem]: https://github.com/acme/example-gem") + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|README:TOP_LOGO_REFS" => a_string_including("https://github.com/acme/example-gem"), + "KJ|README:TOP_LOGO_ROW" => a_string_including("example-gem Logo by Aboling0") + ) + end + end + + it "projects RuboCop LTS template tokens from minimum Ruby" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-rubocop-token-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + spec.required_ruby_version = ">= 3.1" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: packaged + apply: true + entries: + - gemfiles/modular/style.gemfile + YAML + }) + + plan = described_class.plan_project(root, env: {}) + template_report = plan[:recipe_reports].find do |report| + report.fetch(:recipe_name) == "template_source_application_gemfiles_modular_style_gemfile" + end + expect(template_report.dig(:metadata, :template_source_preference)).to include( + selected_source: "gemfiles/modular/style.gemfile.example", + source_relative_path: "gemfiles/modular/style.gemfile.example", + source_root: "packaged" + ) + expect(template_report.dig(:request_envelope, :request, :template_content)).to include( + "We run rubocop on the latest version of Ruby" + ) + expect(template_report.fetch(:final_content)).to include('gem "rubocop-lts", "~> 22.0"') + expect(template_report.fetch(:final_content)).to include('gem "rubocop-ruby3_1"') + expect(template_report.dig(:metadata, :template_tokens)).to include( + "KJ|RUBOCOP_LTS_CONSTRAINT" => "~> 22.0", + "KJ|RUBOCOP_RUBY_GEM" => "rubocop-ruby3_1" + ) + end + end + + it "fails fast when template application leaves unresolved tokens" do + tmp_root = File.join(__dir__, "tmp") + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-template-unresolved-slice", tmp_root) do |root| + write_tree(root, { + "example.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example" + spec.summary = "Example gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + apply: true + entries: + - README.md + YAML + "template/README.md.example" => <<~MARKDOWN, + # {KJ|UNKNOWN} + MARKDOWN + }) + + expect do + described_class.plan_project(root, env: {}) + end.to raise_error(ArgumentError, /unresolved kettle-jem template tokens: \{KJ\|UNKNOWN\}/) + end + end +end diff --git a/gems/kramdown-merge/lib/kramdown/merge/version.rb b/gems/kramdown-merge/lib/kramdown/merge/version.rb index 5bb1799..83251bc 100644 --- a/gems/kramdown-merge/lib/kramdown/merge/version.rb +++ b/gems/kramdown-merge/lib/kramdown/merge/version.rb @@ -2,6 +2,6 @@ module Kramdown module Merge - VERSION = "0.0.0" + VERSION = "0.1.0" end end diff --git a/gems/markdown-merge/lib/markdown/merge/version.rb b/gems/markdown-merge/lib/markdown/merge/version.rb index 0cf825c..fe1b447 100644 --- a/gems/markdown-merge/lib/markdown/merge/version.rb +++ b/gems/markdown-merge/lib/markdown/merge/version.rb @@ -2,6 +2,6 @@ module Markdown module Merge - VERSION = "0.0.0" + VERSION = "0.1.0" end end diff --git a/gems/markly-merge/lib/markly/merge/version.rb b/gems/markly-merge/lib/markly/merge/version.rb index 3f09bfa..11379ec 100644 --- a/gems/markly-merge/lib/markly/merge/version.rb +++ b/gems/markly-merge/lib/markly/merge/version.rb @@ -2,6 +2,6 @@ module Markly module Merge - VERSION = "0.0.0" + VERSION = "0.1.0" end end diff --git a/gems/ruby-merge/lib/ruby/merge.rb b/gems/ruby-merge/lib/ruby/merge.rb index 490e402..466e1cf 100644 --- a/gems/ruby-merge/lib/ruby/merge.rb +++ b/gems/ruby-merge/lib/ruby/merge.rb @@ -13,6 +13,9 @@ module Merge DIRECTIVE_LINE = /\A(?::nocov:|[\w-]+:(?:freeze|unfreeze))\z/ MAGIC_COMMENT_PREFIXES = %w[coding encoding frozen_string_literal shareable_constant_value typed warn_indent].freeze REQUIRE_PATTERN = /^\s*require(?:_relative)?\s+["']([^"']+)["']/.freeze + DSL_CALL_PATTERN = /^(?source|gemspec|git_source|gem|eval_gemfile|platform|group|desc|task)\b/.freeze + RAKEFILE_DEFAULT_TASK_COMMENT = "# Define a base default task early so other files can enhance it." + RAKEFILE_DEFAULT_TASK_DESC = 'desc "Default tasks aggregator"' CLASS_PATTERN = /^\s*class\s+([A-Z]\w*(?:::\w+)*)/.freeze MODULE_PATTERN = /^\s*module\s+([A-Z]\w*(?:::\w+)*)/.freeze DEF_PATTERN = /^\s*def\s+(?:self\.)?([a-zA-Z_]\w*[!?=]?)/.freeze @@ -86,7 +89,7 @@ def match_ruby_owners(template, destination) } end - def merge_ruby(template_source, destination_source, dialect) + def merge_ruby(template_source, destination_source, dialect, merge_template_requires: false) template = parse_ruby(template_source, dialect) return template unless template[:ok] @@ -101,21 +104,31 @@ def merge_ruby(template_source, destination_source, dialect) } end - require_block = collect_ruby_require_entries(destination.dig(:analysis, :source)).map { |entry| entry[:text] }.join("\n").strip + destination_requires = collect_ruby_require_entries(destination.dig(:analysis, :source)) + template_requires = collect_ruby_require_entries(template.dig(:analysis, :source)) destination_declarations = collect_ruby_declaration_entries(destination.dig(:analysis, :source)) template_declarations = collect_ruby_declaration_entries(template.dig(:analysis, :source)) destination_paths = destination_declarations.to_h { |entry| [entry[:path], true] } + destination_dsl = collect_top_level_dsl_entries(destination.dig(:analysis, :source)) + template_dsl = collect_top_level_dsl_entries(template.dig(:analysis, :source)) sections = [] + preamble = collect_ruby_preamble(destination.dig(:analysis, :source)) + sections << preamble unless preamble.empty? + requires = merge_template_requires ? merge_ruby_requires(destination_requires, template_requires) : destination_requires + require_block = requires.map { |entry| entry[:text] }.join("\n").strip sections << require_block unless require_block.empty? + sections.concat(merge_top_level_dsl_entries(destination_dsl, template_dsl).map { |entry| entry[:text] }) sections.concat(destination_declarations.map { |entry| entry[:text] }) sections.concat( template_declarations.reject { |entry| destination_paths[entry[:path]] }.map { |entry| entry[:text] } ) + output = "#{sections.join("\n\n").strip}\n" + { ok: true, diagnostics: [], - output: "#{sections.join("\n\n").strip}\n", + output: normalize_rakefile_default_task_scaffold(output), policies: [DESTINATION_WINS_ARRAY_POLICY] } end @@ -354,10 +367,104 @@ def analyze_ruby_document(source) def collect_ruby_require_entries(source) normalize_source(source).split("\n").filter_map do |line| - next unless REQUIRE_PATTERN.match?(line) + match = REQUIRE_PATTERN.match(line) + next unless match + + { path: "/requires/#{match[1]}", text: line.rstrip } + end + end + + def collect_ruby_preamble(source) + lines = normalize_source(source).split("\n") + preamble = [] + lines.each do |line| + break unless line.strip.empty? || comment_line?(line) + + preamble << line.rstrip + end + preamble.join("\n").strip + end + + def collect_top_level_dsl_entries(source) + lines = normalize_source(source).split("\n") + entries = [] + pending_comments = [] + index = 0 + + while index < lines.length + line = lines[index] + stripped = line.strip + if comment_line?(line) + pending_comments << index + index += 1 + next + end + if stripped.empty? + pending_comments = [] + index += 1 + next + end + if REQUIRE_PATTERN.match?(line) || declaration_for_line(line) + pending_comments = [] + index += 1 + next + end + + if line.match?(/\Abegin\b/) + start_index = pending_comments.first || index + finish_index = ruby_block_finish_index(lines, index) + text = lines[start_index..finish_index].join("\n").strip + signature = begin_block_signature(text) + entries << { path: "/dsl/#{signature}", name: "begin", signature: signature, text: text } + pending_comments = [] + index = finish_index + 1 + next + end + + match = DSL_CALL_PATTERN.match(line) + unless match + pending_comments = [] + index += 1 + next + end + + name = match[:name] + if name == "desc" && next_code_line_is_task?(lines, index + 1) + pending_comments << index + index += 1 + next + end - { text: line.rstrip } + start_index = pending_comments.first || index + finish_index = dsl_entry_finish_index(lines, index) + text = lines[start_index..finish_index].join("\n").strip + signature = dsl_entry_signature(name, line) + entries << { path: "/dsl/#{signature}", name: name, signature: signature, text: text } if signature + pending_comments = [] + index = finish_index + 1 end + + entries + end + + def merge_top_level_dsl_entries(destination_entries, template_entries) + destination_by_signature = destination_entries.to_h { |entry| [entry[:signature], entry] } + template_singletons = template_entries.select { |entry| dsl_singleton_entry?(entry) } + template_singleton_signatures = template_singletons.map { |entry| entry[:signature] }.to_h { |signature| [signature, true] } + result = [] + result.concat(template_singletons) + result.concat(destination_entries.reject { |entry| template_singleton_signatures[entry[:signature]] }) + result.concat( + template_entries.reject do |entry| + dsl_singleton_entry?(entry) || destination_by_signature[entry[:signature]] + end + ) + result + end + + def merge_ruby_requires(destination_requires, template_requires) + destination_paths = destination_requires.to_h { |entry| [entry[:path], true] } + destination_requires + template_requires.reject { |entry| destination_paths[entry[:path]] } end def collect_ruby_declaration_entries(source) @@ -446,6 +553,109 @@ def declaration_for_line(line) end end + def next_code_line_is_task?(lines, start_index) + lines[start_index..].to_a.each do |line| + next if line.strip.empty? || comment_line?(line) + + match = DSL_CALL_PATTERN.match(line) + return match && match[:name] == "task" + end + false + end + + def dsl_entry_finish_index(lines, start_index) + return start_index unless lines[start_index].match?(/\bdo\b/) + + ruby_block_finish_index(lines, start_index) + end + + def ruby_block_finish_index(lines, start_index) + depth = 0 + cursor = start_index + while cursor < lines.length + stripped = lines[cursor].strip + depth += stripped.scan(/\bdo\b/).length + depth += 1 if declaration_for_line(stripped) || stripped.match?(/\A(begin|if|unless|case|while|until|for)\b/) + depth -= 1 if stripped == "end" + return cursor if depth <= 0 && cursor > start_index + + cursor += 1 + end + lines.length - 1 + end + + def begin_block_signature(text) + require_path = text[/^\s*require(?:_relative)?\s+["']([^"']+)["']/, 1] + return "begin:require:#{require_path}" if require_path + + "begin:#{text.lines.first.to_s.strip}" + end + + def dsl_entry_signature(name, line) + case name + when "source", "gemspec" + name + when "git_source", "gem", "eval_gemfile", "platform", "group", "task" + first_argument = line[/\b#{Regexp.escape(name)}\s*(?:\(|\s)\s*["']([^"']+)["']/, 1] || + line[/\b#{Regexp.escape(name)}\s*(?:\(|\s)\s*:([a-zA-Z_]\w*[!?=]?)/, 1] + first_argument ? "#{name}:#{normalize_dsl_argument(name, first_argument)}" : "#{name}:#{line.strip}" + when "desc" + "desc:#{line.strip}" + end + end + + def normalize_dsl_argument(name, argument) + return argument.gsub(%r{/r\d+/}, "/") if name == "eval_gemfile" + + argument + end + + def dsl_singleton_entry?(entry) + %w[source gemspec].include?(entry[:name]) + end + + def normalize_rakefile_default_task_scaffold(content) + lines = normalize_source(content).split("\n") + desc_index = lines.find_index { |line| line.strip == RAKEFILE_DEFAULT_TASK_DESC } + return content unless desc_index + + comment_index = preceding_code_line_index(lines, desc_index - 1) + return content unless comment_index && lines[comment_index].strip == RAKEFILE_DEFAULT_TASK_COMMENT + + next_code_index = next_code_line_index(lines, desc_index + 1) + return content if next_code_index && lines[next_code_index].match?(/\Atask\s+:default\b/) + + task_index = lines.each_index.find { |index| lines[index].match?(/\Atask\s+:default\b/) } + return content unless task_index + + finish_index = dsl_entry_finish_index(lines, task_index) + task_block = lines[task_index..finish_index] + lines[task_index..finish_index] = [] + insertion_index = lines.find_index { |line| line.strip == RAKEFILE_DEFAULT_TASK_DESC } + 1 + insertion = task_block.dup + insertion << "" unless lines[insertion_index].to_s.strip.empty? + lines.insert(insertion_index, *insertion) + "#{lines.join("\n").sub(/\n+\z/, "")}\n" + end + + def preceding_code_line_index(lines, start_index) + start_index.downto(0) do |index| + next if lines[index].strip.empty? + + return index + end + nil + end + + def next_code_line_index(lines, start_index) + start_index.upto(lines.length - 1) do |index| + next if lines[index].strip.empty? + + return index + end + nil + end + def surfaces_for_owner(owner_name:, comment_entries:) filtered_entries = comment_entries.filter { |entry| doc_comment_content?(entry[:raw]) } return [] if filtered_entries.empty? diff --git a/gems/ruby-merge/spec/fixtures_integration_spec.rb b/gems/ruby-merge/spec/fixtures_integration_spec.rb index f0b9ccf..3f732f2 100644 --- a/gems/ruby-merge/spec/fixtures_integration_spec.rb +++ b/gems/ruby-merge/spec/fixtures_integration_spec.rb @@ -107,6 +107,163 @@ def json_ready(value) ) ).to eq(json_ready(invalid_destination_fixture.dig(:expected, :diagnostics))) + gemfile_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + source "https://gem.coop" + gemspec + eval_gemfile "gemfiles/modular/style.gemfile" + gem "rake" + RUBY + <<~RUBY, + source "https://rubygems.org" + gem "rspec" + eval_gemfile "gemfiles/modular/style.gemfile" + RUBY + "ruby" + ) + expect(gemfile_merge[:ok]).to be(true) + expect(gemfile_merge[:output]).to include('source "https://gem.coop"') + expect(gemfile_merge[:output]).to include("gemspec") + expect(gemfile_merge[:output].scan('eval_gemfile "gemfiles/modular/style.gemfile"').size).to eq(1) + expect(gemfile_merge[:output]).to include('gem "rspec"') + expect(gemfile_merge[:output]).to include('gem "rake"') + + modular_gemfile_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + gem "reek", "~> 6.5" + + platform :mri do + gem "rubocop-lts", "~> 23.0" + gem "rubocop-ruby2_3" + end + RUBY + <<~RUBY, + # frozen_string_literal: true + + # Destination style guidance. + + gem "reek", "~> 6.5" + + platform :mri do + gem "rubocop-lts", "~> 24.0" + gem "rubocop-ruby3_2" + end + RUBY + "ruby" + ) + expect(modular_gemfile_merge[:ok]).to be(true) + expect(modular_gemfile_merge[:output]).to include("# frozen_string_literal: true") + expect(modular_gemfile_merge[:output]).to include("# Destination style guidance.") + expect(modular_gemfile_merge[:output]).to include("platform :mri do") + expect(modular_gemfile_merge[:output]).to include('gem "rubocop-ruby3_2"') + + rakefile_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + desc "Default task" + task :default do + puts "template" + end + + desc "CI" + task :ci do + sh "bundle exec rspec" + end + RUBY + <<~RUBY, + desc "Default task" + task :default do + puts "destination" + end + RUBY + "ruby" + ) + expect(rakefile_merge[:ok]).to be(true) + expect(rakefile_merge[:output].scan(/task\s+:default/).size).to eq(1) + expect(rakefile_merge[:output]).to include('puts "destination"') + expect(rakefile_merge[:output]).to include("task :ci") + + relocated_rakefile_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + # Define a base default task early so other files can enhance it. + desc "Default tasks aggregator" + task :default do + puts "Default task complete." + end + + # External gems that define tasks - add here! + require "kettle/dev" + RUBY + <<~RUBY, + # Define a base default task early so other files can enhance it. + desc "Default tasks aggregator" + # External gems that define tasks - add here! + require "kettle/dev" + + task :default do + # :nocov: + puts "Default task complete." + # :nocov: + end + RUBY + "ruby" + ) + expect(relocated_rakefile_merge[:ok]).to be(true) + expect(relocated_rakefile_merge[:output].scan(/task\s+:default/).size).to eq(1) + expect(relocated_rakefile_merge[:output].scan("# :nocov:").size).to eq(2) + expect(relocated_rakefile_merge[:output]).to include('desc "Default tasks aggregator"') + expect(relocated_rakefile_merge[:output]).to include(<<~RUBY) + task :default do + # :nocov: + puts "Default task complete." + # :nocov: + end + RUBY + expect(relocated_rakefile_merge[:output].index('desc "Default tasks aggregator"')).to be < + relocated_rakefile_merge[:output].index("task :default do") + + rescue_task_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + begin + require "kettle/jem" + rescue LoadError + desc("(stub) kettle:jem:selftest is unavailable") + task("kettle:jem:selftest") do + warn("NOTE: not installed") + end + end + RUBY + <<~RUBY, + begin + require "kettle/jem" + rescue LoadError + # :nocov: + desc("(stub) kettle:jem:selftest is unavailable") + task("kettle:jem:selftest") do + warn("NOTE: not installed") + end + # :nocov: + end + RUBY + "ruby" + ) + expect(rescue_task_merge[:ok]).to be(true) + expect(rescue_task_merge[:output].scan('task("kettle:jem:selftest")').size).to eq(1) + expect(rescue_task_merge[:output].scan("# :nocov:").size).to eq(2) + + rakefile_require_merge = RUBY_MERGE.merge_ruby( + <<~RUBY, + require "kettle/dev" + RUBY + <<~RUBY, + require "bundler/setup" + RUBY + "ruby", + merge_template_requires: true + ) + expect(rakefile_require_merge[:ok]).to be(true) + expect(rakefile_require_merge[:output]).to include('require "bundler/setup"') + expect(rakefile_require_merge[:output]).to include('require "kettle/dev"') + surfaces_analysis = RUBY_MERGE.parse_ruby(surfaces_fixture[:source], "ruby") expect(surfaces_analysis[:ok]).to be(true) expect(json_ready(RUBY_MERGE.ruby_discovered_surfaces(surfaces_analysis[:analysis]))).to eq( diff --git a/gems/yaml-merge/lib/yaml/merge.rb b/gems/yaml-merge/lib/yaml/merge.rb index e8c6b12..be25df7 100644 --- a/gems/yaml-merge/lib/yaml/merge.rb +++ b/gems/yaml-merge/lib/yaml/merge.rb @@ -155,26 +155,27 @@ def validate_yaml_node(value, path) if scalar?(value) { ok: true, value: value } elsif value.is_a?(Array) - if value.all? { |item| scalar?(item) } - { ok: true, value: value } - else - unsupported_feature_result("Unsupported YAML sequence value at #{display_path(path)}. Only scalar sequences are supported.") + value.each_with_index.each_with_object({ ok: true, value: [] }) do |(item, index), memo| + validated = validate_yaml_node(item, "#{path}/#{index}") + return validated unless validated[:ok] + + memo[:value] << validated[:value] end elsif value.is_a?(Hash) - value.keys.sort.each_with_object({ ok: true, value: {} }) do |key, memo| + value.keys.each_with_object({ ok: true, value: {} }) do |key, memo| validated = validate_yaml_node(value[key], "#{path}/#{key}") return validated unless validated[:ok] memo[:value][key] = validated[:value] end else - unsupported_feature_result("Unsupported YAML value at #{display_path(path)}. Only mappings, scalar values, and scalar sequences are supported.") + unsupported_feature_result("Unsupported YAML value at #{display_path(path)}. Only mappings, scalar values, and sequences are supported.") end end private_class_method :validate_yaml_node def scalar?(value) - value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false + value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false end private_class_method :scalar? @@ -184,7 +185,9 @@ def display_path(path) private_class_method :display_path def render_yaml_scalar(value) - if value.is_a?(String) + if value.nil? + "" + elsif value.is_a?(String) value.match?(/\A[A-Za-z0-9_.-]+\z/) ? value : JSON.generate(value) elsif value == true || value == false value ? "true" : "false" @@ -197,9 +200,11 @@ def render_yaml_scalar(value) def render_yaml_node(key, value, indent) prefix = " " * indent if value.is_a?(Array) - ["#{prefix}#{key}:"] + value.map { |item| "#{" " * (indent + 2)}- #{render_yaml_scalar(item)}" } + ["#{prefix}#{key}:"] + render_yaml_sequence(value, indent + 2) elsif value.is_a?(Hash) ["#{prefix}#{key}:"] + render_yaml_mapping(value, indent + 2) + elsif value.nil? + ["#{prefix}#{key}:"] else ["#{prefix}#{key}: #{render_yaml_scalar(value)}"] end @@ -207,12 +212,28 @@ def render_yaml_node(key, value, indent) private_class_method :render_yaml_node def render_yaml_mapping(mapping, indent = 0) - mapping.keys.sort.flat_map do |key| + mapping.keys.flat_map do |key| render_yaml_node(key, mapping[key], indent) end end private_class_method :render_yaml_mapping + def render_yaml_sequence(sequence, indent) + prefix = " " * indent + sequence.flat_map do |item| + if scalar?(item) + ["#{prefix}- #{render_yaml_scalar(item)}"] + elsif item.is_a?(Hash) + ["#{prefix}-"] + render_yaml_mapping(item, indent + 2) + elsif item.is_a?(Array) + ["#{prefix}-"] + render_yaml_sequence(item, indent + 2) + else + ["#{prefix}- #{render_yaml_scalar(item)}"] + end + end + end + private_class_method :render_yaml_sequence + def canonical_yaml(mapping) "#{render_yaml_mapping(mapping).join("\n")}\n" end @@ -224,7 +245,11 @@ def collect_yaml_owners(mapping, prefix = "") value = mapping[key] if value.is_a?(Array) [{ path: path, owner_kind: "key_value", match_key: key }] + - value.each_index.map { |index| { path: "#{path}/#{index}", owner_kind: "sequence_item" } } + value.each_with_index.flat_map do |item, index| + item_path = "#{path}/#{index}" + nested = item.is_a?(Hash) ? collect_yaml_owners(item, item_path) : [] + [{ path: item_path, owner_kind: "sequence_item" }] + nested + end elsif value.is_a?(Hash) [{ path: path, owner_kind: "mapping", match_key: key }] + collect_yaml_owners(value, path) else @@ -235,7 +260,7 @@ def collect_yaml_owners(mapping, prefix = "") private_class_method :collect_yaml_owners def merge_yaml_mappings(template, destination) - (template.keys | destination.keys).sort.each_with_object({}) do |key, merged| + ordered_merge_keys(template, destination).each_with_object({}) do |key, merged| if !template.key?(key) merged[key] = destination[key] elsif !destination.key?(key) @@ -249,6 +274,11 @@ def merge_yaml_mappings(template, destination) end private_class_method :merge_yaml_mappings + def ordered_merge_keys(template, destination) + template.keys + destination.keys.reject { |key| template.key?(key) } + end + private_class_method :ordered_merge_keys + def parse_error_result(message) { ok: false, diagnostics: [{ severity: "error", category: "parse_error", message: message }], policies: [] } end diff --git a/gems/yaml-merge/spec/fixtures_integration_spec.rb b/gems/yaml-merge/spec/fixtures_integration_spec.rb index cd694b9..ed1f91b 100644 --- a/gems/yaml-merge/spec/fixtures_integration_spec.rb +++ b/gems/yaml-merge/spec/fixtures_integration_spec.rb @@ -97,6 +97,55 @@ def json_ready(value) ) end + it "merges mapping sequences with destination arrays winning conflicts" do + template = <<~YAML + updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + permissions: + contents: read + YAML + destination = <<~YAML + updates: + - package-ecosystem: bundler + directory: / + version: 2 + YAML + + result = described_class.merge_yaml(template, destination, "yaml", backend: "kreuzberg-language-pack") + expect(result[:ok]).to be(true) + expect(YAML.safe_load(result.fetch(:output))).to eq( + "permissions" => { "contents" => "read" }, + "updates" => [ + { + "directory" => "/", + "package-ecosystem" => "bundler", + }, + ], + "version" => 2 + ) + end + + it "merges null scalar mapping values from template YAML" do + template = <<~YAML + community_bridge: + github: [acme] + YAML + destination = <<~YAML + tidelift: rubygems/example + YAML + + result = described_class.merge_yaml(template, destination, "yaml", backend: "kreuzberg-language-pack") + expect(result[:ok]).to be(true) + expect(YAML.safe_load(result.fetch(:output))).to eq( + "community_bridge" => nil, + "github" => ["acme"], + "tidelift" => "rubygems/example" + ) + end + it "conforms to the slice-183 YAML polyglot backend feature profile fixtures" do fixture = read_json( fixtures_root.join( diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11e8a24..12ca307 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../gems/ast-merge/spec/spec_helper" +require_relative "../gems/kettle-jem/spec/spec_helper" require_relative "../gems/tree_haver/spec/spec_helper" require_relative "../gems/text-merge/spec/spec_helper" require_relative "../gems/json-merge/spec/spec_helper"