From e1e9a01720ca8d13db28a29513c7e06b2b27a9df Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sat, 22 Nov 2025 16:58:14 +0100 Subject: [PATCH 001/107] Make open source the default, and check for sass instead --- Dockerfile | 2 +- Dockerfile.dev | 2 +- Gemfile | 4 +--- Gemfile.lock | 14 ++------------ bin/rails | 6 ------ config/application.rb | 2 ++ config/ci.rb | 3 +-- config/initializers/authentication.rb | 2 -- config/routes.rb | 3 ++- lib/bootstrap.rb | 5 ----- lib/fizzy.rb | 5 +++++ test/controllers/sessions_controller_test.rb | 2 +- test/test_helper.rb | 4 ---- 13 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 config/initializers/authentication.rb delete mode 100644 lib/bootstrap.rb create mode 100644 lib/fizzy.rb diff --git a/Dockerfile b/Dockerfile index 6341f3fc19..3f272de4e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apt-get update -qq && \ # Install application gems COPY Gemfile Gemfile.lock .ruby-version ./ -COPY lib/bootstrap.rb ./lib/bootstrap.rb +COPY lib/fizzy.rb ./lib/fizzy.rb COPY gems ./gems/ RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-permabundle-${RUBY_VERSION},sharing=locked,target=/permabundle \ gem install bundler && \ diff --git a/Dockerfile.dev b/Dockerfile.dev index 75b284570a..9c20fcda5d 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -18,7 +18,7 @@ RUN apt-get update -qq && \ # Install application gems COPY Gemfile Gemfile.lock .ruby-version ./ -COPY lib/bootstrap.rb ./lib/bootstrap.rb +COPY lib/fizzy.rb ./lib/fizzy.rb COPY gems ./gems/ RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-devbundle-${RUBY_VERSION},sharing=locked,target=/devbundle \ gem install bundler foreman && \ diff --git a/Gemfile b/Gemfile index d008f49073..bdd047171a 100644 --- a/Gemfile +++ b/Gemfile @@ -76,8 +76,6 @@ group :test do gem "mocha" end -require_relative "lib/bootstrap" -unless Bootstrap.oss_config? - eval_gemfile "gems/fizzy-saas/Gemfile" +group :saas do gem "fizzy-saas", path: "gems/fizzy-saas" end diff --git a/Gemfile.lock b/Gemfile.lock index f8001333cc..6b7771fd34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,3 @@ -GIT - remote: https://github.com/basecamp/queenbee-plugin - revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f - ref: eb01c697de1ad028afc65cc7d9b5345a7a8e849f - specs: - queenbee (3.2.0) - activeresource - builder - rexml - GIT remote: https://github.com/basecamp/rails-structured-logging revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6 @@ -387,6 +377,8 @@ GEM public_suffix (7.0.0) puma (7.1.0) nio4r (~> 2.0) + queenbee (1.5.1) + json (> 1.8) raabro (1.4.0) racc (1.8.1) rack (3.2.4) @@ -585,7 +577,6 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - activeresource autotuner aws-sdk-s3 bcrypt (~> 3.1.7) @@ -612,7 +603,6 @@ DEPENDENCIES prometheus-client-mmap (~> 1.3) propshaft puma (>= 5.0) - queenbee! rack-mini-profiler rails! rails_structured_logging! diff --git a/bin/rails b/bin/rails index ab22dd7709..0739660237 100755 --- a/bin/rails +++ b/bin/rails @@ -1,10 +1,4 @@ #!/usr/bin/env ruby -require_relative "../lib/bootstrap" -if !Bootstrap.oss_config? - # default from rails/test_unit/runner.rb but adding the saas gem test files - ENV["DEFAULT_TEST"] = "{gems/fizzy-saas/,}test/**/*_test.rb" - ENV["DEFAULT_TEST_EXCLUDE"] = "{gems/fizzy-saas/,}test/{system,dummy,fixtures}/**/*_test.rb" -end APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/config/application.rb b/config/application.rb index 27177a5cb9..0399caacac 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,5 +1,7 @@ require_relative "boot" require "rails/all" +require_relative "../lib/fizzy" + Bundler.require(*Rails.groups) module Fizzy diff --git a/config/ci.rb b/config/ci.rb index f6cbbf953d..5d518c21c7 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -10,8 +10,7 @@ step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Security: Gitleaks audit", "bin/gitleaks-audit" - step "Tests: Rails: SaaS config", "bin/rails test" - step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test" + step "Tests: Rails", "bin/rails test" step "Tests: System", "bin/rails test:system" if success? diff --git a/config/initializers/authentication.rb b/config/initializers/authentication.rb deleted file mode 100644 index 5556a9661e..0000000000 --- a/config/initializers/authentication.rb +++ /dev/null @@ -1,2 +0,0 @@ -require "bootstrap" -Rails.application.config.x.oss_config = Bootstrap.oss_config? diff --git a/config/routes.rb b/config/routes.rb index 73acfa75ed..87fe32b7a3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,7 +224,8 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "pwa#service_worker" - unless Rails.application.config.x.oss_config + # TODO: Can we move this just to the engine + if Fizzy.saas? mount Fizzy::Saas::Engine, at: "/", as: "saas" end diff --git a/lib/bootstrap.rb b/lib/bootstrap.rb deleted file mode 100644 index 6d52f3f49e..0000000000 --- a/lib/bootstrap.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Bootstrap - def self.oss_config? - ENV.fetch("OSS_CONFIG", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__)) - end -end diff --git a/lib/fizzy.rb b/lib/fizzy.rb new file mode 100644 index 0000000000..2a3b6e2211 --- /dev/null +++ b/lib/fizzy.rb @@ -0,0 +1,5 @@ +module Fizzy + def self.saas? + defined?(Fizzy::Saas::Engine) + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 623423fdf0..7fed629008 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -21,7 +21,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest end end - unless Bootstrap.oss_config? + if Fizzy.saas? test "create for a new user" do untenanted do assert_difference -> { Identity.count }, +1 do diff --git a/test/test_helper.rb b/test/test_helper.rb index bad1fdb2a1..722d39d59f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -152,7 +152,3 @@ def uuid_v7_with_timestamp(time, seed_string) ActiveSupport.on_load(:active_record_fixture_set) do prepend(FixturesTestHelper) end - -unless Rails.application.config.x.oss_config - load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__) -end From 6134407f5e7972aee33c95b2f5bc4438972b7067 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sat, 22 Nov 2025 19:08:23 +0100 Subject: [PATCH 002/107] Make sass property depend on txt file or env var --- Gemfile | 5 ++++- Gemfile.lock | 11 ----------- lib/fizzy.rb | 3 ++- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index bdd047171a..65b7be293f 100644 --- a/Gemfile +++ b/Gemfile @@ -76,6 +76,9 @@ group :test do gem "mocha" end -group :saas do +require_relative "lib/fizzy" +if Fizzy.saas? + gem "activeresource", require: "active_resource" + gem "queenbee", git: "https://github.com/basecamp/queenbee-plugin" gem "fizzy-saas", path: "gems/fizzy-saas" end diff --git a/Gemfile.lock b/Gemfile.lock index 6b7771fd34..e2c03955c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,14 +122,6 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) -PATH - remote: gems/fizzy-saas - specs: - fizzy-saas (0.1.0) - queenbee - rails (>= 8.1.0.beta1) - rails_structured_logging - GEM remote: https://rubygems.org/ specs: @@ -377,8 +369,6 @@ GEM public_suffix (7.0.0) puma (7.1.0) nio4r (~> 2.0) - queenbee (1.5.1) - json (> 1.8) raabro (1.4.0) racc (1.8.1) rack (3.2.4) @@ -587,7 +577,6 @@ DEPENDENCIES capybara debug faker - fizzy-saas! geared_pagination (~> 1.2) image_processing (~> 1.14) importmap-rails diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 2a3b6e2211..7bda122529 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -1,5 +1,6 @@ module Fizzy def self.saas? - defined?(Fizzy::Saas::Engine) + return @saas if defined?(@saas) + @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../../tmp/saas.txt", __dir__))) end end From bab914d4e0660341be130e1be1e767c50e84d5fa Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 04:32:22 +0100 Subject: [PATCH 003/107] Move db check to the Fizzy module --- config/database.yml | 9 +++------ lib/fizzy.rb | 27 ++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/config/database.yml b/config/database.yml index 913ba7cba0..2a9a3aa2e6 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,7 +1,4 @@ <% - database_adapter = ENV.fetch("DATABASE_ADAPTER", "mysql") - use_sqlite = database_adapter == "sqlite" - if ENV["MIGRATE"].present? mysql_app_user_key = "MYSQL_ALTER_USER" mysql_app_password_key = "MYSQL_ALTER_PASSWORD" @@ -17,7 +14,7 @@ %> default: &default - <% if use_sqlite %> + <% if Fizzy.db_adapter.sqlite? %> adapter: sqlite3 pool: 5 timeout: 5000 @@ -33,7 +30,7 @@ default: &default <% end %> development: - <% if use_sqlite %> + <% if Fizzy.db_adapter.sqlite? %> primary: <<: *default database: storage/development.sqlite3 @@ -69,7 +66,7 @@ development: # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: - <% if use_sqlite %> + <% if Fizzy.db_adapter.sqlite? %> primary: <<: *default database: storage/test.sqlite3 diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 7bda122529..16a1ca3a46 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -1,6 +1,27 @@ module Fizzy - def self.saas? - return @saas if defined?(@saas) - @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../../tmp/saas.txt", __dir__))) + class << self + def saas? + return @saas if defined?(@saas) + @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../../tmp/saas.txt", __dir__))) + end + + def db_adapter + @db_adapter ||= DbAdapter.new ENV.fetch("DATABASE_ADAPTER", saas? ? "mysql" : "sqlite") + end + end + + class DbAdapter + def initialize(name) + @name = name.to_s + end + + def to_s + @name + end + + # Not using inquiry so that it works before Rails env loads. + def sqlite? + @name == "sqlite" + end end end From cf59ac1da9451cb1cc815e9e41ddd8cbcb2766ff Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:15:52 +0100 Subject: [PATCH 004/107] Split database files --- config/database.mysql.yml | 100 ++++++++++++++++++++++++++++++ config/database.sqlite.yml | 21 +++++++ config/database.yml | 124 +------------------------------------ 3 files changed, 122 insertions(+), 123 deletions(-) create mode 100644 config/database.mysql.yml create mode 100644 config/database.sqlite.yml diff --git a/config/database.mysql.yml b/config/database.mysql.yml new file mode 100644 index 0000000000..3c68fd0810 --- /dev/null +++ b/config/database.mysql.yml @@ -0,0 +1,100 @@ +<% + if ENV["MIGRATE"].present? + mysql_app_user_key = "MYSQL_ALTER_USER" + mysql_app_password_key = "MYSQL_ALTER_PASSWORD" + else + mysql_app_user_key = "MYSQL_APP_USER" + mysql_app_password_key = "MYSQL_APP_PASSWORD" + end + + mysql_app_user = ENV[mysql_app_user_key] + mysql_app_password = ENV[mysql_app_password_key] +%> + +default: &default + adapter: trilogy + host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %> + port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %> + pool: 50 + timeout: 5000 + variables: + transaction_isolation: READ-COMMITTED + +development: + primary: + <<: *default + database: fizzy_development + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: + <<: *default + database: fizzy_development + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: true + cable: + <<: *default + database: development_cable + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/cable_migrate + cache: + <<: *default + database: development_cache + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/cache_migrate + queue: + <<: *default + database: development_queue + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/queue_migrate + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + primary: + <<: *default + database: fizzy_test + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: + <<: *default + database: fizzy_test + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: true + +production: &production + primary: + <<: *default + database: fizzy_production + host: <%= ENV["MYSQL_DATABASE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + replica: + <<: *default + database: fizzy_production + host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %> + username: <%= ENV["MYSQL_READONLY_USER"] %> + password: <%= ENV["MYSQL_READONLY_PASSWORD"] %> + replica: true + cable: + <<: *default + database: fizzy_solidcable_production + host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/cable_migrate + queue: + <<: *default + database: fizzy_solidqueue_production + host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/queue_migrate + cache: + <<: *default + database: fizzy_solidcache_production + host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/cache_migrate + +beta: *production +staging: *production diff --git a/config/database.sqlite.yml b/config/database.sqlite.yml new file mode 100644 index 0000000000..44fcda8cc9 --- /dev/null +++ b/config/database.sqlite.yml @@ -0,0 +1,21 @@ +default: &default + adapter: sqlite3 + pool: 50 + timeout: 5000 + +development: + primary: + <<: *default + database: db/development.sqlite3 + schema_dump: schema_sqlite.rb + +test: + primary: + <<: *default + database: db/test.sqlite3 + schema_dump: schema_sqlite.rb + +production: + primary: + <<: *default + database: db/production.sqlite3 diff --git a/config/database.yml b/config/database.yml index 2a9a3aa2e6..84663579d2 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,123 +1 @@ -<% - if ENV["MIGRATE"].present? - mysql_app_user_key = "MYSQL_ALTER_USER" - mysql_app_password_key = "MYSQL_ALTER_PASSWORD" - max_execution_time_ms = 0 # No limit - else - mysql_app_user_key = "MYSQL_APP_USER" - mysql_app_password_key = "MYSQL_APP_PASSWORD" - max_execution_time_ms = 5_000 - end - - mysql_app_user = ENV[mysql_app_user_key] - mysql_app_password = ENV[mysql_app_password_key] -%> - -default: &default - <% if Fizzy.db_adapter.sqlite? %> - adapter: sqlite3 - pool: 5 - timeout: 5000 - <% else %> - adapter: trilogy - host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %> - port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %> - pool: 50 - timeout: 5000 - variables: - transaction_isolation: READ-COMMITTED - max_execution_time: <%= max_execution_time_ms %> - <% end %> - -development: - <% if Fizzy.db_adapter.sqlite? %> - primary: - <<: *default - database: storage/development.sqlite3 - schema_dump: schema_sqlite.rb - <% else %> - primary: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - cable: - <<: *default - database: development_cable - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cable_migrate - cache: - <<: *default - database: development_cache - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cache_migrate - queue: - <<: *default - database: development_queue - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/queue_migrate - <% end %> - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <% if Fizzy.db_adapter.sqlite? %> - primary: - <<: *default - database: storage/test.sqlite3 - schema_dump: schema_sqlite.rb - <% else %> - primary: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - <% end %> - -production: &production - primary: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - replica: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %> - username: <%= ENV["MYSQL_READONLY_USER"] %> - password: <%= ENV["MYSQL_READONLY_PASSWORD"] %> - replica: true - cable: - <<: *default - database: fizzy_solidcable_production - host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cable_migrate - queue: - <<: *default - database: fizzy_solidqueue_production - host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/queue_migrate - cache: - <<: *default - database: fizzy_solidcache_production - host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cache_migrate - -beta: *production -staging: *production +<%= ERB.new(File.read(File.join(__dir__, "database.#{Fizzy.db_adapter}.yml"))).result %> From 102827ff8d96b9f8b2d22d2159e4cf9d6dc04c51 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:16:22 +0100 Subject: [PATCH 005/107] Use bundler groups instead of conditional So that the gemfile.lock does not change across invocations --- Gemfile | 3 +-- Gemfile.lock | 20 ++++++++++++++++++++ bin/rails | 17 ++++++++++++++--- config/application.rb | 4 +++- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 65b7be293f..5a5f86f78f 100644 --- a/Gemfile +++ b/Gemfile @@ -76,8 +76,7 @@ group :test do gem "mocha" end -require_relative "lib/fizzy" -if Fizzy.saas? +group :saas, optional: true do gem "activeresource", require: "active_resource" gem "queenbee", git: "https://github.com/basecamp/queenbee-plugin" gem "fizzy-saas", path: "gems/fizzy-saas" diff --git a/Gemfile.lock b/Gemfile.lock index e2c03955c8..6e8bbd96b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/basecamp/queenbee-plugin + revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 + specs: + queenbee (3.2.0) + activeresource + builder + rexml + GIT remote: https://github.com/basecamp/rails-structured-logging revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6 @@ -122,6 +131,14 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) +PATH + remote: gems/fizzy-saas + specs: + fizzy-saas (0.1.0) + queenbee + rails (>= 8.1.0.beta1) + rails_structured_logging + GEM remote: https://rubygems.org/ specs: @@ -567,6 +584,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + activeresource autotuner aws-sdk-s3 bcrypt (~> 3.1.7) @@ -577,6 +595,7 @@ DEPENDENCIES capybara debug faker + fizzy-saas! geared_pagination (~> 1.2) image_processing (~> 1.14) importmap-rails @@ -592,6 +611,7 @@ DEPENDENCIES prometheus-client-mmap (~> 1.3) propshaft puma (>= 5.0) + queenbee! rack-mini-profiler rails! rails_structured_logging! diff --git a/bin/rails b/bin/rails index 0739660237..36ee7f7124 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,15 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../config/application', __dir__) -require_relative '../config/boot' -require 'rails/commands' +APP_PATH = File.expand_path("../config/application", __dir__) + +require_relative "../lib/fizzy" + +if Fizzy.saas? + ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"] , "saas" ].compact.join(",") +end + +require_relative "../config/boot" + +puts "SaaS version (#{Fizzy.db_adapter})" if Fizzy.saas? + +require "rails/commands" + diff --git a/config/application.rb b/config/application.rb index 0399caacac..4bfbab4a22 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,7 +2,9 @@ require "rails/all" require_relative "../lib/fizzy" -Bundler.require(*Rails.groups) +groups = Rails.groups +groups << :saas if Fizzy.saas? +Bundler.require(*groups) module Fizzy class Application < Rails::Application From 35b49a417d318f97cc605ce6b7168ed125939b09 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:27:40 +0100 Subject: [PATCH 006/107] Move the SASS database config to the gem --- config/database.mysql.yml | 102 +++------------------------- config/database.yml | 12 +++- gems/fizzy-saas/config/database.yml | 100 +++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 92 deletions(-) create mode 100644 gems/fizzy-saas/config/database.yml diff --git a/config/database.mysql.yml b/config/database.mysql.yml index 3c68fd0810..e3188b1acb 100644 --- a/config/database.mysql.yml +++ b/config/database.mysql.yml @@ -1,100 +1,20 @@ -<% - if ENV["MIGRATE"].present? - mysql_app_user_key = "MYSQL_ALTER_USER" - mysql_app_password_key = "MYSQL_ALTER_PASSWORD" - else - mysql_app_user_key = "MYSQL_APP_USER" - mysql_app_password_key = "MYSQL_APP_PASSWORD" - end - - mysql_app_user = ENV[mysql_app_user_key] - mysql_app_password = ENV[mysql_app_password_key] -%> - default: &default adapter: trilogy - host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %> - port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %> + host: <%= ENV.fetch("MYSQL_HOST", "127.0.0.1") %> + port: <%= ENV.fetch("MYSQL_PORT", "3306") %> + username: <%= ENV.fetch("MYSQL_USER", "root") %> + password: <%= ENV["MYSQL_PASSWORD"] %> pool: 50 timeout: 5000 - variables: - transaction_isolation: READ-COMMITTED development: - primary: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - cable: - <<: *default - database: development_cable - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cable_migrate - cache: - <<: *default - database: development_cache - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cache_migrate - queue: - <<: *default - database: development_queue - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/queue_migrate + <<: *default + database: fizzy_development -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. test: - primary: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - -production: &production - primary: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - replica: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %> - username: <%= ENV["MYSQL_READONLY_USER"] %> - password: <%= ENV["MYSQL_READONLY_PASSWORD"] %> - replica: true - cable: - <<: *default - database: fizzy_solidcable_production - host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cable_migrate - queue: - <<: *default - database: fizzy_solidqueue_production - host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/queue_migrate - cache: - <<: *default - database: fizzy_solidcache_production - host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cache_migrate + <<: *default + database: fizzy_test -beta: *production -staging: *production +production: + <<: *default + database: fizzy_production diff --git a/config/database.yml b/config/database.yml index 84663579d2..0229af3e39 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1 +1,11 @@ -<%= ERB.new(File.read(File.join(__dir__, "database.#{Fizzy.db_adapter}.yml"))).result %> +<% + require_relative "../lib/fizzy" + + config_path = if Fizzy.saas? + gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir + File.join(gem_path, "config", "database.yml") + else + File.join(__dir__, "database.#{Fizzy.db_adapter}.yml") + end +%> +<%= ERB.new(File.read(config_path)).result %> diff --git a/gems/fizzy-saas/config/database.yml b/gems/fizzy-saas/config/database.yml new file mode 100644 index 0000000000..3c68fd0810 --- /dev/null +++ b/gems/fizzy-saas/config/database.yml @@ -0,0 +1,100 @@ +<% + if ENV["MIGRATE"].present? + mysql_app_user_key = "MYSQL_ALTER_USER" + mysql_app_password_key = "MYSQL_ALTER_PASSWORD" + else + mysql_app_user_key = "MYSQL_APP_USER" + mysql_app_password_key = "MYSQL_APP_PASSWORD" + end + + mysql_app_user = ENV[mysql_app_user_key] + mysql_app_password = ENV[mysql_app_password_key] +%> + +default: &default + adapter: trilogy + host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %> + port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %> + pool: 50 + timeout: 5000 + variables: + transaction_isolation: READ-COMMITTED + +development: + primary: + <<: *default + database: fizzy_development + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: + <<: *default + database: fizzy_development + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: true + cable: + <<: *default + database: development_cable + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/cable_migrate + cache: + <<: *default + database: development_cache + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/cache_migrate + queue: + <<: *default + database: development_queue + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + migrations_paths: db/queue_migrate + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + primary: + <<: *default + database: fizzy_test + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: + <<: *default + database: fizzy_test + port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> + replica: true + +production: &production + primary: + <<: *default + database: fizzy_production + host: <%= ENV["MYSQL_DATABASE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + replica: + <<: *default + database: fizzy_production + host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %> + username: <%= ENV["MYSQL_READONLY_USER"] %> + password: <%= ENV["MYSQL_READONLY_PASSWORD"] %> + replica: true + cable: + <<: *default + database: fizzy_solidcable_production + host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/cable_migrate + queue: + <<: *default + database: fizzy_solidqueue_production + host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/queue_migrate + cache: + <<: *default + database: fizzy_solidcache_production + host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %> + username: <%= mysql_app_user %> + password: <%= mysql_app_password %> + migrations_paths: db/cache_migrate + +beta: *production +staging: *production From 2c7079393a360cd688cc2dad925100cc8a323788 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:45:58 +0100 Subject: [PATCH 007/107] Move kamal deploy config to the gem --- bin/kamal | 16 ++++++++++++++++ .../fizzy-saas/config}/deploy.beta.yml | 0 gems/fizzy-saas/config/deploy.dhh.yml | 12 ++++++++++++ .../fizzy-saas/config}/deploy.production.yml | 0 .../fizzy-saas/config}/deploy.staging.yml | 0 {config => gems/fizzy-saas/config}/deploy.yml | 0 6 files changed, 28 insertions(+) rename {config => gems/fizzy-saas/config}/deploy.beta.yml (100%) create mode 100644 gems/fizzy-saas/config/deploy.dhh.yml rename {config => gems/fizzy-saas/config}/deploy.production.yml (100%) rename {config => gems/fizzy-saas/config}/deploy.staging.yml (100%) rename {config => gems/fizzy-saas/config}/deploy.yml (100%) diff --git a/bin/kamal b/bin/kamal index cbe59b95ed..5c8ea6be73 100755 --- a/bin/kamal +++ b/bin/kamal @@ -22,6 +22,22 @@ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this end require "rubygems" + +require_relative "../lib/fizzy" +if Fizzy.saas? + ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"], "saas" ].compact.join(",") +end + require "bundler/setup" +if Fizzy.saas? + gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir + deploy_config = File.join(gem_path, "config", "deploy.yml") + + # Add -c option to ARGV if not already present + unless ARGV.include?("-c") || ARGV.include?("--config-file") + ARGV.unshift("-c", deploy_config) + end +end + load Gem.bin_path("kamal", "kamal") diff --git a/config/deploy.beta.yml b/gems/fizzy-saas/config/deploy.beta.yml similarity index 100% rename from config/deploy.beta.yml rename to gems/fizzy-saas/config/deploy.beta.yml diff --git a/gems/fizzy-saas/config/deploy.dhh.yml b/gems/fizzy-saas/config/deploy.dhh.yml new file mode 100644 index 0000000000..05bf874d41 --- /dev/null +++ b/gems/fizzy-saas/config/deploy.dhh.yml @@ -0,0 +1,12 @@ +servers: + web: + hosts: + - test-1 + jobs: + hosts: + - test-1 +env: + clear: + ARTENANT: test + +proxy: false diff --git a/config/deploy.production.yml b/gems/fizzy-saas/config/deploy.production.yml similarity index 100% rename from config/deploy.production.yml rename to gems/fizzy-saas/config/deploy.production.yml diff --git a/config/deploy.staging.yml b/gems/fizzy-saas/config/deploy.staging.yml similarity index 100% rename from config/deploy.staging.yml rename to gems/fizzy-saas/config/deploy.staging.yml diff --git a/config/deploy.yml b/gems/fizzy-saas/config/deploy.yml similarity index 100% rename from config/deploy.yml rename to gems/fizzy-saas/config/deploy.yml From 5d32e40756b9d3b7c5d05bf230be96d9e126d498 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:49:03 +0100 Subject: [PATCH 008/107] Use kamal from the --- bin/kamal | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/kamal b/bin/kamal index 5c8ea6be73..4cdcdc588d 100755 --- a/bin/kamal +++ b/bin/kamal @@ -34,9 +34,12 @@ if Fizzy.saas? gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir deploy_config = File.join(gem_path, "config", "deploy.yml") - # Add -c option to ARGV if not already present unless ARGV.include?("-c") || ARGV.include?("--config-file") - ARGV.unshift("-c", deploy_config) + if ARGV.empty? || ARGV.first.start_with?("-") + ARGV.unshift("-c", deploy_config) + else + ARGV.insert(1, "-c", deploy_config) + end end end From fb9aa5dfac11c2fe868cc741f3b60a9304495ac3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 05:54:49 +0100 Subject: [PATCH 009/107] Extract common method to configure bundle --- bin/kamal | 4 +--- bin/rails | 5 +---- lib/fizzy.rb | 6 ++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/kamal b/bin/kamal index 4cdcdc588d..862f9036a6 100755 --- a/bin/kamal +++ b/bin/kamal @@ -24,9 +24,7 @@ end require "rubygems" require_relative "../lib/fizzy" -if Fizzy.saas? - ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"], "saas" ].compact.join(",") -end +Fizzy.configure_bundle require "bundler/setup" diff --git a/bin/rails b/bin/rails index 36ee7f7124..e644775f8f 100755 --- a/bin/rails +++ b/bin/rails @@ -2,10 +2,7 @@ APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../lib/fizzy" - -if Fizzy.saas? - ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"] , "saas" ].compact.join(",") -end +Fizzy.configure_bundle require_relative "../config/boot" diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 16a1ca3a46..0a87dd8154 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -8,6 +8,12 @@ def saas? def db_adapter @db_adapter ||= DbAdapter.new ENV.fetch("DATABASE_ADAPTER", saas? ? "mysql" : "sqlite") end + + def configure_bundle + if saas? + ENV["BUNDLE_WITH"] = [ENV["BUNDLE_WITH"], "saas"].compact.join(",") + end + end end class DbAdapter From 13701f0e973ee991d5f9732c2909f1ffb3d7b36f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 06:01:45 +0100 Subject: [PATCH 010/107] Move kamal hooks to the saas gem --- {.kamal => gems/fizzy-saas/.kamal}/hooks/post-deploy | 0 {.kamal => gems/fizzy-saas/.kamal}/hooks/pre-build | 0 {.kamal => gems/fizzy-saas/.kamal}/hooks/pre-connect | 0 gems/fizzy-saas/config/deploy.yml | 1 + 4 files changed, 1 insertion(+) rename {.kamal => gems/fizzy-saas/.kamal}/hooks/post-deploy (100%) rename {.kamal => gems/fizzy-saas/.kamal}/hooks/pre-build (100%) rename {.kamal => gems/fizzy-saas/.kamal}/hooks/pre-connect (100%) diff --git a/.kamal/hooks/post-deploy b/gems/fizzy-saas/.kamal/hooks/post-deploy similarity index 100% rename from .kamal/hooks/post-deploy rename to gems/fizzy-saas/.kamal/hooks/post-deploy diff --git a/.kamal/hooks/pre-build b/gems/fizzy-saas/.kamal/hooks/pre-build similarity index 100% rename from .kamal/hooks/pre-build rename to gems/fizzy-saas/.kamal/hooks/pre-build diff --git a/.kamal/hooks/pre-connect b/gems/fizzy-saas/.kamal/hooks/pre-connect similarity index 100% rename from .kamal/hooks/pre-connect rename to gems/fizzy-saas/.kamal/hooks/pre-connect diff --git a/gems/fizzy-saas/config/deploy.yml b/gems/fizzy-saas/config/deploy.yml index b565e5a504..ea3ca3c49c 100644 --- a/gems/fizzy-saas/config/deploy.yml +++ b/gems/fizzy-saas/config/deploy.yml @@ -1,6 +1,7 @@ service: fizzy image: basecamp/fizzy asset_path: /rails/public/assets +hooks_path: <%= File.join(Gem::Specification.find_by_name("fizzy-saas").gem_dir, ".kamal", "hooks") %> servers: jobs: From f75ccfc2ddeacd8eb76ef1026f1526feeac540b9 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 06:46:25 +0100 Subject: [PATCH 011/107] Remove gem that now lives in GitHub --- gems/fizzy-saas/.github/dependabot.yml | 12 -- gems/fizzy-saas/.github/workflows/ci.yml | 69 ---------- gems/fizzy-saas/.gitignore | 10 -- gems/fizzy-saas/.kamal/hooks/post-deploy | 17 --- gems/fizzy-saas/.kamal/hooks/pre-build | 37 ----- gems/fizzy-saas/.kamal/hooks/pre-connect | 82 ----------- gems/fizzy-saas/.rubocop.yml | 8 -- gems/fizzy-saas/Gemfile | 6 - gems/fizzy-saas/Gemfile.lock | 69 ---------- gems/fizzy-saas/README.md | 28 ---- gems/fizzy-saas/Rakefile | 8 -- .../app/assets/images/fizzy/saas/.keep | 0 .../stylesheets/fizzy/saas/application.css | 15 --- .../fizzy-saas/app/controllers/concerns/.keep | 0 .../signup/completions_controller.rb | 24 ---- gems/fizzy-saas/app/models/concerns/.keep | 0 gems/fizzy-saas/app/models/signup.rb | 127 ------------------ .../models/signup/account_name_generator.rb | 53 -------- gems/fizzy-saas/app/models/subscription.rb | 13 -- .../layouts/fizzy/saas/application.html.erb | 17 --- .../app/views/signup/completions/new.html.erb | 30 ----- gems/fizzy-saas/app/views/signup/new.html.erb | 28 ---- gems/fizzy-saas/bin/rails | 14 -- gems/fizzy-saas/bin/rubocop | 8 -- gems/fizzy-saas/config/database.yml | 100 -------------- gems/fizzy-saas/config/deploy.beta.yml | 53 -------- gems/fizzy-saas/config/deploy.dhh.yml | 12 -- gems/fizzy-saas/config/deploy.production.yml | 72 ---------- gems/fizzy-saas/config/deploy.staging.yml | 69 ---------- gems/fizzy-saas/config/deploy.yml | 35 ----- gems/fizzy-saas/config/routes.rb | 9 -- gems/fizzy-saas/fizzy-saas.gemspec | 27 ---- gems/fizzy-saas/lib/fizzy/saas.rb | 7 - gems/fizzy-saas/lib/fizzy/saas/engine.rb | 31 ----- gems/fizzy-saas/lib/fizzy/saas/metrics.rb | 13 -- .../lib/fizzy/saas/transaction_pinning.rb | 65 --------- gems/fizzy-saas/lib/fizzy/saas/version.rb | 5 - .../lib/tasks/fizzy/saas_tasks.rake | 4 - gems/fizzy-saas/test/controllers/.keep | 0 .../signups/completions_controller_test.rb | 43 ------ gems/fizzy-saas/test/fixtures/files/.keep | 0 gems/fizzy-saas/test/helpers/.keep | 0 gems/fizzy-saas/test/integration/.keep | 0 gems/fizzy-saas/test/mailers/.keep | 0 gems/fizzy-saas/test/models/.keep | 0 .../signup/account_name_generator_test.rb | 61 --------- gems/fizzy-saas/test/models/signup_test.rb | 53 -------- gems/fizzy-saas/test/test_helper.rb | 9 -- 48 files changed, 1343 deletions(-) delete mode 100644 gems/fizzy-saas/.github/dependabot.yml delete mode 100644 gems/fizzy-saas/.github/workflows/ci.yml delete mode 100644 gems/fizzy-saas/.gitignore delete mode 100755 gems/fizzy-saas/.kamal/hooks/post-deploy delete mode 100755 gems/fizzy-saas/.kamal/hooks/pre-build delete mode 100755 gems/fizzy-saas/.kamal/hooks/pre-connect delete mode 100644 gems/fizzy-saas/.rubocop.yml delete mode 100644 gems/fizzy-saas/Gemfile delete mode 100644 gems/fizzy-saas/Gemfile.lock delete mode 100644 gems/fizzy-saas/README.md delete mode 100644 gems/fizzy-saas/Rakefile delete mode 100644 gems/fizzy-saas/app/assets/images/fizzy/saas/.keep delete mode 100644 gems/fizzy-saas/app/assets/stylesheets/fizzy/saas/application.css delete mode 100644 gems/fizzy-saas/app/controllers/concerns/.keep delete mode 100644 gems/fizzy-saas/app/controllers/signup/completions_controller.rb delete mode 100644 gems/fizzy-saas/app/models/concerns/.keep delete mode 100644 gems/fizzy-saas/app/models/signup.rb delete mode 100644 gems/fizzy-saas/app/models/signup/account_name_generator.rb delete mode 100644 gems/fizzy-saas/app/models/subscription.rb delete mode 100644 gems/fizzy-saas/app/views/layouts/fizzy/saas/application.html.erb delete mode 100644 gems/fizzy-saas/app/views/signup/completions/new.html.erb delete mode 100644 gems/fizzy-saas/app/views/signup/new.html.erb delete mode 100755 gems/fizzy-saas/bin/rails delete mode 100755 gems/fizzy-saas/bin/rubocop delete mode 100644 gems/fizzy-saas/config/database.yml delete mode 100644 gems/fizzy-saas/config/deploy.beta.yml delete mode 100644 gems/fizzy-saas/config/deploy.dhh.yml delete mode 100644 gems/fizzy-saas/config/deploy.production.yml delete mode 100644 gems/fizzy-saas/config/deploy.staging.yml delete mode 100644 gems/fizzy-saas/config/deploy.yml delete mode 100644 gems/fizzy-saas/config/routes.rb delete mode 100644 gems/fizzy-saas/fizzy-saas.gemspec delete mode 100644 gems/fizzy-saas/lib/fizzy/saas.rb delete mode 100644 gems/fizzy-saas/lib/fizzy/saas/engine.rb delete mode 100644 gems/fizzy-saas/lib/fizzy/saas/metrics.rb delete mode 100644 gems/fizzy-saas/lib/fizzy/saas/transaction_pinning.rb delete mode 100644 gems/fizzy-saas/lib/fizzy/saas/version.rb delete mode 100644 gems/fizzy-saas/lib/tasks/fizzy/saas_tasks.rake delete mode 100644 gems/fizzy-saas/test/controllers/.keep delete mode 100644 gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb delete mode 100644 gems/fizzy-saas/test/fixtures/files/.keep delete mode 100644 gems/fizzy-saas/test/helpers/.keep delete mode 100644 gems/fizzy-saas/test/integration/.keep delete mode 100644 gems/fizzy-saas/test/mailers/.keep delete mode 100644 gems/fizzy-saas/test/models/.keep delete mode 100644 gems/fizzy-saas/test/models/signup/account_name_generator_test.rb delete mode 100644 gems/fizzy-saas/test/models/signup_test.rb delete mode 100644 gems/fizzy-saas/test/test_helper.rb diff --git a/gems/fizzy-saas/.github/dependabot.yml b/gems/fizzy-saas/.github/dependabot.yml deleted file mode 100644 index 83610cfa4c..0000000000 --- a/gems/fizzy-saas/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: -- package-ecosystem: bundler - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 10 diff --git a/gems/fizzy-saas/.github/workflows/ci.yml b/gems/fizzy-saas/.github/workflows/ci.yml deleted file mode 100644 index ef5e97c73e..0000000000 --- a/gems/fizzy-saas/.github/workflows/ci.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: [ main ] - -jobs: - lint: - runs-on: ubuntu-latest - env: - RUBY_VERSION: ruby-3.4.5 - RUBOCOP_CACHE_ROOT: tmp/rubocop - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ env.RUBY_VERSION }} - bundler-cache: true - - - name: Prepare RuboCop cache - uses: actions/cache@v4 - env: - DEPENDENCIES_HASH: ${{ hashFiles('**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }} - with: - path: ${{ env.RUBOCOP_CACHE_ROOT }} - key: rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }} - restore-keys: | - rubocop-${{ runner.os }}-${{ env.RUBY_VERSION }}-${{ env.DEPENDENCIES_HASH }}- - - - name: Lint code for consistent style - run: bin/rubocop -f github - - test: - runs-on: ubuntu-latest - - # services: - # redis: - # image: valkey/valkey:8 - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ruby-3.4.5 - bundler-cache: true - - - name: Run tests - env: - RAILS_ENV: test - # RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test - - - name: Keep screenshots from failed system tests - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots - path: ${{ github.workspace }}/tmp/screenshots - if-no-files-found: ignore diff --git a/gems/fizzy-saas/.gitignore b/gems/fizzy-saas/.gitignore deleted file mode 100644 index a3ee5aad36..0000000000 --- a/gems/fizzy-saas/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -/.bundle/ -/doc/ -/log/*.log -/pkg/ -/tmp/ -/test/dummy/db/*.sqlite3 -/test/dummy/db/*.sqlite3-* -/test/dummy/log/*.log -/test/dummy/storage/ -/test/dummy/tmp/ diff --git a/gems/fizzy-saas/.kamal/hooks/post-deploy b/gems/fizzy-saas/.kamal/hooks/post-deploy deleted file mode 100755 index 8715060788..0000000000 --- a/gems/fizzy-saas/.kamal/hooks/post-deploy +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -MESSAGE="$KAMAL_PERFORMER deployed $KAMAL_SERVICE_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - -bin/notify_dash_of_deployment "$MESSAGE" $KAMAL_VERSION $KAMAL_PERFORMER $CURRENT_BRANCH $KAMAL_DESTINATION $KAMAL_RUNTIME - -if [[ $CURRENT_BRANCH == "main" && $KAMAL_DESTINATION == "production" ]]; then - gh release create $KAMAL_SERVICE_VERSION --target $KAMAL_VERSION --generate-notes 2> /dev/null || true - - RELEASE_URL=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .url) - RELEASE_BODY=$(gh release view $KAMAL_SERVICE_VERSION --json url,body --jq .body) - - bin/broadcast_to_bc "$MESSAGE "$'\n'"$RELEASE_URL "$'\n'"$RELEASE_BODY" -else - bin/broadcast_to_bc "$MESSAGE" -fi diff --git a/gems/fizzy-saas/.kamal/hooks/pre-build b/gems/fizzy-saas/.kamal/hooks/pre-build deleted file mode 100755 index 99de8a8248..0000000000 --- a/gems/fizzy-saas/.kamal/hooks/pre-build +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env ruby - -def exit_with_error(message) - $stderr.puts message - exit 1 -end - -def check_branch - if ENV["KAMAL_DESTINATION"] == "production" - current_branch = `git branch --show-current`.strip - - if current_branch != "main" - exit_with_error "Only the `main` branch should be deployed to production, current branch is #{current_branch}. If this is expected, try again with `SKIP_GIT_CHECKS=1` prepended to the command" - end - end -end - -def check_for_uncommitted_changes - if `git status --porcelain`.strip.length != 0 - exit_with_error "You have uncommitted changes, aborting" - end -end - -def check_local_and_remote_heads_match - remote_head = `git ls-remote origin --tags $(git branch --show-current) | cut -f1 | head -1`.strip - local_head = `git rev-parse HEAD`.strip - - if local_head != remote_head - exit_with_error "Remote HEAD #{remote_head}, differs from local HEAD #{local_head}, aborting" - end -end - -unless ENV["SKIP_GIT_CHECKS"] - check_branch - check_for_uncommitted_changes - check_local_and_remote_heads_match -end diff --git a/gems/fizzy-saas/.kamal/hooks/pre-connect b/gems/fizzy-saas/.kamal/hooks/pre-connect deleted file mode 100755 index 5f42f2cf2b..0000000000 --- a/gems/fizzy-saas/.kamal/hooks/pre-connect +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash - -# Validate hostnames are FQDNs ending in -int.37signals.com -if command -v yq >/dev/null 2>&1; then - declare -A SUGGESTIONS - while IFS= read -r host; do - if [[ ! $host =~ -int\.37signals\.com$ ]]; then - if [[ $host =~ -4[0-9]{2}$ ]]; then - SUGGESTIONS["$host"]="$host.df-ams-int.37signals.com" - elif [[ $host =~ -1[0-9]{2}$ ]]; then - SUGGESTIONS["$host"]="$host.df-iad-int.37signals.com" - else - SUGGESTIONS["$host"]="$host.sc-chi-int.37signals.com" - fi - fi - done < <(bin/kamal config -d "${KAMAL_DESTINATION:-production}" 2>/dev/null | yq -r '.":hosts"[]') - - if [ ${#SUGGESTIONS[@]} -gt 0 ]; then - echo "Unqualified hostnames found in config/deploy.${KAMAL_DESTINATION:-production}.yml:" >&2 - echo "" >&2 - echo "Update to use fully-qualified hostnames:" >&2 - for host in "${!SUGGESTIONS[@]}"; do - echo " $host → ${SUGGESTIONS[$host]}" >&2 - done - exit 1 - fi -fi - -# Verify Tailscale connection and SSH authentication before deploying. -tailscale_cmd() { - if command -v tailscale >/dev/null 2>&1; then - tailscale "$@" - elif [ -f "/Applications/Tailscale.app/Contents/MacOS/Tailscale" ]; then - env TAILSCALE_BE_CLI=1 /Applications/Tailscale.app/Contents/MacOS/Tailscale "$@" - else - return 1 - fi -} - -on_tailscale() { - tailscale_cmd status --json 2>/dev/null | jq -e '.Self.Online' >/dev/null 2>&1 -} - -# Check Tailscale connection -if ! on_tailscale; then - echo "" >&2 - echo "You must be connected to Tailscale to deploy." >&2 - echo "" >&2 - echo "→ Connect to Tailscale and try again" >&2 - echo "" >&2 - exit 1 -fi - -# Verify SSH access -echo "Deploying via Tailscale. Verifying SSH access…" >&2 - -TEST_HOST="fizzy-app-101" - -SSH_OUTPUT=$(ssh -o ConnectTimeout=5 "app@$TEST_HOST" true 2>&1) -SSH_EXIT=$? - -echo "$SSH_OUTPUT" >&2 - -if echo "$SSH_OUTPUT" | grep -q "Permission denied"; then - GITHUB_USER=$(gh api user 2>/dev/null | jq -r '.login // "unknown"') - GITHUB_KEYS_URL="https://github.com/${GITHUB_USER}.keys" - - echo "" >&2 - echo "ERROR: SSH authentication failed" >&2 - echo "" >&2 - echo "You must deploy with an SSH key that's on your GitHub account." >&2 - echo "" >&2 - echo "→ Verify your public key is at $GITHUB_KEYS_URL" >&2 - echo " Add it at https://github.com/settings/keys if not" >&2 - echo "" >&2 - echo "Note that SSH keys are pulled from GitHub every 5 minutes, so if you've" >&2 - echo "just added a new key to GitHub, try again in five." >&2 - echo "" >&2 - exit 1 -fi - -exit $SSH_EXIT diff --git a/gems/fizzy-saas/.rubocop.yml b/gems/fizzy-saas/.rubocop.yml deleted file mode 100644 index f9d86d4a54..0000000000 --- a/gems/fizzy-saas/.rubocop.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Omakase Ruby styling for Rails -inherit_gem: { rubocop-rails-omakase: rubocop.yml } - -# Overwrite or add rules to create your own house style -# -# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` -# Layout/SpaceInsideArrayLiteralBrackets: -# Enabled: false diff --git a/gems/fizzy-saas/Gemfile b/gems/fizzy-saas/Gemfile deleted file mode 100644 index 396a3192e0..0000000000 --- a/gems/fizzy-saas/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" -git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } - -# 37id and Queenbee integration -gem "queenbee", bc: "queenbee-plugin", ref: "eb01c697de1ad028afc65cc7d9b5345a7a8e849f" -gem "activeresource", require: "active_resource" # needed by queenbee diff --git a/gems/fizzy-saas/Gemfile.lock b/gems/fizzy-saas/Gemfile.lock deleted file mode 100644 index 5d83987cde..0000000000 --- a/gems/fizzy-saas/Gemfile.lock +++ /dev/null @@ -1,69 +0,0 @@ -GIT - remote: https://github.com/basecamp/queenbee-plugin - revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f - ref: eb01c697de1ad028afc65cc7d9b5345a7a8e849f - specs: - queenbee (3.2.0) - activeresource - builder - rexml - -GEM - remote: https://rubygems.org/ - specs: - activemodel (8.0.2.1) - activesupport (= 8.0.2.1) - activemodel-serializers-xml (1.0.3) - activemodel (>= 5.0.0.a) - activesupport (>= 5.0.0.a) - builder (~> 3.1) - activeresource (6.1.4) - activemodel (>= 6.0) - activemodel-serializers-xml (~> 1.0) - activesupport (>= 6.0) - activesupport (8.0.2.1) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - base64 (0.3.0) - benchmark (0.4.1) - bigdecimal (3.2.3) - builder (3.3.0) - concurrent-ruby (1.3.5) - connection_pool (2.5.4) - drb (2.2.3) - i18n (1.14.7) - concurrent-ruby (~> 1.0) - logger (1.7.0) - minitest (5.25.5) - rexml (3.4.4) - securerandom (0.4.1) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uri (1.0.3) - -PLATFORMS - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - activeresource - queenbee! - -BUNDLED WITH - 2.7.0 diff --git a/gems/fizzy-saas/README.md b/gems/fizzy-saas/README.md deleted file mode 100644 index ecaa3ede6f..0000000000 --- a/gems/fizzy-saas/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Fizzy::Saas -Short description and motivation. - -## Usage -How to use my plugin. - -## Installation -Add this line to your application's Gemfile: - -```ruby -gem "fizzy-saas" -``` - -And then execute: -```bash -$ bundle -``` - -Or install it yourself as: -```bash -$ gem install fizzy-saas -``` - -## Contributing -Contribution directions go here. - -## License -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/gems/fizzy-saas/Rakefile b/gems/fizzy-saas/Rakefile deleted file mode 100644 index e7793b5c12..0000000000 --- a/gems/fizzy-saas/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ -require "bundler/setup" - -APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) -load "rails/tasks/engine.rake" - -load "rails/tasks/statistics.rake" - -require "bundler/gem_tasks" diff --git a/gems/fizzy-saas/app/assets/images/fizzy/saas/.keep b/gems/fizzy-saas/app/assets/images/fizzy/saas/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/app/assets/stylesheets/fizzy/saas/application.css b/gems/fizzy-saas/app/assets/stylesheets/fizzy/saas/application.css deleted file mode 100644 index 0ebd7fe829..0000000000 --- a/gems/fizzy-saas/app/assets/stylesheets/fizzy/saas/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/gems/fizzy-saas/app/controllers/concerns/.keep b/gems/fizzy-saas/app/controllers/concerns/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/app/controllers/signup/completions_controller.rb b/gems/fizzy-saas/app/controllers/signup/completions_controller.rb deleted file mode 100644 index d7f09c0865..0000000000 --- a/gems/fizzy-saas/app/controllers/signup/completions_controller.rb +++ /dev/null @@ -1,24 +0,0 @@ -class Signup::CompletionsController < ApplicationController - layout "public" - - disallow_account_scope - - def new - @signup = Signup.new(identity: Current.identity) - end - - def create - @signup = Signup.new(signup_params) - - if @signup.complete - redirect_to landing_url(script_name: @signup.account.slug) - else - render :new, status: :unprocessable_entity - end - end - - private - def signup_params - params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity) - end -end diff --git a/gems/fizzy-saas/app/models/concerns/.keep b/gems/fizzy-saas/app/models/concerns/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/app/models/signup.rb b/gems/fizzy-saas/app/models/signup.rb deleted file mode 100644 index ca76f5b081..0000000000 --- a/gems/fizzy-saas/app/models/signup.rb +++ /dev/null @@ -1,127 +0,0 @@ -class Signup - include ActiveModel::Model - include ActiveModel::Attributes - include ActiveModel::Validations - - attr_accessor :full_name, :email_address, :identity - attr_reader :queenbee_account, :account, :user - - with_options on: :completion do - validates_presence_of :full_name, :identity - end - - def initialize(...) - @full_name = nil - @email_address = nil - @account = nil - @user = nil - @queenbee_account = nil - @identity = nil - - super - - @email_address = @identity.email_address if @identity - end - - def create_identity - @identity = Identity.find_or_create_by!(email_address: email_address) - @identity.send_magic_link - end - - def complete - if valid?(:completion) - begin - create_queenbee_account - create_account - - true - rescue => error - destroy_account - destroy_queenbee_account - - errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.") - Rails.error.report(error, severity: :error) - Rails.logger.error error - Rails.logger.error error.backtrace.join("\n") - - false - end - else - false - end - end - - private - def create_queenbee_account - @account_name = AccountNameGenerator.new(identity: identity, name: full_name).generate - @queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes) - @tenant = queenbee_account.id.to_s - end - - def destroy_queenbee_account - @queenbee_account&.cancel - @queenbee_account = nil - end - - def create_account - @account = Account.create_with_admin_user( - account: { - external_account_id: @tenant, - name: @account_name - }, - owner: { - name: full_name, - identity: identity - } - ) - @user = @account.users.find_by!(role: :admin) - @account.setup_customer_template - end - - def destroy_account - @account&.destroy! - - @user = nil - @account = nil - @tenant = nil - end - - def queenbee_account_attributes - {}.tap do |attributes| - attributes[:product_name] = "fizzy" - attributes[:name] = @account_name - attributes[:owner_name] = full_name - attributes[:owner_email] = email_address - - attributes[:trial] = true - attributes[:subscription] = subscription_attributes - attributes[:remote_request] = request_attributes - - # # TODO: Terms of Service - # attributes[:terms_of_service] = true - - # We've confirmed the email - attributes[:auto_allow] = true - - # Tell Queenbee to skip the request to create a local account. We've created it ourselves. - attributes[:skip_remote] = true - end - end - - def subscription_attributes - subscription = FreeV1Subscription - - {}.tap do |attributes| - attributes[:name] = subscription.to_param - attributes[:price] = subscription.price - end - end - - def request_attributes - {}.tap do |attributes| - attributes[:remote_address] = Current.ip_address - attributes[:user_agent] = Current.user_agent - attributes[:referrer] = Current.referrer - end - end -end diff --git a/gems/fizzy-saas/app/models/signup/account_name_generator.rb b/gems/fizzy-saas/app/models/signup/account_name_generator.rb deleted file mode 100644 index a6844d3a3a..0000000000 --- a/gems/fizzy-saas/app/models/signup/account_name_generator.rb +++ /dev/null @@ -1,53 +0,0 @@ -class Signup::AccountNameGenerator - SUFFIX = "Fizzy".freeze - - attr_reader :identity, :name - - def initialize(identity:, name:) - @identity = identity - @name = name - end - - def generate - next_index = current_index + 1 - - if next_index == 1 - "#{prefix} #{SUFFIX}" - else - "#{prefix} #{next_index.ordinalize} #{SUFFIX}" - end - end - - private - def current_index - existing_indices.max || 0 - end - - def existing_indices - Current.without_account do - identity.accounts.filter_map do |account| - if account.name.match?(first_account_name_regex) - 1 - elsif match = account.name.match(nth_account_name_regex) - match[1].to_i - end - end - end - end - - def first_account_name_regex - @first_account_name_regex ||= /\A#{prefix}\s+#{SUFFIX}\Z/i - end - - def nth_account_name_regex - @nth_account_name_regex ||= /\A#{prefix}\s+(1st|2nd|3rd|\d+th)\s+#{SUFFIX}/i - end - - def prefix - @prefix ||= "#{first_name}'s" - end - - def first_name - name.strip.split(" ", 2).first - end -end diff --git a/gems/fizzy-saas/app/models/subscription.rb b/gems/fizzy-saas/app/models/subscription.rb deleted file mode 100644 index 5efb1a7fac..0000000000 --- a/gems/fizzy-saas/app/models/subscription.rb +++ /dev/null @@ -1,13 +0,0 @@ -class Subscription < Queenbee::Subscription - SHORT_NAMES = %w[ FreeV1 ] - - def self.short_name - name.demodulize - end - - class FreeV1 < Subscription - property :proper_name, "Free Subscription" - property :price, 0 - property :frequency, "yearly" - end -end diff --git a/gems/fizzy-saas/app/views/layouts/fizzy/saas/application.html.erb b/gems/fizzy-saas/app/views/layouts/fizzy/saas/application.html.erb deleted file mode 100644 index 144b378387..0000000000 --- a/gems/fizzy-saas/app/views/layouts/fizzy/saas/application.html.erb +++ /dev/null @@ -1,17 +0,0 @@ - - - - Fizzy saas - <%= csrf_meta_tags %> - <%= csp_meta_tag %> - - <%= yield :head %> - - <%= stylesheet_link_tag "fizzy/saas/application", media: "all" %> - - - -<%= yield %> - - - diff --git a/gems/fizzy-saas/app/views/signup/completions/new.html.erb b/gems/fizzy-saas/app/views/signup/completions/new.html.erb deleted file mode 100644 index aee8f45869..0000000000 --- a/gems/fizzy-saas/app/views/signup/completions/new.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% @page_title = "Complete your sign-up" %> - -
"> -

