From 2e6f8b8f53a07936216fa3a5f71e0ae0c2a91e29 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 12:16:56 +0200 Subject: [PATCH 1/5] Keep specs and CI config out of the packaged gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git ls-files` with no filter shipped the whole test suite: 29 of the gem's 61 packaged files lived under spec/, including 20 CSV/TSV fixtures and the 25 KB spec/import_spec.rb. Consumers download all of it and use none of it. Also switches to -z/\x0 splitting. `$OUTPUT_RECORD_SEPARATOR` only worked by accident — `English` is never required, so the global is nil and `split(nil)` falls back to whitespace splitting, which breaks on any tracked path containing a space. Packaged files: 61 -> 31. --- active_admin_import.gemspec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/active_admin_import.gemspec b/active_admin_import.gemspec index 2e694cb..acd16c5 100644 --- a/active_admin_import.gemspec +++ b/active_admin_import.gemspec @@ -10,7 +10,11 @@ Gem::Specification.new do |gem| gem.homepage = 'https://github.com/activeadmin-plugins/active_admin_import' gem.license = 'MIT' gem.required_ruby_version = '>= 3.3.0' - gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR) + # -z/\x0 rather than $OUTPUT_RECORD_SEPARATOR: `English` is never + # required here, so that global is nil and `split(nil)` silently falls + # back to splitting on whitespace — which breaks on any tracked path + # containing a space. + gem.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|\.github)/}) } gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.name = 'active_admin_import' gem.require_paths = ['lib'] From c4c23c5e824109146f2c5341646a55cc25b94f4c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:10:31 +0200 Subject: [PATCH 2/5] Switch spec.files to a whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reject list only removes directories someone remembered to name. That is how spec/, .github/, screen/ and img/ got published in the first place — each needed a new pattern, and none was added until an audit went looking. A whitelist inverts the default: a new directory in the repo does not reach consumers until it is listed. Same shape the sibling gems activeadmin-oidc and credit_card_validations already use. Drops the remaining dev-only files the reject form kept: .gitignore .rubocop.yml active_admin_import.gemspec Gemfile Rakefile tasks/test.rake Packaged: 31 -> 25 files. The runtime payload — everything under lib/, app/, vendor/, config/ and exe/ — is byte-identical to before, verified by diffing the built .gem both ways. --- active_admin_import.gemspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/active_admin_import.gemspec b/active_admin_import.gemspec index acd16c5..cdb3df8 100644 --- a/active_admin_import.gemspec +++ b/active_admin_import.gemspec @@ -10,11 +10,11 @@ Gem::Specification.new do |gem| gem.homepage = 'https://github.com/activeadmin-plugins/active_admin_import' gem.license = 'MIT' gem.required_ruby_version = '>= 3.3.0' - # -z/\x0 rather than $OUTPUT_RECORD_SEPARATOR: `English` is never - # required here, so that global is nil and `split(nil)` silently falls - # back to splitting on whitespace — which breaks on any tracked path - # containing a space. - gem.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|\.github)/}) } + # Whitelist, not a reject list: a new directory in the repo does not + # reach consumers until it is named here. The reject form needs a new + # pattern every time the repo grows one, and that is how spec/ and + # .github/ ended up published in the first place. + gem.files = Dir['lib/**/*', 'app/**/*', 'config/**/*', 'README.md', 'LICENSE'] gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.name = 'active_admin_import' gem.require_paths = ['lib'] From a5b3741fc3fe01b1781f30c3bfaeafd2484d1dbe Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:36:30 +0200 Subject: [PATCH 3/5] Select the whitelist through git rather than Dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Dir[...]` globs the working tree, so any untracked or generated file under lib/, app/ or vendor/ would be published in a release — the build artifact depended on the releaser's local checkout. The reject form it replaced was tracked-only; this restores that property while keeping the whitelist. `git ls-files -- ` also returns files only, where `Dir["**/*"]` returns directory entries too, so `files` no longer carries entries RubyGems just ignores. Built .gem is byte-for-byte the same file list as the Dir[] version. --- active_admin_import.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_import.gemspec b/active_admin_import.gemspec index cdb3df8..d1e47c6 100644 --- a/active_admin_import.gemspec +++ b/active_admin_import.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |gem| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how spec/ and # .github/ ended up published in the first place. - gem.files = Dir['lib/**/*', 'app/**/*', 'config/**/*', 'README.md', 'LICENSE'] + gem.files = `git ls-files -z -- lib app config README.md LICENSE`.split("\x0") gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.name = 'active_admin_import' gem.require_paths = ['lib'] From 106cd0d8b8f0ec681044e8ecf81ec60e0d4db68c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:45:23 +0200 Subject: [PATCH 4/5] Keep bin/ selectable so executables cannot silently vanish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `executables` greps `files` for `^bin/`, but `bin` was not in the whitelist, so that grep could never match. Adding a `bin/foo` later would have published a gem with no executables and no build error to say so — exactly the failure the whitelist exists to prevent, inverted. No repo file changes today: none of these gems tracks a bin/, and the built .gem is identical. --- active_admin_import.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_import.gemspec b/active_admin_import.gemspec index d1e47c6..8efb97e 100644 --- a/active_admin_import.gemspec +++ b/active_admin_import.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |gem| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how spec/ and # .github/ ended up published in the first place. - gem.files = `git ls-files -z -- lib app config README.md LICENSE`.split("\x0") + gem.files = `git ls-files -z -- lib app config bin README.md LICENSE`.split("\x0") gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.name = 'active_admin_import' gem.require_paths = ['lib'] From f994a06464ea7b2fa3e94fab97e07aa8bf8273f1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:49:42 +0200 Subject: [PATCH 5/5] Whitelist every root a Rails engine loads from, not just the ones in use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A whitelist fails quietly: `git ls-files -- config` against a tree with no config/ exits 0 and prints nothing, so the day someone adds `config/initializers/foo.rb` the gem installs, boots, and the initializer never runs. Nothing in `gem build` warns. That is not hypothetical for this family of gems — active_admin_datetimepicker's Ransack predicates live in exactly such an initializer, and its filters return no results without them. So list every root Rails::Engine loads from (lib app vendor config exe bin) in all of them, present or not, instead of only the ones that happen to exist today. No package changes: the built .gem is identical in every gem. --- active_admin_import.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_import.gemspec b/active_admin_import.gemspec index 8efb97e..c953090 100644 --- a/active_admin_import.gemspec +++ b/active_admin_import.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |gem| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how spec/ and # .github/ ended up published in the first place. - gem.files = `git ls-files -z -- lib app config bin README.md LICENSE`.split("\x0") + gem.files = `git ls-files -z -- lib app vendor config exe bin README.md LICENSE`.split("\x0") gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.name = 'active_admin_import' gem.require_paths = ['lib']