From cdcbb34cbeb830e9f1e576bd4b0acffcfd0db48b Mon Sep 17 00:00:00 2001 From: "developerz-ai[bot]" <286432749+developerz-ai[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:23:44 +0000 Subject: [PATCH 1/3] ci: tie the demo app's Rails defaults to the CI Rails matrix config/application.rb's load_defaults version (7.2) becomes the single source of truth for the Rails floor CI resolves: bin/rails-versions derives its floor from it instead of a hardcoded 7.2, and the new bin/rails-defaults-check fails loudly when the app's floor, the CI matrix's oldest resolved entry and the floor advertised in README.md/llms.txt/AGENTS.md diverge. The gem's advertised Rails floor (Rails 7.2+) is unchanged. Dz-Task-Id: tsk_43ec5176c615e063bb636ec79a26ccd4 --- .github/workflows/ci.yml | 13 ++++-- AGENTS.md | 3 +- bin/rails-defaults-check | 75 ++++++++++++++++++++++++++++++++++ bin/rails-versions | 70 ++++++++++++++++++++++++++++--- config/application.rb | 6 ++- spec/ci/rails_defaults_spec.rb | 43 +++++++++++++++++++ 6 files changed, 199 insertions(+), 11 deletions(-) create mode 100755 bin/rails-defaults-check create mode 100644 spec/ci/rails_defaults_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74485c3..5b191e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,8 +212,15 @@ jobs: steps: - uses: actions/checkout@v7 - id: versions - # Every stable Rails MAJOR.MINOR >= 7.2 (the demo app's floor), from rubygems.org. - run: echo "versions=$(ruby bin/rails-versions 7.2)" >> "$GITHUB_OUTPUT" + name: Resolve the Rails matrix and guard the floor + # Every stable Rails MAJOR.MINOR >= the demo app's floor (config/application.rb), from rubygems.org. + # The floor guard runs in this same step (no extra step): the app's floor, the floor the gem + # advertises (README.md, llms.txt) and the matrix's oldest entry must agree, or this job - + # and with it CI OK - fails. + run: | + versions=$(ruby bin/rails-versions) + echo "versions=$versions" >> "$GITHUB_OUTPUT" + ruby bin/rails-defaults-check "$versions" rails: name: Rails ${{ matrix.rails }} @@ -265,7 +272,7 @@ jobs: if: always() # Every job here must pass; nothing is allowed to fail. Ruby head and Rails edge run monthly in # canary.yml, where the developerz.ai `improve` checkup picks up what they find. - needs: [lint, audit, test, gem-specs, native, strict-ext, sanitizers, fallback, engines, rails, package] + needs: [lint, audit, test, gem-specs, native, strict-ext, sanitizers, fallback, engines, rails-versions, rails, package] runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/AGENTS.md b/AGENTS.md index b7b6d7f..3b9f6a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Writing ruby2html templates/components (here or in a user app)? Load [`llms.txt` | Demo Rails app | `app/`, `config/`, `db/` | Not packaged; loads the gem via `require_relative '../lib/gem/ruby2html'` | | Specs | `spec/gem/` (renderer, attributes, escaping, `render_parity_spec.rb`; `render_spec_helper.rb` runs each group against Native **and** PureRuby in one process), `spec/gem/rails/` (handler, compiler, components, middleware), `spec/features/`, `spec/benchmarks/` | Rails-booting via `spec/rails_helper.rb`; `rake spec:standalone` runs `spec/gem` minus `rails/` without Rails | | Benchmarks | `bench/*.rb` (standalone), `spec/benchmarks/requests_spec.rb` (request-level vs ERB/Slim/Phlex) | | -| Scripts | `bin/gem-smoke`, `bin/changelog-notes`, `bin/build-ext` | Packaging smoke test; print a CHANGELOG section; CI-style extension builds (`--werror`, `--cc=clang`, `--sanitize`) | +| Scripts | `bin/gem-smoke`, `bin/changelog-notes`, `bin/build-ext`, `bin/rails-versions`, `bin/rails-defaults-check` | Packaging smoke test; print a CHANGELOG section; CI-style extension builds (`--werror`, `--cc=clang`, `--sanitize`); the demo app's Rails floor (`bin/rails-versions --floor`) and the CI guard that keeps the app, the advertised floor and the matrix in sync (`bin/rails-defaults-check`) | - Supported: all maintained CRuby versions (>= 3.1, gemspec floor; CI tracks the official list — currently 3.3, 3.4, 4.0), JRuby (pure Ruby + the Java escaper), TruffleRuby (pure Ruby), Rails 7.2+. - Dev Ruby: `.ruby-version` (latest stable). The CRuby CI matrix is generated automatically from the maintained-versions list, so a new release (e.g. a new 4.x) joins CI with no edit. @@ -113,6 +113,7 @@ Writing ruby2html templates/components (here or in a user app)? Load [`llms.txt` 11. Public API used downstream — `Render.new(ctx, &blk)`, `#render`, `#plain`, `#component`, tag methods, `Ruby2html.escape_html`, `Ruby2html.native?`, `Ruby2html::Component`, `RailsHelper#html`, `ComponentHelper#html` / `#__ruby2html_renderer__`, the `.rb` handler, `HtmlBeautifierMiddleware` — breaking changes need a CHANGELOG entry ("Upgrading to 2.0") and a major bump. 12. Style: rubocop-rails_config, single quotes, `# frozen_string_literal: true`, nested modules, Rails-style indented `private` methods. C: gnu99, no warnings under `-Wall -Wextra`. 13. Never commit `lib/ruby2html/*.so`, `*.jar`, `*.class`, `tmp/`, `pkg/`, `*.gem`, `log/`. +14. **The demo app's Rails floor is derived, not hardcoded.** `config.load_defaults` in `config/application.rb` is the single source of truth: `bin/rails-versions` parses it (`--floor` prints it, no network) and derives the CI matrix from it. The matrix's oldest entry and the floor the gem advertises (`Rails X.Y+` in README.md and llms.txt) must equal the app's floor — `bin/rails-defaults-check` fails CI, naming every version compared, when they drift. A matrix entry older than the app's `load_defaults` cannot boot the demo app (`RuntimeError: Unknown version`); Rails edge is not a matrix entry (it runs monthly in `canary.yml`). ## Maintenance model diff --git a/bin/rails-defaults-check b/bin/rails-defaults-check new file mode 100755 index 0000000..7521b75 --- /dev/null +++ b/bin/rails-defaults-check @@ -0,0 +1,75 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Guard: the demo app's Rails floor, the floor the gem advertises and the +# oldest Rails line of the CI matrix must be the same version. +# +# ruby bin/rails-defaults-check # app floor vs README.md + llms.txt +# ruby bin/rails-defaults-check '["7.2","8.0"]' # ... and vs the matrix's oldest entry +# +# Exits 1 and names every version compared when they drift, 0 when they +# agree. Stdlib only: no Bundler, no Rails, no network. + +require 'json' +require 'open3' +require 'rbconfig' + +ROOT = File.expand_path('..', __dir__) +ADVERTISED_FILES = %w[README.md llms.txt].freeze +ADVERTISED_FLOOR = /Rails (\d+(?:\.\d+)*)\+/ + +def app_floor + out, status = Open3.capture2e(RbConfig.ruby, File.join(ROOT, 'bin', 'rails-versions'), '--floor') + abort "bin/rails-versions --floor failed:\n#{out}" unless status.success? + + out.strip +end + +def advertised_floors(file) + source = File.read(File.join(ROOT, file)) + source.scan(ADVERTISED_FLOOR).flatten.uniq +end + +def matrix_floor(json) + lines = JSON.parse(json) + unless lines.is_a?(Array) && !lines.empty? && lines.all?(String) + abort "Rails matrix must be a non-empty array of versions, got: #{json}" + end + + lines.min_by { |line| Gem::Version.new(line) } +rescue JSON::ParserError, ArgumentError => e + abort "Could not read the Rails matrix #{json.inspect}: #{e.message}" +end + +def same_version?(one, other) + Gem::Version.new(one) == Gem::Version.new(other) +rescue ArgumentError + one == other +end + +floor = app_floor +reported = [['app floor (config/application.rb)', floor]] +problems = [] + +ADVERTISED_FILES.each do |file| + versions = advertised_floors(file) + reported << [file, versions.empty? ? 'no "Rails X.Y+" advertised' : versions.join(', ')] + problems << file unless versions.size == 1 && same_version?(versions.first, floor) +end + +matrix = ARGV[0] +if matrix + oldest = matrix_floor(matrix) + reported << ['CI matrix (oldest entry)', oldest] + problems << 'CI matrix' unless same_version?(oldest, floor) +end + +if problems.empty? + puts "Rails floor #{floor}: #{reported.map(&:first).join(', ')} agree." + exit 0 +end + +warn 'Rails floor mismatch:' +reported.each { |label, version| warn " #{label}: #{version}" } +warn "Align #{problems.join(', ')} with config/application.rb: bin/rails-versions derives the CI matrix from it." +exit 1 diff --git a/bin/rails-versions b/bin/rails-versions index 6c063f4..a3d95c4 100755 --- a/bin/rails-versions +++ b/bin/rails-versions @@ -5,17 +5,75 @@ # rubygems.org: every MAJOR.MINOR >= MIN that has a stable release, e.g. # bin/rails-versions 7.2 # => ["7.2","8.0","8.1"] # Used by CI so new Rails releases join the matrix without editing anything. +# +# The floor is the demo app's Rails floor (config/application.rb), the single +# source of truth: the matrix always starts at the Rails the app boots on, so +# the app and the matrix cannot drift apart. An explicit MIN still overrides +# the listing. +# +# bin/rails-versions # => ["7.2","8.0","8.1"] +# bin/rails-versions 8.0 # => ["8.0","8.1"] +# bin/rails-versions --floor # => 7.2 (no network) require 'json' require 'net/http' -min = Gem::Version.new(ARGV.fetch(0, '7.2')) +ROOT = File.expand_path('..', __dir__) +APP_CONFIG = File.join(ROOT, 'config', 'application.rb') +LOAD_DEFAULTS = /^\s*config\.load_defaults\s+(?:"([^"]+)"|'([^']+)'|([0-9]+(?:\.[0-9]+)*))/ +USAGE = <<~TEXT + Usage: bin/rails-versions [--floor] [MIN] + + --floor print the demo app's Rails floor (config/application.rb) and exit + MIN list only Rails lines >= MIN (default: the demo app's floor) +TEXT + +def app_floor + source = File.read(APP_CONFIG) + match = source.match(LOAD_DEFAULTS) + version = match && (match[1] || match[2] || match[3]) + abort "Could not parse `config.load_defaults` from #{APP_CONFIG}" if version.nil? || version.empty? + + version +end + +def parse_version(version) + Gem::Version.new(version) +rescue ArgumentError + abort "Invalid Rails version: #{version.inspect}" +end + +floor = app_floor +min_arg = nil +floor_only = false + +ARGV.each do |arg| + case arg + when '--floor' + floor_only = true + when /\A-/ + abort "Unknown option #{arg.inspect}\n#{USAGE}" + else + abort "Only one MIN argument is allowed\n#{USAGE}" unless min_arg.nil? + + min_arg = arg + end +end + +if floor_only + abort "--floor does not take a MIN argument\n#{USAGE}" unless min_arg.nil? + + puts floor + exit 0 +end + +min = parse_version(min_arg || floor) body = Net::HTTP.get(URI('https://rubygems.org/api/v1/versions/rails.json')) lines = JSON.parse(body) - .reject { |v| v['prerelease'] } - .map { |v| Gem::Version.new(v['number']).segments.first(2).join('.') } - .uniq - .select { |line| Gem::Version.new(line) >= min } - .sort_by { |line| Gem::Version.new(line) } + .reject { |v| v['prerelease'] } + .map { |v| Gem::Version.new(v['number']).segments.first(2).join('.') } + .uniq + .select { |line| Gem::Version.new(line) >= min } + .sort_by { |line| Gem::Version.new(line) } abort 'No Rails versions found' if lines.empty? puts JSON.generate(lines) diff --git a/config/application.rb b/config/application.rb index d8994d1..f34d976 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,7 +23,11 @@ module Ruby2html class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. + # The demo app's Rails floor, and the single source of truth for it: + # bin/rails-versions derives the CI matrix's oldest entry from this value, + # and bin/rails-defaults-check fails CI when the floor the gem advertises + # (README.md:26, llms.txt:5) drifts from it. AGENTS.md:22 states the same + # floor. Raising this raises the app's floor: the owner's call, not CI's. config.load_defaults 7.2 # Please, add to the `ignore` list any other `lib` subdirectories that do diff --git a/spec/ci/rails_defaults_spec.rb b/spec/ci/rails_defaults_spec.rb new file mode 100644 index 0000000..5c07e15 --- /dev/null +++ b/spec/ci/rails_defaults_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'json' +require 'open3' +require 'rbconfig' + +# Offline guard for the demo app's Rails floor: the app, the floor the gem +# advertises and the CI matrix must agree. No Rails, no network. +RSpec.describe 'bin/rails-defaults-check' do + let(:root) { File.expand_path('../..', __dir__) } + let(:app_config) { File.join(root, 'config', 'application.rb') } + let(:app_floor) { File.read(app_config)[/config\.load_defaults\s+(\d+(?:\.\d+)*)/, 1] } + # A Rails version the app does not use: the next major, derived so a + # legitimate floor change needs no edit here. + let(:divergent_floor) { "#{app_floor.to_i + 1}.0" } + + def run_script(*args) + Open3.capture2e(RbConfig.ruby, *args, chdir: root) + end + + it 'derives the Rails floor from the demo app' do + stdout, status = run_script(File.join(root, 'bin', 'rails-versions'), '--floor') + + expect(status).to be_success + expect(app_floor).to match(/\A\d+(\.\d+)*\z/) + expect(stdout.strip).to eq(app_floor) + end + + it 'passes when the app floor and the advertised floor agree' do + stdout, status = run_script(File.join(root, 'bin', 'rails-defaults-check')) + + expect(status).to be_success + expect(stdout).to include(app_floor) + end + + it 'fails when the CI matrix starts at another Rails than the app' do + stdout, status = run_script(File.join(root, 'bin', 'rails-defaults-check'), JSON.generate([divergent_floor])) + + expect(status).not_to be_success + expect(stdout).to include(app_floor).and include(divergent_floor) + expect(stdout).to include('CI matrix') + end +end From 09a4dc126f3a7dc4c7b4e80e62a6290a5f71dea6 Mon Sep 17 00:00:00 2001 From: "developerz-ai[bot]" <286432749+developerz-ai[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:07:23 +0000 Subject: [PATCH 2/3] chore(babysit): land Gemfile.lock Follow-up intent: Fix failing CI on sebyx07/ruby2html#22 Dz-Task-Id: tsk_91807250c18b8bb6f802584975b1aaa4 --- Gemfile.lock | 134 +++++++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b9f16d1..d4cea5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,29 +3,29 @@ GEM specs: action_text-trix (2.1.19) railties - actioncable (8.1.4) - actionpack (= 8.1.4) - activesupport (= 8.1.4) + actioncable (8.1.4.1) + actionpack (= 8.1.4.1) + activesupport (= 8.1.4.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.4) - actionpack (= 8.1.4) - activejob (= 8.1.4) - activerecord (= 8.1.4) - activestorage (= 8.1.4) - activesupport (= 8.1.4) + actionmailbox (8.1.4.1) + actionpack (= 8.1.4.1) + activejob (= 8.1.4.1) + activerecord (= 8.1.4.1) + activestorage (= 8.1.4.1) + activesupport (= 8.1.4.1) mail (>= 2.8.0) - actionmailer (8.1.4) - actionpack (= 8.1.4) - actionview (= 8.1.4) - activejob (= 8.1.4) - activesupport (= 8.1.4) + actionmailer (8.1.4.1) + actionpack (= 8.1.4.1) + actionview (= 8.1.4.1) + activejob (= 8.1.4.1) + activesupport (= 8.1.4.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.4) - actionview (= 8.1.4) - activesupport (= 8.1.4) + actionpack (8.1.4.1) + actionview (= 8.1.4.1) + activesupport (= 8.1.4.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.4) + actiontext (8.1.4.1) action_text-trix (~> 2.1.15) - actionpack (= 8.1.4) - activerecord (= 8.1.4) - activestorage (= 8.1.4) - activesupport (= 8.1.4) + actionpack (= 8.1.4.1) + activerecord (= 8.1.4.1) + activestorage (= 8.1.4.1) + activesupport (= 8.1.4.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.4) - activesupport (= 8.1.4) + actionview (8.1.4.1) + activesupport (= 8.1.4.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (8.1.4) - activesupport (= 8.1.4) + rails-html-sanitizer (~> 1.6, >= 1.7.2) + activejob (8.1.4.1) + activesupport (= 8.1.4.1) globalid (>= 0.3.6) - activemodel (8.1.4) - activesupport (= 8.1.4) - activerecord (8.1.4) - activemodel (= 8.1.4) - activesupport (= 8.1.4) + activemodel (8.1.4.1) + activesupport (= 8.1.4.1) + activerecord (8.1.4.1) + activemodel (= 8.1.4.1) + activesupport (= 8.1.4.1) timeout (>= 0.4.0) - activestorage (8.1.4) - actionpack (= 8.1.4) - activejob (= 8.1.4) - activerecord (= 8.1.4) - activesupport (= 8.1.4) + activestorage (8.1.4.1) + actionpack (= 8.1.4.1) + activejob (= 8.1.4.1) + activerecord (= 8.1.4.1) + activesupport (= 8.1.4.1) marcel (~> 1.0) - activesupport (8.1.4) + activesupport (8.1.4.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -201,20 +201,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.4) - actioncable (= 8.1.4) - actionmailbox (= 8.1.4) - actionmailer (= 8.1.4) - actionpack (= 8.1.4) - actiontext (= 8.1.4) - actionview (= 8.1.4) - activejob (= 8.1.4) - activemodel (= 8.1.4) - activerecord (= 8.1.4) - activestorage (= 8.1.4) - activesupport (= 8.1.4) + rails (8.1.4.1) + actioncable (= 8.1.4.1) + actionmailbox (= 8.1.4.1) + actionmailer (= 8.1.4.1) + actionpack (= 8.1.4.1) + actiontext (= 8.1.4.1) + actionview (= 8.1.4.1) + activejob (= 8.1.4.1) + activemodel (= 8.1.4.1) + activerecord (= 8.1.4.1) + activestorage (= 8.1.4.1) + activesupport (= 8.1.4.1) bundler (>= 1.15.0) - railties (= 8.1.4) + railties (= 8.1.4.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -222,9 +222,9 @@ GEM rails-html-sanitizer (1.7.2) loofah (~> 2.25, >= 2.25.3) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.4) - actionpack (= 8.1.4) - activesupport (= 8.1.4) + railties (8.1.4.1) + actionpack (= 8.1.4.1) + activesupport (= 8.1.4.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -404,17 +404,17 @@ DEPENDENCIES CHECKSUMS action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 - actioncable (8.1.4) sha256=cfcd72f8c861e01ccdc96d772ad8c5ad6ed0ea7c571aef81a794b6027eaa7f11 - actionmailbox (8.1.4) sha256=9c4525e577ebb671ce558f3ea78b8a634f64e56a0e6e8d105b28b873793a06eb - actionmailer (8.1.4) sha256=f82d95d6762ae46ea38f221d3363639266af4f8e56d50e2bd367558674baf093 - actionpack (8.1.4) sha256=245493cc51be195a7645e6fc184880fa7e5b32ff1a08e053ee31f58c76e30ea4 - actiontext (8.1.4) sha256=33ee99bea7da6af911fc7d98b17013a8e1ec62aa01b20b20e9a5366f40dbf644 - actionview (8.1.4) sha256=eff1efd07c4d969abad288322b423bcb67fdbab5e324c91ca37352b80f226076 - activejob (8.1.4) sha256=4ed5b2ed8d4cc5c8f5f09becfef8346f202a4884eee0c8c73cae5ac378824099 - activemodel (8.1.4) sha256=9a0906c127bd118aa5f06157db12b8f750da83dccee72db2f658547a0ae7a0e8 - activerecord (8.1.4) sha256=3b4957ea7f246c9231bd7faa909312eac03a9101aaf87977b6c52733ff00b04f - activestorage (8.1.4) sha256=f4956f3ec9ee63e5e0b332b57c877cb627f82b56f4cc902279310d2aa285c7a8 - activesupport (8.1.4) sha256=891dd3fe43c4fc8391934b237b1189bb1f661c932a84a840043db9d46a4e80df + actioncable (8.1.4.1) sha256=007670f7297973e94809e9f32a56624a7aa60b6e9d6696cd078064ce86d1c7fc + actionmailbox (8.1.4.1) sha256=18b43dd842bf3a3e31b8e38b7917c21e48c128fc8b263c893481cc66ee1a8f78 + actionmailer (8.1.4.1) sha256=a7b71dedf744e48dd6751e97350634edce9a2f7beeceac332dcccb91729ad066 + actionpack (8.1.4.1) sha256=8c6f2e7cbd2653ef0539bde1ff20eb3e2e264e366affe56f0ba7f06d3670cb4b + actiontext (8.1.4.1) sha256=fb59611dc16e2b6bbfcec66793acfae6cdd64b1e35ef3f9175424f31e62c38da + actionview (8.1.4.1) sha256=d25b4d9cf8d0c0195ffcfe8b2cf3fcbe6fea54fd7c17f1a9cb8182881463f12b + activejob (8.1.4.1) sha256=82c4c9311ea1016d5f7f134a0c5f9b6527cad5a6237f5f3f60aa17984ef36229 + activemodel (8.1.4.1) sha256=063caf105372fbc98b2d789e4be23de63ac5c043f7d9330f04e2caddb7012805 + activerecord (8.1.4.1) sha256=bd65599ca0b5d210937f835d42c7aff69d271741c75aab022730be6cbce67e87 + activestorage (8.1.4.1) sha256=0f4e2f114f3584723a80a2c089aacc6170e765a0b2c385c9782e26ed78f05c66 + activesupport (8.1.4.1) sha256=16ac4f341ca65c85a1fcdb8ac73f68891aae9e3c4a6f9c3a27daed8b00017ed7 addressable (2.9.1) sha256=f1212365945455c84e8d031eea40fc6bedf0c663a78d95b3ab93591040bc5c35 ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b @@ -484,10 +484,10 @@ CHECKSUMS rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 rack-test (2.3.0) sha256=b118d177cc002c572fc1b6fd8d19c9ed5881a36e8d6f689f9e618f9f861241e8 rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 - rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b + rails (8.1.4.1) sha256=365c3a0ad2713bd6e60628e5e826622d4ca8dee60b5a7e560e9b20844e9650bc rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d rails-html-sanitizer (1.7.2) sha256=a21ef45b7a6639fc8e2282473c547514cb86ae271616162a946bf89148fa2424 - railties (8.1.4) sha256=b5e7a8e8984e33114d68af62227971960aae1e6c02bd7c9d826ffad97fff583c + railties (8.1.4.1) sha256=ff2ef4b5a62cbc89811f88be11c776e2ed71b14dd012eae6a494d37994aa9782 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rake-compiler (1.3.2) sha256=12fed13fa07f9caee829599935e0332ba76e2047a617f8f7918a222d68427760 From 35271dd6069728bd8a8cfe9bdaad74277abbd768 Mon Sep 17 00:00:00 2001 From: "developerz-ai[bot]" <286432749+developerz-ai[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:39:33 +0000 Subject: [PATCH 3/3] chore(babysit): land AGENTS.md (+3 more) Follow-up intent: Address review findings on sebyx07/ruby2html#22 Dz-Task-Id: tsk_40c49f300874dbad5e9d25a42ee77923 --- AGENTS.md | 2 +- bin/rails-defaults-check | 7 +++--- config/application.rb | 4 ++-- spec/ci/rails_defaults_spec.rb | 41 +++++++++++++++++++++++++++++++--- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3b9f6a8..c491419 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,7 +113,7 @@ Writing ruby2html templates/components (here or in a user app)? Load [`llms.txt` 11. Public API used downstream — `Render.new(ctx, &blk)`, `#render`, `#plain`, `#component`, tag methods, `Ruby2html.escape_html`, `Ruby2html.native?`, `Ruby2html::Component`, `RailsHelper#html`, `ComponentHelper#html` / `#__ruby2html_renderer__`, the `.rb` handler, `HtmlBeautifierMiddleware` — breaking changes need a CHANGELOG entry ("Upgrading to 2.0") and a major bump. 12. Style: rubocop-rails_config, single quotes, `# frozen_string_literal: true`, nested modules, Rails-style indented `private` methods. C: gnu99, no warnings under `-Wall -Wextra`. 13. Never commit `lib/ruby2html/*.so`, `*.jar`, `*.class`, `tmp/`, `pkg/`, `*.gem`, `log/`. -14. **The demo app's Rails floor is derived, not hardcoded.** `config.load_defaults` in `config/application.rb` is the single source of truth: `bin/rails-versions` parses it (`--floor` prints it, no network) and derives the CI matrix from it. The matrix's oldest entry and the floor the gem advertises (`Rails X.Y+` in README.md and llms.txt) must equal the app's floor — `bin/rails-defaults-check` fails CI, naming every version compared, when they drift. A matrix entry older than the app's `load_defaults` cannot boot the demo app (`RuntimeError: Unknown version`); Rails edge is not a matrix entry (it runs monthly in `canary.yml`). +14. **The demo app's Rails floor is derived, not hardcoded.** `config.load_defaults` in `config/application.rb` is the single source of truth: `bin/rails-versions` parses it (`--floor` prints it, no network) and derives the CI matrix from it. The matrix's oldest entry and the floor the gem advertises (`Rails X.Y+` in README.md, llms.txt and AGENTS.md) must equal the app's floor — `bin/rails-defaults-check` fails CI, naming every version compared, when they drift. A matrix entry older than the app's `load_defaults` cannot boot the demo app (`RuntimeError: Unknown version`); Rails edge is not a matrix entry (it runs monthly in `canary.yml`). ## Maintenance model diff --git a/bin/rails-defaults-check b/bin/rails-defaults-check index 7521b75..5121266 100755 --- a/bin/rails-defaults-check +++ b/bin/rails-defaults-check @@ -4,7 +4,7 @@ # Guard: the demo app's Rails floor, the floor the gem advertises and the # oldest Rails line of the CI matrix must be the same version. # -# ruby bin/rails-defaults-check # app floor vs README.md + llms.txt +# ruby bin/rails-defaults-check # app floor vs README.md, llms.txt, AGENTS.md # ruby bin/rails-defaults-check '["7.2","8.0"]' # ... and vs the matrix's oldest entry # # Exits 1 and names every version compared when they drift, 0 when they @@ -15,7 +15,7 @@ require 'open3' require 'rbconfig' ROOT = File.expand_path('..', __dir__) -ADVERTISED_FILES = %w[README.md llms.txt].freeze +ADVERTISED_FILES = %w[README.md llms.txt AGENTS.md].freeze ADVERTISED_FLOOR = /Rails (\d+(?:\.\d+)*)\+/ def app_floor @@ -65,7 +65,8 @@ if matrix end if problems.empty? - puts "Rails floor #{floor}: #{reported.map(&:first).join(', ')} agree." + summary = reported.map { |label, version| "#{label}: #{version}" }.join(', ') + puts "Rails floor #{floor}: #{summary} agree." exit 0 end diff --git a/config/application.rb b/config/application.rb index f34d976..78592a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,8 +26,8 @@ class Application < Rails::Application # The demo app's Rails floor, and the single source of truth for it: # bin/rails-versions derives the CI matrix's oldest entry from this value, # and bin/rails-defaults-check fails CI when the floor the gem advertises - # (README.md:26, llms.txt:5) drifts from it. AGENTS.md:22 states the same - # floor. Raising this raises the app's floor: the owner's call, not CI's. + # (README.md:26, llms.txt:5, AGENTS.md:22) drifts from it. Raising this + # raises the app's floor: the owner's call, not CI's. config.load_defaults 7.2 # Please, add to the `ignore` list any other `lib` subdirectories that do diff --git a/spec/ci/rails_defaults_spec.rb b/spec/ci/rails_defaults_spec.rb index 5c07e15..8503956 100644 --- a/spec/ci/rails_defaults_spec.rb +++ b/spec/ci/rails_defaults_spec.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true +require 'fileutils' require 'json' require 'open3' require 'rbconfig' +require 'tmpdir' # Offline guard for the demo app's Rails floor: the app, the floor the gem # advertises and the CI matrix must agree. No Rails, no network. @@ -10,6 +12,9 @@ let(:root) { File.expand_path('../..', __dir__) } let(:app_config) { File.join(root, 'config', 'application.rb') } let(:app_floor) { File.read(app_config)[/config\.load_defaults\s+(\d+(?:\.\d+)*)/, 1] } + # The files that advertise the floor the gem supports: the guard compares + # each of them with the app's floor and names the ones that drift. + let(:advertised_files) { %w[README.md llms.txt AGENTS.md] } # A Rails version the app does not use: the next major, derived so a # legitimate floor change needs no edit here. let(:divergent_floor) { "#{app_floor.to_i + 1}.0" } @@ -18,6 +23,18 @@ def run_script(*args) Open3.capture2e(RbConfig.ruby, *args, chdir: root) end + # A copy of the files the guard reads, so a drifted copy can be checked + # without touching the checkout. + def copy_guard_tree(dir) + FileUtils.mkdir_p(File.join(dir, 'bin')) + FileUtils.mkdir_p(File.join(dir, 'config')) + FileUtils.cp(app_config, File.join(dir, 'config', 'application.rb')) + %w[rails-defaults-check rails-versions].each do |script| + FileUtils.cp(File.join(root, 'bin', script), File.join(dir, 'bin', script)) + end + advertised_files.each { |file| FileUtils.cp(File.join(root, file), File.join(dir, file)) } + end + it 'derives the Rails floor from the demo app' do stdout, status = run_script(File.join(root, 'bin', 'rails-versions'), '--floor') @@ -30,14 +47,32 @@ def run_script(*args) stdout, status = run_script(File.join(root, 'bin', 'rails-defaults-check')) expect(status).to be_success - expect(stdout).to include(app_floor) + expect(stdout).to include("app floor (config/application.rb): #{app_floor}") + # Every advertised file is compared and reported with the floor found in + # it: a guard that stopped comparing them would not name them at all. + advertised_files.each { |file| expect(stdout).to include("#{file}: #{app_floor}") } + end + + it 'fails when an advertised file drifts from the app floor' do + Dir.mktmpdir do |dir| + copy_guard_tree(dir) + File.write(File.join(dir, 'AGENTS.md'), "Rails #{divergent_floor}+\n") + + stdout, status = run_script(File.join(dir, 'bin', 'rails-defaults-check')) + + expect(status).not_to be_success + expect(stdout).to include("app floor (config/application.rb): #{app_floor}") + expect(stdout).to include("AGENTS.md: #{divergent_floor}") + expect(stdout).to include('Align AGENTS.md with config/application.rb') + end end it 'fails when the CI matrix starts at another Rails than the app' do stdout, status = run_script(File.join(root, 'bin', 'rails-defaults-check'), JSON.generate([divergent_floor])) expect(status).not_to be_success - expect(stdout).to include(app_floor).and include(divergent_floor) - expect(stdout).to include('CI matrix') + expect(stdout).to include("app floor (config/application.rb): #{app_floor}") + expect(stdout).to include("CI matrix (oldest entry): #{divergent_floor}") + expect(stdout).to include('Align CI matrix with config/application.rb') end end