<%= @page_title %>

- - <%= form_with model: @signup, url: saas.signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %> - <%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %> - -

You’re one step away. Just enter your name to get your own Fizzy account.

- - <% if @signup.errors.any? %> -
-
    - <% @signup.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - - - <% end %> -
- -<% content_for :footer do %> - <%= render "sessions/footer" %> -<% end %> diff --git a/gems/fizzy-saas/app/views/signup/new.html.erb b/gems/fizzy-saas/app/views/signup/new.html.erb deleted file mode 100644 index 93166dd2fe..0000000000 --- a/gems/fizzy-saas/app/views/signup/new.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<% @page_title = "Sign up for Fizzy" %> - -
"> -

Sign up

- - <%= form_with model: @signup, url: saas.signup_path, scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: "form" } do |form| %> - <%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %> - - <% if @signup.errors.any? %> -
-
    - <% @signup.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - - - <% end %> -
- -<% content_for :footer do %> - <%= render "sessions/footer" %> -<% end %> diff --git a/gems/fizzy-saas/bin/rails b/gems/fizzy-saas/bin/rails deleted file mode 100755 index 42a0e5bce3..0000000000 --- a/gems/fizzy-saas/bin/rails +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails gems -# installed from the root of your application. - -ENGINE_ROOT = File.expand_path("..", __dir__) -ENGINE_PATH = File.expand_path("../lib/fizzy/saas/engine", __dir__) -APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) - -# Set up gems listed in the Gemfile. -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) - -require "rails/all" -require "rails/engine/commands" diff --git a/gems/fizzy-saas/bin/rubocop b/gems/fizzy-saas/bin/rubocop deleted file mode 100755 index 40330c0ff1..0000000000 --- a/gems/fizzy-saas/bin/rubocop +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -require "rubygems" -require "bundler/setup" - -# explicit rubocop config increases performance slightly while avoiding config confusion. -ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) - -load Gem.bin_path("rubocop", "rubocop") diff --git a/gems/fizzy-saas/config/database.yml b/gems/fizzy-saas/config/database.yml deleted file mode 100644 index 3c68fd0810..0000000000 --- a/gems/fizzy-saas/config/database.yml +++ /dev/null @@ -1,100 +0,0 @@ -<% - if ENV["MIGRATE"].present? - mysql_app_user_key = "MYSQL_ALTER_USER" - mysql_app_password_key = "MYSQL_ALTER_PASSWORD" - else - mysql_app_user_key = "MYSQL_APP_USER" - mysql_app_password_key = "MYSQL_APP_PASSWORD" - end - - mysql_app_user = ENV[mysql_app_user_key] - mysql_app_password = ENV[mysql_app_password_key] -%> - -default: &default - adapter: trilogy - host: <%= ENV.fetch "FIZZY_DB_HOST", "127.0.0.1" %> - port: <%= ENV.fetch "FIZZY_DB_PORT", 3306 %> - pool: 50 - timeout: 5000 - variables: - transaction_isolation: READ-COMMITTED - -development: - primary: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_development - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - cable: - <<: *default - database: development_cable - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cable_migrate - cache: - <<: *default - database: development_cache - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/cache_migrate - queue: - <<: *default - database: development_queue - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - migrations_paths: db/queue_migrate - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - primary: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: - <<: *default - database: fizzy_test - port: <%= ENV.fetch "FIZZY_DB_PORT", 33380 %> - replica: true - -production: &production - primary: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - replica: - <<: *default - database: fizzy_production - host: <%= ENV["MYSQL_DATABASE_REPLICA_HOST"] %> - username: <%= ENV["MYSQL_READONLY_USER"] %> - password: <%= ENV["MYSQL_READONLY_PASSWORD"] %> - replica: true - cable: - <<: *default - database: fizzy_solidcable_production - host: <%= ENV["MYSQL_SOLID_CABLE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cable_migrate - queue: - <<: *default - database: fizzy_solidqueue_production - host: <%= ENV["MYSQL_SOLID_QUEUE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/queue_migrate - cache: - <<: *default - database: fizzy_solidcache_production - host: <%= ENV["MYSQL_SOLID_CACHE_HOST"] %> - username: <%= mysql_app_user %> - password: <%= mysql_app_password %> - migrations_paths: db/cache_migrate - -beta: *production -staging: *production diff --git a/gems/fizzy-saas/config/deploy.beta.yml b/gems/fizzy-saas/config/deploy.beta.yml deleted file mode 100644 index 4c4c323f61..0000000000 --- a/gems/fizzy-saas/config/deploy.beta.yml +++ /dev/null @@ -1,53 +0,0 @@ -servers: - web: - hosts: - - fizzy-beta-app-01.sc-chi-int.37signals.com: sc_chi - - fizzy-beta-app-101.df-iad-int.37signals.com: df_iad - labels: - otel_scrape_enabled: true - -# we don't run the jobs role in beta -allow_empty_roles: true - -proxy: - ssl: false - -ssh: - user: app - -env: - clear: - RAILS_ENV: beta - MYSQL_DATABASE_HOST: fizzy-mysql-primary - MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica - MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary - MYSQL_SOLID_QUEUE_HOST: fizzy-mysql-primary - MYSQL_SOLID_CACHE_HOST: fizzy-beta-solidcache-db-101 - secret: - - RAILS_MASTER_KEY - - MYSQL_ALTER_PASSWORD - - MYSQL_ALTER_USER - - MYSQL_APP_PASSWORD - - MYSQL_APP_USER - - MYSQL_READONLY_PASSWORD - - MYSQL_READONLY_USER - tags: - sc_chi: {} - df_iad: - PRIMARY_DATACENTER: true - df_ams: {} - -accessories: - load-balancer: - image: basecamp/kamal-proxy:lb - host: fizzy-beta-lb-01.sc-chi-int.37signals.com - labels: - otel_role: load-balancer - otel_service: fizzy-load-balancer - otel_scrape_enabled: true - options: - publish: - - 80:80 - - 443:443 - volumes: - - load-balancer:/home/kamal-proxy/.config/kamal-proxy diff --git a/gems/fizzy-saas/config/deploy.dhh.yml b/gems/fizzy-saas/config/deploy.dhh.yml deleted file mode 100644 index 05bf874d41..0000000000 --- a/gems/fizzy-saas/config/deploy.dhh.yml +++ /dev/null @@ -1,12 +0,0 @@ -servers: - web: - hosts: - - test-1 - jobs: - hosts: - - test-1 -env: - clear: - ARTENANT: test - -proxy: false diff --git a/gems/fizzy-saas/config/deploy.production.yml b/gems/fizzy-saas/config/deploy.production.yml deleted file mode 100644 index 495227c1f8..0000000000 --- a/gems/fizzy-saas/config/deploy.production.yml +++ /dev/null @@ -1,72 +0,0 @@ -servers: - web: - hosts: - - fizzy-app-01.sc-chi-int.37signals.com: sc_chi - - fizzy-app-02.sc-chi-int.37signals.com: sc_chi - - fizzy-app-101.df-iad-int.37signals.com: df_iad - - fizzy-app-102.df-iad-int.37signals.com: df_iad - - fizzy-app-401.df-ams-int.37signals.com: df_ams - - fizzy-app-402.df-ams-int.37signals.com: df_ams - labels: - otel_scrape_enabled: true - jobs: - hosts: - - fizzy-jobs-101.df-iad-int.37signals.com: df_iad - - fizzy-jobs-102.df-iad-int.37signals.com: df_iad - labels: - otel_scrape_enabled: true - -proxy: - ssl: false - -ssh: - user: app - -env: - clear: - RAILS_ENV: production - MYSQL_DATABASE_HOST: fizzy-mysql-primary - MYSQL_DATABASE_REPLICA_HOST: fizzy-mysql-replica - MYSQL_SOLID_CABLE_HOST: fizzy-mysql-primary - MYSQL_SOLID_QUEUE_HOST: fizzy-mysql-primary - secret: - - RAILS_MASTER_KEY - - MYSQL_ALTER_PASSWORD - - MYSQL_ALTER_USER - - MYSQL_APP_PASSWORD - - MYSQL_APP_USER - - MYSQL_READONLY_PASSWORD - - MYSQL_READONLY_USER - tags: - sc_chi: - MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-01.sc-chi-int.37signals.com - df_iad: - MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-101.df-iad-int.37signals.com - PRIMARY_DATACENTER: true - df_ams: - MYSQL_SOLID_CACHE_HOST: fizzy-solidcache-db-401.df-ams-int.37signals.com - - -accessories: - load-balancer: - image: basecamp/kamal-proxy:lb - hosts: - - fizzy-lb-101.df-iad-int.37signals.com - - fizzy-lb-102.df-iad-int.37signals.com - - fizzy-lb-01.sc-chi-int.37signals.com - - fizzy-lb-02.sc-chi-int.37signals.com - - fizzy-lb-401.df-ams-int.37signals.com - - fizzy-lb-402.df-ams-int.37signals.com - labels: - otel_role: load-balancer - otel_service: fizzy-load-balancer - otel_scrape_enabled: true - options: - publish: - - 80:80 - - 443:443 - # NFS mount for certificates - # See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061 - mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-production-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2" - volumes: - - load-balancer:/home/kamal-proxy/.config/kamal-proxy diff --git a/gems/fizzy-saas/config/deploy.staging.yml b/gems/fizzy-saas/config/deploy.staging.yml deleted file mode 100644 index 3163c7a7bf..0000000000 --- a/gems/fizzy-saas/config/deploy.staging.yml +++ /dev/null @@ -1,69 +0,0 @@ -servers: - web: - hosts: - - fizzy-staging-app-101.df-iad-int.37signals.com: df_iad - - fizzy-staging-app-102.df-iad-int.37signals.com: df_iad - - fizzy-staging-app-01.sc-chi-int.37signals.com: sc_chi - - fizzy-staging-app-02.sc-chi-int.37signals.com: sc_chi - - fizzy-staging-app-401.df-ams-int.37signals.com: df_ams - - fizzy-staging-app-402.df-ams-int.37signals.com: df_ams - labels: - otel_scrape_enabled: true - jobs: - hosts: - - fizzy-staging-jobs-101.df-iad-int.37signals.com: df_iad - - fizzy-staging-jobs-102.df-iad-int.37signals.com: df_iad - labels: - otel_scrape_enabled: true - -proxy: - ssl: false - -ssh: - user: app - -env: - clear: - RAILS_ENV: staging - MYSQL_DATABASE_HOST: fizzy-staging-mysql-primary - MYSQL_DATABASE_REPLICA_HOST: fizzy-staging-mysql-replica - MYSQL_SOLID_CABLE_HOST: fizzy-staging-mysql-primary - MYSQL_SOLID_QUEUE_HOST: fizzy-staging-mysql-primary - secret: - - RAILS_MASTER_KEY - - MYSQL_ALTER_PASSWORD - - MYSQL_ALTER_USER - - MYSQL_APP_PASSWORD - - MYSQL_APP_USER - - MYSQL_READONLY_PASSWORD - - MYSQL_READONLY_USER - tags: - sc_chi: - MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-01.sc-chi-int.37signals.com - df_iad: - MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-101.df-iad-int.37signals.com - PRIMARY_DATACENTER: true - df_ams: - MYSQL_SOLID_CACHE_HOST: fizzy-staging-solidcache-db-401.df-ams-int.37signals.com - - -accessories: - load-balancer: - image: basecamp/kamal-proxy:lb - hosts: - - fizzy-staging-lb-01.sc-chi-int.37signals.com - - fizzy-staging-lb-101.df-iad-int.37signals.com - - fizzy-staging-lb-401.df-ams-int.37signals.com - labels: - otel_role: load-balancer - otel_service: fizzy-load-balancer - otel_scrape_enabled: true - options: - publish: - - 80:80 - - 443:443 - # NFS mount for certificates - # See https://3.basecamp.com/2914079/buckets/37331921/todos/9180260061 - mount: type=volume,src=certificates,dst=/certificates,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/fizzy-staging-certificates,"volume-opt=o=addr=purestorage.sc-chi-int.37signals.com,nfsvers=3,rw,noatime,nconnect=8,soft,timeo=30,retrans=2" - volumes: - - load-balancer:/home/kamal-proxy/.config/kamal-proxy diff --git a/gems/fizzy-saas/config/deploy.yml b/gems/fizzy-saas/config/deploy.yml deleted file mode 100644 index ea3ca3c49c..0000000000 --- a/gems/fizzy-saas/config/deploy.yml +++ /dev/null @@ -1,35 +0,0 @@ -service: fizzy -image: basecamp/fizzy -asset_path: /rails/public/assets -hooks_path: <%= File.join(Gem::Specification.find_by_name("fizzy-saas").gem_dir, ".kamal", "hooks") %> - -servers: - jobs: - cmd: bin/jobs - -volumes: - - fizzy:/rails/storage - -proxy: - ssl: true - -registry: - server: registry.37signals.com - username: robot$harbor-bot - password: - - BASECAMP_REGISTRY_PASSWORD - -builder: - arch: amd64 - secrets: - - GITHUB_TOKEN - remote: ssh://app@docker-builder-102 - local: <%= ENV.fetch("KAMAL_BUILDER_LOCAL", "true") %> - -env: - secret: - - RAILS_MASTER_KEY - -aliases: - console: app exec -i --reuse "bin/rails console" - ssh: app exec -i --reuse /bin/bash diff --git a/gems/fizzy-saas/config/routes.rb b/gems/fizzy-saas/config/routes.rb deleted file mode 100644 index 2b9639edb2..0000000000 --- a/gems/fizzy-saas/config/routes.rb +++ /dev/null @@ -1,9 +0,0 @@ -Fizzy::Saas::Engine.routes.draw do - get "/signup/new", to: redirect("/session/new") - - namespace :signup do - resource :completion, only: %i[ new create ] - end - - Queenbee.routes(self) -end diff --git a/gems/fizzy-saas/fizzy-saas.gemspec b/gems/fizzy-saas/fizzy-saas.gemspec deleted file mode 100644 index f1b28de00a..0000000000 --- a/gems/fizzy-saas/fizzy-saas.gemspec +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "lib/fizzy/saas/version" - -Gem::Specification.new do |spec| - spec.name = "fizzy-saas" - spec.version = Fizzy::Saas::VERSION - spec.authors = [ "Mike Dalessio" ] - spec.email = [ "mike@37signals.com" ] - spec.homepage = "TODO" - spec.summary = "TODO: Summary of Fizzy::Saas." - spec.description = "TODO: Description of Fizzy::Saas." - - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host" - # to allow pushing to a single host or delete this section to allow pushing to any host. - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." - - spec.files = Dir.chdir(File.expand_path(__dir__)) do - Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - end - - spec.add_dependency "rails", ">= 8.1.0.beta1" - spec.add_dependency "queenbee" - spec.add_dependency "rails_structured_logging" -end diff --git a/gems/fizzy-saas/lib/fizzy/saas.rb b/gems/fizzy-saas/lib/fizzy/saas.rb deleted file mode 100644 index dd0d354926..0000000000 --- a/gems/fizzy-saas/lib/fizzy/saas.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "fizzy/saas/version" -require "fizzy/saas/engine" - -module Fizzy - module Saas - end -end diff --git a/gems/fizzy-saas/lib/fizzy/saas/engine.rb b/gems/fizzy-saas/lib/fizzy/saas/engine.rb deleted file mode 100644 index 14ce037590..0000000000 --- a/gems/fizzy-saas/lib/fizzy/saas/engine.rb +++ /dev/null @@ -1,31 +0,0 @@ -require_relative "metrics" -require_relative "transaction_pinning" - -module Fizzy - module Saas - class Engine < ::Rails::Engine - # moved from config/initializers/queenbee.rb - Queenbee.host_app = Fizzy - - initializer "fizzy_saas.transaction_pinning" do |app| - if ActiveRecord::Base.replica_configured? - app.config.middleware.insert_after( - ActiveRecord::Middleware::DatabaseSelector, - TransactionPinning::Middleware - ) - end - end - - config.to_prepare do - Queenbee::Subscription.short_names = Subscription::SHORT_NAMES - Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token) - - Subscription::SHORT_NAMES.each do |short_name| - const_name = "#{short_name}Subscription" - ::Object.send(:remove_const, const_name) if ::Object.const_defined?(const_name) - ::Object.const_set const_name, Subscription.const_get(short_name, false) - end - end - end - end -end diff --git a/gems/fizzy-saas/lib/fizzy/saas/metrics.rb b/gems/fizzy-saas/lib/fizzy/saas/metrics.rb deleted file mode 100644 index 80a2bc194b..0000000000 --- a/gems/fizzy-saas/lib/fizzy/saas/metrics.rb +++ /dev/null @@ -1,13 +0,0 @@ -Yabeda.configure do - SHORT_HISTOGRAM_BUCKETS = [ 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5 ] - - group :fizzy do - counter :replica_stale, - comment: "Number of requests served from a stale replica" - - histogram :replica_wait, - unit: :seconds, - comment: "Time spent waiting for replica to catch up with transaction", - buckets: SHORT_HISTOGRAM_BUCKETS - end -end diff --git a/gems/fizzy-saas/lib/fizzy/saas/transaction_pinning.rb b/gems/fizzy-saas/lib/fizzy/saas/transaction_pinning.rb deleted file mode 100644 index ed3cf2060d..0000000000 --- a/gems/fizzy-saas/lib/fizzy/saas/transaction_pinning.rb +++ /dev/null @@ -1,65 +0,0 @@ -module TransactionPinning - class Middleware - SESSION_KEY = :last_txn - DEFAULT_MAX_WAIT = 0.25 - - def initialize(app) - @app = app - @timeout = Rails.application.config.x.transaction_pinning&.timeout&.to_f || DEFAULT_MAX_WAIT - end - - def call(env) - request = ActionDispatch::Request.new(env) - replica_metrics = {} - - if ApplicationRecord.current_role == :reading - wait_for_replica_catchup(request, replica_metrics) - end - - status, headers, body = @app.call(env) - headers.merge!(replica_metrics.transform_values(&:to_s)) - - if ApplicationRecord.current_role == :writing - capture_transaction_id(request) - end - - [ status, headers, body ] - end - - private - def wait_for_replica_catchup(request, replica_metrics) - if last_txn = request.session[SESSION_KEY].presence - has_transaction = tracking_replica_wait_time(replica_metrics) do - replica_has_transaction(last_txn) - end - - unless has_transaction - Yabeda.fizzy.replica_stale.increment - replica_metrics["X-Replica-Stale"] = true - end - end - end - - def capture_transaction_id(request) - request.session[SESSION_KEY] = ApplicationRecord.connection.show_variable("global.gtid_executed") - end - - def replica_has_transaction(txn) - sql = ApplicationRecord.sanitize_sql_array([ "SELECT WAIT_FOR_EXECUTED_GTID_SET(?, ?)", txn, @timeout ]) - ApplicationRecord.connection.select_value(sql) == 0 - rescue => e - Sentry.capture_exception(e, extra: { gtid: txn }) - true # Treat as if we're up to date, since we don't know - end - - def tracking_replica_wait_time(replica_metrics) - started_at = Time.current - - Yabeda.fizzy.replica_wait.measure do - yield - end.tap do - replica_metrics["X-Replica-Wait"] = Time.current - started_at - end - end - end -end diff --git a/gems/fizzy-saas/lib/fizzy/saas/version.rb b/gems/fizzy-saas/lib/fizzy/saas/version.rb deleted file mode 100644 index 7a95d2d052..0000000000 --- a/gems/fizzy-saas/lib/fizzy/saas/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Fizzy - module Saas - VERSION = "0.1.0" - end -end diff --git a/gems/fizzy-saas/lib/tasks/fizzy/saas_tasks.rake b/gems/fizzy-saas/lib/tasks/fizzy/saas_tasks.rake deleted file mode 100644 index 8fe948d94a..0000000000 --- a/gems/fizzy-saas/lib/tasks/fizzy/saas_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :fizzy_saas do -# # Task goes here -# end diff --git a/gems/fizzy-saas/test/controllers/.keep b/gems/fizzy-saas/test/controllers/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb deleted file mode 100644 index 49628a188e..0000000000 --- a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "test_helper" - -class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest - setup do - @signup = Signup.new(email_address: "newuser@example.com", full_name: "New User") - - @signup.create_identity || raise("Failed to create identity") - - sign_in_as @signup.identity - end - - test "new" do - untenanted do - get saas.new_signup_completion_path - end - - assert_response :success - end - - test "create" do - untenanted do - post saas.signup_completion_path, params: { - signup: { - full_name: @signup.full_name - } - } - end - - assert_response :redirect, "Valid params should redirect" - end - - test "create with invalid params" do - untenanted do - post saas.signup_completion_path, params: { - signup: { - full_name: "" - } - } - end - - assert_response :unprocessable_entity, "Invalid params should return unprocessable entity" - end -end diff --git a/gems/fizzy-saas/test/fixtures/files/.keep b/gems/fizzy-saas/test/fixtures/files/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/helpers/.keep b/gems/fizzy-saas/test/helpers/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/integration/.keep b/gems/fizzy-saas/test/integration/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/mailers/.keep b/gems/fizzy-saas/test/mailers/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/models/.keep b/gems/fizzy-saas/test/models/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb b/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb deleted file mode 100644 index d22922b29d..0000000000 --- a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require "test_helper" - -class Signup::AccountNameGeneratorTest < ActiveSupport::TestCase - setup do - @identity = Identity.create!(email_address: "newart.userbaum@example.com") - @name = "Newart userbaum" - @generator = Signup::AccountNameGenerator.new(identity: @identity, name: @name) - end - - test "generate" do - account_name = @generator.generate - assert_equal "Newart's Fizzy", account_name, "The 1st account doesn't have 1st in the name" - - first_account = Account.create!(external_account_id: "1st", name: account_name) - Current.without_account do - @identity.users.create!(account: first_account, name: @name) - @identity.reload - end - - account_name = @generator.generate - assert_equal "Newart's 2nd Fizzy", account_name - - second_account = Account.create!(external_account_id: "2nd", name: account_name) - Current.without_account do - @identity.users.create!(account: second_account, name: @name) - @identity.reload - end - - account_name = @generator.generate - assert_equal "Newart's 3rd Fizzy", account_name - - third_account = Account.create!(external_account_id: "3rd", name: account_name) - Current.without_account do - @identity.users.create!(account: third_account, name: @name) - @identity.reload - end - - account_name = @generator.generate - assert_equal "Newart's 4th Fizzy", account_name - - fourth_account = Account.create!(external_account_id: "4th", name: account_name) - Current.without_account do - @identity.users.create!(account: fourth_account, name: @name) - @identity.reload - end - - account_name = @generator.generate - assert_equal "Newart's 5th Fizzy", account_name - end - - test "generate continues from the previous highest index" do - account = Account.create!(external_account_id: "12th", name: "Newart's 12th Fizzy") - Current.without_account do - @identity.users.create!(account: account, name: @name) - @identity.reload - end - - account_name = @generator.generate - assert_equal "Newart's 13th Fizzy", account_name - end -end diff --git a/gems/fizzy-saas/test/models/signup_test.rb b/gems/fizzy-saas/test/models/signup_test.rb deleted file mode 100644 index 69c0e699f9..0000000000 --- a/gems/fizzy-saas/test/models/signup_test.rb +++ /dev/null @@ -1,53 +0,0 @@ -require "test_helper" - -class SignupTest < ActiveSupport::TestCase - test "#create_identity" do - signup = Signup.new(email_address: "brian@example.com") - - assert_difference -> { Identity.count }, 1 do - assert_difference -> { MagicLink.count }, 1 do - assert signup.create_identity - end - end - - assert_empty signup.errors - assert signup.identity - assert signup.identity.persisted? - - signup_existing = Signup.new(email_address: "brian@example.com") - - assert_no_difference -> { Identity.count } do - assert_difference -> { MagicLink.count }, 1 do - assert signup_existing.create_identity, "Should send magic link for existing identity" - end - end - - signup_invalid = Signup.new(email_address: "") - assert_raises do - signup_invalid.create_identity - end - end - - test "#complete" do - Account.any_instance.expects(:setup_customer_template).once - Current.without_account do - signup = Signup.new( - full_name: "Kevin", - identity: identities(:kevin) - ) - - assert signup.complete - - assert signup.account - assert signup.user - assert_equal "Kevin", signup.user.name - - signup_invalid = Signup.new( - full_name: "", - identity: identities(:kevin) - ) - assert_not signup_invalid.complete - assert_not_empty signup_invalid.errors[:full_name] - end - end -end diff --git a/gems/fizzy-saas/test/test_helper.rb b/gems/fizzy-saas/test/test_helper.rb deleted file mode 100644 index eeaf249547..0000000000 --- a/gems/fizzy-saas/test/test_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -require "queenbee/testing/mocks" - -Queenbee::Remote::Account.class_eval do - # because we use the account ID as the tenant name, we need it to be unique in each test to avoid - # parallelized tests clobbering each other. - def next_id - super + Random.rand(1000000) - end -end From c1334f7ffe38fb6d32e74ccea3591d146a79a57d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 07:39:04 +0100 Subject: [PATCH 012/107] Don't choke if no structured logging Temporary workaround, we need a better solution here. --- app/controllers/concerns/authentication.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index f4ad6a0131..4ffca9cb02 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -81,7 +81,8 @@ def start_new_session_for(identity) end def set_current_session(session) - logger.struct " Authorized Identity##{session.identity.id}", authentication: { identity: { id: session.identity.id } } + # TODO: Release structured logging or look for alternative + logger.try :struct, " Authorized Identity##{session.identity.id}", authentication: { identity: { id: session.identity.id } } Current.session = session cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax } end From 277a45fa88315dba35264606f9f810a0fef802e9 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 07:39:19 +0100 Subject: [PATCH 013/107] Add CI step for saas tests --- config/ci.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/ci.rb b/config/ci.rb index 5d518c21c7..85b327d025 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -1,5 +1,7 @@ # Run using bin/ci +require_relative "../lib/fizzy" + CI.run do step "Setup", "bin/setup --skip-server" @@ -10,7 +12,9 @@ step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Security: Gitleaks audit", "bin/gitleaks-audit" - step "Tests: Rails", "bin/rails test" + step "Tests: Fizzy", "bin/rails test" + + step "Tests: SaaS", "SAAS=1 bin/rails test:saas" if Fizzy.saas? step "Tests: System", "bin/rails test:system" if success? From 03483a0526f48ed58a8c01834cd0c72b125a3f00 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 07:39:26 +0100 Subject: [PATCH 014/107] Format --- lib/fizzy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 0a87dd8154..614add8ecb 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -11,7 +11,7 @@ def db_adapter def configure_bundle if saas? - ENV["BUNDLE_WITH"] = [ENV["BUNDLE_WITH"], "saas"].compact.join(",") + ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"], "saas" ].compact.join(",") end end end From 4c5640a655548e6fc921b7615056f6cbbcec1174 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 07:39:33 +0100 Subject: [PATCH 015/107] Make structured logging private --- Gemfile | 6 +++--- Gemfile.lock | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 5a5f86f78f..66c3f901fd 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,6 @@ gem "useragent", bc: "useragent" gem "mission_control-jobs" gem "sentry-ruby" gem "sentry-rails" -gem "rails_structured_logging", bc: "rails-structured-logging" gem "yabeda" gem "yabeda-actioncable" gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs" @@ -78,6 +77,7 @@ end group :saas, optional: true do gem "activeresource", require: "active_resource" - gem "queenbee", git: "https://github.com/basecamp/queenbee-plugin" - gem "fizzy-saas", path: "gems/fizzy-saas" + gem "queenbee", bc: "queenbee-plugin" + gem "fizzy-saas", bc: "fizzy-saas" + gem "rails_structured_logging", bc: "rails-structured-logging" end diff --git a/Gemfile.lock b/Gemfile.lock index 6e8bbd96b5..25d29711c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/basecamp/fizzy-saas + revision: 00de8e2e1ae8e9df5e0a2acf4593a4e9a824c5c8 + specs: + fizzy-saas (0.1.0) + queenbee + rails (>= 8.1.0.beta1) + rails_structured_logging + GIT remote: https://github.com/basecamp/queenbee-plugin revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 @@ -131,14 +140,6 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) -PATH - remote: gems/fizzy-saas - specs: - fizzy-saas (0.1.0) - queenbee - rails (>= 8.1.0.beta1) - rails_structured_logging - GEM remote: https://rubygems.org/ specs: From 3414fea012a0f4e1bd1e734a9700d9dfb71bb3b4 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 09:22:49 +0100 Subject: [PATCH 016/107] Remove comment --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 87fe32b7a3..48741a7a94 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,7 +224,6 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "pwa#service_worker" - # TODO: Can we move this just to the engine if Fizzy.saas? mount Fizzy::Saas::Engine, at: "/", as: "saas" end From 13f1ad19f330c5733012a824cd685442f76f79f8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 09:32:23 +0100 Subject: [PATCH 017/107] The engine automounts now --- config/routes.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 48741a7a94..3afb093a77 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,10 +224,6 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "pwa#service_worker" - if Fizzy.saas? - mount Fizzy::Saas::Engine, at: "/", as: "saas" - end - namespace :admin do mount MissionControl::Jobs::Engine, at: "/jobs" get "stats", to: "stats#show" From ad6f3a9ff71d6b6f345bc71f25bcdcbf8c566ad8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 17:46:54 +0100 Subject: [PATCH 018/107] Instead of a dedicated group, use a completely separate Gemfile for the saas version The group-based approach won't work if you don't have access to the gems! --- Gemfile | 7 - Gemfile.lock | 4 - Gemfile.saas | 8 + Gemfile.saas.lock | 645 ++++++++++++++++++++++++++++++++++++++++++++++ lib/fizzy.rb | 2 +- 5 files changed, 654 insertions(+), 12 deletions(-) create mode 100644 Gemfile.saas create mode 100644 Gemfile.saas.lock diff --git a/Gemfile b/Gemfile index 66c3f901fd..a4cb0a29fe 100644 --- a/Gemfile +++ b/Gemfile @@ -74,10 +74,3 @@ group :test do gem "vcr" gem "mocha" end - -group :saas, optional: true do - gem "activeresource", require: "active_resource" - gem "queenbee", bc: "queenbee-plugin" - gem "fizzy-saas", bc: "fizzy-saas" - gem "rails_structured_logging", bc: "rails-structured-logging" -end diff --git a/Gemfile.lock b/Gemfile.lock index 25d29711c1..6556fd3320 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -585,7 +585,6 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - activeresource autotuner aws-sdk-s3 bcrypt (~> 3.1.7) @@ -596,7 +595,6 @@ DEPENDENCIES capybara debug faker - fizzy-saas! geared_pagination (~> 1.2) image_processing (~> 1.14) importmap-rails @@ -612,10 +610,8 @@ DEPENDENCIES prometheus-client-mmap (~> 1.3) propshaft puma (>= 5.0) - queenbee! rack-mini-profiler rails! - rails_structured_logging! redcarpet rouge rqrcode diff --git a/Gemfile.saas b/Gemfile.saas new file mode 100644 index 0000000000..52f6197c4c --- /dev/null +++ b/Gemfile.saas @@ -0,0 +1,8 @@ +# This Gemfile extends the base Gemfile with SaaS-specific dependencies +eval_gemfile "Gemfile" + +gem "activeresource", require: "active_resource" +gem "queenbee", bc: "queenbee-plugin" +gem "fizzy-saas", bc: "fizzy-saas" +gem "rails_structured_logging", bc: "rails-structured-logging" + diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock new file mode 100644 index 0000000000..4a3483eb95 --- /dev/null +++ b/Gemfile.saas.lock @@ -0,0 +1,645 @@ +GIT + remote: https://github.com/basecamp/fizzy-saas + revision: 7efcfdc7d1b787aa5f023ea96656b9f67bcbfe7f + specs: + fizzy-saas (0.1.0) + queenbee + rails (>= 8.1.0.beta1) + rails_structured_logging + +GIT + remote: https://github.com/basecamp/queenbee-plugin + revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 + specs: + queenbee (3.2.0) + activeresource + builder + rexml + +GIT + remote: https://github.com/basecamp/rails-structured-logging + revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6 + specs: + rails_structured_logging (0.2.1) + json + rails (>= 6.0.0) + +GIT + remote: https://github.com/basecamp/yabeda-activejob.git + revision: 684973f77ff01d8b3dd75874538fae55961e15e6 + branch: bulk-and-scheduled-jobs + specs: + yabeda-activejob (0.6.0) + rails (>= 6.1) + yabeda (~> 0.6) + +GIT + remote: https://github.com/rails/rails.git + revision: 4f7ab01bb5d6be78c7447dbb230c55027d08ae34 + branch: main + specs: + actioncable (8.2.0.alpha) + actionpack (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.2.0.alpha) + actionpack (= 8.2.0.alpha) + activejob (= 8.2.0.alpha) + activerecord (= 8.2.0.alpha) + activestorage (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + mail (>= 2.8.0) + actionmailer (8.2.0.alpha) + actionpack (= 8.2.0.alpha) + actionview (= 8.2.0.alpha) + activejob (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.2.0.alpha) + actionview (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.2.0.alpha) + action_text-trix (~> 2.1.15) + actionpack (= 8.2.0.alpha) + activerecord (= 8.2.0.alpha) + activestorage (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.2.0.alpha) + activesupport (= 8.2.0.alpha) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.2.0.alpha) + activesupport (= 8.2.0.alpha) + globalid (>= 0.3.6) + activemodel (8.2.0.alpha) + activesupport (= 8.2.0.alpha) + activerecord (8.2.0.alpha) + activemodel (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + timeout (>= 0.4.0) + activestorage (8.2.0.alpha) + actionpack (= 8.2.0.alpha) + activejob (= 8.2.0.alpha) + activerecord (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + marcel (~> 1.0) + activesupport (8.2.0.alpha) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + rails (8.2.0.alpha) + actioncable (= 8.2.0.alpha) + actionmailbox (= 8.2.0.alpha) + actionmailer (= 8.2.0.alpha) + actionpack (= 8.2.0.alpha) + actiontext (= 8.2.0.alpha) + actionview (= 8.2.0.alpha) + activejob (= 8.2.0.alpha) + activemodel (= 8.2.0.alpha) + activerecord (= 8.2.0.alpha) + activestorage (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + bundler (>= 1.15.0) + railties (= 8.2.0.alpha) + railties (8.2.0.alpha) + actionpack (= 8.2.0.alpha) + activesupport (= 8.2.0.alpha) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.15) + railties + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activeresource (6.2.0) + activemodel (>= 7.0) + activemodel-serializers-xml (~> 1.0) + activesupport (>= 7.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + anyway_config (2.7.2) + ruby-next-core (~> 1.0) + ast (2.4.3) + autotuner (1.1.0) + aws-eventstream (1.4.0) + aws-partitions (1.1187.0) + aws-sdk-core (3.239.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.205.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.3.0) + bcrypt (3.1.20) + bcrypt_pbkdf (1.1.1) + benchmark (0.5.0) + bigdecimal (3.3.1) + bindex (0.8.1) + bootsnap (1.19.0) + msgpack (~> 1.2) + brakeman (7.1.1) + racc + builder (3.3.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + chunky_png (1.4.0) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crack (1.0.1) + bigdecimal + rexml + crass (1.0.6) + date (3.5.0) + debug (1.11.0) + irb (~> 1.10) + reline (>= 0.3.8) + dotenv (3.1.8) + drb (2.2.3) + dry-initializer (3.2.0) + ed25519 (1.4.0) + erb (6.0.0) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo + faker (3.5.2) + i18n (>= 1.8.11, < 2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + fugit (1.12.1) + et-orbi (~> 1.4) + raabro (~> 1.4) + geared_pagination (1.2.0) + activesupport (>= 5.0) + addressable (>= 2.5.0) + globalid (1.3.0) + activesupport (>= 6.1) + hashdiff (1.2.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.2) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.1) + irb (1.15.3) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + jmespath (1.6.2) + json (2.16.0) + jwt (3.1.2) + base64 + kamal (2.8.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.4) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) + lexxy (0.1.20.beta) + rails (>= 8.0.2) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.3) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (5.26.2) + mission_control-jobs (1.1.0) + actioncable (>= 7.1) + actionpack (>= 7.1) + activejob (>= 7.1) + activerecord (>= 7.1) + importmap-rails (>= 1.2.1) + irb (~> 1.13) + railties (>= 7.1) + stimulus-rails + turbo-rails + mittens (0.3.0) + mocha (2.8.2) + ruby2_keywords (>= 0.0.5) + msgpack (1.8.0) + net-http-persistent (4.0.6) + connection_pool (~> 2.2, >= 2.2.4) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.5) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + openssl (3.3.2) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + platform_agent (1.0.1) + activesupport (>= 5.2.0) + useragent (~> 0.16.3) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.6.0) + prometheus-client-mmap (1.3.0) + base64 + bigdecimal + logger + rb_sys (~> 0.9.117) + prometheus-client-mmap (1.3.0-aarch64-linux-gnu) + base64 + bigdecimal + logger + rb_sys (~> 0.9.117) + prometheus-client-mmap (1.3.0-aarch64-linux-musl) + base64 + bigdecimal + logger + rb_sys (~> 0.9.117) + prometheus-client-mmap (1.3.0-x86_64-linux-gnu) + base64 + bigdecimal + logger + rb_sys (~> 0.9.117) + prometheus-client-mmap (1.3.0-x86_64-linux-musl) + base64 + bigdecimal + logger + rb_sys (~> 0.9.117) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.1.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.2.4) + rack-mini-profiler (4.0.1) + rack (>= 1.2.0) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + 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) + rainbow (3.1.1) + rake (13.3.1) + rake-compiler-dock (1.9.1) + rb_sys (0.9.117) + rake-compiler-dock (= 1.9.1) + rdoc (6.15.1) + erb + psych (>= 4.0.0) + tsort + redcarpet (3.6.1) + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rouge (4.6.1) + rqrcode (3.1.0) + chunky_png (~> 1.0) + rqrcode_core (~> 2.0) + rqrcode_core (2.0.0) + rubocop (1.81.7) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.48.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + ruby-next-core (1.1.2) + ruby-progressbar (1.13.0) + ruby-vips (2.2.5) + ffi (~> 1.12) + logger + ruby2_keywords (0.0.5) + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.38.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + sentry-rails (6.1.1) + railties (>= 5.2.0) + sentry-ruby (~> 6.1.1) + sentry-ruby (6.1.1) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + sniffer (0.5.0) + anyway_config (>= 1.0) + dry-initializer (~> 3) + solid_cable (3.0.12) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.2.4) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) + sqlite3 (2.8.0-aarch64-linux-gnu) + sqlite3 (2.8.0-aarch64-linux-musl) + sqlite3 (2.8.0-arm-linux-gnu) + sqlite3 (2.8.0-arm-linux-musl) + sqlite3 (2.8.0-x86_64-linux-gnu) + sqlite3 (2.8.0-x86_64-linux-musl) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.8) + thor (1.4.0) + thruster (0.1.16) + thruster (0.1.16-aarch64-linux) + thruster (0.1.16-x86_64-linux) + timeout (0.4.4) + trilogy (2.9.0) + tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.1.1) + useragent (0.16.11) + vcr (6.3.1) + base64 + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + web-push (3.0.2) + jwt (~> 3.0) + openssl (~> 3.0) + webmock (3.26.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.1) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + yabeda (0.14.0) + anyway_config (>= 1.0, < 3) + concurrent-ruby + dry-initializer + yabeda-actioncable (0.2.2) + actioncable (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + yabeda (~> 0.8) + yabeda-gc (0.4.0) + yabeda (~> 0.6) + yabeda-http_requests (0.3.0) + anyway_config (>= 1.3, < 3.0) + sniffer + yabeda + yabeda-prometheus-mmap (0.4.0) + prometheus-client-mmap + yabeda (~> 0.10) + yabeda-puma-plugin (0.9.0) + json + puma + yabeda (~> 0.5) + yabeda-rails (0.10.0) + activesupport + anyway_config (>= 1.3, < 3) + railties + yabeda (~> 0.8) + zeitwerk (2.7.3) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + activeresource + autotuner + aws-sdk-s3 + bcrypt (~> 3.1.7) + benchmark + bootsnap + brakeman + bundler-audit + capybara + debug + faker + fizzy-saas! + geared_pagination (~> 1.2) + image_processing (~> 1.14) + importmap-rails + jbuilder + kamal + letter_opener + lexxy + mission_control-jobs + mittens + mocha + net-http-persistent + platform_agent + prometheus-client-mmap (~> 1.1) + propshaft + puma (>= 5.0) + queenbee! + rack-mini-profiler + rails! + rails_structured_logging! + redcarpet + rouge + rqrcode + rubocop-rails-omakase + selenium-webdriver + sentry-rails + sentry-ruby + solid_cable (>= 3.0) + solid_cache (~> 1.0) + solid_queue (~> 1.2) + sqlite3 (>= 2.0) + stimulus-rails + thruster + trilogy (~> 2.9) + turbo-rails + vcr + web-console + web-push + webmock + webrick + yabeda + yabeda-actioncable + yabeda-activejob! + yabeda-gc + yabeda-http_requests + yabeda-prometheus-mmap + yabeda-puma-plugin + yabeda-rails + +BUNDLED WITH + 2.7.2 diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 614add8ecb..616f5bef19 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -11,7 +11,7 @@ def db_adapter def configure_bundle if saas? - ENV["BUNDLE_WITH"] = [ ENV["BUNDLE_WITH"], "saas" ].compact.join(",") + ENV["BUNDLE_GEMFILE"] = "Gemfile.saas" end end end From 97c280ddb1d5258142efc7a84cf4880b00b3a4d9 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:00:21 +0100 Subject: [PATCH 019/107] Prepare bin/setup to work with SAAS env variable / txt file --- bin/setup | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bin/setup b/bin/setup index f410b2f2c4..9835df8359 100755 --- a/bin/setup +++ b/bin/setup @@ -8,6 +8,14 @@ app_root="$( )" export PATH="$app_root/bin:$PATH" +if [ -e tmp/saas.txt ]; then + export SAAS=1 +fi + +if [ -n "$SAAS" ]; then + export BUNDLE_GEMFILE="Gemfile.saas" +fi + # Install gum if needed if ! command -v gum &>/dev/null; then echo @@ -71,14 +79,15 @@ step "Set up gh-signoff" bash -c "gh extension install basecamp/gh-signoff || gh bundle config set --local auto_install true step "Installing RubyGems" bundle install -if [ -e tmp/minio-dev.txt ]; then - step "Starting Docker services" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup minio" +if [ -n "$SAAS" ]; then + if [ -e tmp/minio-dev.txt ]; then + step "Starting Docker services" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup minio" + step "Configuring MinIO" bin/minio-setup + fi - step "Configuring MinIO" bin/minio-setup + step "Starting mysql" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup mysql80" fi -step "Starting mysql" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup mysql80" - if [[ $* == *--reset* ]]; then step "Resetting the database" rails db:reset else From 3caf8d05a230b9b87f0a7e03aedfdd36acb1dce1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:12:33 +0100 Subject: [PATCH 020/107] Remove line, this gets bin/setup confused, better to keep the rails output clean --- bin/rails | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/rails b/bin/rails index e644775f8f..9023575415 100755 --- a/bin/rails +++ b/bin/rails @@ -6,7 +6,5 @@ Fizzy.configure_bundle require_relative "../config/boot" -puts "SaaS version (#{Fizzy.db_adapter})" if Fizzy.saas? - require "rails/commands" From 08cc02804daa237f26484b0171ef82dea754bb53 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:22:32 +0100 Subject: [PATCH 021/107] Invoke setup script in gem --- Gemfile.saas | 2 +- Gemfile.saas.lock | 17 ++++++++--------- bin/setup | 8 ++------ 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Gemfile.saas b/Gemfile.saas index 52f6197c4c..81ff0abb60 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -3,6 +3,6 @@ eval_gemfile "Gemfile" gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", bc: "fizzy-saas" +gem "fizzy-saas", path: "/home/jorge/Work/basecamp/fizzy-saas" gem "rails_structured_logging", bc: "rails-structured-logging" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 4a3483eb95..0a9643f3e8 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/basecamp/fizzy-saas - revision: 7efcfdc7d1b787aa5f023ea96656b9f67bcbfe7f - specs: - fizzy-saas (0.1.0) - queenbee - rails (>= 8.1.0.beta1) - rails_structured_logging - GIT remote: https://github.com/basecamp/queenbee-plugin revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 @@ -134,6 +125,14 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) +PATH + remote: ../fizzy-saas + specs: + fizzy-saas (0.1.0) + queenbee + rails (>= 8.1.0.beta1) + rails_structured_logging + GEM remote: https://rubygems.org/ specs: diff --git a/bin/setup b/bin/setup index 9835df8359..0b7fb10f7a 100755 --- a/bin/setup +++ b/bin/setup @@ -80,12 +80,8 @@ bundle config set --local auto_install true step "Installing RubyGems" bundle install if [ -n "$SAAS" ]; then - if [ -e tmp/minio-dev.txt ]; then - step "Starting Docker services" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup minio" - step "Configuring MinIO" bin/minio-setup - fi - - step "Starting mysql" bash -c "[ -d ~/Work/basecamp/docker-dev ] && git -C ~/Work/basecamp/docker-dev pull || gh repo clone basecamp/docker-dev ~/Work/basecamp/docker-dev && ~/Work/basecamp/docker-dev/setup mysql80" + saas_setup=$(bundle show fizzy-saas)/bin/setup + source "$saas_setup" fi if [[ $* == *--reset* ]]; then From cd231c48d53809cbf8a8918638a14ee49167ec38 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:34:54 +0100 Subject: [PATCH 022/107] Format --- config/ci.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/ci.rb b/config/ci.rb index 85b327d025..c53ef7eb3a 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -12,9 +12,9 @@ step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Security: Gitleaks audit", "bin/gitleaks-audit" - step "Tests: Fizzy", "bin/rails test" + step "Tests: Fizzy", "bin/rails test" - step "Tests: SaaS", "SAAS=1 bin/rails test:saas" if Fizzy.saas? + step "Tests: SaaS", "SAAS=1 bin/rails test:saas" if Fizzy.saas? step "Tests: System", "bin/rails test:system" if success? From fe20d8f2a51ed08b5bd08d37738cc6b7a2e0cdeb Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:35:26 +0100 Subject: [PATCH 023/107] Format --- config/ci.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/ci.rb b/config/ci.rb index c53ef7eb3a..639df7ef31 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -13,8 +13,8 @@ step "Security: Gitleaks audit", "bin/gitleaks-audit" step "Tests: Fizzy", "bin/rails test" - step "Tests: SaaS", "SAAS=1 bin/rails test:saas" if Fizzy.saas? + step "Tests: System", "bin/rails test:system" if success? From f1df9afe722f43833bc44347a8711f115b51fc8d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:36:47 +0100 Subject: [PATCH 024/107] Not needed anymore, no more bundler groups --- config/application.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/application.rb b/config/application.rb index 4bfbab4a22..0399caacac 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,9 +2,7 @@ require "rails/all" require_relative "../lib/fizzy" -groups = Rails.groups -groups << :saas if Fizzy.saas? -Bundler.require(*groups) +Bundler.require(*Rails.groups) module Fizzy class Application < Rails::Application From 8724963b849e98ce30d3f4819a4c4dc52ca53e16 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:44:47 +0100 Subject: [PATCH 025/107] Bring vanilla versions for Dockerfile and deploy config, we are moving those to the private saas gem --- Dockerfile | 81 ++++++++++++++++--------------- config/deploy.yml | 120 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 config/deploy.yml diff --git a/Dockerfile b/Dockerfile index 3f272de4e8..3cd25c7f19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,77 +1,76 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t fizzy . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name fizzy fizzy + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.4.7 -FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails -# Set production environment +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment variables and enable jemalloc for reduced memory usage and latency. ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ - BUNDLE_WITHOUT="development" - + BUNDLE_WITHOUT="development" \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so" # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems RUN apt-get update -qq && \ - apt-get install -y --no-install-recommends -y build-essential pkg-config git libvips libyaml-dev libssl-dev && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install application gems -COPY Gemfile Gemfile.lock .ruby-version ./ -COPY lib/fizzy.rb ./lib/fizzy.rb -COPY gems ./gems/ -RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-permabundle-${RUBY_VERSION},sharing=locked,target=/permabundle \ - gem install bundler && \ - BUNDLE_PATH=/permabundle BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \ - cp -a /permabundle/. "$BUNDLE_PATH"/ && \ - bundle clean --force && \ - rm -rf "$BUNDLE_PATH"/ruby/*/bundler/gems/*/.git && \ - find "$BUNDLE_PATH" -type f \( -name '*.gem' -o -iname '*.a' -o -iname '*.o' -o -iname '*.h' -o -iname '*.c' -o -iname '*.hpp' -o -iname '*.cpp' \) -delete && \ - bundle exec bootsnap precompile --gemfile +COPY Gemfile Gemfile.lock vendor ./ + +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + # -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 + bundle exec bootsnap precompile -j 1 --gemfile # Copy application code COPY . . -# Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ +# Precompile bootsnap code for faster boot times. +# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495 +RUN bundle exec bootsnap precompile -j 1 app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + # Final stage for app image FROM base -# Install packages needed for deployment -RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools sqlite3 libjemalloc-dev && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash +USER 1000:1000 # Copy built artifacts: gems, application -COPY --from=build /usr/local/bundle /usr/local/bundle -COPY --from=build /rails /rails - -# Run and own only the runtime files as a non-root user for security -RUN useradd rails --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp -USER rails:rails +COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --chown=rails:rails --from=build /rails /rails # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] -# Ruby GC tuning values pulled from Autotuner recommendations -ENV RUBY_GC_HEAP_0_INIT_SLOTS=692636 \ - RUBY_GC_HEAP_1_INIT_SLOTS=175943 \ - RUBY_GC_HEAP_2_INIT_SLOTS=148807 \ - RUBY_GC_HEAP_3_INIT_SLOTS=9169 \ - RUBY_GC_HEAP_4_INIT_SLOTS=3054 \ - RUBY_GC_MALLOC_LIMIT=33554432 \ - RUBY_GC_MALLOC_LIMIT_MAX=67108864 \ - LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 - -# Start the server by default, this can be overwritten at runtime -EXPOSE 80 443 9394 +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000000..be1e3c21e6 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,120 @@ +# Name of your application. Used to uniquely configure containers. +service: fizzy + +# Name of the container image (use your-user/app-name on external registries). +image: fizzy + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +# +# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb! +# +# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer). +# +# proxy: +# ssl: true +# host: app.example.com + +# Where you keep your container images. +registry: + # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ... + server: localhost:5555 + + # Needed for authenticated registries. + # username: your-user + + # Always use an access token rather than real password when possible. + # password: + # - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use fizzy-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password" + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "fizzy_storage:/rails/storage" + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: ruby-3.4.7 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data From 53d4ed3749ce3c50d50a4b79841eb8b964a95dac Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:48:51 +0100 Subject: [PATCH 026/107] Point to the remote gem again --- Gemfile.saas | 2 +- Gemfile.saas.lock | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Gemfile.saas b/Gemfile.saas index 81ff0abb60..52f6197c4c 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -3,6 +3,6 @@ eval_gemfile "Gemfile" gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", path: "/home/jorge/Work/basecamp/fizzy-saas" +gem "fizzy-saas", bc: "fizzy-saas" gem "rails_structured_logging", bc: "rails-structured-logging" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 0a9643f3e8..4957ae875e 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/basecamp/fizzy-saas + revision: 38562580daf4d2f4d5f582411086ddf14aaa77a9 + specs: + fizzy-saas (0.1.0) + queenbee + rails (>= 8.1.0.beta1) + rails_structured_logging + GIT remote: https://github.com/basecamp/queenbee-plugin revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 @@ -125,14 +134,6 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) -PATH - remote: ../fizzy-saas - specs: - fizzy-saas (0.1.0) - queenbee - rails (>= 8.1.0.beta1) - rails_structured_logging - GEM remote: https://rubygems.org/ specs: From a7461df0497dc73e2667756225f71059f590648c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:51:25 +0100 Subject: [PATCH 027/107] Update fizzy-saas --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 4957ae875e..814a9a63cd 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 38562580daf4d2f4d5f582411086ddf14aaa77a9 + revision: cb340c7f3b442b904379524de43275e3e0950064 specs: fizzy-saas (0.1.0) queenbee From 9132f6b8351cdd53dfed7771ac3ef53717c90c6c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 18:57:32 +0100 Subject: [PATCH 028/107] Fix path to saas. --- lib/fizzy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 616f5bef19..69b3f9c6f2 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -2,7 +2,7 @@ module Fizzy class << self def saas? return @saas if defined?(@saas) - @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../../tmp/saas.txt", __dir__))) + @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../tmp/saas.txt", __dir__))) end def db_adapter From 9496a207365ca54987fc2cd0096205f210f242cf Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 19:11:14 +0100 Subject: [PATCH 029/107] Update to latest fizzy-saas --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 814a9a63cd..120a939b11 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: cb340c7f3b442b904379524de43275e3e0950064 + revision: 7b096f10526ac797ec9e14314a4752c1482c9152 specs: fizzy-saas (0.1.0) queenbee From a5b02755b25b669c0513df3eb290b67473cb2673 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 23 Nov 2025 20:26:03 +0100 Subject: [PATCH 030/107] Add rake tasks for enabling/disabling saas mode --- Gemfile.saas.lock | 2 +- bin/rails | 1 - lib/tasks/saas.rake | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 lib/tasks/saas.rake diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 120a939b11..7fdfc8bac8 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 7b096f10526ac797ec9e14314a4752c1482c9152 + revision: f901a455ecc8c313bdba7be89619d289fa7e112c specs: fizzy-saas (0.1.0) queenbee diff --git a/bin/rails b/bin/rails index 9023575415..9d935530de 100755 --- a/bin/rails +++ b/bin/rails @@ -7,4 +7,3 @@ Fizzy.configure_bundle require_relative "../config/boot" require "rails/commands" - diff --git a/lib/tasks/saas.rake b/lib/tasks/saas.rake new file mode 100644 index 0000000000..77df8f4215 --- /dev/null +++ b/lib/tasks/saas.rake @@ -0,0 +1,18 @@ +namespace :saas do + SAAS_FILE_PATH = "tmp/saas.txt" + + desc "Enable SaaS mode" + task :enable => :environment do + file_path = Rails.root.join(SAAS_FILE_PATH) + FileUtils.mkdir_p(File.dirname(file_path)) + FileUtils.touch(file_path) + puts "SaaS mode enabled (#{file_path} created)" + end + + desc "Disable SaaS mode" + task :disable => :environment do + file_path = Rails.root.join(SAAS_FILE_PATH) + FileUtils.rm_f(file_path) + puts "SaaS mode disabled (#{file_path} removed)" + end +end From c62e843d2b1c8ffc978ad3e48d778755e2efccd8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 24 Nov 2025 08:36:10 +0100 Subject: [PATCH 031/107] Move bc shorthand to saas gemfile --- Gemfile | 1 - Gemfile.saas | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a4cb0a29fe..e4e7bf772b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,4 @@ source "https://rubygems.org" -git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } gem "rails", github: "rails/rails", branch: "main" diff --git a/Gemfile.saas b/Gemfile.saas index 52f6197c4c..779c8c4198 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -1,6 +1,9 @@ # This Gemfile extends the base Gemfile with SaaS-specific dependencies eval_gemfile "Gemfile" +git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } + + gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" gem "fizzy-saas", bc: "fizzy-saas" From 0754b0e93c3e0fe053df9441d2423e5246a345d7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 24 Nov 2025 15:43:16 +0100 Subject: [PATCH 032/107] Test moved to the gem --- test/controllers/sessions_controller_test.rb | 21 -------------------- 1 file changed, 21 deletions(-) diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 7fed629008..2f5b27b0dc 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -21,22 +21,6 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest end end - if Fizzy.saas? - test "create for a new user" do - untenanted do - assert_difference -> { Identity.count }, +1 do - assert_difference -> { MagicLink.count }, +1 do - post session_path, - params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" }, - headers: http_basic_auth_headers("testname", "testpassword") - end - end - - assert_redirected_to session_magic_link_path - end - end - end - test "destroy" do sign_in_as :kevin @@ -47,9 +31,4 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest assert_not cookies[:session_token].present? end end - - private - def http_basic_auth_headers(user, password) - { "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } - end end From 9e8bc5b30b371b45a937f3545130e617460eb93a Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 24 Nov 2025 16:27:17 +0100 Subject: [PATCH 033/107] Update to last version, tests passing! --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 7fdfc8bac8..1697468f67 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: f901a455ecc8c313bdba7be89619d289fa7e112c + revision: 8b096fa8cb4c2823a726a0dae872214ac3642a34 specs: fizzy-saas (0.1.0) queenbee From 0328149ba206ba459d72a3e525fe96343bd78459 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 24 Nov 2025 16:37:20 +0100 Subject: [PATCH 034/107] Initial github action setup --- .github/workflows/ci.yml | 100 +++++++++++++++++++++++++++++++++++++++ lib/tasks/saas.rake | 4 +- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..6bd2efb26d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + pull_request: + +jobs: + security: + name: Security + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + env: + BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" + BUNDLE_GEMFILE: Gemfile + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Gem audit + run: bin/bundler-audit check --update + + - name: Importmap audit + run: bin/importmap audit + + - name: Brakeman audit + run: bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error + + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + env: + BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" + BUNDLE_GEMFILE: Gemfile + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop + + + test: + name: Tests (${{ matrix.mode }}) + runs-on: ubuntu-latest + + strategy: + matrix: + mode: [SQLite, MySQL, SaaS] + + services: + mysql: + image: ${{ (matrix.mode == 'MySQL' || matrix.mode == 'SaaS') && 'mysql:8.0' || '' }} + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: fizzy_test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + env: + RAILS_ENV: test + DATABASE_ADAPTER: ${{ matrix.mode == 'SQLite' && 'sqlite' || 'mysql' }} + ${{ matrix.mode == 'SaaS' && 'SAAS' || 'SAAS_DISABLED' }}: ${{ matrix.mode == 'SaaS' && '1' || '' }} + BUNDLE_GEMFILE: ${{ matrix.mode == 'SaaS' && 'Gemfile.saas' || 'Gemfile' }} + MYSQL_HOST: 127.0.0.1 + MYSQL_PORT: 3306 + MYSQL_USER: root + + steps: + - name: Install system packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg + + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + env: + BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run tests + run: bin/rails db:setup test + + - name: Run system tests + run: bin/rails test:system diff --git a/lib/tasks/saas.rake b/lib/tasks/saas.rake index 77df8f4215..4318da05a4 100644 --- a/lib/tasks/saas.rake +++ b/lib/tasks/saas.rake @@ -2,7 +2,7 @@ namespace :saas do SAAS_FILE_PATH = "tmp/saas.txt" desc "Enable SaaS mode" - task :enable => :environment do + task enable: :environment do file_path = Rails.root.join(SAAS_FILE_PATH) FileUtils.mkdir_p(File.dirname(file_path)) FileUtils.touch(file_path) @@ -10,7 +10,7 @@ namespace :saas do end desc "Disable SaaS mode" - task :disable => :environment do + task disable: :environment do file_path = Rails.root.join(SAAS_FILE_PATH) FileUtils.rm_f(file_path) puts "SaaS mode disabled (#{file_path} removed)" From 5a1d40ae65bbb6160125479a1384888c77df4f44 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:12:56 +0100 Subject: [PATCH 035/107] Omit empty image --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bd2efb26d..17c2020cb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: services: mysql: - image: ${{ (matrix.mode == 'MySQL' || matrix.mode == 'SaaS') && 'mysql:8.0' || '' }} + image: 'mysql:8.0' env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: fizzy_test From 93c60d09a88678ed37e0d139103c056cfa87f642 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:15:56 +0100 Subject: [PATCH 036/107] Omit MySQL service with if condition --- .github/workflows/ci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17c2020cb4..6c26b49b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,11 +55,18 @@ jobs: strategy: matrix: - mode: [SQLite, MySQL, SaaS] + include: + - mode: SQLite + db_adapter: sqlite + - mode: MySQL + db_adapter: mysql + - mode: SaaS + db_adapter: mysql services: mysql: - image: 'mysql:8.0' + if: ${{ matrix.db_adapter == 'mysql' }} + image: mysql:8.0 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: fizzy_test @@ -73,7 +80,7 @@ jobs: env: RAILS_ENV: test - DATABASE_ADAPTER: ${{ matrix.mode == 'SQLite' && 'sqlite' || 'mysql' }} + DATABASE_ADAPTER: ${{ matrix.db_adapter }} ${{ matrix.mode == 'SaaS' && 'SAAS' || 'SAAS_DISABLED' }}: ${{ matrix.mode == 'SaaS' && '1' || '' }} BUNDLE_GEMFILE: ${{ matrix.mode == 'SaaS' && 'Gemfile.saas' || 'Gemfile' }} MYSQL_HOST: 127.0.0.1 From 127a4ebdc97e8ec29f5ad1fabf0a6c6e6e094c99 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:18:36 +0100 Subject: [PATCH 037/107] LOL if does not exist --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c26b49b6e..1ebe6a84bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,6 @@ jobs: services: mysql: - if: ${{ matrix.db_adapter == 'mysql' }} image: mysql:8.0 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes From ac649f1af086c905f54f93cc7fc435bfe3a00ad0 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:22:27 +0100 Subject: [PATCH 038/107] Try to pin the host --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ebe6a84bc..0e1cda9e5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: DATABASE_ADAPTER: ${{ matrix.db_adapter }} ${{ matrix.mode == 'SaaS' && 'SAAS' || 'SAAS_DISABLED' }}: ${{ matrix.mode == 'SaaS' && '1' || '' }} BUNDLE_GEMFILE: ${{ matrix.mode == 'SaaS' && 'Gemfile.saas' || 'Gemfile' }} - MYSQL_HOST: 127.0.0.1 + MYSQL_HOST: mysql MYSQL_PORT: 3306 MYSQL_USER: root From 00cb9a3638096e9018b76578f1fbd23f1093ae82 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:31:23 +0100 Subject: [PATCH 039/107] Set proper SaaS vars for mysql --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e1cda9e5e..d877e78793 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,8 @@ jobs: MYSQL_HOST: mysql MYSQL_PORT: 3306 MYSQL_USER: root + FIZZY_DB_HOST: ${{ matrix.mode == 'SaaS' && 'mysql' || '' }} + FIZZY_DB_PORT: ${{ matrix.mode == 'SaaS' && '3306' || '' }} steps: - name: Install system packages From d81cf9161e7463c073e6b8f897774dc8bba8a3cb Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 12:34:19 +0100 Subject: [PATCH 040/107] Fix hostname --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d877e78793..f7e211a9c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,10 +82,10 @@ jobs: DATABASE_ADAPTER: ${{ matrix.db_adapter }} ${{ matrix.mode == 'SaaS' && 'SAAS' || 'SAAS_DISABLED' }}: ${{ matrix.mode == 'SaaS' && '1' || '' }} BUNDLE_GEMFILE: ${{ matrix.mode == 'SaaS' && 'Gemfile.saas' || 'Gemfile' }} - MYSQL_HOST: mysql + MYSQL_HOST: 127.0.0.1 MYSQL_PORT: 3306 MYSQL_USER: root - FIZZY_DB_HOST: ${{ matrix.mode == 'SaaS' && 'mysql' || '' }} + FIZZY_DB_HOST: ${{ matrix.mode == 'SaaS' && '127.0.0.1' || '' }} FIZZY_DB_PORT: ${{ matrix.mode == 'SaaS' && '3306' || '' }} steps: From f62bf9fcbf20f2da223ae5c0c5b84377388eab80 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 11:52:11 +0100 Subject: [PATCH 041/107] Bundle! --- Gemfile.lock | 34 ---------------------------------- Gemfile.saas.lock | 10 ++++++++-- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6556fd3320..ded3d30825 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,3 @@ -GIT - remote: https://github.com/basecamp/fizzy-saas - revision: 00de8e2e1ae8e9df5e0a2acf4593a4e9a824c5c8 - specs: - fizzy-saas (0.1.0) - queenbee - rails (>= 8.1.0.beta1) - rails_structured_logging - -GIT - remote: https://github.com/basecamp/queenbee-plugin - revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2 - specs: - queenbee (3.2.0) - activeresource - builder - rexml - -GIT - remote: https://github.com/basecamp/rails-structured-logging - revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6 - specs: - rails_structured_logging (0.2.1) - json - rails (>= 6.0.0) - GIT remote: https://github.com/basecamp/useragent revision: 433ca320a42db1266c4b89df74d0abdb9a880c5e @@ -145,14 +119,6 @@ GEM specs: action_text-trix (2.1.15) railties - activemodel-serializers-xml (1.0.3) - activemodel (>= 5.0.0.a) - activesupport (>= 5.0.0.a) - builder (~> 3.1) - activeresource (6.2.0) - activemodel (>= 7.0) - activemodel-serializers-xml (~> 1.0) - activesupport (>= 7.0) addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) anyway_config (2.7.2) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 1697468f67..21825b4b9a 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -24,6 +24,12 @@ GIT json rails (>= 6.0.0) +GIT + remote: https://github.com/basecamp/useragent + revision: 433ca320a42db1266c4b89df74d0abdb9a880c5e + specs: + useragent (0.16.11) + GIT remote: https://github.com/basecamp/yabeda-activejob.git revision: 684973f77ff01d8b3dd75874538fae55961e15e6 @@ -519,7 +525,6 @@ GEM unicode-emoji (~> 4.1) unicode-emoji (4.1.0) uri (1.1.1) - useragent (0.16.11) vcr (6.3.1) base64 web-console (4.2.1) @@ -605,7 +610,7 @@ DEPENDENCIES mocha net-http-persistent platform_agent - prometheus-client-mmap (~> 1.1) + prometheus-client-mmap (~> 1.3) propshaft puma (>= 5.0) queenbee! @@ -627,6 +632,7 @@ DEPENDENCIES thruster trilogy (~> 2.9) turbo-rails + useragent! vcr web-console web-push From c5d7dc325593894ce8cef5886d81fc23af8fb691 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 11:48:28 +0100 Subject: [PATCH 042/107] Restore bc shorthand --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index e4e7bf772b..ce2324e1b3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" gem "rails", github: "rails/rails", branch: "main" +git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } # Assets & front end gem "importmap-rails" From 99bda234c0ce5a94deefa7cec86e82fd557ab776 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 12:01:44 +0100 Subject: [PATCH 043/107] Bring changes from main --- config/database.sqlite.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/database.sqlite.yml b/config/database.sqlite.yml index 44fcda8cc9..7cc7404c7f 100644 --- a/config/database.sqlite.yml +++ b/config/database.sqlite.yml @@ -1,21 +1,22 @@ default: &default adapter: sqlite3 - pool: 50 + pool: 5 timeout: 5000 development: primary: <<: *default - database: db/development.sqlite3 + database: storage/development.sqlite3 schema_dump: schema_sqlite.rb test: primary: <<: *default - database: db/test.sqlite3 + database: storage/test.sqlite3 schema_dump: schema_sqlite.rb production: primary: <<: *default - database: db/production.sqlite3 + database: storage/production.sqlite3 + schema_dump: schema_sqlite.rb From 624b6680c7990f7a7f7522e6572fad0fb948245f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 12:31:39 +0100 Subject: [PATCH 044/107] Fix condition --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7e211a9c2..8b54f59b4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,8 +80,8 @@ jobs: env: RAILS_ENV: test DATABASE_ADAPTER: ${{ matrix.db_adapter }} - ${{ matrix.mode == 'SaaS' && 'SAAS' || 'SAAS_DISABLED' }}: ${{ matrix.mode == 'SaaS' && '1' || '' }} - BUNDLE_GEMFILE: ${{ matrix.mode == 'SaaS' && 'Gemfile.saas' || 'Gemfile' }} + ${{ inputs.saas && 'SAAS' || 'SAAS_DISABLED' }}: ${{ inputs.saas && '1' || '' }} + BUNDLE_GEMFILE: ${{ inputs.saas && 'Gemfile.saas' || 'Gemfile' }} MYSQL_HOST: 127.0.0.1 MYSQL_PORT: 3306 MYSQL_USER: root From afbc0ca8e5b7ecb36c893de9bae8992f1756abfb Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 13:05:30 +0100 Subject: [PATCH 045/107] Include engine tests in saas mode Notice that test is not a regular rake task you can enhance, but a hard-coded rails command. --- bin/rails | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/rails b/bin/rails index 9d935530de..8f0b2d8b3f 100755 --- a/bin/rails +++ b/bin/rails @@ -7,3 +7,5 @@ Fizzy.configure_bundle require_relative "../config/boot" require "rails/commands" + +Fizzy::Saas.append_test_paths if Fizzy.saas? && Rails.env.test? From 40455bff3aee52f0efbe5405e32962b4407901d6 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 13:06:44 +0100 Subject: [PATCH 046/107] Update fizzy-saas to bring test support --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 21825b4b9a..b1be053417 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 8b096fa8cb4c2823a726a0dae872214ac3642a34 + revision: 6f071760dddb5929d5ae1bab45ad917a1fd8c1fc specs: fizzy-saas (0.1.0) queenbee From dd0f9f174eb87058d0fe7783f52cc174934181d8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 14:58:19 +0100 Subject: [PATCH 047/107] Split CI files to prevent PR workflows from accessing secrets --- .github/workflows/ci-main.yml | 12 ++++++++++++ .github/workflows/ci-pr.yml | 10 ++++++++++ .github/workflows/ci.yml | 29 ++++++++++++++--------------- 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci-main.yml create mode 100644 .github/workflows/ci-pr.yml diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml new file mode 100644 index 0000000000..7e79ee0e70 --- /dev/null +++ b/.github/workflows/ci-main.yml @@ -0,0 +1,12 @@ +name: CI (Main) + +on: + push: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + saas: true + secrets: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 0000000000..c6712ad938 --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,10 @@ +name: CI (PR) + +on: + pull_request: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + saas: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b54f59b4a..5f207f3dbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,14 @@ -name: CI +name: Reusable CI on: - push: - pull_request: + workflow_call: + inputs: + saas: + type: boolean + required: true + secrets: + GH_TOKEN: + required: false jobs: security: @@ -11,11 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - env: - BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" - BUNDLE_GEMFILE: Gemfile with: ruby-version: .ruby-version bundler-cache: true @@ -36,11 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - env: - BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" - BUNDLE_GEMFILE: Gemfile with: ruby-version: .ruby-version bundler-cache: true @@ -60,8 +58,10 @@ jobs: db_adapter: sqlite - mode: MySQL db_adapter: mysql + # Only include SaaS if requested - mode: SaaS db_adapter: mysql + saas: ${{ inputs.saas }} services: mysql: @@ -85,8 +85,9 @@ jobs: MYSQL_HOST: 127.0.0.1 MYSQL_PORT: 3306 MYSQL_USER: root - FIZZY_DB_HOST: ${{ matrix.mode == 'SaaS' && '127.0.0.1' || '' }} - FIZZY_DB_PORT: ${{ matrix.mode == 'SaaS' && '3306' || '' }} + FIZZY_DB_HOST: 127.0.0.1 + FIZZY_DB_PORT: 3306 + BUNDLE_GITHUB__COM: ${{ inputs.saas && format('x-access-token:{0}', secrets.GH_TOKEN) || '' }} steps: - name: Install system packages @@ -95,8 +96,6 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 - env: - BUNDLE_GITHUB__COM: "x-access-token:${{ secrets.GH_TOKEN }}" with: ruby-version: .ruby-version bundler-cache: true From e213e3e5f89f76cef3fad99e42334eb9402e2d26 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 14:58:43 +0100 Subject: [PATCH 048/107] Update fizzy-saas gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index b1be053417..bebc6098fa 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 6f071760dddb5929d5ae1bab45ad917a1fd8c1fc + revision: 3bf089d1af610c828fae1e6a15d5d6b0fbce9b5b specs: fizzy-saas (0.1.0) queenbee From 68bb4a84e23c51eb3b2cff66fa0c23fa82d9f340 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 25 Nov 2025 15:00:56 +0100 Subject: [PATCH 049/107] Rename --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f207f3dbb..7984fca661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Reusable CI +name: Common CI on: workflow_call: @@ -58,7 +58,6 @@ jobs: db_adapter: sqlite - mode: MySQL db_adapter: mysql - # Only include SaaS if requested - mode: SaaS db_adapter: mysql saas: ${{ inputs.saas }} From 499b132b42bf42525c275328925dc54962acdc99 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 13:04:56 +0100 Subject: [PATCH 050/107] Remove script that depends on QB --- script/create-account.rb | 105 --------------------------------------- 1 file changed, 105 deletions(-) delete mode 100755 script/create-account.rb diff --git a/script/create-account.rb b/script/create-account.rb deleted file mode 100755 index a46f4310f7..0000000000 --- a/script/create-account.rb +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env ruby -# Usage: script/create-account "Company Name" "Owner Name" "owner@example.com" - -require_relative "../config/environment" - -# Parse arguments -if ARGV.size != 3 - puts "Usage: script/create-account " - puts "Example: script/create-account 'Acme Corp' 'John Doe' 'john@acme.com'" - exit 1 -end - -company_name, owner_name, owner_email = ARGV - -# Create a minimal Current context for the signup -Current.set( - ip_address: "127.0.0.1", - user_agent: "create-account script", - referrer: nil -) do - puts "Creating account..." - puts " Company: #{company_name}" - puts " Owner: #{owner_name}" - puts " Email: #{owner_email}" - puts - - # Step 1: Create the account in QueenBee - queenbee_account_attributes = { - skip_remote: true, - product_name: "fizzy", - name: company_name, - owner_name: owner_name, - owner_email: owner_email, - trial: true, - subscription: { - name: "FreeV1", - price: 0 - }, - remote_request: { - remote_address: Current.ip_address, - user_agent: Current.user_agent, - referrer: Current.referrer - } - } - - begin - queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes) - puts "✓ Account created in QueenBee" - rescue => error - puts "Error creating QueenBee account:" - puts " - #{error.message}" - exit 1 - end - - # Step 2: Create tenant with the QueenBee account ID - tenant_id = queenbee_account.id.to_s - - begin - ApplicationRecord.create_tenant(tenant_id) do - # Create account with admin user - account = Account.create_with_admin_user( - account: { - external_account_id: tenant_id, - name: company_name - }, - owner: { - name: owner_name, - email_address: owner_email - } - ) - end - - puts "✓ Tenant created" - puts "✓ Account setup completed" - rescue => error - # Clean up QueenBee account if tenant creation fails - queenbee_account&.cancel - - puts "Error setting up tenant:" - puts " - #{error.message}" - exit 1 - end - - # Step 3: Get or create join code - ApplicationRecord.with_tenant(tenant_id) do - account = Current.account - join_code = account.join_code - - puts "✓ Join code ready" - puts - puts "Account created successfully!" - puts "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - puts "Tenant: #{tenant_id}" - puts "Company: #{company_name}" - puts "Owner: #{owner_name}" - puts "Email: #{owner_email}" - puts "Join URL: #{Rails.application.routes.url_helpers.join_url( - join_code: join_code, - script_name: "/#{tenant_id}", - host: Rails.application.config.action_mailer.default_url_options[:host], - port: Rails.application.config.action_mailer.default_url_options[:port], - protocol: Rails.env.production? ? 'https' : 'http' - )}" - end -end From 8baf5b8abd3f677e4cf0fd851fff2799e80f7f6b Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 13:40:46 +0100 Subject: [PATCH 051/107] Make mission control only accessible for staff members --- app/controllers/admin/stats_controller.rb | 2 -- app/controllers/admin_controller.rb | 1 + config/application.rb | 2 ++ config/initializers/mission_control.rb | 4 +--- .../controllers/admin/mission_control_test.rb | 23 +++++++++++++++++++ .../admin/stats_controller_test.rb | 23 +++++++++++++++++++ test/fixtures/identities.yml | 2 ++ 7 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 test/controllers/admin/mission_control_test.rb create mode 100644 test/controllers/admin/stats_controller_test.rb diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb index 25d19ad312..92b49d3ce8 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -1,6 +1,4 @@ class Admin::StatsController < AdminController - disallow_account_scope - layout "public" def show diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index cc268a5675..0da7808129 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -1,3 +1,4 @@ class AdminController < ApplicationController + disallow_account_scope before_action :ensure_staff end diff --git a/config/application.rb b/config/application.rb index 0399caacac..ced9d93054 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,5 +24,7 @@ class Application < Rails::Application config.generators do |g| g.orm :active_record, primary_key_type: :uuid end + + config.mission_control.jobs.http_basic_auth_enabled = false end end diff --git a/config/initializers/mission_control.rb b/config/initializers/mission_control.rb index c2098204e2..2fe3c3ed01 100644 --- a/config/initializers/mission_control.rb +++ b/config/initializers/mission_control.rb @@ -1,5 +1,3 @@ Rails.application.config.before_initialize do - # We don't want normal tenanted authentication on mission control. - # Note that we're using HTTP basic auth configured via credentials. - MissionControl::Jobs.base_controller_class = "ActionController::Base" + MissionControl::Jobs.base_controller_class = "AdminController" end diff --git a/test/controllers/admin/mission_control_test.rb b/test/controllers/admin/mission_control_test.rb new file mode 100644 index 0000000000..265090928d --- /dev/null +++ b/test/controllers/admin/mission_control_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class Admin::MissionControlTest < ActionDispatch::IntegrationTest + test "staff can access mission control jobs" do + sign_in_as :david + + untenanted do + get "/admin/jobs" + end + + assert_response :success + end + + test "non-staff cannot access mission control jobs" do + sign_in_as :jz + + untenanted do + get "/admin/jobs" + end + + assert_response :forbidden + end +end diff --git a/test/controllers/admin/stats_controller_test.rb b/test/controllers/admin/stats_controller_test.rb new file mode 100644 index 0000000000..f87dd7cf44 --- /dev/null +++ b/test/controllers/admin/stats_controller_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class Admin::StatsControllerTest < ActionDispatch::IntegrationTest + test "staff can access stats" do + sign_in_as :david + + untenanted do + get admin_stats_url + end + + assert_response :success + end + + test "non-staff cannot access stats" do + sign_in_as :jz + + untenanted do + get admin_stats_url + end + + assert_response :forbidden + end +end diff --git a/test/fixtures/identities.yml b/test/fixtures/identities.yml index a014e801c2..af4c2a060a 100644 --- a/test/fixtures/identities.yml +++ b/test/fixtures/identities.yml @@ -1,11 +1,13 @@ david: email_address: david@37signals.com + staff: true jz: email_address: jz@37signals.com kevin: email_address: kevin@37signals.com + staff: true mike: email_address: mike@37signals.com From 197090f7543df364f63015d1e9983027a19a93a7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 16:22:00 +0100 Subject: [PATCH 052/107] Move active storage config to gem --- config/storage.oss.yml | 18 ++++++++++++++++++ config/storage.yml | 42 ++++++++++-------------------------------- 2 files changed, 28 insertions(+), 32 deletions(-) create mode 100644 config/storage.oss.yml diff --git a/config/storage.oss.yml b/config/storage.oss.yml new file mode 100644 index 0000000000..e50debd344 --- /dev/null +++ b/config/storage.oss.yml @@ -0,0 +1,18 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage/files") %> + +local: + service: Disk + root: <%= Rails.root.join("storage", Rails.env, "files") %> + +devminio: + service: S3 + bucket: fizzy-dev-activestorage + endpoint: "http://minio.localhost:39000" + force_path_style: true + request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support + response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support + region: us-east-1 # default region required for signer + access_key_id: minioadmin + secret_access_key: minioadmin diff --git a/config/storage.yml b/config/storage.yml index ca2b64633f..302a7a63e9 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -1,33 +1,11 @@ -test: - service: Disk - root: <%= Rails.root.join("tmp/storage/files") %> +<% + require_relative "../lib/fizzy" -local: - service: Disk - root: <%= Rails.root.join("storage", Rails.env, "files") %> - -devminio: - service: S3 - bucket: fizzy-dev-activestorage - endpoint: "http://minio.localhost:39000" - force_path_style: true - request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support - response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support - region: us-east-1 # default region required for signer - access_key_id: minioadmin - secret_access_key: minioadmin - -# We have "development", "staging", and "production" buckets configured. Note that we don't have a -# "beta" bucket. (As of 2025-06-01.) -<% pure_env = Rails.env.beta? ? "production" : Rails.env %> -purestorage: - service: S3 - bucket: fizzy-<%= pure_env %>-activestorage - endpoint: "https://storage.basecamp.com" - ssl_verify_peer: false # FIXME: using self-signed cert internally - force_path_style: true - request_checksum_calculation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support - response_checksum_validation: when_required # default is when_supported with CRC64NVME checksum which FlashBlade doesn't support - region: us-east-1 # default region required for signer - access_key_id: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :access_key_id) %> - secret_access_key: <%= Rails.application.credentials.dig(:active_storage, :purestorage_service, :secret_access_key) %> + config_path = if Fizzy.saas? + gem_path = Gem::Specification.find_by_name("fizzy-saas").gem_dir + File.join(gem_path, "config", "storage.yml") + else + File.join(__dir__, "storage.oss.yml") + end +%> +<%= ERB.new(File.read(config_path)).result %> From 18e1badfb524297f0f1df47063e0df490c672ce7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 16:22:21 +0100 Subject: [PATCH 053/107] Prevent choking when no structured logging for now --- config/environments/development.rb | 2 +- config/environments/production.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index e3833ed313..c13b122e21 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -94,7 +94,7 @@ config.solid_queue.connects_to = { database: { writing: :queue, reading: :queue } } end - if Rails.root.join("tmp/structured-logging.txt").exist? + if config.respond_to?(:structured_logging) && Rails.root.join("tmp/structured-logging.txt").exist? config.structured_logging.logger = ActiveSupport::Logger.new("log/structured-development.log") end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 31b7d27706..1a34453fa6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -55,8 +55,8 @@ # Suppress unstructured log lines config.log_level = :fatal - # Structured JSON logging - config.structured_logging.logger = ActiveSupport::Logger.new(STDOUT) + # Structured JSON logging. Pending to move to new Rails' built-in support. + config.structured_logging.logger = ActiveSupport::Logger.new(STDOUT) if config.respond_to?(:structured_logging) # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] From 44d086e076a7a96fed4b579e3630907f0fd5ee8e Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 16:43:09 +0100 Subject: [PATCH 054/107] Cleanup of credentials Secrets moved to 1password. --- config/credentials/beta.yml.enc | 2 +- config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/credentials/test.yml.enc | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/credentials/beta.yml.enc b/config/credentials/beta.yml.enc index 0fa2c3d57b..a4316dc2e2 100644 --- a/config/credentials/beta.yml.enc +++ b/config/credentials/beta.yml.enc @@ -1 +1 @@ -6Ciuxv75imDwiIXYs8lVxsDClQq+76RRr9gvVSxbDNpQhhuETHtMZZPiK2yfHtnFwwm/CrkpWhIRy+ZUt+9bQjnz3J3dMwPmsHdmKTnxUeiKuj+7k5QPKDHc0G1kfpxRANsB10WEcjt5IeoNcNxTrXYbja3NX2OhHYOFWMW/jLpGIXRPnBw0+JgQElKQI42vm1APix/9DRwqrM7TmgjXjJw4q4C7WeSgc4Y7+sHF5T2wJrtkdmExgD7X+9BTI7FOHkpwmQIYEWe34P1/BSFgFcy6tgwI/5U74QUcsj/dEgtKADukLQK9eMPLbfbJS4X7WUD1bDrOnmix5WQy0kdERq4NGSzdvumOhOY4LKYqS3otnPuTIZObbduLxPY9mgd8VFXfev8EjCrrVcVLVNUvtqTz+ri4fYAaHGTRQXttV2oOKtAik8JAggSrY3W5zZGWPEknDD704jyzi4ZRhJ2goMf+vvNZeH6pcQeWlLrIBxrfghGDrhDiHYFqaub2+2qeHhhkenOGtsttHiG4lpfcgmEa2Sr6ADdcxcmOi9qMt17LFmvplw2jR13aIUfglw4K2mzaikCUkDbgKRPqjKpbwkKbdJOaGFx69Oa4ZtiXX+ecKSgGhxhyIOyg7c8NtGeUNAZbjVEzdG8jqj+WBPLzozvpE+3y/dHgQ8MXegf5fsoov/2cpO2HK9tAepgjPyfAU7Rl5nzLrbGPsvgZ1/VRWr/Li6baZBVKrdZnWrjqLLfgqT28deVIHfwrlspifKq2wtm0eIyNGDs3Sm9wIYc1H2QMBe0zVly9rpQBnD8f901CzFQ4PN9ZIn1ZxdoNsdPdc/26mNHwa1UvVyPxgfLMBhlofRRpLNXLh1HOEzBYxuoHtJ3f86G0UwcxkhG7dTxbm8ujQ8RmYfHfgOEBJET/P7rILflXEtRsKPfkNjZqHIt0AeEjpnA5ZmSYZrpsjAQ8FnblgCj32/QfKBsW7MIvJ18w2AOap+z7v9RyJTB9Pq93KdR232xXg7U9sX/qUMI3j6+Gx0URrTFlWCDXpq8QYK+BN/vqUfe39rSxKh/qy+JCbk1B/Ftd52Cn+gsG/fHnVaKyCK04UZSrYllqg82gMrl6A39v8PAiD/x1VFxiZzSm/oi8rBAVI9elOzxXzSr0lDUr+w0iXmiJUzSzLkgtd6nf74cjEmitZQqRRgo9/YANJQkh2HiABHuevHsBgcebw+FOM4VO9y4lljY/Ex98echppRjZs4DgJYK2/R4uQBOMBTlxuqdwuSGodYSlmI5kW43bZGTTA5KJa7w1DRmpVhQfoReWg5EDxifXh2sUkFqTMOejOauQm5fjN9RxnXA1c7b7mxpvBgkepUEI+o+AcbzNlAXG/orcm9zlt2sC8YoDR1T01blYEhoAM6IwqXCXt6ODxKEgMGQ/S3EhDWQQ989lR2gmoMjjbkwxU75pT/SDWJi/A2kFBC+e952Al+iybFxaUM9VpNfPrLB2iNIOO5McQJCH/EjD8+/ZfhhSin5fAl0GFFZyyk0Shd0+TWkuJk0mtAf3ev1sohzGFieCcB5Shj4oNcqWTvPisb4DCoO2AOnEo3adNPA3zCDRvSWv+rwsXEzqW2w2Wav4cAV8VqyKj/6NYIbvwbjwD3ojnpB41705+OekhFubgTLLCnk3+tbNgaLlBcnUawuHW4akZezmUCUoOYoLRzGEQwxCGMG6Hn9lNDw46Uyv7VsggpaNVORTeYGX1Om25QcDM+OBoYQynD24gg==--UIlvvCt/ukIZVxPZ--en6RIr54eZ9nuOvN+prX4w== \ No newline at end of file +JwcXSv6qlqJvbzfHX3v0TB1WvqgI8CEqVRuR4m760EUTJDe6k6NeX1b41P33Zr341GT/LSyxnlxU5psfx4TxvSZhnJRv4e/YuAr04hFIhWnKnjvJtkwDQRgaZH689O4vgGVnQIEPln/DGag=--Q4Xa+Y5602cPApVn--s3PqHeC1cQ/7Fg2dTJ1w4w== \ No newline at end of file diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index 2f25a36ca3..fac62a2bbc 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -lbv40Nq+bDCBc6DGVp3Q+CBuKLAt26v1vkKhOuUBJ/hPr07N98H13tejmM+C1GHF8DUJsTTsmSLYdYgelng0GDo9qhSpC/k+thZ1+Ne5pmZh00sH58PwItUNPBtLluqlpolRfNrJGMK3kjprWyTCbznAfGV6BZoT7Hbv12ZIri2bKsuTvsPZiIYLLAe2a2CWrNWjs4RezZbGWzbJpIyj+RIj3GKhGNCWNFhq79HyeRChaZNxioMaAgWBfAYKvXvkR2MuKEaspJM3lXnpnyxqVKWEWz4orhTCvlqbZchESDLF7hAuIkzXCXEC2EHpeEbzD0F52K/mLzT+kmi8dI+eLVimPJn0PZslccsHNAnXXHwvLvLOPFwzPz9N0jjGihu6yZKE1vl5r1dzo9mVWVTnsI5Z/g5SAmOtY+DB+pzssQdXSLFQZ/l2JcWvgYA9wCQyQbyGLEeirGe/QeCQciTkE+Z/bj7b+20rBgsRQIyHxNdzWDsmi4onJ75lIz6PA+1X4AIkwnAqUVzuBDwB+VxdwbzRZZeURMLBVzs76Ta/qSemIpuZ782cyt6MRmeYuAT6RMccnG/zwdvPHtpJ6C8fqKyOu+XkU2a49Yx/D6l/uvVtwiOayHTCwn2QmGYDlnniCBDJCzp0y0Y1M4d3XxAdV61XEL6rISMuM0mjdRZL3PYTpR8mrrflnYA4kFrLUnA+hEmcOq8stHSL1RYe83PI4dj6l1/hzQ6up2cGNBk+to7FXpQOhkuYV2+9xu+4PnJnl261G1D/v8zoWV+0OIn4ySC1o/t8BkvoYvFzDJkDIGYwNV6tTdhgitlZ3ljohwTj2HIclr0pZVwptQDJpe/ZYbpxq13uN0Xi6QdAWHJcUWzt3jZnX7N07nY34NW7DhM/D5SSDMkZbjPIPBS4t+n/lShh--MofA9sfR6Z6UuGQ2--jtVAHhYAxdvdXOJ/vb06Ug== \ No newline at end of file +G+p9tWNHPLQC6KK/S5zVtIEgM5wA78puM6WT7XN/GXRTe2NzG9zdVy0Z+MJbuSbi3ZFu7LszkDg72e/uF4Z0pdrT6464N8Goorg6hZyVsTSBjY33xCcMLBkApAWKmcNm1+moY4d3pWqryJhZ/GOTr6jWsmKd/+aoieUWTt7dW0w3a5F6P5TF0FDFFYUjLb7eNepjiBXn134RPXToUr5CtDZxdW3zVp1BL7hd39qkzXU3emnM8X1gUu/KXc4/nw2FBKFHcIwUAQIlaWAansfmQt9dLeCqAuZpxacjwyzl6L472sxi1WSpe/MYfXZk06J+fH7oMUL44KpN0gp6l09HVut/Y+190SAPjGZ6WtD3Yai5eBacqvNfb2l95HR2EGF0RWDXDsLPxHmE+Ujp/M+EP7yu4C7pcIMnzJfAeeC1fDJ07lX36FHjHoiRVnbUmZ3HWB/cwKY7mKD54jmpw3RZWpDaQBLfsqrGvPIgOJi2NZxTa/XGSmDDBjqYusxqvnSWOqX9KEgBPbaPJqaY1GTgJsLEp95apcL3EC7TBZOX0rSikDpn1cGCm06W/yM=--TCPq+jCMikmr301d--+xtTlNeTifxX9cVf6i17pQ== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 6dd50b314a..254ca92de3 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -z0tKJ9f/hCNXOi7b6lZlfzmFm8HAGNafiluMTcRlKmFxTm5ygs6fk0N0JNnJyrxsv6huye3/mptfrqkMOHRQcWzmZQyWDmayLxtmghRkpXmX92QZ8DiSs6jgWFX9zKuMPEW9eFpuLjstF1khPskg70SYVXBc3cTRJbyx5JGhnv1c3qOBfN4PKGYvTWsxacoQ356LYfu3haezmiS0yPzREo1rhFdwNyveV8uZ1QX1ZNZFp+WN5RXeiM632kL4R73KhwkbGBSHj5z9xxl0v4BzLrGM+Jf3VsJFmhYXS10mN+XOsNELCHUaM1wQOqMNHegBfupTzwur6Q3R154XP7rrBLquFT3wkAlkrI8ox+NRJgnGQ/yCc3PM8zhxgdrLzmr9s35eByKRCMsuIkF0WSfcBeWpCfNQaeucw10fuKirlI+G5JsX6HaRfAea3dUvVsuKCeutKn2/YPmw7Qodat2G9sZgUhBOYRzDcmzWaUyudciAMXRzNfoTZqngwFXDvfcAlmq4FrnucnfAsMB/TtqrZzV/td4nPfQLpxZCCl7v3hIggQUJl7gmdR335J6wJ4r8i0eTQjTw+j/Xm8wF1AEHpwxz2DOg8BjAI+/F8Lgqc+YDQCEYINYn+bwy5eMUv7H3aLDGrF3ZKnsEwWj62Q5LtHF/Fi7ZIOWgQyk4d5lLc4+T6hx4qe9iqu29LlduIxwJRG1dWPgNxuikPM2hcl6dQP8ebP9dNpdeeMCGylzR/kZjro7mAawxgIKEdOvWE6NJs8xGkjV2JjqWu7ufiIpk+jurUBlO5mIJuZhVvRumpMgcxVwqXWjJRt5BTVT/qcuLdaPcYY9hUt3TQrqjSGIt5bSFjcCFEfY15DxXsIVe84Oi4KYQNmuM4h2ub1zfYQYYH4UaGg7bLbb2OeQIIkkTwXXAR6o3BKJWtskOD3GTdamk3gKhudhm+BOgbOavoiE4hpvv/4ajjl8OGseGOAjC4bj8hpNnhezjQFRe2pK1515hi8iyYQ2tEScGy4wmsAbSaoA54+tHki0yeLUF8oJ9GTr9UCwnz8nUhQmfrS4VJO6Q/J4MXRCon+mAt3cyHaC6z3RISREw0a4aO3T2PGvUBVDEP0kfhDJmZXM5Of0RdFX27pwaQUPo3hqzQ+BmyenGqUonC1nPd/npojmRWXan9v5iMRKRx8qEBfhm+WQPEQuJqi5szm8pla6gUtNKaL6P4zhJOnIZiKxpYSlnezlMWNof/xcOb5BqYeEYQJ9FTvIxGrmN7OxQFlfk21z+n1OJI82TKIaKB/Oo+bsTtta8BuXONJUtsRSP9HcqMlMnwq2We4jn8JLFf/9O0s+iQtB6um7EAcHkE48jBHkMVzJVWB4/9Z8xe4DIwrg9BZH6UNL/pc/K5/dOeW8QoOPceBWmHP9BGoWFVqmNmEwPeNmnQZ5H/zxGPD94oG5YfiUMfMrOtIiAwS3Qqbmr+nAa5eNe4Ij7/i5nlV5gJ8y3NSNQvjico4iqBU1WAEHL6bHV--JcYezF+/1oGWkoz5--lepA3Pi+iuzJTgNRR4ax8Q== \ No newline at end of file +ZZgOiLwd51WsQRpTwuCdPmDqmErel30sv5ZO+hOk7PrdqSgKuA7SCM3BaaM01gusiWCAq2760SmjTrrhaEBPkydR2dDERxPNcWe+zvqYklczYY8Qs4DqqLq3L8O89REO5GFvlbRulDQlO0w=--LC7VCBgZNnfp05RJ--svimv/2SjVLEJfVrBxC87Q== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 663b370e1b..3cbcf5de64 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -tTZW5futjROLhOOU4sULsj8f7nCQ73Z7Pmh/3HT2Y1hxhz7MxKOREq2ZjwEFPhL2/c/zim/C9PzgNql6MDjtUzYmd26Q+2HzFSS2JpFBqU7DnEgL6mWC7x5s9feTG61ilOhmlLIwZRgNvugwRAUTlCv2KZ8R5iaUviPcwf6fl4dOhM7pW2Fx8MhuJQBSbjkoqisyniIzdCvgsE2s4qIkxBz+qrbjVNhmyTvl/jdttjTcIYlubCOlELZdbT4zNsitcs/8xBTBlxsR4Vt9rBcwr+EPrhQfpvGVKnSleeiatc6EOi0kAniqbjwJFkcpJ95PrU3yhV8TP7RAzHpOIhYHSwDcrQS2ltP9z7fVP2CRicpWEIh7iFR79JtIsp1cYH6pbx08ZAnKKeu240SKkKA/wZH5BzyOrVaTHwQ5q18U8hzWon6loMiQMV9+/jRZtb8rOTHU4IpUi/oM7shUZoHxccBq68ZxpclLEDZs7CLNytXbRq9oaCqyg5tW5qW+Lws+4iRlmR43hTq2SHJ+eMjq4N8WxAdecDGbgPdNFaoY6Ymw9YEwsYiWzHx7fRC0EUYckbUPEccT6b8IuYTJg/QeQQ8HVenboM9U/9Cd6SODR4Vdyn6fGkFmc7mfPke01naCUEconH7RL0EB38yz/MMqW355PhsaZqsv7PlCE7KJIxkejp3gwiBwP8petq4OazxZY2MJahYLwvMQwFt35qu9RJVAMiAMN9DLvRQu7w+oNA0g3OSGOwnIzHtjKMRks3W4YNEgwMGrq3YzI11EmORB/1F3t+UDDN8GOxxGgBmIdVrFHwSazrw5igqv/kTFsrzxSHW1ja3/U27JVYOSGDBMZ5TlAH7CSFD6SgbDHreauN/O0zuesTH9QnkUARhOHw75dfTEvacjy6uIcO9GmrRConJGWzJdZj5joBROhCtoflcn9jfce3W6YJfzHgveUxPnRU1bhaNULGTW/lA+cclDmm1jL2AhpDYzk1hq6/I5we0fY9/VNMgFnm+c/qrkjrXChFNCT4w+EU+n7TaXJ5ocWdCMTyM2/RSnm1dBX8wGoygryOYEnHM+6GDeX64vfGSnWZ2yfpsKrAzn7zVFWAY6Ah2YEXm+pQdUPB4CYwvg6A1liaCvF71P2p9SZgX79YEwGvsODMzrBavxVMIlVa92jtOZ23yoOjfR8gWleqeyoAOCN2JaM4rJfPrzZrs5LKDfYtgpR4Jg36LjD1UKWp3Kab+kHOpxzkPOgq6v8px3Rk3xaGADwCPnE+4qSTFByYaHCdx6FNeQQit3jSEq4h6WNeVkrKnZ6ybCnd7LwBKiu9uQURB6jar7t4IqM6kYThN9J7q/oxp3HS5+TkNT+1Org13CuNL9iF3M6n+R/U/vJCCRUMbM9+O8cYHEt5MJ875qBD1NRxj4UbVCcv5rDXhAFq+GuJqJTLN2+NPWVJ2QhZk1bhmwxIsZJ5vlWTMUXoOZOxUJrLqPK+v8h2x6G8J8Cc0WpzsHAl1B8peN7TxXJgcOyaVkHbPr2CYao/z7mG7D+5SIHcqcbNUB09Nh4iMVbQa1SEpTnWPqum1EwXkmH8QjUcj+IMsXcNWTVkaAfAsme50fMS8XGLiOU9AGfV5QcryaJQ6w250ewQ==--IKMr2eXgh2p/VO+z--2sCRrfBLg9TEjHH93qFmcw== \ No newline at end of file +eqnqN1H7D3SMoWZAS61s1U+Km/tbbbb+1ofw+B+7AlD+Y6aNLBUuIbm6qSZPZ2L4bEl49RG9H3tLKtaawGWbqCCePygCZMw2wMlCt0Y08hKR1IAesRsJdMLRJUUn6vvTywXIm6leO/OdQlU=--H1k6MN/bYQAb3Kre--9jfEjn3753Brzy/63b56SA== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 5374a7f13b..6726323b69 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -OCWdPtq1nxi6erX3KVmolRjjazqRbSQ102v9t/MUwHy2Af7CztQt+S0N2fGBNnGb0QTNGbzAlETRVUo7trc7+pjO8w7lyuHS/eKO84wuuPh7qRkrZfwxF2R0D2jmcaCa2S2Hfs8Ws7xFN2j9xCdjWJKd4khP4OygyLFK9wah9fWW+QvMl1evgnSA8Vtko1Eh+/bBQC4Lb3/tlsjeqfv/N229oPywD6uM6XfRsSS0/tuCp3w1MPhs2lb1ZfTwt4y5Dsk+r0YoUUr0CDPUAPSohmDJn5++8NiipIt7CQoGe72M4dp7WZ4eQAiprID8QIWAQ2I8RdvwbJdjFRKjcjgeB7vYiLUTRgmav8q/7QDk+JxWCU5oQxDmVex63CJ+sZ8NI94EnyppqHqihyLDzViwbBHkOmWj3WsgPOQHNjwhX+LwMeJ8UkWjAJlA7q59EqEDvPPN7MAgpunPuRXL6YXsS6UBpUmpof6fwMyhHbnFpRgTx0QyUFW3Ta7cs2JWCST2PXI5lGQKJBKIhcLB60bO1NtJYtX3xlheywnbNUzvsBTQO3j+ezPxcy07REvW1NfXQInpAgOudsZ4ScW0mblwiO5v/rLGJN5cMUNWAg==--8NVEpFCH347bJZoD--6fw4f/Fad/N8XQH6oTwpQg== \ No newline at end of file +kd4RhBwzDS97pb8kht6DOzdg/NFgRQj0sAezzV7q7wiqk4pAMT2v5pZTJoq3pz0f8URB7nXK1KeHI6ceqN/9GKWMqpwyUyeM8A7LbCK5ffBiaJZH2YFMtKEnX0topl87sEDNlMawY0OT6ySi4KhLkrQMyEGqJx3XXmS1U4aGfF2P7i3GTGOjPpLtPntzSVT62cLU7/GSfDvXdqiW/WDRNvCpQJDqw+J1DdnlZtq+A6lo+B42o2clGHOVw09MY9INHuhfcscQfR035exSkRZ3wsvqDay3fez+D3xvDXOreyRrlCUpDfAxuXHPgavnYuPF73Xhg5Ov48B3EW9RLNHH5Nl57M/laxPvhkSa73IK6VPL9BSv9osW--MmZ9291ZbCvv0tqX--yLAI3tXJEW5rJoA/2KMk+Q== \ No newline at end of file From af42ce43d437ba5213d490636a3bc19748f3d0a5 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 16:56:31 +0100 Subject: [PATCH 055/107] Remove vapid keys in development --- config/credentials/development.yml.enc | 2 +- config/initializers/vapid.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index fac62a2bbc..fadb19009f 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -G+p9tWNHPLQC6KK/S5zVtIEgM5wA78puM6WT7XN/GXRTe2NzG9zdVy0Z+MJbuSbi3ZFu7LszkDg72e/uF4Z0pdrT6464N8Goorg6hZyVsTSBjY33xCcMLBkApAWKmcNm1+moY4d3pWqryJhZ/GOTr6jWsmKd/+aoieUWTt7dW0w3a5F6P5TF0FDFFYUjLb7eNepjiBXn134RPXToUr5CtDZxdW3zVp1BL7hd39qkzXU3emnM8X1gUu/KXc4/nw2FBKFHcIwUAQIlaWAansfmQt9dLeCqAuZpxacjwyzl6L472sxi1WSpe/MYfXZk06J+fH7oMUL44KpN0gp6l09HVut/Y+190SAPjGZ6WtD3Yai5eBacqvNfb2l95HR2EGF0RWDXDsLPxHmE+Ujp/M+EP7yu4C7pcIMnzJfAeeC1fDJ07lX36FHjHoiRVnbUmZ3HWB/cwKY7mKD54jmpw3RZWpDaQBLfsqrGvPIgOJi2NZxTa/XGSmDDBjqYusxqvnSWOqX9KEgBPbaPJqaY1GTgJsLEp95apcL3EC7TBZOX0rSikDpn1cGCm06W/yM=--TCPq+jCMikmr301d--+xtTlNeTifxX9cVf6i17pQ== \ No newline at end of file +vWcWTHpq+ngYMhjXPNuAdQoX0IWy18O+vXn8Ny//U81BzG0tZIw30hOJ2MYx9yqgG9TMo8skPDE8fYGjHjuNCmp0CAgay6tcJzDtue+8l7nosbVBhQDkdW4GGAs8zRzVevQFNVXiYggQBeY=--vOZV11N8QeP6HBLz--enCNPwzwui/5QILC4bceBg== \ No newline at end of file diff --git a/config/initializers/vapid.rb b/config/initializers/vapid.rb index 2739f6ca16..8567180a12 100644 --- a/config/initializers/vapid.rb +++ b/config/initializers/vapid.rb @@ -1,4 +1,4 @@ Rails.application.configure do - config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key)) - config.x.vapid.public_key = ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key)) + config.x.vapid.private_key = ENV["VAPID_PRIVATE_KEY"] + config.x.vapid.public_key = ENV["VAPID_PUBLIC_KEY"] end From 453835707c96b743f76e13051da8d153872387f1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:05:38 +0100 Subject: [PATCH 056/107] Remove test env --- .kamal/secrets.dhh | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .kamal/secrets.dhh diff --git a/.kamal/secrets.dhh b/.kamal/secrets.dhh deleted file mode 100644 index 9d9a33d9f6..0000000000 --- a/.kamal/secrets.dhh +++ /dev/null @@ -1,6 +0,0 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY) - -GITHUB_TOKEN=$(gh config get -h github.com oauth_token) -BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) -DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS) -RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) From f936cf2cfe7563167da243db6e680bc9c2fb2f71 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:09:48 +0100 Subject: [PATCH 057/107] Add missing kamal secrets --- .kamal/secrets.beta | 9 ++++++++- .kamal/secrets.production | 9 ++++++++- .kamal/secrets.staging | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.kamal/secrets.beta b/.kamal/secrets.beta index 87583b01f7..2bd57d56cf 100644 --- a/.kamal/secrets.beta +++ b/.kamal/secrets.beta @@ -1,4 +1,4 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER) +SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET) GITHUB_TOKEN=$(gh config get -h github.com oauth_token) BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) @@ -10,3 +10,10 @@ MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) +VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) +VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) +ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) +ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) +QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) +SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) diff --git a/.kamal/secrets.production b/.kamal/secrets.production index 9ebe90ef66..e776444e70 100644 --- a/.kamal/secrets.production +++ b/.kamal/secrets.production @@ -1,4 +1,4 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER) +SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET) GITHUB_TOKEN=$(gh config get -h github.com oauth_token) BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) @@ -10,3 +10,10 @@ MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) +VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) +VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) +ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) +ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) +QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) +SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) diff --git a/.kamal/secrets.staging b/.kamal/secrets.staging index da2952efa6..8688d8ab93 100644 --- a/.kamal/secrets.staging +++ b/.kamal/secrets.staging @@ -1,4 +1,4 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER) +SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET) GITHUB_TOKEN=$(gh config get -h github.com oauth_token) BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) @@ -10,3 +10,10 @@ MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) +VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) +VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) +ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) +ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) +QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) +SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) From c53cc101a9b323681d1552a74b3c103f92611e1c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:12:01 +0100 Subject: [PATCH 058/107] Update fizzy-saas gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index bebc6098fa..70e99226f3 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 3bf089d1af610c828fae1e6a15d5d6b0fbce9b5b + revision: 7865bd2ac6e5980171caae308f447d646bda80ed specs: fizzy-saas (0.1.0) queenbee From 2ff944b805cc7e3044cdcba79939f8aaa3b9ce4b Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:13:56 +0100 Subject: [PATCH 059/107] Point to branch for now --- Gemfile.saas | 2 +- Gemfile.saas.lock | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Gemfile.saas b/Gemfile.saas index 779c8c4198..04ef1c2dcc 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", bc: "fizzy-saas" +gem "fizzy-saas", bc: "fizzy-saas", branch: "secrets" gem "rails_structured_logging", bc: "rails-structured-logging" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 70e99226f3..e31d94f2b1 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,7 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 7865bd2ac6e5980171caae308f447d646bda80ed + revision: b235d4d3af762e99dc3a556a8eb4303cc8455424 + branch: secrets specs: fizzy-saas (0.1.0) queenbee From fa9ee0381d07904551c4059d85cb0ec5dd338a8d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:31:51 +0100 Subject: [PATCH 060/107] Move more settings to the gem --- config/environments/development.rb | 3 --- config/environments/production.rb | 6 ------ 2 files changed, 9 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index c13b122e21..080fec8b0e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -94,7 +94,4 @@ config.solid_queue.connects_to = { database: { writing: :queue, reading: :queue } } end - if config.respond_to?(:structured_logging) && Rails.root.join("tmp/structured-logging.txt").exist? - config.structured_logging.logger = ActiveSupport::Logger.new("log/structured-development.log") - end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 1a34453fa6..f1d7862790 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -32,9 +32,6 @@ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :purestorage - # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" @@ -55,9 +52,6 @@ # Suppress unstructured log lines config.log_level = :fatal - # Structured JSON logging. Pending to move to new Rails' built-in support. - config.structured_logging.logger = ActiveSupport::Logger.new(STDOUT) if config.respond_to?(:structured_logging) - # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] From 762c45de5d998c2ab7eabf6fef5543e2a4e0a47b Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:32:25 +0100 Subject: [PATCH 061/107] Update gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index e31d94f2b1..ba04c3272e 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: b235d4d3af762e99dc3a556a8eb4303cc8455424 + revision: 87194bc58db064ad6354d304b9b6110452fb337a branch: secrets specs: fizzy-saas (0.1.0) From 1083f715a8a264164f4fdf16fcecdf92a202dd11 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:37:47 +0100 Subject: [PATCH 062/107] This was moved to the gem already by Mike --- config/initializers/tenanting/logging.rb | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 config/initializers/tenanting/logging.rb diff --git a/config/initializers/tenanting/logging.rb b/config/initializers/tenanting/logging.rb deleted file mode 100644 index a8b834f310..0000000000 --- a/config/initializers/tenanting/logging.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport.on_load(:action_controller_base) do - before_action do - if Current.account.present? - logger.try(:struct, account: { queenbee_id: Current.account.external_account_id }) - end - end -end From cd7d3d33292919d3dec70283156083e2b0baf840 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 26 Nov 2025 17:47:30 +0100 Subject: [PATCH 063/107] Move kamal secrets to the gem --- .kamal/secrets.beta | 19 ------------------- .kamal/secrets.production | 19 ------------------- .kamal/secrets.staging | 19 ------------------- Gemfile.lock | 2 +- Gemfile.saas.lock | 6 +++--- 5 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 .kamal/secrets.beta delete mode 100644 .kamal/secrets.production delete mode 100644 .kamal/secrets.staging diff --git a/.kamal/secrets.beta b/.kamal/secrets.beta deleted file mode 100644 index 2bd57d56cf..0000000000 --- a/.kamal/secrets.beta +++ /dev/null @@ -1,19 +0,0 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET) - -GITHUB_TOKEN=$(gh config get -h github.com oauth_token) -BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) -DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS) -RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) -MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS) -MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS) -MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) -MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) -MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) -MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) -SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) -VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) -VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) -ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) -ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) -QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) -SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) diff --git a/.kamal/secrets.production b/.kamal/secrets.production deleted file mode 100644 index e776444e70..0000000000 --- a/.kamal/secrets.production +++ /dev/null @@ -1,19 +0,0 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET) - -GITHUB_TOKEN=$(gh config get -h github.com oauth_token) -BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) -DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS) -RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) -MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS) -MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS) -MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) -MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) -MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) -MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) -SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) -VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) -VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) -ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) -ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) -QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) -SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) diff --git a/.kamal/secrets.staging b/.kamal/secrets.staging deleted file mode 100644 index 8688d8ab93..0000000000 --- a/.kamal/secrets.staging +++ /dev/null @@ -1,19 +0,0 @@ -SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET) - -GITHUB_TOKEN=$(gh config get -h github.com oauth_token) -BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS) -DASH_BASIC_AUTH_SECRET=$(kamal secrets extract DASH_BASIC_AUTH_SECRET $SECRETS) -RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) -MYSQL_ALTER_PASSWORD=$(kamal secrets extract MYSQL_ALTER_PASSWORD $SECRETS) -MYSQL_ALTER_USER=$(kamal secrets extract MYSQL_ALTER_USER $SECRETS) -MYSQL_APP_PASSWORD=$(kamal secrets extract MYSQL_APP_PASSWORD $SECRETS) -MYSQL_APP_USER=$(kamal secrets extract MYSQL_APP_USER $SECRETS) -MYSQL_READONLY_PASSWORD=$(kamal secrets extract MYSQL_READONLY_PASSWORD $SECRETS) -MYSQL_READONLY_USER=$(kamal secrets extract MYSQL_READONLY_USER $SECRETS) -SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS) -VAPID_PUBLIC_KEY=$(kamal secrets extract VAPID_PUBLIC_KEY $SECRETS) -VAPID_PRIVATE_KEY=$(kamal secrets extract VAPID_PRIVATE_KEY $SECRETS) -ACTIVE_STORAGE_ACCESS_KEY_ID=$(kamal secrets extract ACTIVE_STORAGE_ACCESS_KEY_ID $SECRETS) -ACTIVE_STORAGE_SECRET_ACCESS_KEY=$(kamal secrets extract ACTIVE_STORAGE_SECRET_ACCESS_KEY $SECRETS) -QUEENBEE_API_TOKEN=$(kamal secrets extract QUEENBEE_API_TOKEN $SECRETS) -SIGNAL_ID_SECRET=$(kamal secrets extract SIGNAL_ID_SECRET $SECRETS) diff --git a/Gemfile.lock b/Gemfile.lock index ded3d30825..0996ac69bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -225,7 +225,7 @@ GEM json (2.16.0) jwt (3.1.2) base64 - kamal (2.8.2) + kamal (2.9.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index ba04c3272e..7975514834 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 87194bc58db064ad6354d304b9b6110452fb337a + revision: fb0526b4104235031f105a9801b7e451ea83b85b branch: secrets specs: fizzy-saas (0.1.0) @@ -206,7 +206,7 @@ GEM logger (~> 1.5) chunky_png (1.4.0) concurrent-ruby (1.3.5) - connection_pool (2.5.4) + connection_pool (2.5.5) crack (1.0.1) bigdecimal rexml @@ -261,7 +261,7 @@ GEM json (2.16.0) jwt (3.1.2) base64 - kamal (2.8.2) + kamal (2.9.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) From e30709f6a7ebe4a2abfdb4251b9170b87ff6ecd8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:24:53 +0100 Subject: [PATCH 064/107] Remove structured logging moved to the engine --- app/controllers/concerns/authentication.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 4ffca9cb02..23f82dbd29 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -81,8 +81,6 @@ def start_new_session_for(identity) end def set_current_session(session) - # TODO: Release structured logging or look for alternative - logger.try :struct, " Authorized Identity##{session.identity.id}", authentication: { identity: { id: session.identity.id } } Current.session = session cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax } end From 4b9d3a0efda2f204718320030f0b0b500071c79a Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:28:49 +0100 Subject: [PATCH 065/107] Update to latest fizzy-saas --- Gemfile.saas.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 7975514834..90469ffe27 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,7 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: fb0526b4104235031f105a9801b7e451ea83b85b - branch: secrets + revision: 43bee8749f5b84255373c5b3caff46a31d064b13 specs: fizzy-saas (0.1.0) queenbee @@ -206,7 +205,7 @@ GEM logger (~> 1.5) chunky_png (1.4.0) concurrent-ruby (1.3.5) - connection_pool (2.5.5) + connection_pool (2.5.4) crack (1.0.1) bigdecimal rexml @@ -261,7 +260,7 @@ GEM json (2.16.0) jwt (3.1.2) base64 - kamal (2.9.0) + kamal (2.8.2) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) From c61dba06410c5c65229713102cb632490768f14c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:34:40 +0100 Subject: [PATCH 066/107] Format --- config/environments/development.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 080fec8b0e..8b426d2c69 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -93,5 +93,4 @@ config.active_job.queue_adapter = :solid_queue config.solid_queue.connects_to = { database: { writing: :queue, reading: :queue } } end - end From 9e2a174d5df948e2f32391a1b41d88f9d8f61e43 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:37:13 +0100 Subject: [PATCH 067/107] Update bundle --- Gemfile.saas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas b/Gemfile.saas index 04ef1c2dcc..779c8c4198 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", bc: "fizzy-saas", branch: "secrets" +gem "fizzy-saas", bc: "fizzy-saas" gem "rails_structured_logging", bc: "rails-structured-logging" From 25cfa42872db66758e7003b29c3c4a947bb02664 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:47:02 +0100 Subject: [PATCH 068/107] Rename workflows --- .github/workflows/{ci-pr.yml => ci-oss.yml} | 2 +- .github/workflows/{ci-main.yml => ci-saas.yml} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{ci-pr.yml => ci-oss.yml} (87%) rename .github/workflows/{ci-main.yml => ci-saas.yml} (90%) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-oss.yml similarity index 87% rename from .github/workflows/ci-pr.yml rename to .github/workflows/ci-oss.yml index c6712ad938..1aedd63b53 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-oss.yml @@ -1,4 +1,4 @@ -name: CI (PR) +name: CI (OSS) on: pull_request: diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-saas.yml similarity index 90% rename from .github/workflows/ci-main.yml rename to .github/workflows/ci-saas.yml index 7e79ee0e70..69196d1a42 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-saas.yml @@ -1,4 +1,4 @@ -name: CI (Main) +name: CI (SaaS) on: push: From f174058f43e70a047b91af91bef40ab0fb048323 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:57:08 +0100 Subject: [PATCH 069/107] Extract common checks to its own workflow to only run once --- .github/workflows/ci-checks.yml | 41 ++++++++++++++++++++++++++ .github/workflows/ci-oss.yml | 4 +-- .github/workflows/ci-saas.yml | 4 +-- .github/workflows/{ci.yml => test.yml} | 38 +----------------------- 4 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/ci-checks.yml rename .github/workflows/{ci.yml => test.yml} (69%) diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml new file mode 100644 index 0000000000..b795667cfc --- /dev/null +++ b/.github/workflows/ci-checks.yml @@ -0,0 +1,41 @@ +name: Checks + +on: + push: + pull_request: + +jobs: + security: + name: Security + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Gem audit + run: bin/bundler-audit check --update + + - name: Importmap audit + run: bin/importmap audit + + - name: Brakeman audit + run: bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error + + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop diff --git a/.github/workflows/ci-oss.yml b/.github/workflows/ci-oss.yml index 1aedd63b53..724cc3ec6a 100644 --- a/.github/workflows/ci-oss.yml +++ b/.github/workflows/ci-oss.yml @@ -4,7 +4,7 @@ on: pull_request: jobs: - call-ci: - uses: ./.github/workflows/ci.yml + test: + uses: ./.github/workflows/test.yml with: saas: false diff --git a/.github/workflows/ci-saas.yml b/.github/workflows/ci-saas.yml index 69196d1a42..27c33e7cd7 100644 --- a/.github/workflows/ci-saas.yml +++ b/.github/workflows/ci-saas.yml @@ -4,8 +4,8 @@ on: push: jobs: - call-ci: - uses: ./.github/workflows/ci.yml + test: + uses: ./.github/workflows/test.yml with: saas: true secrets: diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 69% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index 7984fca661..da6dc4a616 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Common CI +name: Test on: workflow_call: @@ -11,42 +11,6 @@ on: required: false jobs: - security: - name: Security - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - - name: Gem audit - run: bin/bundler-audit check --update - - - name: Importmap audit - run: bin/importmap audit - - - name: Brakeman audit - run: bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error - - - lint: - name: Lint - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - - name: Lint code for consistent style - run: bin/rubocop - - test: name: Tests (${{ matrix.mode }}) runs-on: ubuntu-latest From 15e654b5260f79c51886d4f1b4f2490aa90794a3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 07:58:02 +0100 Subject: [PATCH 070/107] Checks only in PRs --- .github/workflows/ci-checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index b795667cfc..4fdc1bcba5 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -1,7 +1,6 @@ name: Checks on: - push: pull_request: jobs: From e15c4987c1dea2a4964800ed30eeaae3ae9b85ae Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 08:42:26 +0100 Subject: [PATCH 071/107] Remove saas from matrix or it will always run --- .github/workflows/ci-oss.yml | 1 + .github/workflows/ci-saas.yml | 8 +++++++- .github/workflows/test.yml | 3 --- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-oss.yml b/.github/workflows/ci-oss.yml index 724cc3ec6a..7ccd00bf77 100644 --- a/.github/workflows/ci-oss.yml +++ b/.github/workflows/ci-oss.yml @@ -2,6 +2,7 @@ name: CI (OSS) on: pull_request: + if: github.event.pull_request.head.repo.full_name != github.repository jobs: test: diff --git a/.github/workflows/ci-saas.yml b/.github/workflows/ci-saas.yml index 27c33e7cd7..f1d8875b44 100644 --- a/.github/workflows/ci-saas.yml +++ b/.github/workflows/ci-saas.yml @@ -4,7 +4,13 @@ on: push: jobs: - test: + test_oss: + name: Test (OSS) + uses: ./.github/workflows/test.yml + with: + saas: false + test_saas: + name: Test (SaaS) uses: ./.github/workflows/test.yml with: saas: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da6dc4a616..8b5925a13e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,9 +22,6 @@ jobs: db_adapter: sqlite - mode: MySQL db_adapter: mysql - - mode: SaaS - db_adapter: mysql - saas: ${{ inputs.saas }} services: mysql: From 96caeacb03d6c4b906d228280ddddec9f720f443 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 09:25:24 +0100 Subject: [PATCH 072/107] Move to ENV (kamal secrets) --- config/initializers/sentry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 8d35e2670e..11f352c97a 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -1,6 +1,6 @@ if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank? Sentry.init do |config| - config.dsn = "https://ca338fb1fe6f677d6aeec2336a86f0ee@o33603.ingest.us.sentry.io/4508093839179776" + config.dsn = ENV["SENTRY_DSN"] config.breadcrumbs_logger = %i[ active_support_logger http_logger ] config.send_default_pii = false config.release = ENV["GIT_REVISION"] From bf75767a4e1671cf86482d280b13a67b6fe52b28 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 09:34:03 +0100 Subject: [PATCH 073/107] Update saas gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 90469ffe27..4974a04dee 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 43bee8749f5b84255373c5b3caff46a31d064b13 + revision: 40f68d8604a1b410ba514f5793a93e9b11c00219 specs: fizzy-saas (0.1.0) queenbee From e77858fb3ca0965eb3a20132b71b6dda53823916 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 09:34:57 +0100 Subject: [PATCH 074/107] Update kamal --- Gemfile.saas.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 4974a04dee..cc24a91470 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -205,7 +205,7 @@ GEM logger (~> 1.5) chunky_png (1.4.0) concurrent-ruby (1.3.5) - connection_pool (2.5.4) + connection_pool (2.5.5) crack (1.0.1) bigdecimal rexml @@ -260,7 +260,7 @@ GEM json (2.16.0) jwt (3.1.2) base64 - kamal (2.8.2) + kamal (2.9.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) From 07b4c55185f7b4c7a8222ea33074d0c423f87453 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 10:56:55 +0100 Subject: [PATCH 075/107] Auto-increment external_account_id automatically by default to allow upcoming simple signups --- app/models/account.rb | 6 ++++++ app/models/external_account.rb | 5 +++++ .../20251127000001_create_external_accounts.rb | 15 +++++++++++++++ test/models/account_test.rb | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 app/models/external_account.rb create mode 100644 db/migrate/20251127000001_create_external_accounts.rb diff --git a/app/models/account.rb b/app/models/account.rb index 29912092c8..7e79779670 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -11,6 +11,7 @@ class Account < ApplicationRecord has_many_attached :uploads + before_create :assign_external_account_id after_create :create_join_code validates :name, presence: true @@ -35,4 +36,9 @@ def account def system_user users.where(role: :system).first! end + + private + def assign_external_account_id + self.external_account_id ||= ExternalAccount.create!.id + end end diff --git a/app/models/external_account.rb b/app/models/external_account.rb new file mode 100644 index 0000000000..11b5c4d5e1 --- /dev/null +++ b/app/models/external_account.rb @@ -0,0 +1,5 @@ +# This is meant to provide a sequential ID for +external_account_id+ when creating +# accounts without one. The SaaS version of the app uses that column, but most apps +# can just go with the default handling here. +class ExternalAccount < ApplicationRecord +end diff --git a/db/migrate/20251127000001_create_external_accounts.rb b/db/migrate/20251127000001_create_external_accounts.rb new file mode 100644 index 0000000000..8dee2feafc --- /dev/null +++ b/db/migrate/20251127000001_create_external_accounts.rb @@ -0,0 +1,15 @@ +class CreateExternalAccounts < ActiveRecord::Migration[8.0] + def up + create_table :external_accounts do |t| + end + + max_id = Account.maximum(:external_account_id) || 0 + if max_id > 0 + execute "INSERT INTO external_accounts (id) VALUES (#{max_id})" + end + end + + def down + drop_table :external_accounts + end +end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 7b212e8ab6..57f2406be0 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -59,4 +59,22 @@ class AccountTest < ActiveSupport::TestCase account.system_user end end + + test "external_account_id auto-increments on creation" do + account1 = Account.create!(name: "First Account") + account2 = Account.create!(name: "Second Account") + + assert_not_nil account1.external_account_id + assert_not_nil account2.external_account_id + assert_equal account1.external_account_id + 1, account2.external_account_id + end + + test "external_account_id can be overridden without creating sequence entry" do + custom_id = Account.last.id + 99999 + + assert_no_difference -> { ExternalAccount.count } do + account = Account.create!(name: "Custom ID Account", external_account_id: custom_id) + assert_equal custom_id, account.external_account_id + end + end end From 2061ab4ab2efc787959a8015af48a3f42ee211ac Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 11:55:47 +0100 Subject: [PATCH 076/107] Use dedicated sequence record instead of deal with issues with ids and uuids when using the previous approach --- app/models/account.rb | 2 +- app/models/account/external_id_sequence.rb | 26 ++++++++++++ app/models/external_account.rb | 5 --- ...01_create_account_external_id_sequences.rb | 9 ++++ ...20251127000001_create_external_accounts.rb | 15 ------- db/schema_sqlite.rb | 7 +++- .../account/external_id_sequence_test.rb | 42 +++++++++++++++++++ test/models/account_test.rb | 13 +++--- 8 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 app/models/account/external_id_sequence.rb delete mode 100644 app/models/external_account.rb create mode 100644 db/migrate/20251127000001_create_account_external_id_sequences.rb delete mode 100644 db/migrate/20251127000001_create_external_accounts.rb create mode 100644 test/models/account/external_id_sequence_test.rb diff --git a/app/models/account.rb b/app/models/account.rb index 7e79779670..887ce708dd 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -39,6 +39,6 @@ def system_user private def assign_external_account_id - self.external_account_id ||= ExternalAccount.create!.id + self.external_account_id ||= ExternalIdSequence.next end end diff --git a/app/models/account/external_id_sequence.rb b/app/models/account/external_id_sequence.rb new file mode 100644 index 0000000000..8d9cecf6de --- /dev/null +++ b/app/models/account/external_id_sequence.rb @@ -0,0 +1,26 @@ +# Provides sequential IDs for +external_account_id+ when creating accounts without one. +class Account::ExternalIdSequence < ApplicationRecord + class << self + def next + with_lock do |sequence| + sequence.increment!(:value).value + end + end + + def value + first&.value + end + + private + def with_lock + transaction do + sequence = lock.first_or_create!(value: initial_value) + yield sequence + end + end + + def initial_value + Account.maximum(:external_account_id) || 0 + end + end +end diff --git a/app/models/external_account.rb b/app/models/external_account.rb deleted file mode 100644 index 11b5c4d5e1..0000000000 --- a/app/models/external_account.rb +++ /dev/null @@ -1,5 +0,0 @@ -# This is meant to provide a sequential ID for +external_account_id+ when creating -# accounts without one. The SaaS version of the app uses that column, but most apps -# can just go with the default handling here. -class ExternalAccount < ApplicationRecord -end diff --git a/db/migrate/20251127000001_create_account_external_id_sequences.rb b/db/migrate/20251127000001_create_account_external_id_sequences.rb new file mode 100644 index 0000000000..834b80c5b9 --- /dev/null +++ b/db/migrate/20251127000001_create_account_external_id_sequences.rb @@ -0,0 +1,9 @@ +class CreateAccountExternalIdSequences < ActiveRecord::Migration[8.0] + def change + create_table :account_external_id_sequences do |t| + t.bigint :value, null: false, default: 0 + + t.index :value, unique: true + end + end +end diff --git a/db/migrate/20251127000001_create_external_accounts.rb b/db/migrate/20251127000001_create_external_accounts.rb deleted file mode 100644 index 8dee2feafc..0000000000 --- a/db/migrate/20251127000001_create_external_accounts.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CreateExternalAccounts < ActiveRecord::Migration[8.0] - def up - create_table :external_accounts do |t| - end - - max_id = Account.maximum(:external_account_id) || 0 - if max_id > 0 - execute "INSERT INTO external_accounts (id) VALUES (#{max_id})" - end - end - - def down - drop_table :external_accounts - end -end diff --git a/db/schema_sqlite.rb b/db/schema_sqlite.rb index fe2aad1158..e4475e7eae 100644 --- a/db/schema_sqlite.rb +++ b/db/schema_sqlite.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do +ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do create_table "accesses", id: :uuid, force: :cascade do |t| t.datetime "accessed_at" t.uuid "account_id", null: false @@ -25,6 +25,11 @@ t.index ["user_id"], name: "index_accesses_on_user_id" end + create_table "account_external_id_sequences", force: :cascade do |t| + t.bigint "value", default: 0, null: false + t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true + end + create_table "account_join_codes", id: :uuid, force: :cascade do |t| t.uuid "account_id", null: false t.string "code", limit: 255, null: false diff --git a/test/models/account/external_id_sequence_test.rb b/test/models/account/external_id_sequence_test.rb new file mode 100644 index 0000000000..a7f8ffd18f --- /dev/null +++ b/test/models/account/external_id_sequence_test.rb @@ -0,0 +1,42 @@ +require "test_helper" + +class Account::ExternalIdSequenceTest < ActiveSupport::TestCase + setup do + Account::ExternalIdSequence.delete_all + end + + test ".next returns sequential values" do + first_value = Account::ExternalIdSequence.next + second_value = Account::ExternalIdSequence.next + third_value = Account::ExternalIdSequence.next + + assert_equal first_value + 1, second_value + assert_equal second_value + 1, third_value + end + + test ".next initializes from maximum external_account_id" do + max_id = Account.maximum(:external_account_id) || 0 + + first_value = Account::ExternalIdSequence.next + + assert_equal max_id + 1, first_value + end + + test ".next creates single sequence record" do + 3.times { Account::ExternalIdSequence.next } + + assert_equal 1, Account::ExternalIdSequence.count + end + + test ".next is concurrency-safe" do + values = 20.times.map do + Thread.new do + Account::ExternalIdSequence.next + end + end.map(&:value) + + assert_equal 20, values.uniq.size, "All values should be unique" + assert_equal values.min..values.max, values.sort.first..values.sort.last + assert_equal 20, values.max - values.min + 1, "Values should be sequential with no gaps" + end +end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 57f2406be0..541ceb654c 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -69,12 +69,13 @@ class AccountTest < ActiveSupport::TestCase assert_equal account1.external_account_id + 1, account2.external_account_id end - test "external_account_id can be overridden without creating sequence entry" do - custom_id = Account.last.id + 99999 + test "external_account_id can be overridden" do + custom_id = 999999 + sequence_value_before = Account::ExternalIdSequence.first_or_create!(value: 0).value - assert_no_difference -> { ExternalAccount.count } do - account = Account.create!(name: "Custom ID Account", external_account_id: custom_id) - assert_equal custom_id, account.external_account_id - end + account = Account.create!(name: "Custom ID Account", external_account_id: custom_id) + + assert_equal custom_id, account.external_account_id + assert_equal sequence_value_before, Account::ExternalIdSequence.value end end From 4e09352c09d40df866d52450b805b7a6f1b5ebdb Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 12:57:36 +0100 Subject: [PATCH 077/107] Bring simple signup flow from the fizzy-saas gem We skip the QB code and we fill external account ids automatically on creation with a sequence See: https://github.com/basecamp/fizzy-saas/pull/7 --- Gemfile.saas | 2 +- Gemfile.saas.lock | 3 +- app/controllers/application_controller.rb | 1 - app/controllers/concerns/saas.rb | 11 -- app/controllers/sessions_controller.rb | 12 ++- .../signup/completions_controller.rb | 24 +++++ app/models/signup.rb | 100 ++++++++++++++++++ app/models/signup/account_name_generator.rb | 53 ++++++++++ app/views/sessions/menus/show.html.erb | 12 +-- app/views/signup/completions/new.html.erb | 30 ++++++ app/views/signup/new.html.erb | 28 +++++ config/routes.rb | 6 ++ ...01_create_account_external_id_sequences.rb | 2 +- db/schema.rb | 7 +- db/schema_sqlite.rb | 2 +- test/controllers/sessions_controller_test.rb | 26 +++-- .../signup/completions_controller_test.rb | 43 ++++++++ .../signup/account_name_generator_test.rb | 61 +++++++++++ test/models/signup_test.rb | 54 ++++++++++ 19 files changed, 443 insertions(+), 34 deletions(-) delete mode 100644 app/controllers/concerns/saas.rb create mode 100644 app/controllers/signup/completions_controller.rb create mode 100644 app/models/signup.rb create mode 100644 app/models/signup/account_name_generator.rb create mode 100644 app/views/signup/completions/new.html.erb create mode 100644 app/views/signup/new.html.erb create mode 100644 test/controllers/signup/completions_controller_test.rb create mode 100644 test/models/signup/account_name_generator_test.rb create mode 100644 test/models/signup_test.rb diff --git a/Gemfile.saas b/Gemfile.saas index 779c8c4198..955a663035 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", bc: "fizzy-saas" +gem "fizzy-saas", bc: "fizzy-saas", branch: "extract-signup" gem "rails_structured_logging", bc: "rails-structured-logging" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index cc24a91470..b8f6dc0e78 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,7 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 40f68d8604a1b410ba514f5793a93e9b11c00219 + revision: 3b012b9ffa7c8fdc1e4b9f5aa7d866ed4108a8ff + branch: extract-signup specs: fizzy-saas (0.1.0) queenbee diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 89b94f079e..7f1cb65658 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,7 +4,6 @@ class ApplicationController < ActionController::Base include CurrentRequest, CurrentTimezone, SetPlatform include RequestForgeryProtection include TurboFlash, ViewTransitions - include Saas include RoutingHeaders etag { "v1" } diff --git a/app/controllers/concerns/saas.rb b/app/controllers/concerns/saas.rb deleted file mode 100644 index fd4ca84915..0000000000 --- a/app/controllers/concerns/saas.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Saas - extend ActiveSupport::Concern - - included do - helper_method :signups_allowed? - end - - def signups_allowed? - defined?(Signup) && defined?(saas) - end -end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8c623b6ef0..65cb443a0e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -22,10 +22,8 @@ def create magic_link = identity.send_magic_link flash[:magic_link_code] = magic_link&.code if Rails.env.development? redirect_to session_magic_link_path - elsif signups_allowed? - Signup.new(email_address: email_address).create_identity - session[:return_to_after_authenticating] = saas.new_signup_completion_path - redirect_to session_magic_link_path + elsif + process_new_signup end end @@ -38,4 +36,10 @@ def destroy def email_address params.expect(:email_address) end + + def process_new_signup + Signup.new(email_address: email_address).create_identity + session[:return_to_after_authenticating] = new_signup_completion_path + redirect_to session_magic_link_path + end end diff --git a/app/controllers/signup/completions_controller.rb b/app/controllers/signup/completions_controller.rb new file mode 100644 index 0000000000..d7f09c0865 --- /dev/null +++ b/app/controllers/signup/completions_controller.rb @@ -0,0 +1,24 @@ +class Signup::CompletionsController < ApplicationController + layout "public" + + disallow_account_scope + + def new + @signup = Signup.new(identity: Current.identity) + end + + def create + @signup = Signup.new(signup_params) + + if @signup.complete + redirect_to landing_url(script_name: @signup.account.slug) + else + render :new, status: :unprocessable_entity + end + end + + private + def signup_params + params.expect(signup: %i[ full_name ]).with_defaults(identity: Current.identity) + end +end diff --git a/app/models/signup.rb b/app/models/signup.rb new file mode 100644 index 0000000000..230aafd0b5 --- /dev/null +++ b/app/models/signup.rb @@ -0,0 +1,100 @@ +class Signup + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attr_accessor :full_name, :email_address, :identity + attr_reader :account, :user + + with_options on: :completion do + validates_presence_of :full_name, :identity + end + + def initialize(...) + super + + @email_address = @identity.email_address if @identity + end + + def create_identity + @identity = Identity.find_or_create_by!(email_address: email_address) + @identity.send_magic_link + end + + def complete + if valid?(:completion) + begin + @tenant = create_tenant + create_account + true + rescue => error + destroy_account + handle_account_creation_error(error) + + errors.add(:base, "Something went wrong, and we couldn't create your account. Please give it another try.") + Rails.error.report(error, severity: :error) + Rails.logger.error error + Rails.logger.error error.backtrace.join("\n") + + false + end + else + false + end + end + + private + # Override to customize the handling of external accounts associated to the account. + def create_tenant + nil + end + + # Override to inject custom handling for account creation errors + def handle_account_creation_error(error) + end + + def create_account + @account = Account.create_with_admin_user( + account: { + external_account_id: @tenant, + name: generate_account_name + }, + owner: { + name: full_name, + identity: identity + } + ) + @user = @account.users.find_by!(role: :admin) + @account.setup_customer_template + end + + def generate_account_name + AccountNameGenerator.new(identity: identity, name: full_name).generate + end + + + def destroy_account + @account&.destroy! + + @user = nil + @account = nil + @tenant = nil + end + + def subscription_attributes + subscription = FreeV1Subscription + + {}.tap do |attributes| + attributes[:name] = subscription.to_param + attributes[:price] = subscription.price + end + end + + def request_attributes + {}.tap do |attributes| + attributes[:remote_address] = Current.ip_address + attributes[:user_agent] = Current.user_agent + attributes[:referrer] = Current.referrer + end + end +end diff --git a/app/models/signup/account_name_generator.rb b/app/models/signup/account_name_generator.rb new file mode 100644 index 0000000000..a6844d3a3a --- /dev/null +++ b/app/models/signup/account_name_generator.rb @@ -0,0 +1,53 @@ +class Signup::AccountNameGenerator + SUFFIX = "Fizzy".freeze + + attr_reader :identity, :name + + def initialize(identity:, name:) + @identity = identity + @name = name + end + + def generate + next_index = current_index + 1 + + if next_index == 1 + "#{prefix} #{SUFFIX}" + else + "#{prefix} #{next_index.ordinalize} #{SUFFIX}" + end + end + + private + def current_index + existing_indices.max || 0 + end + + def existing_indices + Current.without_account do + identity.accounts.filter_map do |account| + if account.name.match?(first_account_name_regex) + 1 + elsif match = account.name.match(nth_account_name_regex) + match[1].to_i + end + end + end + end + + def first_account_name_regex + @first_account_name_regex ||= /\A#{prefix}\s+#{SUFFIX}\Z/i + end + + def nth_account_name_regex + @nth_account_name_regex ||= /\A#{prefix}\s+(1st|2nd|3rd|\d+th)\s+#{SUFFIX}/i + end + + def prefix + @prefix ||= "#{first_name}'s" + end + + def first_name + name.strip.split(" ", 2).first + end +end diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index 26b3d25bd7..8a36261bf2 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -24,13 +24,11 @@

