From 48eae8ba51a443c7a1dd17ec53cdb4000f4b91f5 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 12:22:51 +0200 Subject: [PATCH 01/10] Trim dev-only paths out of the packaged gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `screen/screen.png` is 529 KB — 97% of every download — for a README demo image referenced as a repo-relative path, so GitHub renders it from the repo and nothing ever reads it out of the package. `.github/` is CI config. Packaged: 21 files / 548 KB -> 19 files / 44 KB. lib/, app/, config/ and tasks/ untouched. --- active_admin_datetimepicker.gemspec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec index 018908e..a716f88 100644 --- a/active_admin_datetimepicker.gemspec +++ b/active_admin_datetimepicker.gemspec @@ -16,7 +16,10 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 3.1.0' - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + # `screen/` is the README demo png — 529 KB, 97% of the published gem, + # for an image that renders from GitHub and is never read from the + # package. `.github/` is CI config. + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|screen|\.github)/}) } spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From 33012b2709e85924b065ebb66acfa00f8bf3c459 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 13:22:13 +0200 Subject: [PATCH 02/10] Give Chrome 30s to start instead of Ferrum's default 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI fails a matrix leg or two per run with Ferrum::ProcessTimeoutError: Browser did not produce websocket url within 10 seconds, try to increase `:process_timeout` Always on the first `visit`, never on a later one, and a rerun of the same commit goes green — the browser starts fine, it just does not always make 10 seconds on a GitHub runner. Raise the startup budget; `default_max_wait_time` is untouched because nothing here is waiting on an element. --- spec/support/capybara.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index e09d7e9..f728bd6 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -2,7 +2,12 @@ Capybara.server = :webrick Capybara.register_driver :cuprite do |app| - Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800]) + # process_timeout: Ferrum's default is 10s for Chrome to hand back a + # websocket URL. GitHub runners miss that often enough to fail a leg + # per run with Ferrum::ProcessTimeoutError; the browser is starting + # fine, just slowly. + Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800], + process_timeout: 30) end Capybara.javascript_driver = :cuprite Capybara.default_max_wait_time = 5 From 6dea1d5e1a8d3658395374a6af5a12cbe04cf8fc Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:10:32 +0200 Subject: [PATCH 03/10] 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 active_admin_datetimepicker.gemspec CODE_OF_CONDUCT.md Gemfile package.json Rakefile tasks/test.rake Packaged: 19 -> 12 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_datetimepicker.gemspec | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec index a716f88..537774d 100644 --- a/active_admin_datetimepicker.gemspec +++ b/active_admin_datetimepicker.gemspec @@ -16,10 +16,12 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 3.1.0' - # `screen/` is the README demo png — 529 KB, 97% of the published gem, - # for an image that renders from GitHub and is never read from the - # package. `.github/` is CI config. - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|screen|\.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 the 529 KB README png under screen/ + # ended up published in the first place. + spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", + "README.md", "LICENSE.txt"] spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From b2b4c20bb95a0b842d5ec9d234815c0fd065a474 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:16:09 +0200 Subject: [PATCH 04/10] Give Chrome the flags it needs to start in a container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier process_timeout bump was not enough — 30 seconds times out too: Ferrum::ProcessTimeoutError: Browser did not produce websocket url within 30 seconds Never receiving the websocket URL after 30s means Chrome is stuck, not slow, so raising the budget again would only lengthen the wait before the same failure. --disable-dev-shm-usage is the fix: containers mount /dev/shm at 64 MB and Chrome deadlocks once it fills. --no-sandbox goes with it because the runner is already an unprivileged user, and --disable-gpu drops a subsystem that has nothing to do in headless. process_timeout: 30 stays — it costs nothing and covers a genuinely slow start. --- spec/support/capybara.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index f728bd6..0edfed7 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -2,12 +2,20 @@ Capybara.server = :webrick Capybara.register_driver :cuprite do |app| - # process_timeout: Ferrum's default is 10s for Chrome to hand back a - # websocket URL. GitHub runners miss that often enough to fail a leg - # per run with Ferrum::ProcessTimeoutError; the browser is starting - # fine, just slowly. + # Chrome hangs at startup on GitHub runners and never hands back a + # websocket URL, failing a leg per run with + # Ferrum::ProcessTimeoutError. Raising :process_timeout alone did not + # fix it -- 30s timed out too, so the browser is stuck rather than + # slow. --disable-dev-shm-usage is the cause: containers give /dev/shm + # 64 MB and Chrome deadlocks when it runs out. --no-sandbox is needed + # because the runner already runs as an unprivileged user. Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800], - process_timeout: 30) + process_timeout: 30, + browser_options: { + 'no-sandbox': nil, + 'disable-dev-shm-usage': nil, + 'disable-gpu': nil + }) end Capybara.javascript_driver = :cuprite Capybara.default_max_wait_time = 5 From 9f562a4fb6147c410d3187460ed7e1343df0b63f Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:36:54 +0200 Subject: [PATCH 05/10] 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_datetimepicker.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec index 537774d..03f0c55 100644 --- a/active_admin_datetimepicker.gemspec +++ b/active_admin_datetimepicker.gemspec @@ -20,8 +20,7 @@ Gem::Specification.new do |spec| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how the 529 KB README png under screen/ # ended up published in the first place. - spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", - "README.md", "LICENSE.txt"] + spec.files = `git ls-files -z -- lib app config README.md LICENSE.txt`.split("\x0") spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From f7ff480bf303d573b5e803bee972ccdf8efc5fea Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:36:54 +0200 Subject: [PATCH 06/10] Fix the reversed explanation of --disable-dev-shm-usage The comment read as if the flag causes the deadlock, when it is the mitigation: the undersized 64 MB /dev/shm is the cause, and the flag moves Chrome's scratch space off it. As written it invited a future maintainer to remove the fix. --- spec/support/capybara.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 0edfed7..62c9dfb 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -6,9 +6,11 @@ # websocket URL, failing a leg per run with # Ferrum::ProcessTimeoutError. Raising :process_timeout alone did not # fix it -- 30s timed out too, so the browser is stuck rather than - # slow. --disable-dev-shm-usage is the cause: containers give /dev/shm - # 64 MB and Chrome deadlocks when it runs out. --no-sandbox is needed - # because the runner already runs as an unprivileged user. + # slow. The cause is /dev/shm: containers mount it at 64 MB and Chrome + # deadlocks once it fills, so --disable-dev-shm-usage moves that + # scratch space to /tmp. --no-sandbox is needed because the runner + # already runs as an unprivileged user, and --disable-gpu drops a + # subsystem with nothing to do in headless. Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800], process_timeout: 30, browser_options: { From 57477dacee74f3de3a7804817b9bb6a8423ac1d5 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:45:27 +0200 Subject: [PATCH 07/10] 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_datetimepicker.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec index 03f0c55..2c740bf 100644 --- a/active_admin_datetimepicker.gemspec +++ b/active_admin_datetimepicker.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how the 529 KB README png under screen/ # ended up published in the first place. - spec.files = `git ls-files -z -- lib app config README.md LICENSE.txt`.split("\x0") + spec.files = `git ls-files -z -- lib app config bin README.md LICENSE.txt`.split("\x0") spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From 8f78002b318054299b9eb144c94b03c0d7b3e838 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:48:56 +0200 Subject: [PATCH 08/10] Keep only --no-sandbox; the /dev/shm diagnosis was wrong `--disable-dev-shm-usage` is already in Ferrum's Chrome defaults (Ferrum::Browser::Options::Chrome::DEFAULT_OPTIONS), so the run that timed out at 30s was *already* started with it. It cannot have been the cause, and passing it again only put a duplicate switch on the command line. --disable-gpu is equally inert: Ferrum adds it on Windows only, and headless Chrome has no GPU subsystem to drop. That leaves --no-sandbox as the only flag in the set that changes anything, and its earlier justification was inverted too. Running as an unprivileged user is the condition under which the sandbox *works*; --no-sandbox is what you need when it cannot initialise. On ubuntu-latest, now 24.04, kernel.apparmor_restrict_unprivileged_userns=1 blocks the user-namespace sandbox for binaries with no AppArmor profile, and Chrome can hang before emitting a websocket URL. The runner is a single-tenant VM serving only this suite's dummy app, so dropping the sandbox costs nothing. Framed honestly: the flake has not reproduced since, which is not the same as proven fixed. One leg out of ~35 was failing. --- spec/support/capybara.rb | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 62c9dfb..63d45a3 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -2,22 +2,24 @@ Capybara.server = :webrick Capybara.register_driver :cuprite do |app| - # Chrome hangs at startup on GitHub runners and never hands back a - # websocket URL, failing a leg per run with - # Ferrum::ProcessTimeoutError. Raising :process_timeout alone did not - # fix it -- 30s timed out too, so the browser is stuck rather than - # slow. The cause is /dev/shm: containers mount it at 64 MB and Chrome - # deadlocks once it fills, so --disable-dev-shm-usage moves that - # scratch space to /tmp. --no-sandbox is needed because the runner - # already runs as an unprivileged user, and --disable-gpu drops a - # subsystem with nothing to do in headless. + # Chrome sometimes never hands back a websocket URL on GitHub runners + # and the leg dies with Ferrum::ProcessTimeoutError. --no-sandbox is + # the fix: ubuntu-latest is now 24.04, which ships + # kernel.apparmor_restrict_unprivileged_userns=1, and that blocks the + # user-namespace sandbox for binaries without an AppArmor profile -- + # Chrome then hangs instead of starting. These runners are + # single-tenant VMs rendering only this suite's own dummy app, so + # dropping the sandbox costs nothing here. + # + # Ferrum already passes --disable-dev-shm-usage by default + # (Ferrum::Browser::Options::Chrome::DEFAULT_OPTIONS), so /dev/shm was + # never the problem and adding the flag again changed nothing. + # + # process_timeout is kept at 30s to cover a genuinely slow start; it + # was not enough on its own. Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800], process_timeout: 30, - browser_options: { - 'no-sandbox': nil, - 'disable-dev-shm-usage': nil, - 'disable-gpu': nil - }) + browser_options: { 'no-sandbox': nil }) end Capybara.javascript_driver = :cuprite Capybara.default_max_wait_time = 5 From 1e5ce53837ae466765b99b780eac04dcbeb9fa92 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:49:42 +0200 Subject: [PATCH 09/10] 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_datetimepicker.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_datetimepicker.gemspec b/active_admin_datetimepicker.gemspec index 2c740bf..843e25f 100644 --- a/active_admin_datetimepicker.gemspec +++ b/active_admin_datetimepicker.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| # reach consumers until it is named here. The reject form needs a new # pattern every time the repo grows one, and that is how the 529 KB README png under screen/ # ended up published in the first place. - spec.files = `git ls-files -z -- lib app config bin README.md LICENSE.txt`.split("\x0") + spec.files = `git ls-files -z -- lib app vendor config exe bin README.md LICENSE.txt`.split("\x0") spec.bindir = "bin" spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] From 4d541c66146b9319ac7c4a99487ab12a3c5ae583 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 30 Sep 2026 14:57:06 +0200 Subject: [PATCH 10/10] Stop claiming a cause for the Chrome startup flake Second theory ruled out. --no-sandbox was credited with fixing it on the AppArmor-userns reasoning, but the sibling repo capybara_active_admin already passes --no-sandbox and --disable-setuid-sandbox (spec/rails_helper.rb) and still failed with Ferrum::ProcessTimeoutError on a run today. If the flag were the fix, that repo would not flake. The comment now says what is actually known: cause unestablished, both tried explanations disproved, --no-sandbox and process_timeout kept as cheap and plausibly useful rather than as a fix, rerun when it happens. --- spec/support/capybara.rb | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 63d45a3..afdcda4 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -2,21 +2,25 @@ Capybara.server = :webrick Capybara.register_driver :cuprite do |app| - # Chrome sometimes never hands back a websocket URL on GitHub runners - # and the leg dies with Ferrum::ProcessTimeoutError. --no-sandbox is - # the fix: ubuntu-latest is now 24.04, which ships - # kernel.apparmor_restrict_unprivileged_userns=1, and that blocks the - # user-namespace sandbox for binaries without an AppArmor profile -- - # Chrome then hangs instead of starting. These runners are - # single-tenant VMs rendering only this suite's own dummy app, so - # dropping the sandbox costs nothing here. + # Chrome intermittently fails to hand back a websocket URL on GitHub + # runners and the leg dies with Ferrum::ProcessTimeoutError. The cause + # is not established. Two theories were tried and both are ruled out: # - # Ferrum already passes --disable-dev-shm-usage by default - # (Ferrum::Browser::Options::Chrome::DEFAULT_OPTIONS), so /dev/shm was - # never the problem and adding the flag again changed nothing. + # * /dev/shm exhaustion -- Ferrum already passes + # --disable-dev-shm-usage by default + # (Ferrum::Browser::Options::Chrome::DEFAULT_OPTIONS), so the runs + # that failed were already using it. + # * the ubuntu-24.04 AppArmor restriction on unprivileged user + # namespaces -- the sibling repo capybara_active_admin passes + # --no-sandbox and --disable-setuid-sandbox already, and flakes + # the same way. # - # process_timeout is kept at 30s to cover a genuinely slow start; it - # was not enough on its own. + # So: --no-sandbox is kept because it is harmless on a single-tenant + # CI VM serving only this suite's dummy app and may still help, and + # process_timeout is raised from Ferrum's 10s default because a slow + # start is at least a failure mode a budget can cover. Neither is + # demonstrated to fix it. A rerun is currently the answer when it + # happens. Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800], process_timeout: 30, browser_options: { 'no-sandbox': nil })