You don’t have any Fizzy accounts.

<% end %> - <% if signups_allowed? %> -
- <%= link_to saas.new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %> - Sign up for a new Fizzy account - <% end %> -
- <% end %> +
+ <%= link_to new_signup_completion_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %> + Sign up for a new Fizzy account + <% end %> +
<% end %> diff --git a/app/views/signup/completions/new.html.erb b/app/views/signup/completions/new.html.erb new file mode 100644 index 0000000000..971c39b1df --- /dev/null +++ b/app/views/signup/completions/new.html.erb @@ -0,0 +1,30 @@ +<% @page_title = "Complete your sign-up" %> + +
"> +

<%= @page_title %>

+ + <%= form_with model: @signup, url: signup_completion_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %> + <%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %> + +

You're one step away. Just enter your name to get your own Fizzy account.

+ + <% if @signup.errors.any? %> +
+
    + <% @signup.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + + <% end %> +
+ +<% content_for :footer do %> + <%= render "sessions/footer" %> +<% end %> diff --git a/app/views/signup/new.html.erb b/app/views/signup/new.html.erb new file mode 100644 index 0000000000..14da6328ed --- /dev/null +++ b/app/views/signup/new.html.erb @@ -0,0 +1,28 @@ +<% @page_title = "Sign up for Fizzy" %> + +
"> +

Sign up

+ + <%= form_with model: @signup, url: signup_path, scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: "form" } do |form| %> + <%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %> + + <% if @signup.errors.any? %> +
+
    + <% @signup.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + + <% end %> +
+ +<% content_for :footer do %> + <%= render "sessions/footer" %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 3afb093a77..d4e4b17e60 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,6 +155,12 @@ end end + get "/signup/new", to: redirect("/session/new") + + namespace :signup do + resource :completion, only: %i[ new create ] + end + resource :landing namespace :my do diff --git a/db/migrate/20251127000001_create_account_external_id_sequences.rb b/db/migrate/20251127000001_create_account_external_id_sequences.rb index 834b80c5b9..6f03cb27f4 100644 --- a/db/migrate/20251127000001_create_account_external_id_sequences.rb +++ b/db/migrate/20251127000001_create_account_external_id_sequences.rb @@ -1,6 +1,6 @@ class CreateAccountExternalIdSequences < ActiveRecord::Migration[8.0] def change - create_table :account_external_id_sequences do |t| + create_table :account_external_id_sequences, id: :uuid do |t| t.bigint :value, null: false, default: 0 t.index :value, unique: true diff --git a/db/schema.rb b/db/schema.rb index b3a9f05033..236c61df05 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do +ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.datetime "accessed_at" t.uuid "account_id", null: false @@ -25,6 +25,11 @@ t.index ["user_id"], name: "index_accesses_on_user_id" end + create_table "account_external_id_sequences", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "value", default: 0, null: false + t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true + end + create_table "account_join_codes", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.uuid "account_id", null: false t.string "code", null: false diff --git a/db/schema_sqlite.rb b/db/schema_sqlite.rb index e4475e7eae..fd3cedd44a 100644 --- a/db/schema_sqlite.rb +++ b/db/schema_sqlite.rb @@ -25,7 +25,7 @@ t.index ["user_id"], name: "index_accesses_on_user_id" end - create_table "account_external_id_sequences", force: :cascade do |t| + create_table "account_external_id_sequences", id: :uuid, force: :cascade do |t| t.bigint "value", default: 0, null: false t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 2f5b27b0dc..e4a2163b1d 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -21,14 +21,28 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest end end - test "destroy" do - sign_in_as :kevin - + test "create for a new user" do untenanted do - delete session_path + assert_difference -> { Identity.count }, +1 do + assert_difference -> { MagicLink.count }, +1 do + post session_path, + params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" } + end + end - assert_redirected_to new_session_path - assert_not cookies[:session_token].present? + assert_redirected_to session_magic_link_path end end + + private + test "destroy" do + sign_in_as :kevin + + untenanted do + delete session_path + + assert_redirected_to new_session_path + assert_not cookies[:session_token].present? + end + end end diff --git a/test/controllers/signup/completions_controller_test.rb b/test/controllers/signup/completions_controller_test.rb new file mode 100644 index 0000000000..1b788ce312 --- /dev/null +++ b/test/controllers/signup/completions_controller_test.rb @@ -0,0 +1,43 @@ +require "test_helper" + +class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest + setup do + @signup = Signup.new(email_address: "newuser@example.com", full_name: "New User") + + @signup.create_identity || raise("Failed to create identity") + + sign_in_as @signup.identity + end + + test "new" do + untenanted do + get new_signup_completion_path + end + + assert_response :success + end + + test "create" do + untenanted do + post signup_completion_path, params: { + signup: { + full_name: @signup.full_name + } + } + end + + assert_response :redirect, "Valid params should redirect" + end + + test "create with invalid params" do + untenanted do + post signup_completion_path, params: { + signup: { + full_name: "" + } + } + end + + assert_response :unprocessable_entity, "Invalid params should return unprocessable entity" + end +end diff --git a/test/models/signup/account_name_generator_test.rb b/test/models/signup/account_name_generator_test.rb new file mode 100644 index 0000000000..d22922b29d --- /dev/null +++ b/test/models/signup/account_name_generator_test.rb @@ -0,0 +1,61 @@ +require "test_helper" + +class Signup::AccountNameGeneratorTest < ActiveSupport::TestCase + setup do + @identity = Identity.create!(email_address: "newart.userbaum@example.com") + @name = "Newart userbaum" + @generator = Signup::AccountNameGenerator.new(identity: @identity, name: @name) + end + + test "generate" do + account_name = @generator.generate + assert_equal "Newart's Fizzy", account_name, "The 1st account doesn't have 1st in the name" + + first_account = Account.create!(external_account_id: "1st", name: account_name) + Current.without_account do + @identity.users.create!(account: first_account, name: @name) + @identity.reload + end + + account_name = @generator.generate + assert_equal "Newart's 2nd Fizzy", account_name + + second_account = Account.create!(external_account_id: "2nd", name: account_name) + Current.without_account do + @identity.users.create!(account: second_account, name: @name) + @identity.reload + end + + account_name = @generator.generate + assert_equal "Newart's 3rd Fizzy", account_name + + third_account = Account.create!(external_account_id: "3rd", name: account_name) + Current.without_account do + @identity.users.create!(account: third_account, name: @name) + @identity.reload + end + + account_name = @generator.generate + assert_equal "Newart's 4th Fizzy", account_name + + fourth_account = Account.create!(external_account_id: "4th", name: account_name) + Current.without_account do + @identity.users.create!(account: fourth_account, name: @name) + @identity.reload + end + + account_name = @generator.generate + assert_equal "Newart's 5th Fizzy", account_name + end + + test "generate continues from the previous highest index" do + account = Account.create!(external_account_id: "12th", name: "Newart's 12th Fizzy") + Current.without_account do + @identity.users.create!(account: account, name: @name) + @identity.reload + end + + account_name = @generator.generate + assert_equal "Newart's 13th Fizzy", account_name + end +end diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb new file mode 100644 index 0000000000..bcdfc8a6cf --- /dev/null +++ b/test/models/signup_test.rb @@ -0,0 +1,54 @@ +require "test_helper" + +class SignupTest < ActiveSupport::TestCase + test "#create_identity" do + signup = Signup.new(email_address: "brian@example.com") + + assert_difference -> { Identity.count }, 1 do + assert_difference -> { MagicLink.count }, 1 do + assert signup.create_identity + end + end + + assert_empty signup.errors + assert signup.identity + assert signup.identity.persisted? + + signup_existing = Signup.new(email_address: "brian@example.com") + + assert_no_difference -> { Identity.count } do + assert_difference -> { MagicLink.count }, 1 do + assert signup_existing.create_identity, "Should send magic link for existing identity" + end + end + + signup_invalid = Signup.new(email_address: "") + assert_raises do + signup_invalid.create_identity + end + end + + test "#complete" do + Account.any_instance.expects(:setup_customer_template).once + + Current.without_account do + signup = Signup.new( + full_name: "Kevin", + identity: identities(:kevin) + ) + + assert signup.complete + + assert signup.account + assert signup.user + assert_equal "Kevin", signup.user.name + + signup_invalid = Signup.new( + full_name: "", + identity: identities(:kevin) + ) + assert_not signup_invalid.complete + assert_not_empty signup_invalid.errors[:full_name] + end + end +end From 166ff836117a5620b0baf0ba5c14f90b952e5e77 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:09:18 +0100 Subject: [PATCH 078/107] Rename tests --- test/models/account/external_id_sequence_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/models/account/external_id_sequence_test.rb b/test/models/account/external_id_sequence_test.rb index a7f8ffd18f..0ce72a0a8a 100644 --- a/test/models/account/external_id_sequence_test.rb +++ b/test/models/account/external_id_sequence_test.rb @@ -5,7 +5,7 @@ class Account::ExternalIdSequenceTest < ActiveSupport::TestCase Account::ExternalIdSequence.delete_all end - test ".next returns sequential values" do + test "generate sequential values" do first_value = Account::ExternalIdSequence.next second_value = Account::ExternalIdSequence.next third_value = Account::ExternalIdSequence.next @@ -14,7 +14,7 @@ class Account::ExternalIdSequenceTest < ActiveSupport::TestCase assert_equal second_value + 1, third_value end - test ".next initializes from maximum external_account_id" do + test "start from the maximum existing external account id" do max_id = Account.maximum(:external_account_id) || 0 first_value = Account::ExternalIdSequence.next @@ -22,13 +22,13 @@ class Account::ExternalIdSequenceTest < ActiveSupport::TestCase assert_equal max_id + 1, first_value end - test ".next creates single sequence record" do + test "use a single record for the sequence" do 3.times { Account::ExternalIdSequence.next } assert_equal 1, Account::ExternalIdSequence.count end - test ".next is concurrency-safe" do + test "handle concurrent access safely" do values = 20.times.map do Thread.new do Account::ExternalIdSequence.next From d4ad62109c54078f4e12787cfa089dd2880da91a Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:13:12 +0100 Subject: [PATCH 079/107] Rename/extract methods --- app/controllers/sessions_controller.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 65cb443a0e..aa765919da 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -19,11 +19,9 @@ def new def create if identity = Identity.find_by_email_address(email_address) - magic_link = identity.send_magic_link - flash[:magic_link_code] = magic_link&.code if Rails.env.development? - redirect_to session_magic_link_path + handle_existing_user(identity) elsif - process_new_signup + handle_new_signup end end @@ -37,7 +35,13 @@ def email_address params.expect(:email_address) end - def process_new_signup + def handle_existing_user(identity) + magic_link = identity.send_magic_link + flash[:magic_link_code] = magic_link&.code if Rails.env.development? + redirect_to session_magic_link_path + end + + def handle_new_signup Signup.new(email_address: email_address).create_identity session[:return_to_after_authenticating] = new_signup_completion_path redirect_to session_magic_link_path From 6bfed6d048006f61423dea457a343f9d6969c026 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:14:06 +0100 Subject: [PATCH 080/107] Format --- test/models/signup_test.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/models/signup_test.rb b/test/models/signup_test.rb index bcdfc8a6cf..30910bf810 100644 --- a/test/models/signup_test.rb +++ b/test/models/signup_test.rb @@ -32,10 +32,7 @@ class SignupTest < ActiveSupport::TestCase Account.any_instance.expects(:setup_customer_template).once Current.without_account do - signup = Signup.new( - full_name: "Kevin", - identity: identities(:kevin) - ) + signup = Signup.new(full_name: "Kevin", identity: identities(:kevin)) assert signup.complete From e101e4dbaebaa48232ea1f448cce575985e0623f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:14:56 +0100 Subject: [PATCH 081/107] Point back to main since the PR was merged --- Gemfile.saas | 2 +- Gemfile.saas.lock | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile.saas b/Gemfile.saas index 955a663035..779c8c4198 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -6,6 +6,6 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" -gem "fizzy-saas", bc: "fizzy-saas", branch: "extract-signup" +gem "fizzy-saas", bc: "fizzy-saas" gem "rails_structured_logging", bc: "rails-structured-logging" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index b8f6dc0e78..0907bd3428 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,7 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 3b012b9ffa7c8fdc1e4b9f5aa7d866ed4108a8ff - branch: extract-signup + revision: 8a651bff6133259fa8ea0370ccc5b51588cb3526 specs: fizzy-saas (0.1.0) queenbee From f90dd87795a571ebf791db6f5a8a0ffc7b6e9f6f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:27:14 +0100 Subject: [PATCH 082/107] Update gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 0907bd3428..618c1dc44b 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 8a651bff6133259fa8ea0370ccc5b51588cb3526 + revision: f251028405bd0ba586fe836d1a8c56428b8abff0 specs: fizzy-saas (0.1.0) queenbee From e962ba82ed5d0e3c4b536faaeddf571f8dc87224 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:43:01 +0100 Subject: [PATCH 083/107] Simplify local ci --- config/ci.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/ci.rb b/config/ci.rb index 639df7ef31..85f0aabdf8 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -12,9 +12,7 @@ step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" step "Security: Gitleaks audit", "bin/gitleaks-audit" - step "Tests: Fizzy", "bin/rails test" - step "Tests: SaaS", "SAAS=1 bin/rails test:saas" if Fizzy.saas? - + step "Tests: Open source", "bin/rails test" step "Tests: System", "bin/rails test:system" if success? From 76d3ec0fc4c2cc20ca205927b28a80d2c1aa9170 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 13:58:41 +0100 Subject: [PATCH 084/107] Remove local ci for now --- bin/ci | 6 ------ config/ci.rb | 23 ----------------------- 2 files changed, 29 deletions(-) delete mode 100755 bin/ci delete mode 100644 config/ci.rb diff --git a/bin/ci b/bin/ci deleted file mode 100755 index 4137ad5bb0..0000000000 --- a/bin/ci +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -require_relative "../config/boot" -require "active_support/continuous_integration" - -CI = ActiveSupport::ContinuousIntegration -require_relative "../config/ci.rb" diff --git a/config/ci.rb b/config/ci.rb deleted file mode 100644 index 85f0aabdf8..0000000000 --- a/config/ci.rb +++ /dev/null @@ -1,23 +0,0 @@ -# Run using bin/ci - -require_relative "../lib/fizzy" - -CI.run do - step "Setup", "bin/setup --skip-server" - - step "Style: Ruby", "bin/rubocop" - - step "Security: Gem audit", "bin/bundler-audit check --update" - step "Security: Importmap audit", "bin/importmap audit" - step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" - step "Security: Gitleaks audit", "bin/gitleaks-audit" - - step "Tests: Open source", "bin/rails test" - step "Tests: System", "bin/rails test:system" - - if success? - step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" - else - failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." - end -end From b6aa93dd7904f0233803b21bfb639223f374b338 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 14:03:57 +0100 Subject: [PATCH 085/107] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff8b48cb96..2a4bd1a2aa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Setting up for development -First get everything installed and configured with: +First, get everything installed and configured with: bin/setup From cebd5d35f6fa53dd0eb3291c33a052d6c3bcb702 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 14:57:42 +0100 Subject: [PATCH 086/107] Revert "Remove local ci for now" This reverts commit 7d6ed4a459f78d14767f1523b32553df84e627e5. --- bin/ci | 6 ++++++ config/ci.rb | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100755 bin/ci create mode 100644 config/ci.rb diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..9ba7b979db --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,22 @@ +# Run using bin/ci + +require_relative "../lib/fizzy" + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit check --update" + step "Security: Importmap audit", "bin/importmap audit" + step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + + step "Tests: Open source", "bin/rails test" + step "Tests: System", "bin/rails test:system" + + if success? + step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + else + failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + end +end From f03fbba5acac86fcf17d138f79f8cab57de1f88d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 16:14:57 +0100 Subject: [PATCH 087/107] Restore local CI, sensible setup for open source users --- Gemfile.saas.lock | 2 +- bin/setup | 42 ++++++++++++++++++++++++++++++++++++------ config/ci.rb | 13 +++++++++---- lib/fizzy.rb | 2 +- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 618c1dc44b..78f4a172c7 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: f251028405bd0ba586fe836d1a8c56428b8abff0 + revision: 720bedaf37faf8dff18c9586e1fa7031bad1d2a9 specs: fizzy-saas (0.1.0) queenbee diff --git a/bin/setup b/bin/setup index 0b7fb10f7a..3c3c02fdcf 100755 --- a/bin/setup +++ b/bin/setup @@ -82,16 +82,46 @@ step "Installing RubyGems" bundle install if [ -n "$SAAS" ]; then saas_setup=$(bundle show fizzy-saas)/bin/setup source "$saas_setup" +else + if ! nc -z localhost 3306 2>/dev/null; then + if docker ps -aq -f name=fizzy-mysql | grep -q .; then + step "Starting MySQL" docker start fizzy-mysql + else + step "Setting up MySQL" bash -c ' + docker pull mysql:8.4 + docker run -d \ + --name fizzy-mysql \ + -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \ + -p 3306:3306 \ + mysql:8.4 + echo "MySQL is starting… (it may take a few seconds)" + ' + fi + fi fi -if [[ $* == *--reset* ]]; then - step "Resetting the database" rails db:reset -else - step "Preparing the database" rails db:prepare +if [ -n "$SAAS" ]; then + if [[ $* == *--reset* ]]; then + step "Resetting the database" rails db:reset + else + step "Preparing the database" rails db:prepare - if needs_seeding; then - step "Seeding the database" rails db:seed + if needs_seeding; then + step "Seeding the database" rails db:seed + fi fi +else + for adapter in sqlite mysql; do + if [[ $* == *--reset* ]]; then + step "Resetting the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:reset + else + step "Preparing the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:prepare + + if needs_seeding; then + step "Seeding the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:seed + fi + fi + done fi step "Cleaning up logs and tempfiles" rails log:clear tmp:clear diff --git a/config/ci.rb b/config/ci.rb index 9ba7b979db..b642c25341 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -7,12 +7,17 @@ step "Style: Ruby", "bin/rubocop" - step "Security: Gem audit", "bin/bundler-audit check --update" + step "Security: Gem audit", "bin/bundler-audit check --update" step "Security: Importmap audit", "bin/importmap audit" - step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + + if Fizzy.saas? + step "Tests: SaaS", "SAAS=true BUNDLE_GEMFILE=Gemfile.saas bin/rails test:all" + else + step "Tests: MySQL", "SAAS=false BUNDLE_GEMFILE=Gemfile DATABASE_ADAPTER=mysql bin/rails test:all" + step "Tests: SQLite", "SAAS=false BUNDLE_GEMFILE=Gemfile DATABASE_ADAPTER=sqlite bin/rails test:all" + end - step "Tests: Open source", "bin/rails test" - step "Tests: System", "bin/rails test:system" if success? step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" diff --git a/lib/fizzy.rb b/lib/fizzy.rb index 69b3f9c6f2..e009c8f78d 100644 --- a/lib/fizzy.rb +++ b/lib/fizzy.rb @@ -2,7 +2,7 @@ module Fizzy class << self def saas? return @saas if defined?(@saas) - @saas = !!(ENV["SAAS"] || File.exist?(File.expand_path("../tmp/saas.txt", __dir__))) + @saas = !!(((ENV["SAAS"] || File.exist?(File.expand_path("../tmp/saas.txt", __dir__))) && ENV["SAAS"] != "false")) end def db_adapter From 81656ac2603f748594bc5e69bb23176c764258c9 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 16:23:08 +0100 Subject: [PATCH 088/107] Run SQLite in saas mode too --- bin/setup | 43 +++++++++++++++++++++++++------------------ config/ci.rb | 11 +++++++---- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/bin/setup b/bin/setup index 3c3c02fdcf..5a3eda8820 100755 --- a/bin/setup +++ b/bin/setup @@ -58,6 +58,24 @@ needs_seeding() { fi } + +setup_database() { + local adapter="$1" + local reset="$2" + local label="${adapter:+ ($adapter)}" + local env_cmd="${adapter:+env DATABASE_ADAPTER=$adapter}" + + if [ "$reset" = "true" ]; then + step "Resetting the database$label" $env_cmd rails db:reset + else + step "Preparing the database$label" $env_cmd rails db:prepare + + if needs_seeding; then + step "Seeding the database$label" $env_cmd rails db:seed + fi + fi +} + echo gum style --foreground 153 " ˚ ∘ ∘ ˚ " gum style --foreground 111 --bold " ∘˚˳°∘° 𝒻𝒾𝓏𝓏𝓎 °∘°˳˚∘ " @@ -100,27 +118,16 @@ else fi fi -if [ -n "$SAAS" ]; then - if [[ $* == *--reset* ]]; then - step "Resetting the database" rails db:reset - else - step "Preparing the database" rails db:prepare +reset_flag="" +[[ $* == *--reset* ]] && reset_flag="true" - if needs_seeding; then - step "Seeding the database" rails db:seed - fi - fi +if [ -n "$SAAS" ]; then + for adapter in saas sqlite; do + setup_database "$adapter" "$reset_flag" + done else for adapter in sqlite mysql; do - if [[ $* == *--reset* ]]; then - step "Resetting the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:reset - else - step "Preparing the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:prepare - - if needs_seeding; then - step "Seeding the database ($adapter)" env DATABASE_ADAPTER=$adapter rails db:seed - fi - fi + setup_database "$adapter" "$reset_flag" done fi diff --git a/config/ci.rb b/config/ci.rb index b642c25341..d4bdfd9ea8 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -2,6 +2,9 @@ require_relative "../lib/fizzy" +OSS_ENV = "SAAS=false BUNDLE_GEMFILE=Gemfile" +SAAS_ENV = "SAAS=true BUNDLE_GEMFILE=Gemfile.saas" + CI.run do step "Setup", "bin/setup --skip-server" @@ -12,13 +15,13 @@ step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" if Fizzy.saas? - step "Tests: SaaS", "SAAS=true BUNDLE_GEMFILE=Gemfile.saas bin/rails test:all" + step "Tests: SaaS", "#{SAAS_ENV} bin/rails test:all" + step "Tests: SQLite", "#{OSS_ENV} DATABASE_ADAPTER=sqlite bin/rails test:all" else - step "Tests: MySQL", "SAAS=false BUNDLE_GEMFILE=Gemfile DATABASE_ADAPTER=mysql bin/rails test:all" - step "Tests: SQLite", "SAAS=false BUNDLE_GEMFILE=Gemfile DATABASE_ADAPTER=sqlite bin/rails test:all" + step "Tests: MySQL", "#{OSS_ENV} DATABASE_ADAPTER=mysql bin/rails test:all" + step "Tests: SQLite", "#{OSS_ENV} DATABASE_ADAPTER=sqlite bin/rails test:all" end - if success? step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" else From 94c49fb3f638a7a980ac39a227701b2e02b7fdea Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Thu, 27 Nov 2025 16:37:36 +0100 Subject: [PATCH 089/107] Move readme bits to fizzy-saas (WIP) --- README.md | 46 ++++++---------------------------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 2a4bd1a2aa..1f9cb24796 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ First, get everything installed and configured with: bin/setup -If you'd like to load fixtures: - - bin/rails db:fixtures:load - And then run the development server: bin/dev You'll be able to access the app in development at http://fizzy.localhost:3006 +You can reset the database and seed it with: + + bin/setup --reset + ## Running tests For fast feedback loops, unit tests can be run with: @@ -26,47 +26,13 @@ The full continuous integration tests can be run with: bin/ci -### Tests - -### Outbound Emails - -#### Development +## Outbound Emails You can view email previews at http://fizzy.localhost:3006/rails/mailers. -You can enable or disable [`letter_opener`](https://github.com/ryanb/letter_opener) to -open sent emails automatically with: +You can enable or disable [`letter_opener`](https://github.com/ryanb/letter_opener) to open sent emails automatically with: bin/rails dev:email Under the hood, this will create or remove `tmp/email-dev.txt`. -## Environments - -Fizzy is deployed with Kamal. You'll need to have the 1Password CLI set up in order to access the secrets that are used when deploying. Provided you have that, it should be as simple as `bin/kamal deploy` to the correct environment. - -### Beta - -Beta is primarily intended for testing product features. - -Beta tenant is: - -- https://fizzy-beta.37signals.com - -This environment uses local disk for Active Storage. - - -### Staging - -Staging is primarily intended for testing infrastructure changes. - -- https://fizzy.37signals-staging.com/ - -This environment uses a FlashBlade bucket for blob storage, and shares nothing with Production. We may periodically copy data here from production. - - -### Production - -- https://app.fizzy.do/ - -This environment uses a FlashBlade bucket for blob storage. From c87f8a87f5dc620df8063f70d4df7db3d956f1ca Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 09:03:46 +0100 Subject: [PATCH 090/107] Review styles for controller/models --- STYLE.md | 51 ++++++++++++++++----------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/STYLE.md b/STYLE.md index bf9dd3be3e..498a8e720b 100644 --- a/STYLE.md +++ b/STYLE.md @@ -135,54 +135,35 @@ class SomeModule end ``` -## CRUD operations from controllers -In general, we favor a vanilla Rails approach to CRUD operations. We create and update models from Rails controllers passing the parameters directly to the model constructor or update method. We do not use services or form objects to handle these operations. +## Controller and model interactions -There are exceptional scenarios where we need to perform more complex operations, and we use form objects or higher-level service methods to handle them. We use the same pattern for both creations and updates. +In general, we favor a [vanilla Rails](https://dev.37signals.com/vanilla-rails-is-plenty/) approach with thin controllers directly invoking a rich domain model. In general, we don't use services or other artifacts to connect the two. -Related to this, we prefer to avoid [nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). If you find yourself wanting to use `accepts_nested_attributes_for`, that's a good smell that you might want to consider using a form object instead. - -As an example, you can check how we create and update messages in HEY's: `MessagesController`: +Invoking plain Active Record operations is totally fine: ```ruby -class MessagesController < ApplicationController +class Cards::CommentsController < ApplicationController def create - @entry = Entry.enter \ - new_message, - on: new_topic, - status: :drafted, - address: entry_addressed_param, - scheduled_delivery_at: entry_scheduled_delivery_at_param, - scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param - - respond_to_saved_entry @entry + @comment = @card.comments.create!(comment_params) end +end +``` - def update - previously_scheduled = @entry.scheduled_delivery - - @entry.revise \ - message_params, - status: :drafted, - is_delivery_imminent: !entry_status_param.drafted?, - address: entry_addressed_param, - scheduled_delivery_at: entry_scheduled_delivery_at_param, - scheduled_bubble_up_on: entry_scheduled_bubble_up_on_param +For more complex behavior, we prefer clear, intention-revealing model APIs that controllers call directly: - respond_to_saved_entry(@entry, previously_scheduled: previously_scheduled) +```ruby +class Cards::GoldnessesController < ApplicationController + def create + @card.gild end end +``` -class Entry < ApplicationRecord - def self.enter(*args, **kwargs) - Entry::Enter.new(*args, **kwargs).perform - end +When justified, it is fine to use services or form objects, but don't treat those as special artifacts: - def revise(*args, **kwargs) - Entry::Revise.new(self, *args, **kwargs).perform - end -end +```ruby +Signup.new(email_address: email_address).create_identity ``` ## Run async operations in jobs From d836a1c6c78c1056caa2b2e9cc9895c4d86618cd Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 09:10:00 +0100 Subject: [PATCH 091/107] Add note on CRUD controllers --- STYLE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/STYLE.md b/STYLE.md index 498a8e720b..196551ba61 100644 --- a/STYLE.md +++ b/STYLE.md @@ -135,6 +135,22 @@ class SomeModule end ``` +## CRUD controllers + +We model web endpoints as CRUD operations on resources (REST). When an action doesn't map cleanly to a standard CRUD verb, we introduce a new resource rather than adding custom actions. + +```ruby +# Bad +resources :cards do + post :close + post :reopen +end + +# Good +resources :cards do + resource :closure +end +``` ## Controller and model interactions From bfa1ff12446db9a9916125ebe3ba8b1e381becff Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 09:12:39 +0100 Subject: [PATCH 092/107] Style copy --- STYLE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STYLE.md b/STYLE.md index 196551ba61..b31f49cab6 100644 --- a/STYLE.md +++ b/STYLE.md @@ -154,7 +154,7 @@ end ## Controller and model interactions -In general, we favor a [vanilla Rails](https://dev.37signals.com/vanilla-rails-is-plenty/) approach with thin controllers directly invoking a rich domain model. In general, we don't use services or other artifacts to connect the two. +In general, we favor a [vanilla Rails](https://dev.37signals.com/vanilla-rails-is-plenty/) approach with thin controllers directly invoking a rich domain model. We don't use services or other artifacts to connect the two. Invoking plain Active Record operations is totally fine: From fa1fe476ff3844e4cfc5bb88c1b1947be29abc08 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 10:12:29 +0100 Subject: [PATCH 093/107] Initial README and LICENSE --- LICENSE.md | 10 ++++++++++ README.md | 44 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..4839325c29 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +# O'Saasy License Agreement + +Copyright © 2025, 37signals LLC. + +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: + +1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +2. No licensee or downstream recipient may use the Software (including any modified or derivative versions) to directly compete with the original Licensor by offering it to third parties as a hosted, managed, or Software-as-a-Service (SaaS) product or cloud service where the primary value of the service is the functionality of the Software itself. + +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. \ No newline at end of file diff --git a/README.md b/README.md index 1f9cb24796..8e98b5f42b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ # Fizzy -## Setting up for development +This is the source code of [Fizzy](https://fizzy.do/), the Kanban tracking tool for issues and ideas by [37signals](https://37signals.com). + +## Development + +### Setting up First, get everything installed and configured with: - bin/setup +```sh +bin/setup +bin/setup --reset # Reset the database and seed it +``` And then run the development server: - bin/dev - -You'll be able to access the app in development at http://fizzy.localhost:3006 +```sh +bin/dev +``` -You can reset the database and seed it with: +You'll be able to access the app in development at http://fizzy.localhost:3006. - bin/setup --reset +To login, enter `david@37signals.com` and grab the verification code from the browser console to sign in. -## Running tests +### Running tests For fast feedback loops, unit tests can be run with: @@ -26,7 +33,15 @@ The full continuous integration tests can be run with: bin/ci -## Outbound Emails +### Database configuration + +Fizzy supports SQLite (default, recommended for most scenarios) and MySQL. You can switch adapters with the `DATABASE_ADAPTER` environment variable. + +```sh +DATABASE_ADAPTER=mysql bin/rails +``` + +### Outbound Emails You can view email previews at http://fizzy.localhost:3006/rails/mailers. @@ -36,3 +51,14 @@ You can enable or disable [`letter_opener`](https://github.com/ryanb/letter_open Under the hood, this will create or remove `tmp/email-dev.txt`. +## Deployment + +We recommend [Kamal](https://kamal-deploy.org/) for deploying Fizzy. This project comes with a vanilla Rails template, you can find our production setup in [`fizzy-saas`](https://github.com/basecamp/fizzy-saas). + +## SaaS gem companion + +37signals bundles Fizzy with [`fizzy-saas`](https://github.com/basecamp/fizzy-saas), a companion gem that links Fizzy with our billing system and provides our production database and deployment setup. + +## License + +Fizzy is released under the [O'Saasy License](LICENSE.md). From b2bcc12337c852aff4ae375b72adf64bedf1f3f4 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 10:14:19 +0100 Subject: [PATCH 094/107] Review README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8e98b5f42b..135e9a538d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Fizzy supports SQLite (default, recommended for most scenarios) and MySQL. You c ```sh DATABASE_ADAPTER=mysql bin/rails +DATABASE_ADAPTER=mysql bin/test +bin/ci # Runs tests against both SQLite and MySQL ``` ### Outbound Emails From 317abde3aee2c1b77a94b530076a086366928872 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 10:30:33 +0100 Subject: [PATCH 095/107] Review README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 135e9a538d..d828fd46bf 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,11 @@ Under the hood, this will create or remove `tmp/email-dev.txt`. We recommend [Kamal](https://kamal-deploy.org/) for deploying Fizzy. This project comes with a vanilla Rails template, you can find our production setup in [`fizzy-saas`](https://github.com/basecamp/fizzy-saas). -## SaaS gem companion +## SaaS gem -37signals bundles Fizzy with [`fizzy-saas`](https://github.com/basecamp/fizzy-saas), a companion gem that links Fizzy with our billing system and provides our production database and deployment setup. +37signals bundles Fizzy with [`fizzy-saas`](https://github.com/basecamp/fizzy-saas), a companion gem that links Fizzy with our billing system and contains our production setup. + +This gem depends on some private git repositories and it is not meant to be used by third parties. But we hope it can serve as inspiration for anyone wanting to run fizzy on their own infrastructure. ## License From 1897cc238f0bd38fbd31dc260c415fea3459cd90 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 10:51:18 +0100 Subject: [PATCH 096/107] Only staff can access beta/staging https://app.fizzy.do/5986089/cards/3208 --- app/controllers/concerns/authorization.rb | 5 +++ .../non_production_remote_access_test.rb | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 test/controllers/non_production_remote_access_test.rb diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 46769992e1..81bd0c781b 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -3,6 +3,7 @@ module Authorization included do before_action :ensure_can_access_account, if: -> { Current.account.present? && authenticated? } + before_action :ensure_only_staff_can_access_non_production_remote_environments, if: :authenticated? end class_methods do @@ -29,6 +30,10 @@ def ensure_can_access_account redirect_to session_menu_url(script_name: nil) if Current.user.blank? || !Current.user.active? end + def ensure_only_staff_can_access_non_production_remote_environments + head :forbidden unless Rails.env.local? || Rails.env.production? || Current.identity.staff? + end + def redirect_existing_user redirect_to root_path if Current.user end diff --git a/test/controllers/non_production_remote_access_test.rb b/test/controllers/non_production_remote_access_test.rb new file mode 100644 index 0000000000..cb84eda35d --- /dev/null +++ b/test/controllers/non_production_remote_access_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class NonProductionRemoteAccessTest < ActionDispatch::IntegrationTest + test "staff can access in staging environment" do + sign_in_as :david + + Rails.stubs(:env).returns(ActiveSupport::EnvironmentInquirer.new("staging")) + get cards_path + assert_response :success + end + + test "non-staff cannot access in staging environment" do + sign_in_as :jz + + Rails.stubs(:env).returns(ActiveSupport::EnvironmentInquirer.new("staging")) + get cards_path + assert_response :forbidden + end + + test "non-staff can access in production environment" do + sign_in_as :jz + + Rails.stubs(:env).returns(ActiveSupport::EnvironmentInquirer.new("production")) + get cards_path + assert_response :success + end + + test "non-staff can access in local environment" do + sign_in_as :jz + + get cards_path + assert_response :success + end +end From 197f4bd06f4158dd543571ff189cc7dcde34593f Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 11:22:48 +0100 Subject: [PATCH 097/107] Add note about VAPID --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index d828fd46bf..929ab66e49 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,22 @@ Under the hood, this will create or remove `tmp/email-dev.txt`. We recommend [Kamal](https://kamal-deploy.org/) for deploying Fizzy. This project comes with a vanilla Rails template, you can find our production setup in [`fizzy-saas`](https://github.com/basecamp/fizzy-saas). +### Web Push Notifications + +Fizzy uses VAPID (Voluntary Application Server Identification) keys to send browser push notifications. You'll need to generate a key pair and set these environment variables: + +- `VAPID_PRIVATE_KEY` +- `VAPID_PUBLIC_KEY` + +Generate them with the `web-push` gem: + +```ruby +vapid_key = WebPush.generate_key + +puts "VAPID_PRIVATE_KEY=#{vapid_key.private_key}" +puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}" +``` + ## SaaS gem 37signals bundles Fizzy with [`fizzy-saas`](https://github.com/basecamp/fizzy-saas), a companion gem that links Fizzy with our billing system and contains our production setup. From 3b0ddf4cfbff7ab52f80675ddd293ef2c386634c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 11:30:20 +0100 Subject: [PATCH 098/107] Move sentry to engine --- Gemfile | 2 -- Gemfile.lock | 8 -------- Gemfile.saas | 2 ++ app/controllers/concerns/request_forgery_protection.rb | 2 +- config/initializers/sentry.rb | 9 --------- 5 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 config/initializers/sentry.rb diff --git a/Gemfile b/Gemfile index ce2324e1b3..66d7cbac32 100644 --- a/Gemfile +++ b/Gemfile @@ -38,8 +38,6 @@ gem "useragent", bc: "useragent" # Telemetry, logging, and operations gem "mission_control-jobs" -gem "sentry-ruby" -gem "sentry-rails" gem "yabeda" gem "yabeda-actioncable" gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs" diff --git a/Gemfile.lock b/Gemfile.lock index 0996ac69bf..7f0fd21f4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -433,12 +433,6 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - sentry-rails (6.1.2) - railties (>= 5.2.0) - sentry-ruby (~> 6.1.2) - sentry-ruby (6.1.2) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) sniffer (0.5.0) anyway_config (>= 1.0) dry-initializer (~> 3) @@ -583,8 +577,6 @@ DEPENDENCIES rqrcode rubocop-rails-omakase selenium-webdriver - sentry-rails - sentry-ruby solid_cable (>= 3.0) solid_cache (~> 1.0) solid_queue (~> 1.2) diff --git a/Gemfile.saas b/Gemfile.saas index 779c8c4198..250038269c 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -8,4 +8,6 @@ gem "activeresource", require: "active_resource" gem "queenbee", bc: "queenbee-plugin" gem "fizzy-saas", bc: "fizzy-saas" gem "rails_structured_logging", bc: "rails-structured-logging" +gem "sentry-ruby" +gem "sentry-rails" diff --git a/app/controllers/concerns/request_forgery_protection.rb b/app/controllers/concerns/request_forgery_protection.rb index f11ef8ad05..056832a345 100644 --- a/app/controllers/concerns/request_forgery_protection.rb +++ b/app/controllers/concerns/request_forgery_protection.rb @@ -43,7 +43,7 @@ def report_on_forgery_protection_results(origin:, token:, sec_fetch_site:) Rails.logger.info "CSRF protection check: " + info.map { it.join(" ") }.join(", ") if (origin && token) != sec_fetch_site - Sentry.capture_message "CSRF protection mismatch", level: :info, extra: { info: info } + Sentry.try :capture_message, "CSRF protection mismatch", level: :info, extra: { info: info } end end end diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb deleted file mode 100644 index 11f352c97a..0000000000 --- a/config/initializers/sentry.rb +++ /dev/null @@ -1,9 +0,0 @@ -if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank? - Sentry.init do |config| - config.dsn = ENV["SENTRY_DSN"] - config.breadcrumbs_logger = %i[ active_support_logger http_logger ] - config.send_default_pii = false - config.release = ENV["GIT_REVISION"] - config.excluded_exceptions += [ "ActiveRecord::ConcurrentMigrationError" ] - end -end From 6ccf65559782bc183befbf6284ac5ac57b13c299 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 11:50:51 +0100 Subject: [PATCH 099/107] Move yabeda/prometheus stuff to the gem --- Gemfile | 14 +--- Gemfile.lock | 82 ------------------- Gemfile.saas | 11 +++ Gemfile.saas.lock | 13 ++- .../concerns/request_forgery_protection.rb | 4 +- config/initializers/autotuner.rb | 18 +--- config/initializers/solid_queue.rb | 5 -- config/initializers/yabeda.rb | 15 ---- config/puma.rb | 12 +-- lib/yabeda/solid_queue.rb | 27 ------ 10 files changed, 38 insertions(+), 163 deletions(-) delete mode 100644 config/initializers/solid_queue.rb delete mode 100644 config/initializers/yabeda.rb delete mode 100644 lib/yabeda/solid_queue.rb diff --git a/Gemfile b/Gemfile index 66d7cbac32..63c98f89c2 100644 --- a/Gemfile +++ b/Gemfile @@ -36,19 +36,9 @@ gem "net-http-persistent" gem "mittens" gem "useragent", bc: "useragent" -# Telemetry, logging, and operations -gem "mission_control-jobs" -gem "yabeda" -gem "yabeda-actioncable" -gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs" -gem "yabeda-gc" -gem "yabeda-http_requests" -gem "yabeda-prometheus-mmap" -gem "yabeda-puma-plugin" -gem "yabeda-rails" -gem "webrick" # required for yabeda-prometheus metrics server -gem "prometheus-client-mmap", "~> 1.3" +# Operations gem "autotuner" +gem "mission_control-jobs" gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 7f0fd21f4b..502c14965d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,15 +4,6 @@ GIT specs: useragent (0.16.11) -GIT - remote: https://github.com/basecamp/yabeda-activejob.git - revision: 684973f77ff01d8b3dd75874538fae55961e15e6 - branch: bulk-and-scheduled-jobs - specs: - yabeda-activejob (0.6.0) - rails (>= 6.1) - yabeda (~> 0.6) - GIT remote: https://github.com/rails/rails.git revision: b22cb0c0cb39b103d816a66d560991acf0a57163 @@ -121,8 +112,6 @@ GEM railties addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) - anyway_config (2.7.2) - ruby-next-core (~> 1.0) ast (2.4.3) autotuner (1.1.0) aws-eventstream (1.4.0) @@ -182,7 +171,6 @@ GEM reline (>= 0.3.8) dotenv (3.1.8) drb (2.2.3) - dry-initializer (3.2.0) ed25519 (1.4.0) erb (6.0.0) erubi (1.13.1) @@ -318,31 +306,6 @@ GEM prettyprint prettyprint (0.2.0) prism (1.6.0) - prometheus-client-mmap (1.3.0) - base64 - bigdecimal - logger - rb_sys (~> 0.9.117) - prometheus-client-mmap (1.3.0-arm64-darwin) - base64 - bigdecimal - logger - rb_sys (~> 0.9.117) - prometheus-client-mmap (1.3.0-x86_64-darwin) - base64 - bigdecimal - logger - rb_sys (~> 0.9.117) - prometheus-client-mmap (1.3.0-x86_64-linux-gnu) - base64 - bigdecimal - logger - rb_sys (~> 0.9.117) - prometheus-client-mmap (1.3.0-x86_64-linux-musl) - base64 - bigdecimal - logger - rb_sys (~> 0.9.117) propshaft (1.3.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -374,9 +337,6 @@ GEM 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) rainbow (3.1.1) rake (13.3.1) - rake-compiler-dock (1.9.1) - rb_sys (0.9.117) - rake-compiler-dock (= 1.9.1) rdoc (6.16.0) erb psych (>= 4.0.0) @@ -419,7 +379,6 @@ GEM rubocop (>= 1.72) rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) - ruby-next-core (1.1.2) ruby-progressbar (1.13.0) ruby-vips (2.2.5) ffi (~> 1.12) @@ -433,9 +392,6 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - sniffer (0.5.0) - anyway_config (>= 1.0) - dry-initializer (~> 3) solid_cable (3.0.12) actioncable (>= 7.2) activejob (>= 7.2) @@ -499,7 +455,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) websocket (1.2.11) websocket-driver (0.8.0) base64 @@ -507,33 +462,6 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - yabeda (0.14.0) - anyway_config (>= 1.0, < 3) - concurrent-ruby - dry-initializer - yabeda-actioncable (0.2.2) - actioncable (>= 7.2) - activesupport (>= 7.2) - railties (>= 7.2) - yabeda (~> 0.8) - yabeda-gc (0.4.0) - yabeda (~> 0.6) - yabeda-http_requests (0.3.0) - anyway_config (>= 1.3, < 3.0) - sniffer - yabeda - yabeda-prometheus-mmap (0.4.0) - prometheus-client-mmap - yabeda (~> 0.10) - yabeda-puma-plugin (0.9.0) - json - puma - yabeda (~> 0.5) - yabeda-rails (0.10.0) - activesupport - anyway_config (>= 1.3, < 3) - railties - yabeda (~> 0.8) zeitwerk (2.7.3) PLATFORMS @@ -567,7 +495,6 @@ DEPENDENCIES mocha net-http-persistent platform_agent - prometheus-client-mmap (~> 1.3) propshaft puma (>= 5.0) rack-mini-profiler @@ -590,15 +517,6 @@ DEPENDENCIES web-console web-push webmock - webrick - yabeda - yabeda-actioncable - yabeda-activejob! - yabeda-gc - yabeda-http_requests - yabeda-prometheus-mmap - yabeda-puma-plugin - yabeda-rails BUNDLED WITH 2.7.2 diff --git a/Gemfile.saas b/Gemfile.saas index 250038269c..a56a7b6825 100644 --- a/Gemfile.saas +++ b/Gemfile.saas @@ -11,3 +11,14 @@ gem "rails_structured_logging", bc: "rails-structured-logging" gem "sentry-ruby" gem "sentry-rails" +# Telemetry +gem "yabeda" +gem "yabeda-actioncable" +gem "yabeda-activejob", github: "basecamp/yabeda-activejob", branch: "bulk-and-scheduled-jobs" +gem "yabeda-gc" +gem "yabeda-http_requests" +gem "yabeda-prometheus-mmap" +gem "yabeda-puma-plugin" +gem "yabeda-rails" +gem "webrick" # required for yabeda-prometheus metrics server +gem "prometheus-client-mmap", "~> 1.3" diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 78f4a172c7..0d6fb6bc60 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,11 +1,22 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 720bedaf37faf8dff18c9586e1fa7031bad1d2a9 + revision: 071e657caad9d42210d3ec3fea269a355b202395 specs: fizzy-saas (0.1.0) + prometheus-client-mmap queenbee rails (>= 8.1.0.beta1) rails_structured_logging + sentry-rails + sentry-ruby + yabeda + yabeda-actioncable + yabeda-activejob + yabeda-gc + yabeda-http_requests + yabeda-prometheus-mmap + yabeda-puma-plugin + yabeda-rails GIT remote: https://github.com/basecamp/queenbee-plugin diff --git a/app/controllers/concerns/request_forgery_protection.rb b/app/controllers/concerns/request_forgery_protection.rb index 056832a345..fadc886721 100644 --- a/app/controllers/concerns/request_forgery_protection.rb +++ b/app/controllers/concerns/request_forgery_protection.rb @@ -42,8 +42,8 @@ def report_on_forgery_protection_results(origin:, token:, sec_fetch_site:) Rails.logger.info "CSRF protection check: " + info.map { it.join(" ") }.join(", ") - if (origin && token) != sec_fetch_site - Sentry.try :capture_message, "CSRF protection mismatch", level: :info, extra: { info: info } + if defined?(Sentry) && (origin && token) != sec_fetch_site + Sentry.capture_message "CSRF protection mismatch", level: :info, extra: { info: info } end end end diff --git a/config/initializers/autotuner.rb b/config/initializers/autotuner.rb index 84b4de8de0..ec8572b117 100644 --- a/config/initializers/autotuner.rb +++ b/config/initializers/autotuner.rb @@ -7,18 +7,8 @@ # or somewhere else! Autotuner.reporter = proc do |report| Rails.logger.info "GCAUTOTUNE: #{report}" - Sentry.capture_message "Autotuner suggestion", level: :info, extra: { report: report.to_s } -end -# # This (optional) callback is called to provide metrics that can give you -# # insights about the performance of your app. It's recommended to send this -# # data to your observability service (e.g. Datadog, Prometheus, New Relic, etc). -# # Use a metric type that would allow you to calculate the average and percentiles. -# # On Datadog this would be the distribution type. On Prometheus this would be -# # the histogram type. -# Autotuner.metrics_reporter = proc do |metrics| -# # stats is a hash of metric name (string) to integer value. -# metrics.each do |key, val| -# StatsD.gauge(key, val) -# end -# end + if Fizzy.saas? + Sentry.capture_message "Autotuner suggestion", level: :info, extra: { report: report.to_s } + end +end diff --git a/config/initializers/solid_queue.rb b/config/initializers/solid_queue.rb deleted file mode 100644 index 5fe44c0261..0000000000 --- a/config/initializers/solid_queue.rb +++ /dev/null @@ -1,5 +0,0 @@ -SolidQueue.on_start do - Process.warmup - - Yabeda::Prometheus::Exporter.start_metrics_server! -end diff --git a/config/initializers/yabeda.rb b/config/initializers/yabeda.rb deleted file mode 100644 index cad88f80a5..0000000000 --- a/config/initializers/yabeda.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "prometheus/client/support/puma" - -Prometheus::Client.configuration.logger = Rails.logger -Prometheus::Client.configuration.pid_provider = Prometheus::Client::Support::Puma.method(:worker_pid_provider) -Yabeda::Rails.config.controller_name_case = :camel - -Yabeda::ActiveJob.install! - -require "yabeda/solid_queue" -Yabeda::SolidQueue.install! - -Yabeda::ActionCable.configure do |config| - # Fizzy relies primarily on Turbo::StreamsChannel - config.channel_class_name = "ActionCable::Channel::Base" -end diff --git a/config/puma.rb b/config/puma.rb index 60b3712107..8d09b00acc 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -10,12 +10,14 @@ # Run Solid Queue with Puma plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] -# Expose Prometheus metrics at http://0.0.0.0:9394/metrics. +# Expose Prometheus metrics at http://0.0.0.0:9394/metrics (SaaS only). # In dev, overridden to http://127.0.0.1:9306/metrics in .mise.toml. -control_uri = Rails.env.local? ? "unix://tmp/pumactl.sock" : "auto" -activate_control_app control_uri, no_token: true -plugin :yabeda -plugin :yabeda_prometheus +if Fizzy.saas? + control_uri = Rails.env.local? ? "unix://tmp/pumactl.sock" : "auto" + activate_control_app control_uri, no_token: true + plugin :yabeda + plugin :yabeda_prometheus +end if !Rails.env.local? # Because we expect fewer I/O waits than Rails apps that connect to the diff --git a/lib/yabeda/solid_queue.rb b/lib/yabeda/solid_queue.rb deleted file mode 100644 index 082ac89363..0000000000 --- a/lib/yabeda/solid_queue.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Yabeda - module SolidQueue - def self.install! - Yabeda.configure do - group :solid_queue - - gauge :jobs_failed_count, comment: "Number of failed jobs" - gauge :jobs_unreleased_count, comment: "Number of claimed jobs that don't belong to any process" - gauge :jobs_scheduled_and_delayed_count, comment: "Number of scheduled jobs that have over 5 minutes delay" - gauge :recurring_tasks_count, comment: "Number of recurring jobs scheduled" - gauge :recurring_tasks_delayed_count, comment: "Number of recurring jobs that haven't been enqueued within their schedule" - - collect do - if ::SolidQueue.supervisor? - solid_queue.jobs_failed_count.set({}, ::SolidQueue::FailedExecution.count) - solid_queue.jobs_unreleased_count.set({}, ::SolidQueue::ClaimedExecution.where(process: nil).count) - solid_queue.jobs_scheduled_and_delayed_count.set({}, ::SolidQueue::ScheduledExecution.where(scheduled_at: ..5.minutes.ago).count) - solid_queue.recurring_tasks_count.set({}, ::SolidQueue::RecurringTask.count) - solid_queue.recurring_tasks_delayed_count.set({}, ::SolidQueue::RecurringTask.count do |task| - task.last_enqueued_time.present? && (task.previous_time - task.last_enqueued_time) > 5.minutes - end) - end - end - end - end - end -end From 3223ba53c3840fc240a166488bfcdd86b9eb6de1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 12:14:48 +0100 Subject: [PATCH 100/107] We need the check in both test/code --- .../concerns/request_forgery_protection.rb | 2 +- .../request_forgery_protection_test.rb | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/request_forgery_protection.rb b/app/controllers/concerns/request_forgery_protection.rb index fadc886721..ddc47065fe 100644 --- a/app/controllers/concerns/request_forgery_protection.rb +++ b/app/controllers/concerns/request_forgery_protection.rb @@ -42,7 +42,7 @@ def report_on_forgery_protection_results(origin:, token:, sec_fetch_site:) Rails.logger.info "CSRF protection check: " + info.map { it.join(" ") }.join(", ") - if defined?(Sentry) && (origin && token) != sec_fetch_site + if Fizzy.saas? && (origin && token) != sec_fetch_site Sentry.capture_message "CSRF protection mismatch", level: :info, extra: { info: info } end end diff --git a/test/controllers/concerns/request_forgery_protection_test.rb b/test/controllers/concerns/request_forgery_protection_test.rb index 797c2039a4..f34790caa3 100644 --- a/test/controllers/concerns/request_forgery_protection_test.rb +++ b/test/controllers/concerns/request_forgery_protection_test.rb @@ -33,7 +33,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "fail and report when token doesn't match, regardless of Sec-Fetch-Site" do assert_report - assert_log(includes: [ "CSRF protection check", "sec_fetch_site pass (same-origin)", "token fail" ]) do + assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)", "token fail"]) do assert_no_difference -> { Board.count } do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: "invalid-token" }, @@ -45,8 +45,8 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "fail and report when Origin doesn't match, regardless of Sec-Fetch-Site" do assert_report - assert_log(includes: [ "CSRF protection check", "sec_fetch_site pass (same-origin)", - "token pass", "origin fail (evil-site.com)" ]) do + assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)", + "token pass", "origin fail (evil-site.com)"]) do assert_no_difference -> { Board.count } do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -58,7 +58,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is cross-site and CSRF token matches" do assert_report - assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail (cross-site)", "token pass" ]) do + assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (cross-site)", "token pass"]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -70,7 +70,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is none" do assert_report - assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail (none)", "token pass" ]) do + assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (none)", "token pass"]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -82,7 +82,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is missing" do assert_report - assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail ()", "token pass" ]) do + assert_log(includes: ["CSRF protection check", "sec_fetch_site fail ()", "token pass"]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token } end @@ -134,8 +134,10 @@ def assert_log(includes: [], excludes: [], &block) end def assert_report - Sentry.expects(:capture_message).with do |message, **kwargs| - message == "CSRF protection mismatch" && kwargs[:level] == :info + if Fizzy.saas? + Sentry.expects(:capture_message).with do |message, **kwargs| + message == "CSRF protection mismatch" && kwargs[:level] == :info + end end end From efe307049985f1409a475f55470f4811f5720edc Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 12:17:19 +0100 Subject: [PATCH 101/107] Add gitleaks from main --- config/ci.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/ci.rb b/config/ci.rb index d4bdfd9ea8..b206cad0df 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -13,6 +13,7 @@ step "Security: Gem audit", "bin/bundler-audit check --update" step "Security: Importmap audit", "bin/importmap audit" step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + step "Security: Gitleaks audit", "bin/gitleaks-audit" if Fizzy.saas? step "Tests: SaaS", "#{SAAS_ENV} bin/rails test:all" From 2f4a05a39f309cdb1b7960f548d1ebe1d91b9b18 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 12:17:21 +0100 Subject: [PATCH 102/107] Format --- .../concerns/request_forgery_protection_test.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/controllers/concerns/request_forgery_protection_test.rb b/test/controllers/concerns/request_forgery_protection_test.rb index f34790caa3..8ac5327c2e 100644 --- a/test/controllers/concerns/request_forgery_protection_test.rb +++ b/test/controllers/concerns/request_forgery_protection_test.rb @@ -33,7 +33,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "fail and report when token doesn't match, regardless of Sec-Fetch-Site" do assert_report - assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)", "token fail"]) do + assert_log(includes: [ "CSRF protection check", "sec_fetch_site pass (same-origin)", "token fail" ]) do assert_no_difference -> { Board.count } do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: "invalid-token" }, @@ -45,8 +45,8 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "fail and report when Origin doesn't match, regardless of Sec-Fetch-Site" do assert_report - assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)", - "token pass", "origin fail (evil-site.com)"]) do + assert_log(includes: [ "CSRF protection check", "sec_fetch_site pass (same-origin)", + "token pass", "origin fail (evil-site.com)" ]) do assert_no_difference -> { Board.count } do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -58,7 +58,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is cross-site and CSRF token matches" do assert_report - assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (cross-site)", "token pass"]) do + assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail (cross-site)", "token pass" ]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -70,7 +70,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is none" do assert_report - assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (none)", "token pass"]) do + assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail (none)", "token pass" ]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }, @@ -82,7 +82,7 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest test "succeed and report when Sec-Fetch-Site is missing" do assert_report - assert_log(includes: ["CSRF protection check", "sec_fetch_site fail ()", "token pass"]) do + assert_log(includes: [ "CSRF protection check", "sec_fetch_site fail ()", "token pass" ]) do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token } end From 8e0445ddf5862bbe0a6e08a9524bc2039df3304a Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 12:46:07 +0100 Subject: [PATCH 103/107] Add yabeda metrics check only in SaaS mode --- config/recurring.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/recurring.yml b/config/recurring.yml index bdabec6498..2afc11c854 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -1,3 +1,5 @@ +<% require_relative "../lib/fizzy" %> + production: &production # Application functionality: notifications and summaries deliver_bundled_notifications: @@ -26,10 +28,12 @@ production: &production command: "MagicLink.cleanup" schedule: every 4 hours +<% if Fizzy.saas? %> # Metrics yabeda_actioncable: command: "Yabeda::ActionCable.measure" schedule: every 60 seconds +<% end %> beta: *production staging: *production From 3cbe27755cecb10967858a56703a99cbce5683e3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 13:14:04 +0100 Subject: [PATCH 104/107] Moving this to shipyard --- AGENTS.md | 62 +------------------------------------------------------ 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 51407a6fb6..897bfc29f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,73 +137,13 @@ Key recurring tasks (via `config/recurring.yml`): - Search records denormalized for performance - Models in `app/models/search/` -## Production Observability - -Grafana MCP tools provide access to production metrics and logs for performance analysis. - -### Datasources -| Name | UID | Use | -|------|-----|-----| -| Thanos (Prometheus) | `PC96415006F908B67` | Metrics, latencies | -| Loki | `e38bdfea-097e-47fa-a7ab-774fd2487741` | Application logs | - -### Key Metrics -- `rails_request_duration_seconds_bucket:rate1m:sum_by_app:quantiles{app="fizzy"}` - Request latency percentiles -- `rails_request_total:rate1m:sum_by_controller_action{app="fizzy"}` - Request rates by endpoint -- `fizzy_replica_wait_seconds` - Database replica consistency wait times - -### Loki Log Labels and Query Patterns - -**Base label selector:** -```logql -{service_namespace="fizzy", deployment_environment_name="production", service_name="rails"} -``` - -**Useful JSON fields:** `event_duration_ms`, `performance_time_db_ms`, `performance_time_cpu_ms`, `rails_endpoint`, `rails_controller`, `url_path`, `authentication_identity_id`, `http_response_status_code` - -**Query patterns:** -- Filter by fields: `{labels} | field_name = "value"` -- Multiple field filters: `{labels} | field1 = "value1" | field2 = "value2"` -- Reduce returned labels: `{labels} | filters | keep field1,field2,field3` (reduces label payload) -- Minimize log line content: `{labels} | filters | line_format "{{.field_name}}"` (replaces raw log line) -- Combine both for minimal tokens: `{labels} | filters | keep field1,field2 | line_format "{{.field1}}"` -- **Important:** Fields are pre-parsed by the OTel collector. Don't use string search (`|=`) when filtering structured fields -- **Important:** Do NOT use `| json` - it will cause JSONParserErr since fields are already parsed as labels - -**Token management (CRITICAL):** -- Always probe with `limit: 3` first to check response size before running larger queries -- Aggregations return time series (many data points), not single values - can explode token usage -- NEVER use `sum by (field)` - returns a time series per unique value, easily exceeds token limits -- For breakdowns by field: fetch raw logs with `| keep field | line_format "{{.field}}"` and count client-side - -**Aggregations for statistics (use instead of fetching raw logs):** -- `mcp__grafana__query_loki_logs` returns limited results (default 10, max ~100) and large responses get truncated; use aggregations for statistics on large datasets -- Count: `sum(count_over_time({labels} | filters [12h]))` -- Percentiles: `quantile_over_time(0.95, {labels} | filters | unwrap field_name | __error__="" [12h]) by ()` -- Average: `avg_over_time({labels} | filters | unwrap field_name | __error__="" [12h]) by ()` -- Min/Max: `min_over_time(...)` / `max_over_time(...)` -- The `| unwrap field_name | __error__=""` pattern extracts numeric values from pre-parsed labels -- Use `by ()` or wrap in `sum()` to avoid cardinality limits - -**Documentation:** For advanced LogQL syntax (aggregations, pattern matching, etc.), consult https://grafana.com/docs/loki/latest/query/ - -### Instrumentation -Yabeda-based metrics exported at `:9394/metrics`. Config in `config/initializers/yabeda.rb`. - ### Chrome MCP (Local Dev) + URL: `http://fizzy.localhost:3006` Login: david@37signals.com (passwordless magic link auth - check rails console for link) Use Chrome MCP tools to interact with the running dev app for UI testing and debugging. -### Sentry Error Tracking -Organization: `basecamp` | Project: `fizzy` | Region: `https://us.sentry.io` - -Use Sentry MCP tools to investigate production errors: -- `search_issues` - Find grouped issues by natural language query -- `get_issue_details` - Get full stacktrace and context for a specific issue -- `analyze_issue_with_seer` - AI-powered root cause analysis with code fix suggestions - ## Coding style Please read the separate file `STYLE.md` for some guidance on coding style. From a7b2fe2b817ff9cab0f35d955e2c6c3ce8e5b2e4 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 13:19:39 +0100 Subject: [PATCH 105/107] Moved to sentry --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 897bfc29f4..8bdaa4481b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,12 +137,14 @@ Key recurring tasks (via `config/recurring.yml`): - Search records denormalized for performance - Models in `app/models/search/` +## Tools + ### Chrome MCP (Local Dev) URL: `http://fizzy.localhost:3006` Login: david@37signals.com (passwordless magic link auth - check rails console for link) -Use Chrome MCP tools to interact with the running dev app for UI testing and debugging. +Use Chrome MCP tools to interact with the running dev app for UI testing and debugging.`` ## Coding style From a89f2f2c92e6d2f3de4da715543322a6ea6c3a01 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 14:40:55 +0100 Subject: [PATCH 106/107] Update to latest gem --- Gemfile.saas.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 0d6fb6bc60..fc1bcd7584 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/fizzy-saas - revision: 071e657caad9d42210d3ec3fea269a355b202395 + revision: 7f392bbbf9f5170d334b6ee2f6d240569bd157ed specs: fizzy-saas (0.1.0) prometheus-client-mmap From d561a64ae840d0a05cdfc901bd54d95bf925459e Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Fri, 28 Nov 2025 15:48:57 +0100 Subject: [PATCH 107/107] Test --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 929ab66e49..cb0c199905 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,4 @@ This gem depends on some private git repositories and it is not meant to be used ## License Fizzy is released under the [O'Saasy License](LICENSE.md). +