From 0ac7ef769d98ba194f2c8d7168ba89e67f102bb3 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Thu, 22 May 2025 09:53:09 +0100 Subject: [PATCH 001/106] fix: deprecation warning for web_console in development --- config/environments/development.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 60a1ef6c3..9384f66f4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -115,7 +115,7 @@ } # Fixes error "Cannot render console from 172.22.0.1!" when in Docker - config.web_console.whitelisted_ips = [ + config.web_console.allowed_ips = [ '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16' From 3e75128ea741badce18ed43185ad79a227ec7b7e Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Thu, 22 May 2025 11:48:53 +0100 Subject: [PATCH 002/106] chore: fix a bug where db restores would sometimes fail due to failure to resolve public.squish function --- db/functions/squish_null/20250522113100.sql | 12 ++++++++++++ db/helpers/000_helpers.sql | 9 --------- .../20250522113100_update_squish_null_function.rb | 13 +++++++++++++ db/structure.sql | 8 ++++++-- 4 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 db/functions/squish_null/20250522113100.sql create mode 100644 db/migrate/20250522113100_update_squish_null_function.rb diff --git a/db/functions/squish_null/20250522113100.sql b/db/functions/squish_null/20250522113100.sql new file mode 100644 index 000000000..487632b46 --- /dev/null +++ b/db/functions/squish_null/20250522113100.sql @@ -0,0 +1,12 @@ +CREATE OR REPLACE FUNCTION public.squish_null(TEXT) RETURNS TEXT + LANGUAGE SQL IMMUTABLE +AS $fn$ + SELECT + CASE WHEN public.squish($1) = '' + THEN NULL + ELSE public.squish($1) + END; +$fn$; + +COMMENT ON FUNCTION public.squish_null(TEXT) IS + 'Squishes whitespace characters in a string and returns null for empty string'; diff --git a/db/helpers/000_helpers.sql b/db/helpers/000_helpers.sql index 5393152a8..4d96c0ea3 100644 --- a/db/helpers/000_helpers.sql +++ b/db/helpers/000_helpers.sql @@ -9,15 +9,6 @@ AS $FUNCTION$ ); $FUNCTION$; -CREATE OR REPLACE FUNCTION squish_null(TEXT) RETURNS TEXT - LANGUAGE SQL IMMUTABLE - AS $$ - SELECT CASE WHEN SQUISH($1) = '' THEN NULL ELSE SQUISH($1) END; - $$; - -COMMENT ON FUNCTION squish_null(TEXT) IS - 'Squishes whitespace characters in a string and returns null for empty string'; - -- This function previously had a different signature - ensure that the old version is gone DROP FUNCTION IF EXISTS full_name_with_spp(rank_name VARCHAR(255), full_name VARCHAR(255)); diff --git a/db/migrate/20250522113100_update_squish_null_function.rb b/db/migrate/20250522113100_update_squish_null_function.rb new file mode 100644 index 000000000..ef6b7293f --- /dev/null +++ b/db/migrate/20250522113100_update_squish_null_function.rb @@ -0,0 +1,13 @@ +class UpdateSquishNullFunction < ActiveRecord::Migration[7.1] + def up + safety_assured do + execute function_sql('20250522113100', 'squish_null') + end + end + + def down + safety_assured do + execute function_sql('20150421071444', 'squish_null') + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 3c2ec3806..1849748f6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -6775,8 +6775,12 @@ COMMENT ON FUNCTION public.squish(text) IS 'Squishes whitespace characters in a CREATE FUNCTION public.squish_null(text) RETURNS text LANGUAGE sql IMMUTABLE AS $_$ - SELECT CASE WHEN SQUISH($1) = '' THEN NULL ELSE SQUISH($1) END; - $_$; + SELECT + CASE WHEN public.squish($1) = '' + THEN NULL + ELSE public.squish($1) + END; +$_$; -- From 66c6a1dfc9ad8071d06ae9a1d53036108e326904 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 23 May 2025 10:04:41 +0100 Subject: [PATCH 003/106] chore: fix db_trim tasks --- lib/tasks/db_trim.rake | 241 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 215 insertions(+), 26 deletions(-) diff --git a/lib/tasks/db_trim.rake b/lib/tasks/db_trim.rake index c75375267..5393435cf 100644 --- a/lib/tasks/db_trim.rake +++ b/lib/tasks/db_trim.rake @@ -1,7 +1,25 @@ +## +# In addition to deleting data, some consideration should be given to database +# maintenance: +# +# ANALYSE calculates summary stats about a table and its columns. These stats +# are used in query planning, so bad stats +# +# REINDEX is called because some large tables have gigabytes locked away in +# indexes, and REINDEX is the quickest way to free up that space. +# +# VACUUM can only reclaim space from dead tuples so often is not worth it. +# Furthermore space it reclaims is still reserved for the table, so is not +# released by the file system - for that VACUUM FULL is required, which requires +# a full table rewrite, which takes up additional disk space which may not be +# free. + namespace :db do desc 'Deletes historic and sensitive data, runs cleanup of temporary tables and rebuilds' task trim: [ :environment, + 'db:trim_ahoy', + 'db:trim_api_requests', 'db:common_names:cleanup', 'db:taxon_names:cleanup', 'db:trim_trade', @@ -11,29 +29,99 @@ namespace :db do 'db:trim_users', 'import:drop_import_tables', 'db:migrate:rebuild', - 'db:drop_temporary_tables' + 'db:drop_temporary_tables', + 'db:vacuum_full' ] + ## + # Drop all tables of the form /^trade_sandbox_\d+$/ + # and their associated views and indexes + task trim_trade_sandboxes: :environment do + sandbox_group_count = 50 + + sandbox_query = + <<-SQL.squish + SELECT count(*) FROM information_schema.tables + WHERE table_name LIKE 'trade_sandbox%' + AND table_name != 'trade_sandbox_template' + AND table_type != 'VIEW' + SQL + + sandbox_count = + ApplicationRecord.connection.execute( + sandbox_query + )[0]['count'].to_i + + ( + sandbox_count.to_f / sandbox_group_count + ).ceil.times do + ApplicationRecord.connection.execute <<-SQL.squish + DO $do$ + DECLARE + current_table_name TEXT; + BEGIN + FOR current_table_name + IN #{sandbox_query} + LIMIT #{sandbox_group_count} + LOOP + EXECUTE 'DROP TABLE ' || current_table_name || ' CASCADE'; + END LOOP; + RETURN; + END; + $do$; + SQL + end + end + task trim_trade: :environment do - puts 'Deleting old shipments' year = Date.today.year - 5 - ApplicationRecord.connection.execute "DELETE FROM trade_shipments WHERE year <= #{year}" - puts 'Clearing permit and annual report data' - ApplicationRecord.connection.execute 'UPDATE trade_shipments SET + + puts "Deleting shipments prior to #{year}" + ApplicationRecord.connection.execute <<-SQL.squish + DELETE FROM trade_shipments WHERE year <= #{year} + SQL + + ApplicationRecord.connection.execute 'ANALYSE trade_shipments;' + ApplicationRecord.connection.execute 'REINDEX TABLE trade_shipments;' + + puts 'Clearing permits' + # Note: where clause don't make unnecessary writes to rows + ApplicationRecord.connection.execute <<-SQL.squish + UPDATE trade_shipments SET import_permit_number = NULL, export_permit_number = NULL, origin_permit_number = NULL, - import_permits_ids = \'{}\'::INT[], - export_permits_ids = \'{}\'::INT[], - origin_permits_ids = \'{}\'::INT[], + import_permits_ids = '{}'::INT[], + export_permits_ids = '{}'::INT[], + origin_permits_ids = '{}'::INT[], trade_annual_report_upload_id = NULL, - sandbox_id = NULL' - puts 'Dropping sandboxes' - ApplicationRecord.connection.execute 'SELECT * FROM drop_trade_sandboxes()' - puts 'Truncating annual reports' - ApplicationRecord.connection.execute 'DELETE FROM trade_annual_report_uploads' + sandbox_id = NULL + WHERE origin_permit_number IS NOT NULL + OR export_permit_number IS NOT NULL + OR origin_permit_number IS NOT NULL + OR trade_annual_report_upload_id IS NOT NULL + OR sandbox_id IS NOT NULL + ; + SQL + puts 'Truncating permits' ApplicationRecord.connection.execute 'TRUNCATE trade_permits' + ApplicationRecord.connection.execute 'REINDEX TABLE trade_permits' + + ## + # drop_trade_sandboxes() does not work when there are many sandboxes + # + # puts 'Dropping sandboxes' + # ApplicationRecord.connection.execute 'SELECT * FROM drop_trade_sandboxes()' + + puts "Deleting annual reports uploaded prior to #{year}" + ApplicationRecord.connection.execute <<-SQL.squish + DELETE FROM trade_annual_report_uploads + WHERE updated_at <= '#{year}-01-01'; + SQL + + ApplicationRecord.connection.execute 'ANALYSE trade_annual_report_uploads;' + ApplicationRecord.connection.execute 'REINDEX TABLE trade_annual_report_uploads;' end task trim_listing_changes: :environment do @@ -47,28 +135,32 @@ namespace :db do ON lc.parent_id = nc_lc.id ), listing_changes_to_delete AS ( SELECT * FROM non_current_listing_changes - UNION + EXCEPT SELECT * FROM exceptions ), deleted_listing_distributions AS ( DELETE FROM listing_distributions USING listing_changes_to_delete lc WHERE lc.id = listing_distributions.listing_change_id - ), deleted_annotations AS ( - DELETE FROM annotations - USING listing_changes_to_delete lc - WHERE annotations.id = lc.annotation_id ), updated_original_id AS ( UPDATE listing_changes SET original_id = NULL FROM listing_changes_to_delete WHERE listing_changes.original_id = listing_changes_to_delete.id + ), updated_parent_id AS ( + UPDATE listing_changes + SET parent_id = NULL + FROM listing_changes_to_delete + WHERE listing_changes.parent_id = listing_changes_to_delete.id ) DELETE FROM listing_changes USING listing_changes_to_delete lc - WHERE lc.id = listing_changes.id + WHERE lc.id = listing_changes.id; SQL + puts 'Deleting old listing changes' ApplicationRecord.connection.execute sql + ApplicationRecord.connection.execute 'ANALYSE listing_changes;' + ApplicationRecord.connection.execute 'REINDEX TABLE listing_changes;' end task trim_trade_restrictions: :environment do @@ -76,6 +168,10 @@ namespace :db do WITH trade_restrictions_to_delete AS ( SELECT * FROM trade_restrictions WHERE NOT is_current + ), deleted_cites_suspension_confirmations AS ( + DELETE FROM cites_suspension_confirmations + USING trade_restrictions_to_delete + WHERE cites_suspension_confirmations.cites_suspension_id = trade_restrictions_to_delete.id ), deleted_restriction_purposes AS ( DELETE FROM trade_restriction_purposes USING trade_restrictions_to_delete @@ -96,29 +192,122 @@ namespace :db do ) DELETE FROM trade_restrictions USING trade_restrictions_to_delete tr - WHERE tr.id = trade_restrictions.id + WHERE tr.id = trade_restrictions.id; SQL + puts 'Deleting old trade restrictions' ApplicationRecord.connection.execute sql + ApplicationRecord.connection.execute 'ANALYSE trade_restrictions;' + ApplicationRecord.connection.execute 'REINDEX TABLE trade_restrictions;' end task trim_eu_decisions: :environment do - sql = 'DELETE FROM eu_decisions WHERE NOT is_current' puts 'Deleting old EU decisions' - ApplicationRecord.connection.execute sql + ApplicationRecord.connection.execute <<-SQL.squish + DELETE FROM eu_decisions + WHERE NOT eu_decisions.is_current AND NOT EXISTS ( + SELECT TRUE + FROM eu_decision_confirmations + WHERE eu_decision_id = eu_decisions.id + ); + SQL + + ApplicationRecord.connection.execute 'ANALYSE eu_decisions;' + ApplicationRecord.connection.execute 'REINDEX TABLE eu_decisions;' end task trim_users: :environment do - puts 'Clearing user data' + puts 'Pseudonymising user data' ApplicationRecord.connection.execute <<-SQL.squish - UPDATE users SET - name = 'user ' || users.id, - email = 'user.' || users.id || '@test.org' + UPDATE "users" u SET + "name" = 'User ' || u.id, + "email" = 'user.' || u.id || '@test.local', + "current_sign_in_ip" = '192.168.1.' || (u.id % 256), + "last_sign_in_ip" = '192.168.1.' || (u.id % 256) + WHERE "email" NOT LIKE '%@unep-wcmc.org' + AND "email" NOT LIKE '%@test.local' SQL end + task trim_ahoy: :environment do + puts 'Removing analytics data' + + ApplicationRecord.connection.execute <<-SQL.squish + TRUNCATE TABLE ahoy_events; + REINDEX TABLE ahoy_events; + TRUNCATE TABLE ahoy_visits; + REINDEX TABLE ahoy_visits; + SQL + end + + task trim_api_requests: :environment do + cutoff = 2.years.ago.to_date.to_s + + puts "Removing records of API Request data prior to #{cutoff}" + + ApplicationRecord.connection.execute <<-SQL.squish + DELETE FROM api_requests + WHERE updated_at <= '#{cutoff}' + ; + SQL + + ApplicationRecord.connection.execute 'ANALYSE api_requests;' + ApplicationRecord.connection.execute 'REINDEX TABLE api_requests;' + end + task drop_temporary_tables: :environment do puts 'Dropping temporary tables' + ApplicationRecord.connection.execute 'SELECT * FROM drop_eu_lc_mviews()' end + + ## + # Reclaims space from any table where the number of dead tuples (old versions + # of rows left over by postgres as a result of updates/deletes) is greater + # than the number of live tuples. In effect, this means that wherever the size + # on disk of a table can be reduced by about 50% or more, a full-table rewrite + # will be performed. This requires a exclusive lock on the entire table. + task vacuum_full: :environment do + puts 'Finding tables that need vacuuming' + + vaccumable_sql = <<-SQL.squish + SELECT + st.schemaname, + st.relname, + n_live_tup, + n_dead_tup + FROM pg_catalog.pg_stat_all_tables st + JOIN pg_catalog.pg_class r + ON st.relid = r.oid + AND r.relkind = 't' + WHERE st.schemaname = 'public' + AND n_dead_tup > n_live_tup + SQL + + vacuumables = ApplicationRecord.connection.execute vaccumable_sql + + puts ( + vacuumables.map do |row| + %Q("#{row['schemaname']}"."#{row['relname']}": #{row['n_dead_tup']} dead, #{row['n_live_tup']} live") + end.join("\n") + ) + + ApplicationRecord.connection.execute <<-SQL.squish + DO $do$ + DECLARE + schemaname TEXT; + relname TEXT; + BEGIN + FOR schemaname, relname IN #{vaccumable_sql} + LOOP + EXECUTE format( + 'VACUUM FULL ANALYSE %I$1.%I$2', + schemaname, relname + ); + END LOOP; + RETURN; + END; + $do$; + SQL + end end From b31edbf6bea62b9d1704eacf53adb3232370b34e Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Fri, 23 May 2025 10:15:13 +0100 Subject: [PATCH 004/106] chore: interim docker-compose for side-by-side work on pg10, pg17 --- docker-compose.yml | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 95816b64e..649ef24b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,8 +45,8 @@ services: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin - db: - container_name: sapi-db + db-pg10: + container_name: sapi-db-pg10 image: postgres:10 command: postgres -c max_wal_size=2GB healthcheck: @@ -66,6 +66,27 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" POSTGRES_DB: "sapi_development" + db-pg17: + container_name: sapi-db-pg17 + image: postgres:17 + command: postgres -c max_wal_size=2GB + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + timeout: 45s + interval: 10s + retries: 10 + volumes: + - ./db_init:/docker-entrypoint-initdb.d + - ./db/structure.sql:/docker-entrypoint-initdb.d/sapi_schema.sql + - 'pg17data:/var/lib/postgresql/data' + ports: + - "${SAPI_CONTAINER_DB_PORT:-5417}:5432" + networks: + - sapi + environment: + POSTGRES_HOST_AUTH_METHOD: "trust" + POSTGRES_DB: "sapi_development" + rails: container_name: sapi-rails build: @@ -114,17 +135,18 @@ services: tty: true environment: MAILER_ADDRESS: sapi-mailcatcher - SAPI_DATABASE_HOST: sapi-db + SAPI_DATABASE_HOST: sapi-db-pg17 SAPI_DATABASE_USERNAME: postgres SAPI_DATABASE_PORT: 5432 SAPI_SIDEKIQ_REDIS_URL: redis://sapi-redis:6379/0 SAPI_SIDEKIQ_REDIS_CACHE_URL: redis://sapi-redis-cache:6380/0 - CAPTIVE_BREEDING_DATABASE_HOST: sapi-db + CAPTIVE_BREEDING_DATABASE_HOST: sapi-db-pg17 SAPI_S3_PORT: '${SAPI_CONTAINER_S3_PORT:-9000}' depends_on: - redis - redis_cache - - db + - db-pg10 + - db-pg17 - mailcatcher - minio deploy: @@ -138,7 +160,7 @@ services: stdin_open: true tty: true environment: - SAPI_DATABASE_HOST: sapi-db + SAPI_DATABASE_HOST: sapi-db-pg17 SAPI_SIDEKIQ_REDIS_URL: redis://sapi-redis:6379/0 # Defaults to blank; used by AppSignal: USER: "$USER" @@ -176,7 +198,8 @@ services: networks: - sapi depends_on: - - db + - db-pg10 + - db-pg17 - redis - redis_cache - mailcatcher @@ -185,14 +208,13 @@ services: volumes: *rails_volumes environment: MAILER_ADDRESS: sapi-mailcatcher - SAPI_DATABASE_HOST: sapi-db + SAPI_DATABASE_HOST: sapi-db-pg17 SAPI_DATABASE_USERNAME: postgres SAPI_DATABASE_PORT: 5432 - SAPI_SIDEKIQ_REDIS_URL: redis://sapi-redis:6379/0 SAPI_S3_PORT: '${SAPI_CONTAINER_S3_PORT:-9000}' + SAPI_SIDEKIQ_REDIS_URL: redis://sapi-redis:6379/0 SAPI_SIDEKIQ_REDIS_CACHE_URL: redis://sapi-redis-cache:6380/0 - CAPTIVE_BREEDING_DATABASE_HOST: sapi-db - + CAPTIVE_BREEDING_DATABASE_HOST: sapi-db-pg17 mailcatcher: container_name: sapi-mailcatcher image: sj26/mailcatcher @@ -203,6 +225,7 @@ services: volumes: pgdata: + pg17data: bundler_gems: redis_data: minio-data: From c88a35fc9b167accb398e230a89f81184350aac7 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Mon, 9 Jun 2025 09:22:14 +0100 Subject: [PATCH 005/106] chore: install postgres 17 client in Dockerfile --- Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5b18e7dcc..ee36821fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,15 @@ FROM ruby:3.2.5 # socat is just for binding ports within docker, not needed for the application RUN apt-get update && apt-get install -y --force-yes \ libsodium-dev libgmp3-dev libssl-dev \ - libpq-dev postgresql-client \ + postgresql-common \ nodejs \ socat \ texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ - ; -# NB: Postgres client from Debian is 9.4 - not sure if this is acceptable +; + +# pg_dump requires that the client library >= the server (major) version +RUN yes | /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh +RUN apt-get install -y --force-yes libpq-dev postgresql-client-17 RUN mkdir /SAPI WORKDIR /SAPI From 5a7216233beaddf9b903f87b2896d88ee31e7cb3 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 10:48:27 +0100 Subject: [PATCH 006/106] pin ruby/kamal version for kamal deploy --- deploy/.ruby-version | 1 + deploy/Gemfile | 4 +++ deploy/Gemfile.lock | 72 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 deploy/.ruby-version create mode 100644 deploy/Gemfile create mode 100644 deploy/Gemfile.lock diff --git a/deploy/.ruby-version b/deploy/.ruby-version new file mode 100644 index 000000000..f9892605c --- /dev/null +++ b/deploy/.ruby-version @@ -0,0 +1 @@ +3.4.4 diff --git a/deploy/Gemfile b/deploy/Gemfile new file mode 100644 index 000000000..aa43c464f --- /dev/null +++ b/deploy/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' +ruby '3.4.4' + +gem "kamal", "~> 2.7" diff --git a/deploy/Gemfile.lock b/deploy/Gemfile.lock new file mode 100644 index 000000000..5baa55c1c --- /dev/null +++ b/deploy/Gemfile.lock @@ -0,0 +1,72 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.2) + 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) + bcrypt_pbkdf (1.1.1) + benchmark (0.4.1) + bigdecimal (3.2.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) + dotenv (3.1.8) + drb (2.2.3) + ed25519 (1.4.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + kamal (2.7.0) + 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) + logger (1.7.0) + minitest (5.25.5) + 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-ssh (7.3.0) + ostruct (0.6.3) + securerandom (0.4.1) + sshkit (1.24.0) + base64 + logger + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + thor (1.3.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (1.0.3) + zeitwerk (2.7.3) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + kamal (~> 2.7) + +RUBY VERSION + ruby 3.4.4p34 + +BUNDLED WITH + 2.6.7 From 8f2eb4353b448fbc426b8fe3017026a623790aad Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:01:09 +0100 Subject: [PATCH 007/106] pin nodejs version, and optimise Dockefile, the image size down from 4GB to 2.4GB --- .node-version | 1 + Dockerfile | 27 ++++++++++++++++++++++----- docker-compose.yml | 8 ++------ 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..08b7109d0 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.20.8 diff --git a/Dockerfile b/Dockerfile index 5b18e7dcc..08cedd4f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,21 +4,38 @@ # or SUS-ORS project. # Dockerfile -FROM ruby:3.2.5 +FROM ruby:3.2.5-slim # Rails and SAPI has some additional dependencies, e.g. rake requires a JS # runtime, so attempt to get these from apt, where possible # socat is just for binding ports within docker, not needed for the application -RUN apt-get update && apt-get install -y --force-yes \ +RUN apt-get update && apt-get install --no-install-recommends -y --force-yes \ + # ? libsodium-dev libgmp3-dev libssl-dev \ + # PSQL libpq-dev postgresql-client \ - nodejs \ + # node js + curl xz-utils \ + # For minio, local s3, development only. socat \ + # latex (huge file size) texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ - ; + # Clean up + && rm -rf /var/lib/apt/lists/* # NB: Postgres client from Debian is 9.4 - not sure if this is acceptable -RUN mkdir /SAPI +# Install Node.js 18.20.8 manually +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +# Map Docker TARGETARCH to Node.js archive name +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + WORKDIR /SAPI # diff --git a/docker-compose.yml b/docker-compose.yml index 329955999..f54462985 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,9 +68,7 @@ services: rails: container_name: sapi-rails - build: - context: ./ - dockerfile: Dockerfile + build: . command: bundle exec rails server -p 3000 -b '0.0.0.0' volumes: &rails_volumes # Used for both rails and sidekiq @@ -175,9 +173,7 @@ services: sidekiq: container_name: sapi-sidekiq - build: - context: . - dockerfile: Dockerfile + build: . networks: - sapi depends_on: From 162b1ac07114fbd46710a62d9d69bf89560a0bba Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:37:43 +0100 Subject: [PATCH 008/106] config.hosts and ALLOWED_HOSTS only use in local docker development. --- config/environments/production.rb | 1 - config/environments/staging.rb | 2 -- 2 files changed, 3 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index d93986aa5..c700c15a1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -116,7 +116,6 @@ # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - config.hosts += ENV['ALLOWED_HOSTS'].split(',') if ENV['ALLOWED_HOSTS'].present? ### # Everything below are WCMC custom settings. diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 6b20dc228..85a35f2c9 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -108,8 +108,6 @@ # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } - config.hosts += ENV['ALLOWED_HOSTS'].split(',') if ENV['ALLOWED_HOSTS'].present? - # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector From 5d8da2218e8649cc01e8d7dabc7ca58b0b42b765 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 14:43:57 +0100 Subject: [PATCH 009/106] mailer config can't use Rails credentials: 1) should migrate to ENV so DevOps can make changes without developers; 2) assets:precompile without RAILS_MASTER_KEY not able to run coz failed to load credentials. --- config/environments/production.rb | 24 ++++++++---------------- config/environments/staging.rb | 24 ++++++++---------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index c700c15a1..3c38ce0d0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -124,31 +124,23 @@ config.ember.variant = :production # Custom email settings - mailer_credentials = Rails.application.credentials[:mailer] - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: mailer_credentials[:address], - port: mailer_credentials[:port], - domain: mailer_credentials[:domain], - user_name: mailer_credentials[:username], - password: mailer_credentials[:password], + address: "smtp.sendgrid.net", + port: 587, + domain: "unep-wcmc.org", + user_name: ENV['MAIL_USERNAME'], + password: ENV['MAIL_PASSWORD'], authentication: :login, enable_starttls_auto: true } config.action_mailer.default_url_options = { - host: mailer_credentials[:host] + host: "www.speciesplus.net" } - # fix for current version of mail gem: https://github.com/mikel/mail/issues/1538 - # config.action_mailer.delivery_method = :sendmail - # config.action_mailer.sendmail_settings = { - # location: '/usr/sbin/sendmail', arguments: ['-i'] - # } - config.action_mailer.default_options = { - from: mailer_credentials[:from], - reply_to: mailer_credentials[:from] + from: "no-reply@unep-wcmc.org", + reply_to: "no-reply@unep-wcmc.org" } end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 85a35f2c9..ab9c3621e 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -134,31 +134,23 @@ config.ember.variant = :production # Custom email settings - mailer_credentials = Rails.application.credentials[:mailer] - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: mailer_credentials[:address], - port: mailer_credentials[:port], - domain: mailer_credentials[:domain], - user_name: mailer_credentials[:username], - password: mailer_credentials[:password], + address: "smtp.sendgrid.net", + port: 587, + domain: "unep-wcmc.org", + user_name: ENV['MAIL_USERNAME'], + password: ENV['MAIL_PASSWORD'], authentication: :login, enable_starttls_auto: true } config.action_mailer.default_url_options = { - host: mailer_credentials[:host] + host: "sapi.sapi-staging.linode.unep-wcmc.org" } - # fix for current version of mail gem: https://github.com/mikel/mail/issues/1538 - # config.action_mailer.delivery_method = :sendmail - # config.action_mailer.sendmail_settings = { - # location: '/usr/sbin/sendmail', arguments: ['-i'] - # } - config.action_mailer.default_options = { - from: mailer_credentials[:from], - reply_to: mailer_credentials[:from] + from: "no-reply@unep-wcmc.org", + reply_to: "no-reply@unep-wcmc.org" } end From a0fd60c975e01bbd2065e7a90c86171d5fb51458 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 15:46:47 +0100 Subject: [PATCH 010/106] Things that can't raise error, when running assets:precompile without RAILS_MASTER_KEY. --- config/initializers/devise.rb | 2 +- config/initializers/schema_dump_options.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 66d3f7fe7..afd5a0394 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -4,7 +4,7 @@ # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. - config.secret_key = Rails.application.credentials.secret_key_base! + config.secret_key = Rails.application.credentials.secret_key_base # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, diff --git a/config/initializers/schema_dump_options.rb b/config/initializers/schema_dump_options.rb index 37c23a120..4a5323ef6 100644 --- a/config/initializers/schema_dump_options.rb +++ b/config/initializers/schema_dump_options.rb @@ -1,2 +1,4 @@ -ActiveRecord::SchemaDumper.ignore_tables << - ActiveRecord::Base.connection.data_sources.grep(/^trade_sandbox_\d+/) +if ENV['SECRET_KEY_BASE_DUMMY'].blank? + ActiveRecord::SchemaDumper.ignore_tables << + ActiveRecord::Base.connection.data_sources.grep(/^trade_sandbox_\d+/) +end From c3d3be07acfc989bed42d1d4926fb47be019c590 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 15:57:41 +0100 Subject: [PATCH 011/106] Production Dockerfile and .dockerignore --- .dockerignore | 47 +++++++++++++++++++++++++ Dockerfile.deploy | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 Dockerfile.deploy diff --git a/.dockerignore b/.dockerignore index e3cd043e9..541931e5d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -45,3 +45,50 @@ # Ignore Docker-related files /.dockerignore /Dockerfile* + +# SAPI +/coverage +/rdoc +/private +/public/system +/public/downloads/*.pdf +#LaTeX +/public/latex/*.aux +/public/latex/*.out +/public/latex/*.log +/public/latex/*.gz +/public/latex/index.pdf +/public/latex/history.pdf +/public/sitemap* + +#checklist downloads +/public/downloads/checklist/*.* + +#exports csvs +/public/downloads/documents/*.csv +/public/downloads/quotas/*.csv +/public/downloads/cites_listings/*.csv +/public/downloads/cites_suspensions/*.csv +/public/downloads/eu_listings/*.csv +/public/downloads/eu_decisions/*.csv +/public/downloads/cms_listings/*.csv +/public/downloads/checklist/*.pdf +/public/downloads/checklist/*.csv +/public/downloads/checklist/*.json +/public/downloads/taxon_concepts_names/*.csv +/public/downloads/synonyms_and_trade_names/*.csv +/public/downloads/taxon_concepts_distributions/*.csv +/public/downloads/shipments/*.csv +/public/downloads/comptab/*.csv +/public/downloads/gross_exports/*.csv +/public/downloads/gross_imports/*.csv +/public/downloads/net_exports/*.csv +/public/downloads/net_imports/*.csv +/public/downloads/trade_download_stats/*.csv +/public/downloads/species_reference_output/*.csv +/public/downloads/standard_reference_output/*.csv +/public/downloads/common_names/*.csv +/public/downloads/iucn_mappings/*.csv +/public/downloads/cms_mappings/*.csv +/public/downloads/orphaned_taxon_concepts/*.csv +/public/uploads/* diff --git a/Dockerfile.deploy b/Dockerfile.deploy new file mode 100644 index 000000000..369f38c5a --- /dev/null +++ b/Dockerfile.deploy @@ -0,0 +1,90 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand. + +# 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.2.5 +FROM ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ + # ? + libsodium-dev libgmp3-dev libssl-dev \ + # latex (huge file size) + texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV NODE_ENV="production" \ + RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# 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 --no-install-recommends -y build-essential git libyaml-dev pkg-config \ + # install node js + curl xz-utils \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install Node.js 18.20.8 manually +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +# Map Docker TARGETARCH to Node.js archive name +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile 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 + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# 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 && \ + chown -R rails:rails db log tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server +EXPOSE 80 +# CMD ["./bin/rails", "server"] +CMD ["tail", "-f", "/dev/null"] From d353f2b61a0669a9d88f6bf2b17c5705c4e5d334 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 17:08:28 +0100 Subject: [PATCH 012/106] Align ENV name with DevOps --- config/database.yml | 4 ++-- config/storage.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/database.yml b/config/database.yml index 3b51f642b..26026bfbb 100644 --- a/config/database.yml +++ b/config/database.yml @@ -6,7 +6,7 @@ default: &default host: <%= ENV.fetch("SAPI_DATABASE_HOST", Rails.application.credentials.dig(:db, :host)) %> username: <%= ENV.fetch("SAPI_DATABASE_USERNAME", Rails.application.credentials.dig(:db, :username)) %> password: <%= ENV.fetch("SAPI_DATABASE_PASSWORD", Rails.application.credentials.dig(:db, :password)) %> - port: <%= ENV.fetch("SAPI_DATABASE_PORT", Rails.application.credentials.dig(:db, :port)) %> + port: <%= ENV.fetch("SAPI_DATABASE_PORT", Rails.application.credentials.dig(:db, :port) || '5432') %> database: <%= ENV.fetch("SAPI_DATABASE_NAME", "sapi_#{Rails.env}") %> variables: # It is important that ordinary queries do not hang while waiting for a @@ -24,7 +24,7 @@ default: &default username: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_USERNAME", Rails.application.credentials.dig(:captive_breeding_db, :username) || 'postgres') %> password: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_PASSWORD", Rails.application.credentials.dig(:captive_breeding_db, :password)) %> port: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_PORT", Rails.application.credentials.dig(:captive_breeding_db, :port) || '5432') %> - database: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE", Rails.application.credentials.dig(:captive_breeding_db, :database) || "captive_breeding_database_#{Rails.env}") %> + database: <%= ENV.fetch("CAPTIVE_BREEDING_DATABASE_NAME", Rails.application.credentials.dig(:captive_breeding_db, :database) || "captive_breeding_database_#{Rails.env}") %> database_tasks: false development: diff --git a/config/storage.yml b/config/storage.yml index 90da9d2bd..34b151869 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -14,9 +14,9 @@ local: # secret_access_key: xXXxxXXXx0Xx/XXxx000xxXX0XxX/xxx00X0xx0X amazon: &amazon service: S3 - access_key_id: <%= ENV.fetch("AWS_S3_ACCESS_KEY_ID") { Rails.application.credentials.dig(:storage, :aws, :access_key_id) } %> - secret_access_key: <%= ENV.fetch("AWS_S3_SECRET_ACCESS_KEY") { Rails.application.credentials.dig(:storage, :aws, :secret_access_key) } %> - region: <%= ENV.fetch("AWS_S3_REGION") { Rails.application.credentials.dig(:storage, :aws, :region) || 'eu-west-2' } %> + access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID") { Rails.application.credentials.dig(:storage, :aws, :access_key_id) } %> + secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY") { Rails.application.credentials.dig(:storage, :aws, :secret_access_key) } %> + region: <%= ENV.fetch("AWS_REGION") { Rails.application.credentials.dig(:storage, :aws, :region) || 'eu-west-2' } %> bucket: <%= ENV.fetch("AWS_S3_BUCKET") { Rails.application.credentials.dig(:storage, :aws, :bucket) || "species-plus-#{Rails.env}" } %> local_s3: From c950332a160cbc58e6ece81f51732b8252f6c49b Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:25:39 +0100 Subject: [PATCH 013/106] sidekiq admin username/password move to credentials. Was nginx .htpasswd. --- config/credentials/production.yml.enc | 2 +- config/credentials/staging.yml.enc | 2 +- config/environments/development.rb | 2 +- config/routes.rb | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index bd1ef26c3..ed1380f72 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--4qJJGNVpgQ3/1fEB--8mjgJXG+SInWCclUQkgTqA== \ No newline at end of file +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--3BxCon5UxLls1Gsi--aRhvTbWT82GnXRKdWvv4Cw== \ No newline at end of file diff --git a/config/credentials/staging.yml.enc b/config/credentials/staging.yml.enc index 76ab6ed9a..a1a494783 100644 --- a/config/credentials/staging.yml.enc +++ b/config/credentials/staging.yml.enc @@ -1 +1 @@ -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--m9FT/eiK2duMeoPd--TPeu8jM4DG29Rd1MfvgePw== \ No newline at end of file +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--Y3cbBcS9LVL6HEYv--ydc+IIWgrsJdp3h/Two1PA== \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index f994d2d70..2e18abc54 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,7 +24,7 @@ config.action_controller.enable_fragment_cache_logging = true # Use a redis instance as a cache store in local development. - config.cache_store = :redis_cache_store, { url: ENV.fetch('SAPI_SIDEKIQ_REDIS_CACHE_URL') } + config.cache_store = :redis_cache_store, { url: ENV.fetch('SAPI_SIDEKIQ_REDIS_CACHE_URL', '') } config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } diff --git a/config/routes.rb b/config/routes.rb index ce44a452b..481f14a42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,22 @@ require 'sidekiq/web' require 'sidekiq/cron/web' +if %w[test development].exclude?(Rails.env) + Sidekiq::Web.use Rack::Auth::Basic do |username, password| + # Protect against timing attacks: + # - See https://codahale.com/a-lesson-in-timing-attacks/ + # - See https://thisdata.com/blog/timing-attacks-against-string-comparison/ + # - Use & (do not use &&) so that it doesn't short circuit. + # - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare) + sidekiq_username = Rails.application.credentials.sidekiq.username! + sidekiq_password = Rails.application.credentials.sidekiq.password! + ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(username), + Digest::SHA256.hexdigest(sidekiq_username)) & + ActiveSupport::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(password), + Digest::SHA256.hexdigest(sidekiq_password)) + end +end + Rails.application.routes.draw do # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. From 92d816a635d48ed0b6b021ed1e6418669e00e540 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:48:22 +0100 Subject: [PATCH 014/106] =?UTF-8?q?Remove=20the=20bundle=20install=20comma?= =?UTF-8?q?nd=20from=20the=20entrypoint.=20In=20staging=20and=20production?= =?UTF-8?q?,=20it=E2=80=99s=20already=20run=20when=20the=20Docker=20image?= =?UTF-8?q?=20is=20built;=20Move=20it=20into=20the=20Docker=20Compose=20co?= =?UTF-8?q?nfiguration=20for=20development=20only.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/docker-entrypoint | 2 -- docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index cd970544f..5c404a181 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -4,8 +4,6 @@ if [[ "${@}" =~ "rails server" ]]; then rm -f ./tmp/pids/server.pid; fi -bundle install - mkdir -p {./,spec/}public/downloads/checklist mkdir -p {./,spec/}public/downloads/cites_listings mkdir -p {./,spec/}public/downloads/cites_suspensions diff --git a/docker-compose.yml b/docker-compose.yml index f54462985..ca05afb0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,7 +69,7 @@ services: rails: container_name: sapi-rails build: . - command: bundle exec rails server -p 3000 -b '0.0.0.0' + command: bundle install && bundle exec rails server -p 3000 -b '0.0.0.0' volumes: &rails_volumes # Used for both rails and sidekiq From acb3d4d40c8d30fd7e5ea82fb9c6a6ff642db8e5 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 20:49:42 +0100 Subject: [PATCH 015/106] kamal 2 config --- .kamal/hooks/docker-setup.sample | 3 + .kamal/hooks/post-app-boot.sample | 3 + .kamal/hooks/post-deploy.sample | 14 +++ .kamal/hooks/post-proxy-reboot.sample | 3 + .kamal/hooks/pre-app-boot.sample | 3 + .kamal/hooks/pre-build | 51 +++++++++++ .kamal/hooks/pre-connect.sample | 47 ++++++++++ .kamal/hooks/pre-deploy.sample | 122 ++++++++++++++++++++++++++ .kamal/hooks/pre-proxy-reboot.sample | 3 + .kamal/secrets-common | 39 ++++++++ Dockerfile | 2 + config/database.yml.sample | 29 ------ config/deploy.production.yml | 36 ++++++++ config/deploy.staging.yml | 35 ++++++++ config/deploy.yml | 48 ++++++++++ 15 files changed, 409 insertions(+), 29 deletions(-) create mode 100755 .kamal/hooks/docker-setup.sample create mode 100755 .kamal/hooks/post-app-boot.sample create mode 100755 .kamal/hooks/post-deploy.sample create mode 100755 .kamal/hooks/post-proxy-reboot.sample create mode 100755 .kamal/hooks/pre-app-boot.sample create mode 100755 .kamal/hooks/pre-build create mode 100755 .kamal/hooks/pre-connect.sample create mode 100755 .kamal/hooks/pre-deploy.sample create mode 100755 .kamal/hooks/pre-proxy-reboot.sample create mode 100644 .kamal/secrets-common delete mode 100644 config/database.yml.sample create mode 100644 config/deploy.production.yml create mode 100644 config/deploy.staging.yml create mode 100644 config/deploy.yml diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 000000000..2fb07d7d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 000000000..70f9c4bc9 --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 000000000..fd364c2a7 --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 000000000..1435a677f --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 000000000..45f735504 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build b/.kamal/hooks/pre-build new file mode 100755 index 000000000..c5a55678b --- /dev/null +++ b/.kamal/hooks/pre-build @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 000000000..77744bdca --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 000000000..05b3055b7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,122 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLES (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = github_repo_from_remote_url + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end + + private + def github_repo_from_remote_url + url = `git config --get remote.origin.url`.strip.delete_suffix(".git") + if url.start_with?("https://github.com/") + url.delete_prefix("https://github.com/") + elsif url.start_with?("git@github.com:") + url.delete_prefix("git@github.com:") + else + url + end + end +end + + +$stdout.sync = true + +begin + puts "Checking build status..." + + attempts = 0 + checks = GithubStatusChecks.new + + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 000000000..061f8059e --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets-common b/.kamal/secrets-common new file mode 100644 index 000000000..d32adb401 --- /dev/null +++ b/.kamal/secrets-common @@ -0,0 +1,39 @@ +# Minimal Secrets Template - Backend Rails API Kamal Deployment +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Registry Configuration (ALWAYS REQUIRED) +KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Rails Configuration (REQUIRED) +RAILS_MASTER_KEY=$RAILS_MASTER_KEY + +# Database Configuration (special, 2 database) +SAPI_DATABASE_HOST=$SAPI_DATABASE_HOST +SAPI_DATABASE_NAME=$SAPI_DATABASE_NAME +SAPI_DATABASE_USERNAME=$SAPI_DATABASE_USERNAME +SAPI_DATABASE_PASSWORD=$SAPI_DATABASE_PASSWORD +SAPI_DATABASE_PORT=$SAPI_DATABASE_PORT + +CAPTIVE_BREEDING_DATABASE_HOST=$CAPTIVE_BREEDING_DATABASE_HOST +CAPTIVE_BREEDING_DATABASE_NAME=$CAPTIVE_BREEDING_DATABASE_NAME +CAPTIVE_BREEDING_DATABASE_USERNAME=$CAPTIVE_BREEDING_DATABASE_USERNAME +CAPTIVE_BREEDING_DATABASE_PASSWORD=$CAPTIVE_BREEDING_DATABASE_PASSWORD +CAPTIVE_BREEDING_DATABASE_PORT=$CAPTIVE_BREEDING_DATABASE_PORT + +# Mail Configuration +MAIL_USERNAME=$MAIL_USERNAME +MAIL_PASSWORD=$MAIL_PASSWORD + +# AWS Configuration +AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY +AWS_REGION=$AWS_REGION + +# Redis for Sidekiq +SAPI_SIDEKIQ_REDIS_URL=$SAPI_SIDEKIQ_REDIS_URL + +# Redis for cache +SAPI_SIDEKIQ_REDIS_CACHE_URL=$SAPI_SIDEKIQ_REDIS_CACHE_URL diff --git a/Dockerfile b/Dockerfile index 08cedd4f1..61ba877a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ FROM ruby:3.2.5-slim RUN apt-get update && apt-get install --no-install-recommends -y --force-yes \ # ? libsodium-dev libgmp3-dev libssl-dev \ + # Editor + vim nano \ # PSQL libpq-dev postgresql-client \ # node js diff --git a/config/database.yml.sample b/config/database.yml.sample deleted file mode 100644 index 64521c841..000000000 --- a/config/database.yml.sample +++ /dev/null @@ -1,29 +0,0 @@ - -default: &default - host: <%= ENV.fetch("SAPI_DATABASE_HOST", 'localhost') %> - adapter: postgresql - encoding: unicode - # For details on connection pooling, see Rails configuration guide - # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("SAPI_RAILS_MAX_THREADS") { 5 } %> - username: <%= ENV.fetch("SAPI_DATABASE_USERNAME", 'postgres') %> - port: <%= ENV.fetch("SAPI_DATABASE_PORT", 5432) %> - -development: - <<: *default - database: sapi_development - timeout: 5000 - -test: - <<: *default - database: sapi_test - timeout: 5000 - -staging: - <<: *default - database: sapi_development - port: 5432 - -production: - <<: *default - database: sapi_development diff --git a/config/deploy.production.yml b/config/deploy.production.yml new file mode 100644 index 000000000..58bd56641 --- /dev/null +++ b/config/deploy.production.yml @@ -0,0 +1,36 @@ +# Name of the container image +image: ghcr.io/unepwcmc/sapi/rails-production + +# Define services (servers for Kamal 2.5.2 compatibility) +servers: + web: + hosts: + - example.com # TODO: Ruan + proxy: + hosts: + - www.speciesplus.net # Public-facing domain + # TODO: Ruan, do we need to add `speciesplus.net` (without www)? + ssl: true # Proxy terminates SSL + forward_headers: true # Forward headers like X-Forwarded-Proto + healthcheck: + path: /up + logging: + request_headers: + - Cache-Control + - User-Agent + - X-Forwarded-Proto # Critical for Rails to detect HTTPS + response_headers: + - X-Request-ID + job: + hosts: + - example.com # TODO: Ruan + options: + add-host: host.docker.internal:host-gateway + cmd: bundle exec sidekiq -C config/sidekiq.yml + healthcheck: + cmd: /rails/bin/docker-sidekiq-healthcheck + +# Environment variables +env: + clear: + RAILS_ENV: production diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml new file mode 100644 index 000000000..3b7cf46ef --- /dev/null +++ b/config/deploy.staging.yml @@ -0,0 +1,35 @@ +# Name of the container image +image: ghcr.io/unepwcmc/sapi/rails-staging + +# Define services (servers for Kamal 2.5.2 compatibility) +servers: + web: + hosts: + - example.com # TODO: Ruan + proxy: + hosts: + - sapi.sapi-staging.linode.unep-wcmc.org # Public-facing domain + ssl: true # Proxy terminates SSL + forward_headers: true # Forward headers like X-Forwarded-Proto + healthcheck: + path: /up + logging: + request_headers: + - Cache-Control + - User-Agent + - X-Forwarded-Proto # Critical for Rails to detect HTTPS + response_headers: + - X-Request-ID + job: + hosts: + - example.com # TODO: Ruan + options: + add-host: host.docker.internal:host-gateway + cmd: bundle exec sidekiq -C config/sidekiq.yml + healthcheck: + cmd: /rails/bin/docker-sidekiq-healthcheck + +# Environment variables +env: + clear: + RAILS_ENV: staging diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 000000000..f67661cba --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,48 @@ +# Name of your application. Used to uniquely configure containers. +service: sapi + +# Credentials for your image host. +registry: + server: ghcr.io + username: + - KAMAL_REGISTRY_USERNAME + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) +env: + clear: + RAILS_LOG_LEVEL: warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs + PORT: 80 + RAILS_SERVE_STATIC_FILES: 1 + RAILS_LOG_TO_STDOUT: 1 # Need this before upgrade to Rails 7.1, which then default is STDOUT. + secret: + - RAILS_MASTER_KEY + - SAPI_DATABASE_HOST + - SAPI_DATABASE_NAME + - SAPI_DATABASE_USERNAME + - SAPI_DATABASE_PASSWORD + - SAPI_DATABASE_PORT + - CAPTIVE_BREEDING_DATABASE_HOST + - CAPTIVE_BREEDING_DATABASE_NAME + - CAPTIVE_BREEDING_DATABASE_USERNAME + - CAPTIVE_BREEDING_DATABASE_PASSWORD + - CAPTIVE_BREEDING_DATABASE_PORT + - MAIL_USERNAME + - MAIL_PASSWORD + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_REGION + - SAPI_SIDEKIQ_REDIS_URL + - SAPI_SIDEKIQ_REDIS_CACHE_URL + +# Use a different ssh user than root +ssh: + user: wcmc + +# Configure builder setup +builder: + arch: amd64 + dockerfile: Dockerfile.deploy + args: + RUBY_VERSION: 3.2.5 From 19ba0c95a906cd770190639388fb20940c1c7ac0 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:06:11 +0100 Subject: [PATCH 016/106] add sidekiq healthcheck script --- bin/docker-sidekiq-healthcheck | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 bin/docker-sidekiq-healthcheck diff --git a/bin/docker-sidekiq-healthcheck b/bin/docker-sidekiq-healthcheck new file mode 100755 index 000000000..975e7c159 --- /dev/null +++ b/bin/docker-sidekiq-healthcheck @@ -0,0 +1,11 @@ +#!/bin/bash + +# Cannot use `sidekiqmon`, because it rely on REDIS_URL env. +# We do not store redis_url in REDIS_URL: +# - it has password, is serect, should be in rails credentials; +# - there maybe more than one redis (e.g. One for sidekiq [maxmemory-policy=noeviction]; one for cache [maxmemory-policy=lfu]) +if [ $(ps aux | grep 'sidekiq ' | grep -v grep | wc -l) -gt 0 ]; then + exit 0 +else + exit 1 +fi From ed76285662bc478328c0c4fed1a34e07b6833be7 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:07:27 +0100 Subject: [PATCH 017/106] nodejs only use for assets:precompile, but somehow it broken if not available in runtime, maybe due to terser? --- Dockerfile.deploy | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Dockerfile.deploy b/Dockerfile.deploy index 369f38c5a..c678f6183 100644 --- a/Dockerfile.deploy +++ b/Dockerfile.deploy @@ -17,6 +17,8 @@ RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ # ? libsodium-dev libgmp3-dev libssl-dev \ + # install node js + curl xz-utils \ # latex (huge file size) texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ && rm -rf /var/lib/apt/lists /var/cache/apt/archives @@ -28,16 +30,6 @@ ENV NODE_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" -# 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 --no-install-recommends -y build-essential git libyaml-dev pkg-config \ - # install node js - curl xz-utils \ - && rm -rf /var/lib/apt/lists /var/cache/apt/archives - # Install Node.js 18.20.8 manually ARG NODE_VERSION=18.20.8 ARG TARGETARCH @@ -50,6 +42,15 @@ RUN case "$TARGETARCH" in \ curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ | tar -xJ -C /usr/local --strip-components=1 + +# 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 --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 ./ RUN bundle install && \ From 60e88d392520b580f5d12fcd439cea370bb5caee Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:08:41 +0100 Subject: [PATCH 018/106] No longer need to remove the pid file in production mode, only need for development. Move to docker compose --- bin/docker-entrypoint | 6 +++-- config/puma.rb | 60 ++++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 5c404a181..3054757cc 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,7 +1,9 @@ #!/bin/bash -e -if [[ "${@}" =~ "rails server" ]]; then - rm -f ./tmp/pids/server.pid; +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD fi mkdir -p {./,spec/}public/downloads/checklist diff --git a/config/puma.rb b/config/puma.rb index 58e1c205b..a248513b2 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,35 +1,41 @@ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } -min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies that the worker count should equal the number of processors in production. -if ENV['RAILS_ENV'] == 'production' - require 'concurrent-ruby' - worker_count = Integer(ENV.fetch('WEB_CONCURRENCY') { Concurrent.physical_processor_count }) - workers worker_count if worker_count > 1 -end - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch('PORT') { 3000 } +port ENV.fetch("PORT", 3000) -# Specifies the `environment` that Puma will run in. -environment ENV.fetch('RAILS_ENV') { 'development' } +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] -# Allow puma to be restarted by `rails restart` command. -plugin :tmp_restart +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] From 36d46f865c7b246d72cb99e4aac14ca7e9041616 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 25 Jul 2025 22:09:37 +0100 Subject: [PATCH 019/106] fix production config, to serve assets from puma --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 3c38ce0d0..df9557e54 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -21,7 +21,7 @@ # config.require_master_key = true # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - config.public_file_server.enabled = false + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :terser From 95ad590502545683ffcb8d4d553322ff643a234c Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 1 Aug 2025 17:02:04 +0100 Subject: [PATCH 020/106] add remove puma pid file in docker compose for development only --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ca05afb0d..26167db01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,7 +69,7 @@ services: rails: container_name: sapi-rails build: . - command: bundle install && bundle exec rails server -p 3000 -b '0.0.0.0' + command: /bin/bash -l -c "bundle install && rm -rf /SAPI/tmp/pids/server.pid && bundle exec rails server -p 3000 -b '0.0.0.0'" volumes: &rails_volumes # Used for both rails and sidekiq From c6d05d28c6c52bb8c3d8035b7b48c817e371c44b Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Fri, 1 Aug 2025 17:08:02 +0100 Subject: [PATCH 021/106] add redis cache port in compose --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 26167db01..9dcc5f458 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,6 +164,8 @@ services: redis_cache: container_name: sapi-redis-cache image: redis:7.2.0 + ports: + - "${SAPI_CONTAINER_REDIS_PORT:-6380}:6380" networks: - sapi command: redis-server /usr/local/etc/redis/redis.conf From db16764c138efa604a64b2a718a7517e7a2f61d5 Mon Sep 17 00:00:00 2001 From: Leonardo Wong Date: Tue, 17 Mar 2026 17:11:54 +0000 Subject: [PATCH 022/106] Update Kamal deploy configuration --- .kamal/secrets-common | 4 ---- config/deploy.production.yml | 1 - config/deploy.staging.yml | 1 - config/deploy.yml | 6 +----- deploy/Gemfile | 4 ++-- deploy/Gemfile.lock | 41 +++++++++++++++++++----------------- 6 files changed, 25 insertions(+), 32 deletions(-) diff --git a/.kamal/secrets-common b/.kamal/secrets-common index d32adb401..4189767a8 100644 --- a/.kamal/secrets-common +++ b/.kamal/secrets-common @@ -3,10 +3,6 @@ # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. -# Registry Configuration (ALWAYS REQUIRED) -KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME -KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD - # Rails Configuration (REQUIRED) RAILS_MASTER_KEY=$RAILS_MASTER_KEY diff --git a/config/deploy.production.yml b/config/deploy.production.yml index 58bd56641..a687b2def 100644 --- a/config/deploy.production.yml +++ b/config/deploy.production.yml @@ -1,7 +1,6 @@ # Name of the container image image: ghcr.io/unepwcmc/sapi/rails-production -# Define services (servers for Kamal 2.5.2 compatibility) servers: web: hosts: diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 3b7cf46ef..542072ad6 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -1,7 +1,6 @@ # Name of the container image image: ghcr.io/unepwcmc/sapi/rails-staging -# Define services (servers for Kamal 2.5.2 compatibility) servers: web: hosts: diff --git a/config/deploy.yml b/config/deploy.yml index f67661cba..77deb7521 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -3,11 +3,7 @@ service: sapi # Credentials for your image host. registry: - server: ghcr.io - username: - - KAMAL_REGISTRY_USERNAME - password: - - KAMAL_REGISTRY_PASSWORD + server: localhost:5555 # Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) env: diff --git a/deploy/Gemfile b/deploy/Gemfile index aa43c464f..091be5259 100644 --- a/deploy/Gemfile +++ b/deploy/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -ruby '3.4.4' +ruby '3.4.9' -gem "kamal", "~> 2.7" +gem "kamal", "~> 2.10" diff --git a/deploy/Gemfile.lock b/deploy/Gemfile.lock index 5baa55c1c..e53430700 100644 --- a/deploy/Gemfile.lock +++ b/deploy/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - activesupport (8.0.2) + activesupport (8.1.2) base64 - benchmark (>= 0.3) 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) base64 (0.3.0) - bcrypt_pbkdf (1.1.1) - benchmark (0.4.1) - bigdecimal (3.2.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) - dotenv (3.1.8) + bcrypt_pbkdf (1.1.2) + bigdecimal (4.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + dotenv (3.2.0) drb (2.2.3) ed25519 (1.4.0) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) - kamal (2.7.0) + json (2.19.1) + kamal (2.10.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -37,36 +37,39 @@ GEM thor (~> 1.3) zeitwerk (>= 2.6.18, < 3.0) logger (1.7.0) - minitest (5.25.5) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) 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-ssh (7.3.0) + net-ssh (7.3.1) ostruct (0.6.3) + prism (1.9.0) securerandom (0.4.1) - sshkit (1.24.0) + sshkit (1.25.0) base64 logger net-scp (>= 1.1.2) net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ostruct - thor (1.3.2) + thor (1.5.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uri (1.0.3) - zeitwerk (2.7.3) + uri (1.1.1) + zeitwerk (2.7.5) PLATFORMS arm64-darwin-24 ruby DEPENDENCIES - kamal (~> 2.7) + kamal (~> 2.10) RUBY VERSION - ruby 3.4.4p34 + ruby 3.4.9p82 BUNDLED WITH - 2.6.7 + 2.6.9 From 7092abd875fbcd2b269878d6a0a017bfa027e05b Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 11:26:23 +0000 Subject: [PATCH 023/106] Configure Kamal v2 staging deployment for proxmox - Update config/deploy.staging.yml: set web/job hosts to 172.20.0.146, add host.docker.internal gateway for Redis, wire wildcard SSL certs, update proxy hostname to sapi-web-staging-01.internal.unep-wcmc.org - Update config/deploy/staging.rb: point Capistrano at new internal hostname - Add .github/workflows/deploy.yml: Kamal v2 deploy workflow for staging/production - Add .github/workflows/kamal-setup.yml: one-time Kamal setup workflow --- .github/workflows/deploy.yml | 196 ++++++++++++++++++++++++++++++ .github/workflows/kamal-setup.yml | 174 ++++++++++++++++++++++++++ config/deploy.staging.yml | 14 ++- config/deploy/staging.rb | 4 +- 4 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/kamal-setup.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..1de82f8a5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,196 @@ +name: Deploy SAPI Rails API +on: + push: + branches: + - deploy/staging/rails-api + - deploy/production/rails-api + release: + types: [published] + +jobs: + determine-environment: + runs-on: self-hosted + outputs: + environment: ${{ steps.env.outputs.environment }} + deploy-type: ${{ steps.env.outputs.deploy-type }} + kamal-secrets-file: ${{ steps.env.outputs.kamal-secrets-file }} + steps: + - id: env + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy-type=release" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🚀 Production deployment triggered by release: ${{ github.event.release.tag_name }}" + elif [[ "${{ github.ref }}" == "refs/heads/deploy/production/rails-api" ]]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy-type=branch" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🚀 Production deployment triggered by push to deploy/production/rails-api" + elif [[ "${{ github.ref }}" == "refs/heads/deploy/staging/rails-api" ]]; then + echo "environment=staging" >> $GITHUB_OUTPUT + echo "deploy-type=branch" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🔧 Staging deployment triggered by push to deploy/staging/rails-api" + else + echo "❌ No deployment configured for this trigger" + exit 1 + fi + + deploy: + needs: determine-environment + runs-on: self-hosted + environment: ${{ needs.determine-environment.outputs.environment }} + steps: + - name: Set workflow start time + run: | + echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + echo "ACTION_TYPE=Deploy" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Notify deployment start + id: notify-start + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: started + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + + - name: Export and validate kamal secrets + shell: bash + env: + SECRETS_JSON: ${{ toJSON(secrets) }} + KAMAL_SECRETS_FILE: ${{ needs.determine-environment.outputs.kamal-secrets-file }} + run: | + if [[ -z "$KAMAL_SECRETS_FILE" ]]; then + echo "Error: kamal secrets file path not provided." + exit 1 + fi + if [[ ! -f "$KAMAL_SECRETS_FILE" ]]; then + echo "Error: $KAMAL_SECRETS_FILE file not found." + exit 1 + fi + + temp_secrets="$(mktemp)" + trap 'rm -f "$temp_secrets"' EXIT + printf '%s' "$SECRETS_JSON" > "$temp_secrets" + + needed_vars=() + while IFS= read -r line; do + trimmed="$(echo "$line" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [[ -z "$trimmed" || "$trimmed" == \#* ]] && continue + [[ "$trimmed" != *=* ]] && continue + + value="${trimmed#*=}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + + [[ "$value" != \$* ]] && continue + [[ "$value" == '$('* ]] && continue + + if [[ "$value" == \$\{* ]]; then + if [[ "$value" =~ ^\$\{([A-Z0-9_]+) ]]; then + needed_vars+=("${BASH_REMATCH[1]}") + fi + elif [[ "$value" =~ ^\$([A-Z0-9_]+) ]]; then + needed_vars+=("${BASH_REMATCH[1]}") + fi + done < "$KAMAL_SECRETS_FILE" + + IFS=$'\n' read -r -d '' -a unique_vars < <(printf '%s\n' "${needed_vars[@]}" | sort -u && printf '\0') + + for var in "${unique_vars[@]}"; do + value=$(jq -r --arg key "$var" '.[$key] // empty' "$temp_secrets") + if [[ -z "$value" ]]; then + echo "Missing environment variable required in .kamal/secrets-common: ${var}" >&2 + exit 1 + fi + + { + echo "${var}<> "$GITHUB_ENV" + done + + echo "✅ All required kamal secrets are validated and available as environment variables" + + - name: Deploy with Kamal v2 + uses: unepwcmc/devops-actions/.github/actions/kamal-v2.x-deploy@v1 + with: + environment: ${{ needs.determine-environment.outputs.environment }} + working-directory: . + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Set workflow end time and calculate duration + if: always() + run: | + echo "END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + if [[ -n "$START_TIME" ]]; then + start_timestamp=$(date -d "$START_TIME" +%s) + end_timestamp=$(date -d "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +%s) + duration=$((end_timestamp - start_timestamp)) + echo "DEPLOYMENT_DURATION=${duration}" >> $GITHUB_ENV + else + echo "DEPLOYMENT_DURATION=0" >> $GITHUB_ENV + fi + + - name: Notify deployment success + if: success() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: success + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} + + - name: Notify deployment failure + if: failure() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: failure + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml new file mode 100644 index 000000000..2eab50ae6 --- /dev/null +++ b/.github/workflows/kamal-setup.yml @@ -0,0 +1,174 @@ +name: Kamal v2 SAPI Setup (One-time per Environment) +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to setup' + required: true + type: choice + options: + - staging + - production + confirm: + description: 'Type "CONFIRM" to proceed with setup' + required: true + type: string + +jobs: + validate-input: + runs-on: self-hosted + steps: + - name: Validate confirmation + run: | + if [[ "${{ github.event.inputs.confirm }}" != "CONFIRM" ]]; then + echo "❌ Setup cancelled. You must type 'CONFIRM' to proceed." + echo "⚠️ This is a one-time setup operation that will:" + echo " • Initialize Kamal v2 configuration for ${{ github.event.inputs.environment }}" + echo " • Set up Docker containers and services" + echo " • Configure the deployment infrastructure" + exit 1 + fi + echo "✅ Setup confirmed for ${{ github.event.inputs.environment }} environment" + + setup: + needs: validate-input + runs-on: self-hosted + environment: ${{ github.event.inputs.environment }} + steps: + - name: Set workflow start time + run: | + echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + echo "ACTION_TYPE=Setup" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Notify setup start + id: notify-start + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: started + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + + - name: Validate and Populate Secrets + uses: unepwcmc/devops-actions/.github/actions/validate-secrets@v1 + with: + secrets-file: '.kamal/secrets-common' + environment: ${{ github.event.inputs.environment }} + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + + - name: Setup SAPI with Kamal v2 + uses: unepwcmc/devops-actions/.github/actions/kamal-v2-setup@v1 + with: + environment: ${{ github.event.inputs.environment }} + working-directory: '.' + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + + - name: Set workflow end time and calculate duration + if: always() + run: | + echo "END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + if [[ -n "$START_TIME" ]]; then + start_timestamp=$(date -d "$START_TIME" +%s) + end_timestamp=$(date -d "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +%s) + duration=$((end_timestamp - start_timestamp)) + echo "DEPLOYMENT_DURATION=${duration}" >> $GITHUB_ENV + else + echo "DEPLOYMENT_DURATION=0" >> $GITHUB_ENV + fi + + - name: Notify setup success + if: success() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: success + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} + + - name: Notify setup failure + if: failure() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: failure + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 542072ad6..5ef9ea345 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -4,11 +4,15 @@ image: ghcr.io/unepwcmc/sapi/rails-staging servers: web: hosts: - - example.com # TODO: Ruan + - 172.20.0.146 # sapi-web-staging-01.internal.unep-wcmc.org + options: + add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis proxy: hosts: - - sapi.sapi-staging.linode.unep-wcmc.org # Public-facing domain - ssl: true # Proxy terminates SSL + - sapi-web-staging-01.internal.unep-wcmc.org # Public-facing domain + ssl: true + tls_certificate_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.crt + tls_private_key_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.key forward_headers: true # Forward headers like X-Forwarded-Proto healthcheck: path: /up @@ -21,9 +25,9 @@ servers: - X-Request-ID job: hosts: - - example.com # TODO: Ruan + - 172.20.0.146 # sapi-web-staging-01.internal.unep-wcmc.org options: - add-host: host.docker.internal:host-gateway + add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis cmd: bundle exec sidekiq -C config/sidekiq.yml healthcheck: cmd: /rails/bin/docker-sidekiq-healthcheck diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb index cbec89947..5e5dc6593 100644 --- a/config/deploy/staging.rb +++ b/config/deploy/staging.rb @@ -1,9 +1,9 @@ set :stage, :staging set :branch, ENV['CAP_BRANCH'] || 'develop' -server 'sapi-staging.linode.unep-wcmc.org', user: 'wcmc', roles: %w[app web db] +server 'sapi-web-staging-01.internal.unep-wcmc.org', user: 'wcmc', roles: %w[app web db] -set :domain, 'sapi-staging.linode.unep-wcmc.org' +set :domain, 'sapi-web-staging-01.internal.unep-wcmc.org' set :application, 'sapi' From 088a7622d63a6c33ebc8a4a0ffa00e42b77fd081 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 11:27:00 +0000 Subject: [PATCH 024/106] Add Kamal v2 GitHub Actions workflows --- .github/workflows/deploy.yml | 196 ++++++++++++++++++++++++++++++ .github/workflows/kamal-setup.yml | 174 ++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/kamal-setup.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..1de82f8a5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,196 @@ +name: Deploy SAPI Rails API +on: + push: + branches: + - deploy/staging/rails-api + - deploy/production/rails-api + release: + types: [published] + +jobs: + determine-environment: + runs-on: self-hosted + outputs: + environment: ${{ steps.env.outputs.environment }} + deploy-type: ${{ steps.env.outputs.deploy-type }} + kamal-secrets-file: ${{ steps.env.outputs.kamal-secrets-file }} + steps: + - id: env + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy-type=release" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🚀 Production deployment triggered by release: ${{ github.event.release.tag_name }}" + elif [[ "${{ github.ref }}" == "refs/heads/deploy/production/rails-api" ]]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "deploy-type=branch" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🚀 Production deployment triggered by push to deploy/production/rails-api" + elif [[ "${{ github.ref }}" == "refs/heads/deploy/staging/rails-api" ]]; then + echo "environment=staging" >> $GITHUB_OUTPUT + echo "deploy-type=branch" >> $GITHUB_OUTPUT + echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT + echo "🔧 Staging deployment triggered by push to deploy/staging/rails-api" + else + echo "❌ No deployment configured for this trigger" + exit 1 + fi + + deploy: + needs: determine-environment + runs-on: self-hosted + environment: ${{ needs.determine-environment.outputs.environment }} + steps: + - name: Set workflow start time + run: | + echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + echo "ACTION_TYPE=Deploy" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Notify deployment start + id: notify-start + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: started + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + + - name: Export and validate kamal secrets + shell: bash + env: + SECRETS_JSON: ${{ toJSON(secrets) }} + KAMAL_SECRETS_FILE: ${{ needs.determine-environment.outputs.kamal-secrets-file }} + run: | + if [[ -z "$KAMAL_SECRETS_FILE" ]]; then + echo "Error: kamal secrets file path not provided." + exit 1 + fi + if [[ ! -f "$KAMAL_SECRETS_FILE" ]]; then + echo "Error: $KAMAL_SECRETS_FILE file not found." + exit 1 + fi + + temp_secrets="$(mktemp)" + trap 'rm -f "$temp_secrets"' EXIT + printf '%s' "$SECRETS_JSON" > "$temp_secrets" + + needed_vars=() + while IFS= read -r line; do + trimmed="$(echo "$line" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [[ -z "$trimmed" || "$trimmed" == \#* ]] && continue + [[ "$trimmed" != *=* ]] && continue + + value="${trimmed#*=}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + + [[ "$value" != \$* ]] && continue + [[ "$value" == '$('* ]] && continue + + if [[ "$value" == \$\{* ]]; then + if [[ "$value" =~ ^\$\{([A-Z0-9_]+) ]]; then + needed_vars+=("${BASH_REMATCH[1]}") + fi + elif [[ "$value" =~ ^\$([A-Z0-9_]+) ]]; then + needed_vars+=("${BASH_REMATCH[1]}") + fi + done < "$KAMAL_SECRETS_FILE" + + IFS=$'\n' read -r -d '' -a unique_vars < <(printf '%s\n' "${needed_vars[@]}" | sort -u && printf '\0') + + for var in "${unique_vars[@]}"; do + value=$(jq -r --arg key "$var" '.[$key] // empty' "$temp_secrets") + if [[ -z "$value" ]]; then + echo "Missing environment variable required in .kamal/secrets-common: ${var}" >&2 + exit 1 + fi + + { + echo "${var}<> "$GITHUB_ENV" + done + + echo "✅ All required kamal secrets are validated and available as environment variables" + + - name: Deploy with Kamal v2 + uses: unepwcmc/devops-actions/.github/actions/kamal-v2.x-deploy@v1 + with: + environment: ${{ needs.determine-environment.outputs.environment }} + working-directory: . + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Set workflow end time and calculate duration + if: always() + run: | + echo "END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + if [[ -n "$START_TIME" ]]; then + start_timestamp=$(date -d "$START_TIME" +%s) + end_timestamp=$(date -d "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +%s) + duration=$((end_timestamp - start_timestamp)) + echo "DEPLOYMENT_DURATION=${duration}" >> $GITHUB_ENV + else + echo "DEPLOYMENT_DURATION=0" >> $GITHUB_ENV + fi + + - name: Notify deployment success + if: success() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: success + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} + + - name: Notify deployment failure + if: failure() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: failure + action-type: Deploy + environment: ${{ needs.determine-environment.outputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml new file mode 100644 index 000000000..2eab50ae6 --- /dev/null +++ b/.github/workflows/kamal-setup.yml @@ -0,0 +1,174 @@ +name: Kamal v2 SAPI Setup (One-time per Environment) +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to setup' + required: true + type: choice + options: + - staging + - production + confirm: + description: 'Type "CONFIRM" to proceed with setup' + required: true + type: string + +jobs: + validate-input: + runs-on: self-hosted + steps: + - name: Validate confirmation + run: | + if [[ "${{ github.event.inputs.confirm }}" != "CONFIRM" ]]; then + echo "❌ Setup cancelled. You must type 'CONFIRM' to proceed." + echo "⚠️ This is a one-time setup operation that will:" + echo " • Initialize Kamal v2 configuration for ${{ github.event.inputs.environment }}" + echo " • Set up Docker containers and services" + echo " • Configure the deployment infrastructure" + exit 1 + fi + echo "✅ Setup confirmed for ${{ github.event.inputs.environment }} environment" + + setup: + needs: validate-input + runs-on: self-hosted + environment: ${{ github.event.inputs.environment }} + steps: + - name: Set workflow start time + run: | + echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + echo "ACTION_TYPE=Setup" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + + - name: Notify setup start + id: notify-start + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: started + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + + - name: Validate and Populate Secrets + uses: unepwcmc/devops-actions/.github/actions/validate-secrets@v1 + with: + secrets-file: '.kamal/secrets-common' + environment: ${{ github.event.inputs.environment }} + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + + - name: Setup SAPI with Kamal v2 + uses: unepwcmc/devops-actions/.github/actions/kamal-v2-setup@v1 + with: + environment: ${{ github.event.inputs.environment }} + working-directory: '.' + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + + - name: Set workflow end time and calculate duration + if: always() + run: | + echo "END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV + if [[ -n "$START_TIME" ]]; then + start_timestamp=$(date -d "$START_TIME" +%s) + end_timestamp=$(date -d "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +%s) + duration=$((end_timestamp - start_timestamp)) + echo "DEPLOYMENT_DURATION=${duration}" >> $GITHUB_ENV + else + echo "DEPLOYMENT_DURATION=0" >> $GITHUB_ENV + fi + + - name: Notify setup success + if: success() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: success + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} + + - name: Notify setup failure + if: failure() + uses: unepwcmc/devops-actions/.github/actions/slack-notify@v1 + with: + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + notification-type: failure + action-type: Setup + environment: ${{ github.event.inputs.environment }} + repository: ${{ github.repository }} + repository-url: ${{ github.server_url }}/${{ github.repository }} + action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + actor: ${{ github.actor }} + actor-url: ${{ github.server_url }}/${{ github.actor }} + workflow-name: ${{ github.workflow }} + run-id: ${{ github.run_id }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} + runner-name: ${{ runner.name }} + start-time: ${{ env.START_TIME }} + end-time: ${{ env.END_TIME }} + deployment-duration: ${{ env.DEPLOYMENT_DURATION }} + update-message-ts: ${{ steps.notify-start.outputs.message-ts }} From 7662ecd5786c166feb82fcab9f2bd829f1c5655c Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 11:35:47 +0000 Subject: [PATCH 025/106] Fix workflows: staging-only, trigger on staging branch push --- .github/workflows/deploy.yml | 65 +++++++------------------------ .github/workflows/kamal-setup.yml | 1 - 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1de82f8a5..73bad07a6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,50 +2,16 @@ name: Deploy SAPI Rails API on: push: branches: - - deploy/staging/rails-api - - deploy/production/rails-api - release: - types: [published] + - staging jobs: - determine-environment: - runs-on: self-hosted - outputs: - environment: ${{ steps.env.outputs.environment }} - deploy-type: ${{ steps.env.outputs.deploy-type }} - kamal-secrets-file: ${{ steps.env.outputs.kamal-secrets-file }} - steps: - - id: env - run: | - if [[ "${{ github.event_name }}" == "release" ]]; then - echo "environment=production" >> $GITHUB_OUTPUT - echo "deploy-type=release" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🚀 Production deployment triggered by release: ${{ github.event.release.tag_name }}" - elif [[ "${{ github.ref }}" == "refs/heads/deploy/production/rails-api" ]]; then - echo "environment=production" >> $GITHUB_OUTPUT - echo "deploy-type=branch" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🚀 Production deployment triggered by push to deploy/production/rails-api" - elif [[ "${{ github.ref }}" == "refs/heads/deploy/staging/rails-api" ]]; then - echo "environment=staging" >> $GITHUB_OUTPUT - echo "deploy-type=branch" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🔧 Staging deployment triggered by push to deploy/staging/rails-api" - else - echo "❌ No deployment configured for this trigger" - exit 1 - fi - deploy: - needs: determine-environment runs-on: self-hosted - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging steps: - name: Set workflow start time run: | echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV - echo "ACTION_TYPE=Deploy" >> $GITHUB_ENV - uses: actions/checkout@v4 @@ -57,7 +23,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: started action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -65,7 +31,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} @@ -73,14 +39,9 @@ jobs: shell: bash env: SECRETS_JSON: ${{ toJSON(secrets) }} - KAMAL_SECRETS_FILE: ${{ needs.determine-environment.outputs.kamal-secrets-file }} run: | - if [[ -z "$KAMAL_SECRETS_FILE" ]]; then - echo "Error: kamal secrets file path not provided." - exit 1 - fi - if [[ ! -f "$KAMAL_SECRETS_FILE" ]]; then - echo "Error: $KAMAL_SECRETS_FILE file not found." + if [[ ! -f ".kamal/secrets-common" ]]; then + echo "Error: .kamal/secrets-common file not found." exit 1 fi @@ -108,7 +69,7 @@ jobs: elif [[ "$value" =~ ^\$([A-Z0-9_]+) ]]; then needed_vars+=("${BASH_REMATCH[1]}") fi - done < "$KAMAL_SECRETS_FILE" + done < ".kamal/secrets-common" IFS=$'\n' read -r -d '' -a unique_vars < <(printf '%s\n' "${needed_vars[@]}" | sort -u && printf '\0') @@ -126,12 +87,12 @@ jobs: } >> "$GITHUB_ENV" done - echo "✅ All required kamal secrets are validated and available as environment variables" + echo "✅ All required kamal secrets validated" - name: Deploy with Kamal v2 uses: unepwcmc/devops-actions/.github/actions/kamal-v2.x-deploy@v1 with: - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging working-directory: . env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} @@ -157,7 +118,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: success action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -165,7 +126,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} end-time: ${{ env.END_TIME }} @@ -180,7 +141,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: failure action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -188,7 +149,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} end-time: ${{ env.END_TIME }} diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 2eab50ae6..2f8c0340f 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -8,7 +8,6 @@ on: type: choice options: - staging - - production confirm: description: 'Type "CONFIRM" to proceed with setup' required: true From d8898855d06fc221f6038d1b3ac63774ca7d3505 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 11:35:55 +0000 Subject: [PATCH 026/106] Fix workflows: staging-only, trigger on staging branch push --- .github/workflows/deploy.yml | 65 +++++++------------------------ .github/workflows/kamal-setup.yml | 1 - 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1de82f8a5..73bad07a6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,50 +2,16 @@ name: Deploy SAPI Rails API on: push: branches: - - deploy/staging/rails-api - - deploy/production/rails-api - release: - types: [published] + - staging jobs: - determine-environment: - runs-on: self-hosted - outputs: - environment: ${{ steps.env.outputs.environment }} - deploy-type: ${{ steps.env.outputs.deploy-type }} - kamal-secrets-file: ${{ steps.env.outputs.kamal-secrets-file }} - steps: - - id: env - run: | - if [[ "${{ github.event_name }}" == "release" ]]; then - echo "environment=production" >> $GITHUB_OUTPUT - echo "deploy-type=release" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🚀 Production deployment triggered by release: ${{ github.event.release.tag_name }}" - elif [[ "${{ github.ref }}" == "refs/heads/deploy/production/rails-api" ]]; then - echo "environment=production" >> $GITHUB_OUTPUT - echo "deploy-type=branch" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🚀 Production deployment triggered by push to deploy/production/rails-api" - elif [[ "${{ github.ref }}" == "refs/heads/deploy/staging/rails-api" ]]; then - echo "environment=staging" >> $GITHUB_OUTPUT - echo "deploy-type=branch" >> $GITHUB_OUTPUT - echo "kamal-secrets-file=.kamal/secrets-common" >> $GITHUB_OUTPUT - echo "🔧 Staging deployment triggered by push to deploy/staging/rails-api" - else - echo "❌ No deployment configured for this trigger" - exit 1 - fi - deploy: - needs: determine-environment runs-on: self-hosted - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging steps: - name: Set workflow start time run: | echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV - echo "ACTION_TYPE=Deploy" >> $GITHUB_ENV - uses: actions/checkout@v4 @@ -57,7 +23,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: started action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -65,7 +31,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} @@ -73,14 +39,9 @@ jobs: shell: bash env: SECRETS_JSON: ${{ toJSON(secrets) }} - KAMAL_SECRETS_FILE: ${{ needs.determine-environment.outputs.kamal-secrets-file }} run: | - if [[ -z "$KAMAL_SECRETS_FILE" ]]; then - echo "Error: kamal secrets file path not provided." - exit 1 - fi - if [[ ! -f "$KAMAL_SECRETS_FILE" ]]; then - echo "Error: $KAMAL_SECRETS_FILE file not found." + if [[ ! -f ".kamal/secrets-common" ]]; then + echo "Error: .kamal/secrets-common file not found." exit 1 fi @@ -108,7 +69,7 @@ jobs: elif [[ "$value" =~ ^\$([A-Z0-9_]+) ]]; then needed_vars+=("${BASH_REMATCH[1]}") fi - done < "$KAMAL_SECRETS_FILE" + done < ".kamal/secrets-common" IFS=$'\n' read -r -d '' -a unique_vars < <(printf '%s\n' "${needed_vars[@]}" | sort -u && printf '\0') @@ -126,12 +87,12 @@ jobs: } >> "$GITHUB_ENV" done - echo "✅ All required kamal secrets are validated and available as environment variables" + echo "✅ All required kamal secrets validated" - name: Deploy with Kamal v2 uses: unepwcmc/devops-actions/.github/actions/kamal-v2.x-deploy@v1 with: - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging working-directory: . env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} @@ -157,7 +118,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: success action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -165,7 +126,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} end-time: ${{ env.END_TIME }} @@ -180,7 +141,7 @@ jobs: slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }} notification-type: failure action-type: Deploy - environment: ${{ needs.determine-environment.outputs.environment }} + environment: staging repository: ${{ github.repository }} repository-url: ${{ github.server_url }}/${{ github.repository }} action-run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -188,7 +149,7 @@ jobs: actor-url: ${{ github.server_url }}/${{ github.actor }} workflow-name: ${{ github.workflow }} run-id: ${{ github.run_id }} - commit-message: ${{ github.event.head_commit.message || github.event.release.name || 'Manual workflow trigger' }} + commit-message: ${{ github.event.head_commit.message || 'Manual workflow trigger' }} runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} end-time: ${{ env.END_TIME }} diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 2eab50ae6..2f8c0340f 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -8,7 +8,6 @@ on: type: choice options: - staging - - production confirm: description: 'Type "CONFIRM" to proceed with setup' required: true From 33274529f21f2178b6b7758f5190a5f5fb128f7f Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 12:00:21 +0000 Subject: [PATCH 027/106] Pass dummy registry creds to satisfy kamal-v2-setup action check --- .github/workflows/kamal-setup.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 2f8c0340f..cdb39532d 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -94,6 +94,8 @@ jobs: working-directory: '.' env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + KAMAL_REGISTRY_USERNAME: local + KAMAL_REGISTRY_PASSWORD: local RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} From e128b75d79a4a57113cb23b29099f96db62b3442 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 12:00:24 +0000 Subject: [PATCH 028/106] Pass dummy registry creds to satisfy kamal-v2-setup action check --- .github/workflows/kamal-setup.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 2f8c0340f..cdb39532d 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -94,6 +94,8 @@ jobs: working-directory: '.' env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + KAMAL_REGISTRY_USERNAME: local + KAMAL_REGISTRY_PASSWORD: local RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} From 37dd6b4c2b1f7f4ec78d4cb1eed38b23ced7f848 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 12:02:15 +0000 Subject: [PATCH 029/106] Map SAPI_DATABASE_* to generic names required by kamal-v2-setup action --- .github/workflows/kamal-setup.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index cdb39532d..0fc9a62bf 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -96,6 +96,11 @@ jobs: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} KAMAL_REGISTRY_USERNAME: local KAMAL_REGISTRY_PASSWORD: local + # The kamal-v2-setup action requires generic DATABASE_* names + DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} From 53d2b7a638463aaaaadc12ce86938e60c9d27a17 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 12:02:17 +0000 Subject: [PATCH 030/106] Map SAPI_DATABASE_* to generic names required by kamal-v2-setup action --- .github/workflows/kamal-setup.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index cdb39532d..0fc9a62bf 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -96,6 +96,11 @@ jobs: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} KAMAL_REGISTRY_USERNAME: local KAMAL_REGISTRY_PASSWORD: local + # The kamal-v2-setup action requires generic DATABASE_* names + DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} From 5620b10217a080bd0c0c7edb6bc8dc453d92bfe2 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:34:22 +0000 Subject: [PATCH 031/106] Fix kamal setup: inline steps, prefixed secrets, corrected image and registry - Replace broken kamal-v2-setup@v1 action with direct SSH + bundle exec kamal setup - Revert secrets-common to use SAPI_ and CAPTIVE_BREEDING_ prefixed GitHub secret names - Fix deploy.staging.yml image to remove ghcr.io prefix - Fix deploy.yml registry to use dcr.internal.unep-wcmc.org --- .github/workflows/kamal-setup.yml | 88 +++++++++++-------------------- .kamal/secrets-common | 3 +- config/deploy.staging.yml | 2 +- config/deploy.yml | 2 +- 4 files changed, 35 insertions(+), 60 deletions(-) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 0fc9a62bf..01242c569 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -33,11 +33,29 @@ jobs: needs: validate-input runs-on: self-hosted environment: ${{ github.event.inputs.environment }} + env: + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} steps: - name: Set workflow start time run: | echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV - echo "ACTION_TYPE=Setup" >> $GITHUB_ENV - uses: actions/checkout@v4 @@ -61,64 +79,20 @@ jobs: runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} - - name: Validate and Populate Secrets - uses: unepwcmc/devops-actions/.github/actions/validate-secrets@v1 + - name: Set up SSH agent + uses: webfactory/ssh-agent@v0.9.0 with: - secrets-file: '.kamal/secrets-common' - environment: ${{ github.event.inputs.environment }} - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} - CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} - CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} - CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} - CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} - CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} - SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Setup SAPI with Kamal v2 - uses: unepwcmc/devops-actions/.github/actions/kamal-v2-setup@v1 - with: - environment: ${{ github.event.inputs.environment }} - working-directory: '.' - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - KAMAL_REGISTRY_USERNAME: local - KAMAL_REGISTRY_PASSWORD: local - # The kamal-v2-setup action requires generic DATABASE_* names - DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} - CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} - CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} - CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} - CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} - CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} - SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + - name: Install Kamal + run: | + cd deploy + bundle install --quiet + + - name: Run Kamal setup + run: | + cd deploy + bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() diff --git a/.kamal/secrets-common b/.kamal/secrets-common index 4189767a8..a8be83b2a 100644 --- a/.kamal/secrets-common +++ b/.kamal/secrets-common @@ -6,13 +6,14 @@ # Rails Configuration (REQUIRED) RAILS_MASTER_KEY=$RAILS_MASTER_KEY -# Database Configuration (special, 2 database) +# SAPI Database Configuration SAPI_DATABASE_HOST=$SAPI_DATABASE_HOST SAPI_DATABASE_NAME=$SAPI_DATABASE_NAME SAPI_DATABASE_USERNAME=$SAPI_DATABASE_USERNAME SAPI_DATABASE_PASSWORD=$SAPI_DATABASE_PASSWORD SAPI_DATABASE_PORT=$SAPI_DATABASE_PORT +# Captive Breeding Database Configuration CAPTIVE_BREEDING_DATABASE_HOST=$CAPTIVE_BREEDING_DATABASE_HOST CAPTIVE_BREEDING_DATABASE_NAME=$CAPTIVE_BREEDING_DATABASE_NAME CAPTIVE_BREEDING_DATABASE_USERNAME=$CAPTIVE_BREEDING_DATABASE_USERNAME diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 5ef9ea345..40c65ce0d 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -1,5 +1,5 @@ # Name of the container image -image: ghcr.io/unepwcmc/sapi/rails-staging +image: unepwcmc/sapi/rails-staging servers: web: diff --git a/config/deploy.yml b/config/deploy.yml index 77deb7521..ae809e475 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -3,7 +3,7 @@ service: sapi # Credentials for your image host. registry: - server: localhost:5555 + server: dcr.internal.unep-wcmc.org # Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) env: From e43fe3eec331e678118a7b31d022c7a1a8fe4ba8 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:34:40 +0000 Subject: [PATCH 032/106] Sync kamal workflow files from staging branch --- .github/workflows/kamal-setup.yml | 88 +++++++++++-------------------- .kamal/secrets-common | 36 +++++++++++++ config/deploy.staging.yml | 38 +++++++++++++ config/deploy.yml | 44 ++++++++++++++++ 4 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 .kamal/secrets-common create mode 100644 config/deploy.staging.yml create mode 100644 config/deploy.yml diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 0fc9a62bf..01242c569 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -33,11 +33,29 @@ jobs: needs: validate-input runs-on: self-hosted environment: ${{ github.event.inputs.environment }} + env: + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} + SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} + SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} + SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} + SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} + CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} + CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} + CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} + CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} + CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} + MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} + SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} steps: - name: Set workflow start time run: | echo "START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_ENV - echo "ACTION_TYPE=Setup" >> $GITHUB_ENV - uses: actions/checkout@v4 @@ -61,64 +79,20 @@ jobs: runner-name: ${{ runner.name }} start-time: ${{ env.START_TIME }} - - name: Validate and Populate Secrets - uses: unepwcmc/devops-actions/.github/actions/validate-secrets@v1 + - name: Set up SSH agent + uses: webfactory/ssh-agent@v0.9.0 with: - secrets-file: '.kamal/secrets-common' - environment: ${{ github.event.inputs.environment }} - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} - CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} - CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} - CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} - CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} - CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} - SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Setup SAPI with Kamal v2 - uses: unepwcmc/devops-actions/.github/actions/kamal-v2-setup@v1 - with: - environment: ${{ github.event.inputs.environment }} - working-directory: '.' - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - KAMAL_REGISTRY_USERNAME: local - KAMAL_REGISTRY_PASSWORD: local - # The kamal-v2-setup action requires generic DATABASE_* names - DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - SAPI_DATABASE_HOST: ${{ secrets.SAPI_DATABASE_HOST }} - SAPI_DATABASE_NAME: ${{ secrets.SAPI_DATABASE_NAME }} - SAPI_DATABASE_USERNAME: ${{ secrets.SAPI_DATABASE_USERNAME }} - SAPI_DATABASE_PASSWORD: ${{ secrets.SAPI_DATABASE_PASSWORD }} - SAPI_DATABASE_PORT: ${{ secrets.SAPI_DATABASE_PORT }} - CAPTIVE_BREEDING_DATABASE_HOST: ${{ secrets.CAPTIVE_BREEDING_DATABASE_HOST }} - CAPTIVE_BREEDING_DATABASE_NAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_NAME }} - CAPTIVE_BREEDING_DATABASE_USERNAME: ${{ secrets.CAPTIVE_BREEDING_DATABASE_USERNAME }} - CAPTIVE_BREEDING_DATABASE_PASSWORD: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PASSWORD }} - CAPTIVE_BREEDING_DATABASE_PORT: ${{ secrets.CAPTIVE_BREEDING_DATABASE_PORT }} - MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} - SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + - name: Install Kamal + run: | + cd deploy + bundle install --quiet + + - name: Run Kamal setup + run: | + cd deploy + bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() diff --git a/.kamal/secrets-common b/.kamal/secrets-common new file mode 100644 index 000000000..a8be83b2a --- /dev/null +++ b/.kamal/secrets-common @@ -0,0 +1,36 @@ +# Minimal Secrets Template - Backend Rails API Kamal Deployment +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Rails Configuration (REQUIRED) +RAILS_MASTER_KEY=$RAILS_MASTER_KEY + +# SAPI Database Configuration +SAPI_DATABASE_HOST=$SAPI_DATABASE_HOST +SAPI_DATABASE_NAME=$SAPI_DATABASE_NAME +SAPI_DATABASE_USERNAME=$SAPI_DATABASE_USERNAME +SAPI_DATABASE_PASSWORD=$SAPI_DATABASE_PASSWORD +SAPI_DATABASE_PORT=$SAPI_DATABASE_PORT + +# Captive Breeding Database Configuration +CAPTIVE_BREEDING_DATABASE_HOST=$CAPTIVE_BREEDING_DATABASE_HOST +CAPTIVE_BREEDING_DATABASE_NAME=$CAPTIVE_BREEDING_DATABASE_NAME +CAPTIVE_BREEDING_DATABASE_USERNAME=$CAPTIVE_BREEDING_DATABASE_USERNAME +CAPTIVE_BREEDING_DATABASE_PASSWORD=$CAPTIVE_BREEDING_DATABASE_PASSWORD +CAPTIVE_BREEDING_DATABASE_PORT=$CAPTIVE_BREEDING_DATABASE_PORT + +# Mail Configuration +MAIL_USERNAME=$MAIL_USERNAME +MAIL_PASSWORD=$MAIL_PASSWORD + +# AWS Configuration +AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY +AWS_REGION=$AWS_REGION + +# Redis for Sidekiq +SAPI_SIDEKIQ_REDIS_URL=$SAPI_SIDEKIQ_REDIS_URL + +# Redis for cache +SAPI_SIDEKIQ_REDIS_CACHE_URL=$SAPI_SIDEKIQ_REDIS_CACHE_URL diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml new file mode 100644 index 000000000..40c65ce0d --- /dev/null +++ b/config/deploy.staging.yml @@ -0,0 +1,38 @@ +# Name of the container image +image: unepwcmc/sapi/rails-staging + +servers: + web: + hosts: + - 172.20.0.146 # sapi-web-staging-01.internal.unep-wcmc.org + options: + add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis + proxy: + hosts: + - sapi-web-staging-01.internal.unep-wcmc.org # Public-facing domain + ssl: true + tls_certificate_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.crt + tls_private_key_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.key + forward_headers: true # Forward headers like X-Forwarded-Proto + healthcheck: + path: /up + logging: + request_headers: + - Cache-Control + - User-Agent + - X-Forwarded-Proto # Critical for Rails to detect HTTPS + response_headers: + - X-Request-ID + job: + hosts: + - 172.20.0.146 # sapi-web-staging-01.internal.unep-wcmc.org + options: + add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis + cmd: bundle exec sidekiq -C config/sidekiq.yml + healthcheck: + cmd: /rails/bin/docker-sidekiq-healthcheck + +# Environment variables +env: + clear: + RAILS_ENV: staging diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 000000000..ae809e475 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,44 @@ +# Name of your application. Used to uniquely configure containers. +service: sapi + +# Credentials for your image host. +registry: + server: dcr.internal.unep-wcmc.org + +# Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) +env: + clear: + RAILS_LOG_LEVEL: warn # @see https://github.com/heartcombo/devise#password-reset-tokens-and-rails-logs + PORT: 80 + RAILS_SERVE_STATIC_FILES: 1 + RAILS_LOG_TO_STDOUT: 1 # Need this before upgrade to Rails 7.1, which then default is STDOUT. + secret: + - RAILS_MASTER_KEY + - SAPI_DATABASE_HOST + - SAPI_DATABASE_NAME + - SAPI_DATABASE_USERNAME + - SAPI_DATABASE_PASSWORD + - SAPI_DATABASE_PORT + - CAPTIVE_BREEDING_DATABASE_HOST + - CAPTIVE_BREEDING_DATABASE_NAME + - CAPTIVE_BREEDING_DATABASE_USERNAME + - CAPTIVE_BREEDING_DATABASE_PASSWORD + - CAPTIVE_BREEDING_DATABASE_PORT + - MAIL_USERNAME + - MAIL_PASSWORD + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_REGION + - SAPI_SIDEKIQ_REDIS_URL + - SAPI_SIDEKIQ_REDIS_CACHE_URL + +# Use a different ssh user than root +ssh: + user: wcmc + +# Configure builder setup +builder: + arch: amd64 + dockerfile: Dockerfile.deploy + args: + RUBY_VERSION: 3.2.5 From a43d2f0a540ea7ea08e463fab31838fb8198c995 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:36:05 +0000 Subject: [PATCH 033/106] Fix deploy Ruby version to 3.4.9 to match Gemfile --- deploy/.ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/.ruby-version b/deploy/.ruby-version index f9892605c..7bcbb3808 100644 --- a/deploy/.ruby-version +++ b/deploy/.ruby-version @@ -1 +1 @@ -3.4.4 +3.4.9 From c23a85c5254981265b2976fa6fde3e3ac87e19e3 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:36:07 +0000 Subject: [PATCH 034/106] Sync deploy Ruby version fix to master --- deploy/.ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 deploy/.ruby-version diff --git a/deploy/.ruby-version b/deploy/.ruby-version new file mode 100644 index 000000000..7bcbb3808 --- /dev/null +++ b/deploy/.ruby-version @@ -0,0 +1 @@ +3.4.9 From 45a069dfeb7b10388ca2edefa0392193b82e10eb Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:38:32 +0000 Subject: [PATCH 035/106] Remove invalid healthcheck key from job role in deploy.staging.yml --- config/deploy.staging.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 40c65ce0d..8546e064d 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -29,8 +29,6 @@ servers: options: add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis cmd: bundle exec sidekiq -C config/sidekiq.yml - healthcheck: - cmd: /rails/bin/docker-sidekiq-healthcheck # Environment variables env: From a4741ee49fe941eb20bd7af0a78a9537dab76e81 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:38:34 +0000 Subject: [PATCH 036/106] Sync deploy.staging.yml fix to master --- config/deploy.staging.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 40c65ce0d..8546e064d 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -29,8 +29,6 @@ servers: options: add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis cmd: bundle exec sidekiq -C config/sidekiq.yml - healthcheck: - cmd: /rails/bin/docker-sidekiq-healthcheck # Environment variables env: From 9ded9626a164f88e4563ac2b54c79210af71f117 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:39:50 +0000 Subject: [PATCH 037/106] Add ruby/setup-ruby step to kamal-setup workflow --- .github/workflows/kamal-setup.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 01242c569..316cf4547 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -84,15 +84,19 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + working-directory: deploy + bundler-cache: false + - name: Install Kamal - run: | - cd deploy - bundle install --quiet + working-directory: deploy + run: bundle install --quiet - name: Run Kamal setup - run: | - cd deploy - bundle exec kamal setup -d ${{ github.event.inputs.environment }} + working-directory: deploy + run: bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() From 00012d0c55c9a21faaf5244940977ea21f5a36dd Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:39:52 +0000 Subject: [PATCH 038/106] Sync kamal-setup ruby fix to master --- .github/workflows/kamal-setup.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 01242c569..316cf4547 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -84,15 +84,19 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + working-directory: deploy + bundler-cache: false + - name: Install Kamal - run: | - cd deploy - bundle install --quiet + working-directory: deploy + run: bundle install --quiet - name: Run Kamal setup - run: | - cd deploy - bundle exec kamal setup -d ${{ github.event.inputs.environment }} + working-directory: deploy + run: bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() From e66992dcaf3c462319aa9aea606bae944d15c389 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:44:25 +0000 Subject: [PATCH 039/106] Fix kamal setup path and disable auto-deploy until setup complete - Run kamal from repo root using BUNDLE_GEMFILE=deploy/Gemfile - Switch deploy workflow to workflow_dispatch only --- .github/workflows/deploy.yml | 4 +--- .github/workflows/kamal-setup.yml | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73bad07a6..dfa578b12 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,6 @@ name: Deploy SAPI Rails API on: - push: - branches: - - staging + workflow_dispatch: jobs: deploy: diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 316cf4547..0ff35fcd8 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -95,8 +95,7 @@ jobs: run: bundle install --quiet - name: Run Kamal setup - working-directory: deploy - run: bundle exec kamal setup -d ${{ github.event.inputs.environment }} + run: BUNDLE_GEMFILE=deploy/Gemfile bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() From 73996a87e1d5edc94a7f3848028c7055c6830f92 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:44:27 +0000 Subject: [PATCH 040/106] Sync workflow fixes to master --- .github/workflows/deploy.yml | 4 +--- .github/workflows/kamal-setup.yml | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73bad07a6..dfa578b12 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,6 @@ name: Deploy SAPI Rails API on: - push: - branches: - - staging + workflow_dispatch: jobs: deploy: diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 316cf4547..0ff35fcd8 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -95,8 +95,7 @@ jobs: run: bundle install --quiet - name: Run Kamal setup - working-directory: deploy - run: bundle exec kamal setup -d ${{ github.event.inputs.environment }} + run: BUNDLE_GEMFILE=deploy/Gemfile bundle exec kamal setup -d ${{ github.event.inputs.environment }} - name: Set workflow end time and calculate duration if: always() From e21a7381fbf1a54da030b72b9cfdbe05461d0d25 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:48:13 +0000 Subject: [PATCH 041/106] Fix SSL config to use kamal v2 certificate_pem/private_key_pem secrets --- .kamal/secrets-common | 4 ++++ config/deploy.staging.yml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.kamal/secrets-common b/.kamal/secrets-common index a8be83b2a..70635feb0 100644 --- a/.kamal/secrets-common +++ b/.kamal/secrets-common @@ -34,3 +34,7 @@ SAPI_SIDEKIQ_REDIS_URL=$SAPI_SIDEKIQ_REDIS_URL # Redis for cache SAPI_SIDEKIQ_REDIS_CACHE_URL=$SAPI_SIDEKIQ_REDIS_CACHE_URL + +# SSL certificates +CERTIFICATE_PEM=$CERTIFICATE_PEM +PRIVATE_KEY_PEM=$PRIVATE_KEY_PEM diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 8546e064d..b91fd1449 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -10,9 +10,9 @@ servers: proxy: hosts: - sapi-web-staging-01.internal.unep-wcmc.org # Public-facing domain - ssl: true - tls_certificate_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.crt - tls_private_key_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.key + ssl: + certificate_pem: CERTIFICATE_PEM + private_key_pem: PRIVATE_KEY_PEM forward_headers: true # Forward headers like X-Forwarded-Proto healthcheck: path: /up From 1929e8881a0d4395173e23500f5abc37d774a223 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:48:15 +0000 Subject: [PATCH 042/106] Sync SSL config fix to master --- .kamal/secrets-common | 4 ++++ config/deploy.staging.yml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.kamal/secrets-common b/.kamal/secrets-common index a8be83b2a..70635feb0 100644 --- a/.kamal/secrets-common +++ b/.kamal/secrets-common @@ -34,3 +34,7 @@ SAPI_SIDEKIQ_REDIS_URL=$SAPI_SIDEKIQ_REDIS_URL # Redis for cache SAPI_SIDEKIQ_REDIS_CACHE_URL=$SAPI_SIDEKIQ_REDIS_CACHE_URL + +# SSL certificates +CERTIFICATE_PEM=$CERTIFICATE_PEM +PRIVATE_KEY_PEM=$PRIVATE_KEY_PEM diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 8546e064d..b91fd1449 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -10,9 +10,9 @@ servers: proxy: hosts: - sapi-web-staging-01.internal.unep-wcmc.org # Public-facing domain - ssl: true - tls_certificate_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.crt - tls_private_key_path: /etc/ssl/kamal/star.internal.unep-wcmc.org.key + ssl: + certificate_pem: CERTIFICATE_PEM + private_key_pem: PRIVATE_KEY_PEM forward_headers: true # Forward headers like X-Forwarded-Proto healthcheck: path: /up From 786ab5e11f4ac7d0c369511becab95371e45db66 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:53:24 +0000 Subject: [PATCH 043/106] Use localhost:5555 local registry instead of Harbour (no auth needed) --- config/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deploy.yml b/config/deploy.yml index ae809e475..77deb7521 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -3,7 +3,7 @@ service: sapi # Credentials for your image host. registry: - server: dcr.internal.unep-wcmc.org + server: localhost:5555 # Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) env: From 6a4970bd5c32e21bc46407f46ad9f5c1d2e6cb1a Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 13:53:24 +0000 Subject: [PATCH 044/106] Use localhost:5555 local registry instead of Harbour (no auth needed) --- config/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/deploy.yml b/config/deploy.yml index ae809e475..77deb7521 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -3,7 +3,7 @@ service: sapi # Credentials for your image host. registry: - server: dcr.internal.unep-wcmc.org + server: localhost:5555 # Inject ENV variables into containers (secrets come from .kamal/secrets.{environment}) env: From e4c211dabb00ad49c6e2790e014443354ff64c35 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 14:06:21 +0000 Subject: [PATCH 045/106] Add buildx setup and remote builder on server for localhost:5555 registry --- .github/workflows/kamal-setup.yml | 3 +++ config/deploy.yml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 0ff35fcd8..d4e71dd2a 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -84,6 +84,9 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/config/deploy.yml b/config/deploy.yml index 77deb7521..37ec91439 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -36,9 +36,10 @@ env: ssh: user: wcmc -# Configure builder setup +# Configure builder setup — builds remotely on the server so localhost:5555 registry works builder: arch: amd64 dockerfile: Dockerfile.deploy args: RUBY_VERSION: 3.2.5 + remote: ssh://wcmc@172.20.0.146 From cb3e94a9e87b357a68a6464c673e18c6a56449a4 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 14:06:21 +0000 Subject: [PATCH 046/106] Add buildx setup and remote builder on server for localhost:5555 registry --- .github/workflows/kamal-setup.yml | 3 +++ config/deploy.yml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index 0ff35fcd8..d4e71dd2a 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -84,6 +84,9 @@ jobs: with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/config/deploy.yml b/config/deploy.yml index 77deb7521..37ec91439 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -36,9 +36,10 @@ env: ssh: user: wcmc -# Configure builder setup +# Configure builder setup — builds remotely on the server so localhost:5555 registry works builder: arch: amd64 dockerfile: Dockerfile.deploy args: RUBY_VERSION: 3.2.5 + remote: ssh://wcmc@172.20.0.146 From 33255ee266d21e0a088a0dd41ddd84849dec3b19 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 14:25:37 +0000 Subject: [PATCH 047/106] Add CERTIFICATE_PEM and PRIVATE_KEY_PEM to kamal-setup env --- .github/workflows/kamal-setup.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index d4e71dd2a..ad607227a 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -52,6 +52,8 @@ jobs: AWS_REGION: ${{ secrets.AWS_REGION }} SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }} + PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }} steps: - name: Set workflow start time run: | From 96c2a78d3c2e12e5507e43e06658315c5bff0aa5 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 14:25:37 +0000 Subject: [PATCH 048/106] Add CERTIFICATE_PEM and PRIVATE_KEY_PEM to kamal-setup env --- .github/workflows/kamal-setup.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/kamal-setup.yml b/.github/workflows/kamal-setup.yml index d4e71dd2a..ad607227a 100644 --- a/.github/workflows/kamal-setup.yml +++ b/.github/workflows/kamal-setup.yml @@ -52,6 +52,8 @@ jobs: AWS_REGION: ${{ secrets.AWS_REGION }} SAPI_SIDEKIQ_REDIS_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_URL }} SAPI_SIDEKIQ_REDIS_CACHE_URL: ${{ secrets.SAPI_SIDEKIQ_REDIS_CACHE_URL }} + CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }} + PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }} steps: - name: Set workflow start time run: | From 9b2a6130818503ee2eddc102341c545a67d0e88f Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 15:50:38 +0000 Subject: [PATCH 049/106] Add Dockerfile.staging with puma CMD; wire into deploy.staging.yml --- Dockerfile.staging | 68 +++++++++++++++++++++++++++++++++++++++ config/deploy.staging.yml | 4 +++ 2 files changed, 72 insertions(+) create mode 100644 Dockerfile.staging diff --git a/Dockerfile.staging b/Dockerfile.staging new file mode 100644 index 000000000..243139f99 --- /dev/null +++ b/Dockerfile.staging @@ -0,0 +1,68 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# Staging Dockerfile — identical to Dockerfile.deploy but with puma as CMD. +# Dockerfile.deploy uses `tail -f /dev/null` for production (Capistrano manages the process). +# Kamal manages the process via Docker, so we start puma directly here. + +ARG RUBY_VERSION=3.2.5 +FROM ruby:$RUBY_VERSION-slim AS base + +WORKDIR /rails + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ + libsodium-dev libgmp3-dev libssl-dev \ + curl xz-utils \ + texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +ENV NODE_ENV="production" \ + RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + + +FROM base AS build + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +COPY . . + +RUN bundle exec bootsnap precompile app/ lib/ + +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +FROM base + +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log tmp +USER 1000:1000 + +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +EXPOSE 80 +CMD ["./bin/rails", "server", "-b", "0.0.0.0"] diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index b91fd1449..61fdd0ea6 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -30,6 +30,10 @@ servers: add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis cmd: bundle exec sidekiq -C config/sidekiq.yml +# Use staging-specific Dockerfile that starts puma (Dockerfile.deploy uses tail -f /dev/null for Capistrano) +builder: + dockerfile: Dockerfile.staging + # Environment variables env: clear: From 798a2671fcd2f8ac0577ab413b7b558d8b663dab Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Wed, 18 Mar 2026 15:50:38 +0000 Subject: [PATCH 050/106] Add Dockerfile.staging with puma CMD; wire into deploy.staging.yml --- Dockerfile.staging | 68 +++++++++++++++++++++++++++++++++++++++ config/deploy.staging.yml | 4 +++ 2 files changed, 72 insertions(+) create mode 100644 Dockerfile.staging diff --git a/Dockerfile.staging b/Dockerfile.staging new file mode 100644 index 000000000..243139f99 --- /dev/null +++ b/Dockerfile.staging @@ -0,0 +1,68 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# Staging Dockerfile — identical to Dockerfile.deploy but with puma as CMD. +# Dockerfile.deploy uses `tail -f /dev/null` for production (Capistrano manages the process). +# Kamal manages the process via Docker, so we start puma directly here. + +ARG RUBY_VERSION=3.2.5 +FROM ruby:$RUBY_VERSION-slim AS base + +WORKDIR /rails + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libpq-dev \ + libsodium-dev libgmp3-dev libssl-dev \ + curl xz-utils \ + texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +ENV NODE_ENV="production" \ + RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +ARG NODE_VERSION=18.20.8 +ARG TARGETARCH +RUN case "$TARGETARCH" in \ + amd64) NODE_ARCH=x64 ;; \ + arm64) NODE_ARCH=arm64 ;; \ + *) echo "Unsupported architecture: $TARGETARCH"; exit 1 ;; \ + esac && \ + curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz \ + | tar -xJ -C /usr/local --strip-components=1 + + +FROM base AS build + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives + +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +COPY . . + +RUN bundle exec bootsnap precompile app/ lib/ + +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +FROM base + +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log tmp +USER 1000:1000 + +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +EXPOSE 80 +CMD ["./bin/rails", "server", "-b", "0.0.0.0"] diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index b91fd1449..61fdd0ea6 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -30,6 +30,10 @@ servers: add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis cmd: bundle exec sidekiq -C config/sidekiq.yml +# Use staging-specific Dockerfile that starts puma (Dockerfile.deploy uses tail -f /dev/null for Capistrano) +builder: + dockerfile: Dockerfile.staging + # Environment variables env: clear: From e6813d3515ac4d9a0f80789ee558c5cb0788fc94 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Thu, 19 Mar 2026 07:15:26 +0000 Subject: [PATCH 051/106] Enable auto-deploy on staging branch push --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dfa578b12..1a7a1d3ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,8 @@ name: Deploy SAPI Rails API on: + push: + branches: + - staging workflow_dispatch: jobs: From 374864338a4ec1415887d029d734b2da90aba8f0 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Thu, 19 Mar 2026 07:15:26 +0000 Subject: [PATCH 052/106] Enable auto-deploy on staging branch push --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dfa578b12..1a7a1d3ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,8 @@ name: Deploy SAPI Rails API on: + push: + branches: + - staging workflow_dispatch: jobs: From 8da02eaa497c233f062b5f4123661666c4cc4387 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Thu, 19 Mar 2026 07:37:17 +0000 Subject: [PATCH 053/106] Add staging.speciesplus.net Cloudflare tunnel host to kamal-proxy --- config/deploy.staging.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 61fdd0ea6..b8824d49f 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -9,7 +9,8 @@ servers: add-host: host.docker.internal:host-gateway # Required to reach host-bound Redis proxy: hosts: - - sapi-web-staging-01.internal.unep-wcmc.org # Public-facing domain + - sapi-web-staging-01.internal.unep-wcmc.org + - staging.speciesplus.net # Cloudflare tunnel endpoint ssl: certificate_pem: CERTIFICATE_PEM private_key_pem: PRIVATE_KEY_PEM From 7bea977a476437dab955f27f8bf9e8bf0c5950d3 Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Thu, 19 Mar 2026 07:54:57 +0000 Subject: [PATCH 054/106] =?UTF-8?q?Remove=20staging.speciesplus.net=20from?= =?UTF-8?q?=20kamal-proxy=20=E2=80=94=20Cloudflare=20routes=20to=20interna?= =?UTF-8?q?l=20hostname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/deploy.staging.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index b8824d49f..05e4baa36 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -10,7 +10,6 @@ servers: proxy: hosts: - sapi-web-staging-01.internal.unep-wcmc.org - - staging.speciesplus.net # Cloudflare tunnel endpoint ssl: certificate_pem: CERTIFICATE_PEM private_key_pem: PRIVATE_KEY_PEM From 04e0dc302f53e55dfbc0e4348b3446020e7501fa Mon Sep 17 00:00:00 2001 From: Ruan du Toit Date: Thu, 19 Mar 2026 12:00:18 +0000 Subject: [PATCH 055/106] Remove stale server.pid in entrypoint to prevent crash-restart loop --- bin/docker-entrypoint | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 3054757cc..ba34b5cb1 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -35,5 +35,9 @@ mkdir -p {./,spec/}public/downloads/taxon_concepts_names # ./bin/rails db:prepare # fi +# Remove stale PID file — survives Docker restarts on the same writable layer +# and causes Rails to refuse to start (finds a PID it thinks is already running) +rm -f /rails/tmp/pids/server.pid + # This is the main rails/sidekiq command exec "${@}" From 528d27ced36bb4d1105eff515e4958a51d967234 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Tue, 7 Apr 2026 11:52:15 +0100 Subject: [PATCH 056/106] chore: upgrade Rails from 7.1.3.4 to 7.2.3.1 (Gemfile only) --- Gemfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 100a8b7da..cf512d207 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby '3.2.5' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '7.1.3.4' +gem 'rails', '7.2.3.1' # Configure Cross-Origin resource sharing gem 'rack-cors' @@ -34,10 +34,10 @@ gem 'coffee-rails', '~> 5.0' # gem 'mini_racer', platforms: :ruby gem 'active_model_serializers', '0.8.4' # Deprecated -gem "active_storage_validations", "~> 2.0" +gem 'active_storage_validations', '~> 2.0' # Use redis for caching -gem "redis", "~> 4.8" +gem 'redis', '~> 4.8' # Use PostgreSQL database gem 'pg', '~> 1.5', '>= 1.5.4' @@ -229,5 +229,3 @@ gem 'handlebars-source', '1.0.12' # TODO: just a wrapwrapper. Any update will ch # It might be possible to fix this if we had an nginx version which supported # the config: `passenger_preload_bundler on;` gem 'base64', '0.1.1' - - From 8a4a4dc1fe0d5a7f7a455ecc9cf9977b580db5a4 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 10:21:36 +0100 Subject: [PATCH 057/106] chore: bundle upgrade rails to 7.2 and acts-as-taggable-on --- Gemfile | 2 +- Gemfile.lock | 487 ++++++++++++++++++++++++++++----------------------- 2 files changed, 265 insertions(+), 224 deletions(-) diff --git a/Gemfile b/Gemfile index cf512d207..3c466031c 100644 --- a/Gemfile +++ b/Gemfile @@ -70,7 +70,7 @@ gem 'httparty', '~> 0.21.0' gem 'kaminari', '~> 1.2', '>= 1.2.2' # TODO: Suggest migrate to pagy gem. -gem 'acts-as-taggable-on', '~> 10.0' # TODO: refuses to install against Rails 7.2 +gem 'acts-as-taggable-on', '~> 12.0' gem 'carrierwave', '~> 3.0', '>= 3.0.5' # PDF diff --git a/Gemfile.lock b/Gemfile.lock index d9acef088..b8ba233dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,52 +2,49 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.0.3) - actioncable (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + actioncable (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3.4) - actionpack (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) + mail (>= 2.8.0) + actionmailer (7.2.3.1) + actionpack (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activesupport (= 7.2.3.1) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionpack (7.2.3.1) + actionview (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.4) - actionpack (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + useragent (~> 0.16) + actiontext (7.2.3.1) + actionpack (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.2.3.1) + activesupport (= 7.2.3.1) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) @@ -55,67 +52,74 @@ GEM ember-data-source (>= 1.13, < 3.0) active_model_serializers (0.8.4) activemodel (>= 3.0) - active_storage_validations (2.0.3) + active_storage_validations (2.0.4) activejob (>= 6.1.4) activemodel (>= 6.1.4) activestorage (>= 6.1.4) activesupport (>= 6.1.4) marcel (>= 1.0.3) - activejob (7.1.3.4) - activesupport (= 7.1.3.4) + activejob (7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.3.6) - activemodel (7.1.3.4) - activesupport (= 7.1.3.4) - activerecord (7.1.3.4) - activemodel (= 7.1.3.4) - activesupport (= 7.1.3.4) + activemodel (7.2.3.1) + activesupport (= 7.2.3.1) + activerecord (7.2.3.1) + activemodel (= 7.2.3.1) + activesupport (= 7.2.3.1) timeout (>= 0.4.0) - activestorage (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activesupport (= 7.1.3.4) + activestorage (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activesupport (= 7.2.3.1) marcel (~> 1.0) - activesupport (7.1.3.4) + activesupport (7.2.3.1) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - acts-as-taggable-on (10.0.0) - activerecord (>= 6.1, < 7.2) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + acts-as-taggable-on (12.0.0) + activerecord (>= 7.1, < 8.1) + zeitwerk (>= 2.4, < 3.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) afm (0.2.2) - ahoy_matey (5.1.0) - activesupport (>= 6.1) + ahoy_matey (5.4.2) + activesupport (>= 7.1) + cgi device_detector (>= 1) safely_block (>= 0.4) - airbrussh (1.5.2) + airbrussh (1.6.1) sshkit (>= 1.6.1, != 1.7.0) annotaterb (4.10.2) appsignal (3.13.1) rack - ast (2.4.2) - aws-eventstream (1.3.0) - aws-partitions (1.961.0) - aws-sdk-core (3.201.3) + ast (2.4.3) + aws-eventstream (1.4.0) + aws-partitions (1.1236.0) + aws-sdk-core (3.244.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + logger + aws-sdk-kms (1.123.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.157.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.219.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -125,11 +129,12 @@ GEM ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) base64 (0.1.1) - bcrypt (3.1.20) + bcrypt (3.1.22) bcrypt_pbkdf (1.1.0) - bigdecimal (3.1.8) + benchmark (0.5.0) + bigdecimal (4.1.1) bindex (0.8.1) - bootsnap (1.18.3) + bootsnap (1.23.0) msgpack (~> 1.2) bootstrap-sass (2.3.2.2) sass (~> 3.2) @@ -137,7 +142,8 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.3.0) - byebug (11.1.3) + byebug (13.0.0) + reline (>= 0.6.0) cancancan (3.6.1) capistrano (3.18.0) airbrussh (>= 1.0.0) @@ -171,14 +177,15 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - carrierwave (3.0.7) + carrierwave (3.1.2) activemodel (>= 6.0.0) activesupport (>= 6.0.0) addressable (~> 2.6) image_processing (~> 1.1) marcel (~> 1.0.0) ssrf_filter (~> 1.0) - chartkick (5.0.7) + cgi (0.5.1) + chartkick (5.2.1) chronic_duration (0.10.6) numerizer (~> 0.1.1) coffee-rails (5.0.0) @@ -188,21 +195,21 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) + concurrent-ruby (1.3.6) + connection_pool (2.5.5) coveralls_reborn (0.28.0) simplecov (~> 0.22.0) term-ansicolor (~> 1.7) thor (~> 1.2) tins (~> 1.32) crass (1.0.6) - database_cleaner (2.0.2) + database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.2.0) + database_cleaner-active_record (2.2.2) activerecord (>= 5.a) - database_cleaner-core (~> 2.0.0) + database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) - date (3.4.1) + date (3.5.1) device_detector (1.1.3) devise (4.9.4) bcrypt (~> 3.0) @@ -210,12 +217,12 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.5.1) + diff-lcs (1.6.2) docile (1.4.1) dotenv (2.0.1) dotenv-rails (2.0.1) dotenv (= 2.0.1) - drb (2.2.1) + drb (2.2.3) ed25519 (1.2.4) ember-cli-assets (0.0.37) ember-data-source (1.13.0) @@ -238,59 +245,62 @@ GEM railties (>= 4.2) ember-source (1.8.0) handlebars-source (~> 1.0) - erubi (1.13.0) - et-orbi (1.2.11) + erb (6.0.2) + erubi (1.13.1) + et-orbi (1.4.0) tzinfo - execjs (2.9.1) + execjs (2.10.1) factory_bot (5.2.0) activesupport (>= 4.2.0) factory_bot_rails (5.2.0) factory_bot (~> 5.2.0) railties (>= 4.2.0) - ffi (1.17.0) + ffi (1.17.4) file_exists (0.2.0) - fugit (1.11.0) - et-orbi (~> 1, >= 1.2.11) + fugit (1.12.1) + et-orbi (~> 1.4) raabro (~> 1.4) geoip (1.3.5) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - gon (6.4.0) + gon (6.6.0) actionpack (>= 3.0.20) i18n (>= 0.7) multi_json request_store (>= 1.0) - groupdate (6.4.0) - activesupport (>= 6.1) + groupdate (6.7.0) + activesupport (>= 7.1) handlebars-source (1.0.12) - has_scope (0.8.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) hashery (2.1.2) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.5) + i18n (1.14.8) concurrent-ruby (~> 1.0) - image_processing (1.13.0) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) inherited_resources (1.14.0) actionpack (>= 6.0) has_scope (>= 0.6) railties (>= 6.0) responders (>= 2) - io-console (0.7.2) - irb (1.14.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) - jquery-rails (4.6.0) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jslint_on_rails (1.1.1) - json (2.7.2) + json (2.19.3) json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) @@ -306,65 +316,72 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - language_server-protocol (3.17.0.3) + language_server-protocol (3.17.0.5) launchy (2.4.3) addressable (~> 2.3) - listen (3.9.0) + lint_roller (1.1.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) - loofah (2.24.0) + logger (1.7.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) - mini_magick (4.13.2) + matrix (0.4.3) + mini_magick (5.3.1) + logger mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.24.1) - mobility (1.2.9) + mini_portile2 (2.8.9) + minitest (5.27.0) + mize (0.6.1) + mobility (1.3.2) i18n (>= 0.6.10, < 2) request_store (~> 1.0) - msgpack (1.7.2) - multi_json (1.15.0) - multi_xml (0.6.0) - mutex_m (0.2.0) + msgpack (1.8.0) + multi_json (1.19.1) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) nested-hstore (0.1.2) activerecord activesupport nested_form (0.3.2) - net-imap (0.4.20) + net-imap (0.6.3) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-scp (4.0.0) + 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.0) + net-smtp (0.5.1) net-protocol - net-ssh (7.2.3) - nio4r (2.7.3) - nokogiri (1.18.8) + net-ssh (7.3.2) + nio4r (2.7.5) + nokogiri (1.19.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) numerizer (0.1.1) - oj (3.16.4) + oj (3.16.16) bigdecimal (>= 3.0) + ostruct (>= 0.2) orm_adapter (0.5.0) + ostruct (0.6.3) paper_trail (15.1.0) activerecord (>= 6.1) request_store (~> 1.4) - parallel (1.25.1) - parser (3.3.4.0) + parallel (1.28.0) + parser (3.3.11.1) ast (~> 2.4.1) racc pdf-reader (1.4.1) @@ -374,69 +391,76 @@ GEM ruby-rc4 ttfunk pdfkit (0.8.7.3) - pg (1.5.7) + pg (1.6.3) pg_array_parser (0.0.9) - pg_search (2.3.6) - activerecord (>= 5.2) - activesupport (>= 5.2) + pg_search (2.3.7) + activerecord (>= 6.1) + activesupport (>= 6.1) + pp (0.6.3) + prettyprint prawn (0.13.2) pdf-reader (~> 1.2) ruby-rc4 ttfunk (~> 1.0.3) - psych (5.1.2) + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date stringio - public_suffix (6.0.1) - puma (5.6.8) + public_suffix (7.0.5) + puma (5.6.9) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.14) + rack (2.2.23) rack-cors (2.0.2) rack (>= 2.0.0) rack-mini-profiler (2.3.4) rack (>= 1.2.0) rack-session (1.0.2) rack (< 3) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rackup (1.0.0) + rackup (1.0.1) rack (< 3) webrick - rails (7.1.3.4) - actioncable (= 7.1.3.4) - actionmailbox (= 7.1.3.4) - actionmailer (= 7.1.3.4) - actionpack (= 7.1.3.4) - actiontext (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activemodel (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rails (7.2.3.1) + actioncable (= 7.2.3.1) + actionmailbox (= 7.2.3.1) + actionmailer (= 7.2.3.1) + actionpack (= 7.2.3.1) + actiontext (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activemodel (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) bundler (>= 1.15.0) - railties (= 7.1.3.4) + railties (= 7.2.3.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) - irb + railties (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.2.1) + rake (13.3.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -444,33 +468,37 @@ GEM ffi rbnacl-libsodium (1.0.16) rbnacl (>= 3.0.1) - rdoc (6.7.0) + rdoc (7.2.0) + erb psych (>= 4.0.0) + tsort + readline (0.0.4) + reline redis (4.8.1) - regexp_parser (2.9.2) - reline (0.5.9) + regexp_parser (2.12.0) + reline (0.6.3) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - rexml (3.3.9) - rspec (3.13.0) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.0) + rspec-core (3.13.6) rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.3) + rspec-rails (6.1.5) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -478,52 +506,55 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.1) - rubocop (1.65.1) + rspec-support (3.13.7) + rubocop (1.86.0) json (~> 2.3) - language_server-protocol (>= 3.17.0) + 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.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) - rubocop-capybara (2.21.0) - rubocop (~> 1.41) - rubocop-factory_bot (2.26.1) - rubocop (~> 1.61) - rubocop-minitest (0.35.1) - rubocop (>= 1.61, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-performance (1.21.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.1) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-factory_bot (2.28.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + 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.3) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails-omakase (1.0.0) - rubocop - rubocop-minitest - rubocop-performance - rubocop-rails - rubocop-rspec (3.0.3) - rubocop (~> 1.61) - rubocop-rspec_rails (2.30.0) - rubocop (~> 1.61) - rubocop-rspec (~> 3, >= 3.0.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) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + rubocop-rspec_rails (2.32.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-vips (2.2.2) + ruby-vips (2.3.0) ffi (~> 1.12) logger - rubyzip (2.3.2) - safely_block (0.4.0) + rubyzip (2.4.1) + safely_block (0.5.0) sass (3.4.25) sass-rails (5.1.0) railties (>= 5.2.0) @@ -531,6 +562,7 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + securerandom (0.4.1) selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) @@ -556,14 +588,14 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) sitemap_generator (6.3.0) builder (~> 3.0) slackistrano (0.1.9) capistrano (>= 3.0.1) json - spring (4.2.1) + spring (4.4.2) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -571,32 +603,40 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.23.0) + sshkit (1.25.0) base64 + logger net-scp (>= 1.1.2) net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) - ssrf_filter (1.1.2) - stringio (3.1.1) + ostruct + ssrf_filter (1.5.0) + stringio (3.2.0) strong_migrations (1.8.0) activerecord (>= 5.2) susy (2.2.14) sass (>= 3.3.0, < 3.5) sync (0.5.0) - term-ansicolor (1.11.1) - tins (~> 1.0) - terser (1.2.3) + term-ansicolor (1.11.3) + tins (~> 1) + terser (1.2.7) execjs (>= 0.3.0, < 3) - thor (1.3.2) - tilt (2.4.0) - timeout (0.4.3) - tins (1.33.0) + thor (1.5.0) + tilt (2.7.0) + timeout (0.6.1) + tins (1.52.0) bigdecimal + mize (~> 0.6) + readline sync + tsort (0.2.0) ttfunk (1.0.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + useragent (0.16.11) uuidtools (2.2.0) warden (1.2.9) rack (>= 2.0.9) @@ -609,17 +649,18 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0) - webrick (1.8.2) + webrick (1.9.2) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wicked (2.0.0) railties (>= 3.0.7) - wkhtmltopdf-binary (0.12.6.7) + wkhtmltopdf-binary (0.12.6.10) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.17) + zeitwerk (2.7.5) PLATFORMS ruby @@ -627,7 +668,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (= 0.8.4) active_storage_validations (~> 2.0) - acts-as-taggable-on (~> 10.0) + acts-as-taggable-on (~> 12.0) ahoy_matey (~> 5.0, >= 5.0.2) annotaterb (~> 4.10.2) appsignal (~> 3.13.1) @@ -685,7 +726,7 @@ DEPENDENCIES puma (~> 5.0) rack-cors rack-mini-profiler (~> 2.0) - rails (= 7.1.3.4) + rails (= 7.2.3.1) rails-controller-testing rbnacl (= 4.0.2) rbnacl-libsodium (= 1.0.16) From b0ecec85e2aeb9a03d122df766c04782e1ec9e2b Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 12:30:50 +0100 Subject: [PATCH 058/106] chore: bundle exec rails app:update (to Rails 7.2) --- bin/rubocop | 8 ++ bin/setup | 5 ++ config/environments/development.rb | 6 +- config/environments/test.rb | 2 + .../0_new_framework_defaults_7_2.rb | 70 +++++++++++++++ .../initializers/filter_parameter_logging.rb | 2 +- config/puma.rb | 53 ++++++----- public/404.html | 88 +++++++++---------- public/422.html | 88 +++++++++---------- public/500.html | 88 +++++++++---------- 10 files changed, 245 insertions(+), 165 deletions(-) create mode 100755 bin/rubocop create mode 100644 config/initializers/0_new_framework_defaults_7_2.rb diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 000000000..40330c0ff --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/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/bin/setup b/bin/setup index 3cd5a9d78..bb61d6ef0 100755 --- a/bin/setup +++ b/bin/setup @@ -3,6 +3,7 @@ require "fileutils" # path to your application root. APP_ROOT = File.expand_path("..", __dir__) +APP_NAME = "sapi" def system!(*args) system(*args, exception: true) @@ -30,4 +31,8 @@ FileUtils.chdir APP_ROOT do puts "\n== Restarting application server ==" system! "bin/rails restart" + + # puts "\n== Configuring puma-dev ==" + # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" + # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" end diff --git a/config/environments/development.rb b/config/environments/development.rb index f994d2d70..296375591 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -40,6 +40,8 @@ # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. @@ -80,12 +82,12 @@ # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true + config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true - # Raise error when a before_action's only/except options reference missing actions + # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true ### diff --git a/config/environments/test.rb b/config/environments/test.rb index 0ecd0a654..6d4d4e6f9 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -38,6 +38,8 @@ # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. diff --git a/config/initializers/0_new_framework_defaults_7_2.rb b/config/initializers/0_new_framework_defaults_7_2.rb new file mode 100644 index 000000000..b549c4a25 --- /dev/null +++ b/config/initializers/0_new_framework_defaults_7_2.rb @@ -0,0 +1,70 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 7.2 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `7.2`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Controls whether Active Job's `#perform_later` and similar methods automatically defer +# the job queuing to after the current Active Record transaction is committed. +# +# Example: +# Topic.transaction do +# topic = Topic.create(...) +# NewTopicNotificationJob.perform_later(topic) +# end +# +# In this example, if the configuration is set to `:never`, the job will +# be enqueued immediately, even though the `Topic` hasn't been committed yet. +# Because of this, if the job is picked up almost immediately, or if the +# transaction doesn't succeed for some reason, the job will fail to find this +# topic in the database. +# +# If `enqueue_after_transaction_commit` is set to `:default`, the queue adapter +# will define the behaviour. +# +# Note: Active Job backends can disable this feature. This is generally done by +# backends that use the same database as Active Record as a queue, hence they +# don't need this feature. +#++ +# Rails.application.config.active_job.enqueue_after_transaction_commit = :default + +### +# Adds image/webp to the list of content types Active Storage considers as an image +# Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, as they support gif, jpeg, and png. +# This is possible due to broad browser support for WebP, but older browsers and email clients may still not support +# WebP. Requires imagemagick/libvips built with WebP support. +#++ +# Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp] + +### +# Enable validation of migration timestamps. When set, an ActiveRecord::InvalidMigrationTimestampError +# will be raised if the timestamp prefix for a migration is more than a day ahead of the timestamp +# associated with the current time. This is done to prevent forward-dating of migration files, which can +# impact migration generation and other migration commands. +# +# Applications with existing timestamped migrations that do not adhere to the +# expected format can disable validation by setting this config to `false`. +#++ +# Rails.application.config.active_record.validate_migration_timestamps = true + +### +# Controls whether the PostgresqlAdapter should decode dates automatically with manual queries. +# +# Example: +# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date") #=> Date +# +# This query used to return a `String`. +#++ +# Rails.application.config.active_record.postgresql_adapter_decode_dates = true + +### +# Enables YJIT as of Ruby 3.3, to bring sizeable performance improvements. If you are +# deploying to a memory constrained environment you may want to set this to `false`. +#++ +# Rails.application.config.yjit = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c2d89e28a..c010b83dd 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -4,5 +4,5 @@ # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] diff --git a/config/puma.rb b/config/puma.rb index 58e1c205b..03c166f4c 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -2,34 +2,33 @@ # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } -min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies that the worker count should equal the number of processors in production. -if ENV['RAILS_ENV'] == 'production' - require 'concurrent-ruby' - worker_count = Integer(ENV.fetch('WEB_CONCURRENCY') { Concurrent.physical_processor_count }) - workers worker_count if worker_count > 1 -end - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# to prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch('PORT') { 3000 } - -# Specifies the `environment` that Puma will run in. -environment ENV.fetch('RAILS_ENV') { 'development' } +port ENV.fetch("PORT", 3000) -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } - -# Allow puma to be restarted by `rails restart` command. +# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/public/404.html b/public/404.html index f028a6e83..2be3af26f 100644 --- a/public/404.html +++ b/public/404.html @@ -1,57 +1,56 @@ - The page you were looking for doesn't exist (404) @@ -65,5 +64,4 @@

The page you were looking for doesn't exist.

If you are the application owner check the logs for more information.

- diff --git a/public/422.html b/public/422.html index 33dda348e..c08eac0d1 100644 --- a/public/422.html +++ b/public/422.html @@ -1,57 +1,56 @@ - The change you wanted was rejected (422) @@ -65,5 +64,4 @@

The change you wanted was rejected.

If you are the application owner check the logs for more information.

- diff --git a/public/500.html b/public/500.html index 90beade75..78a030af2 100644 --- a/public/500.html +++ b/public/500.html @@ -1,57 +1,56 @@ - We're sorry, but something went wrong (500) @@ -64,5 +63,4 @@

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

- From f4a4901eb3c1edd49ca4bec6eeb3f111b6a7d642 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 14:17:37 +0100 Subject: [PATCH 059/106] chore: Rails 7.2 changes informed by railsdiff and FastRuby and official Rails upgrade guides --- Gemfile | 6 ++- Gemfile.lock | 6 ++- app/controllers/application_controller.rb | 3 ++ app/models/ahoy/visit.rb | 2 + bin/docker-entrypoint | 5 ++ config/environments/test.rb | 3 ++ public/406-unsupported-browser.html | 66 +++++++++++++++++++++++ 7 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 public/406-unsupported-browser.html diff --git a/Gemfile b/Gemfile index 3c466031c..9e43c0d15 100644 --- a/Gemfile +++ b/Gemfile @@ -145,7 +145,11 @@ group :development do gem 'bcrypt_pbkdf', '1.1.0' gem 'ed25519', '1.2.4' - # @TODO: bring back when ruby updated to > 2.6 # gem 'net-ssh', '7.0.0.beta1' # openssl 3.0 compatibility @see https://stackoverflow.com/q/72068406/1090438 + ## + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem 'brakeman', require: false + + gem 'net-ssh', '7.0.0.beta1' end group :test, :development do diff --git a/Gemfile.lock b/Gemfile.lock index b8ba233dd..a401d600b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -138,6 +138,8 @@ GEM msgpack (~> 1.2) bootstrap-sass (2.3.2.2) sass (~> 3.2) + brakeman (8.0.4) + racc brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) @@ -366,7 +368,7 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.1) net-protocol - net-ssh (7.3.2) + net-ssh (7.0.0.beta1) nio4r (2.7.5) nokogiri (1.19.2) mini_portile2 (~> 2.8.2) @@ -677,6 +679,7 @@ DEPENDENCIES bcrypt_pbkdf (= 1.1.0) bootsnap (>= 1.4.4) bootstrap-sass (= 2.3.2.2) + brakeman byebug cancancan (~> 3.5) capistrano (= 3.18.0) @@ -715,6 +718,7 @@ DEPENDENCIES mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) + net-ssh (= 7.0.0.beta1) nokogiri (~> 1.18) oj (~> 3.16, >= 3.16.3) paper_trail (= 15.1.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 815be2559..dbb08db6e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,7 @@ class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern + before_action :track_who_does_it_current_user before_action :set_locale before_action :configure_permitted_parameters, if: :devise_controller? diff --git a/app/models/ahoy/visit.rb b/app/models/ahoy/visit.rb index 648a62e0e..fb3bfdeca 100644 --- a/app/models/ahoy/visit.rb +++ b/app/models/ahoy/visit.rb @@ -48,6 +48,8 @@ class Visit < ApplicationRecord # (https://github.com/ankane/ahoy/blob/v1.0.1/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) # However it has changed since version 1.4.0, from `id` to `visit_token`, and from `visitor_id` to `visitor_token`. # (https://github.com/ankane/ahoy/blob/v1.4.0/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb) + # Note that this will bypass custom methods on the original attribute, which thankfully we don't have: + # (https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#alias-attribute-now-bypasses-custom-methods-on-the-original-attribute) alias_attribute :visit_token, :id alias_attribute :visitor_token, :visitor_id end diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index cd970544f..0690f2576 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -4,6 +4,11 @@ if [[ "${@}" =~ "rails server" ]]; then rm -f ./tmp/pids/server.pid; fi +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then + export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" +fi + bundle install mkdir -p {./,spec/}public/downloads/checklist diff --git a/config/environments/test.rb b/config/environments/test.rb index 6d4d4e6f9..67ea3090c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -29,6 +29,9 @@ config.action_controller.perform_caching = false config.cache_store = :null_store + # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#all-tests-now-respect-the-active-job-queue-adapter-config + config.active_job.queue_adapter = :test + # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 000000000..7cf1e168e --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,66 @@ + + + + Your browser is not supported (406) + + + + + + +
+
+

Your browser is not supported.

+

Please upgrade your browser to continue.

+
+
+ + From 8815168a7d16aa5058aebcf68fbc8ffb03aa1f11 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 15:37:31 +0100 Subject: [PATCH 060/106] chore: more dependency upgrades, to fix a sprockets build issue --- Gemfile | 30 ++++++------- Gemfile.lock | 69 ++++++++++++++++-------------- app/views/layouts/species.html.erb | 2 +- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/Gemfile b/Gemfile index 9e43c0d15..410277377 100644 --- a/Gemfile +++ b/Gemfile @@ -14,15 +14,10 @@ gem 'rack-cors' gem 'puma', '~> 5.0' # Use SCSS for stylesheets -# TODO: Can't upgrade sass-rails to 6.0, it raise the following error when running `RAILS_ENV=staging rake assets:precompile`. -# SassC::SyntaxError: Error: Invalid CSS after "...in-bottom:-3px;": expected "}", was ".margin-bottom:-3px" -# on line 3712:5063 of stdin -# >> ction=135,Strength=3)";_margin-bottom:-3px;.margin-bottom:-3px;}/*!Add round -# gem 'sass-rails', '>= 6' -gem 'sass-rails', '~> 5.0' +gem 'sass-rails', '~> 6' # https://stackoverflow.com/questions/55213868/rails-6-how-to-disable-webpack-and-use-sprockets-instead -gem 'sprockets', '3.7.2' +gem 'sprockets', '~> 4' gem 'sprockets-rails', require: 'sprockets/railtie' # Use Terser as compressor for JavaScript assets @@ -98,17 +93,22 @@ gem 'bootsnap', '>= 1.4.4', require: false # To use Jbuilder templates for JSON # gem 'jbuilder', '~> 2.7' +gem 'erb', '~> 6.0.2' + group :development do + ## # Adds comments at the top of models describing table column # (replaces annotate) - gem 'annotaterb', '~> 4.10.2' + gem 'annotaterb', '~> 4.22.0' + ## # Access an interactive console on exception pages or by calling 'console' anywhere in the code. - gem 'web-console', '>= 4.1.0' + gem 'web-console' + # Display performance information such as SQL time and flame graphs for each request in your browser. # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md - gem 'rack-mini-profiler', '~> 2.0' - gem 'listen', '~> 3.3' + gem 'rack-mini-profiler', '~> 4.0.1' + gem 'listen', '~> 3.10.0' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' @@ -149,11 +149,11 @@ group :development do # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem 'brakeman', require: false - gem 'net-ssh', '7.0.0.beta1' + gem 'net-ssh', '7.3.2' end group :test, :development do - gem 'rspec-rails', '~> 6.1', '>= 6.1.1' + gem 'rspec-rails', '~> 7.1' gem 'rspec-collection_matchers', '~> 1.2', '>= 1.2.1' gem 'json_spec', '~> 1.1', '>= 1.1.5' gem 'database_cleaner', '~> 2.0', '>= 2.0.2' @@ -170,7 +170,7 @@ group :test do gem 'webdrivers' gem 'rails-controller-testing' - gem 'factory_bot_rails', '5.2.0' + gem 'factory_bot_rails', '~> 6.5.1' gem 'simplecov', '~> 0.22.0', require: false gem 'coveralls_reborn', '~> 0.28.0', require: false end @@ -178,7 +178,7 @@ end gem 'geoip', '1.3.5' # TODO: no change logs, no idea if safe to update. Latest version is 1.6.4 @ 2018 gem 'request_store', '~> 1.5', '>= 1.5.1' -gem 'paper_trail', '15.1.0' +gem 'paper_trail', '~> 17.0.0' gem 'dotenv-rails', '2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index a401d600b..17e836f52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,9 @@ GEM safely_block (>= 0.4) airbrussh (1.6.1) sshkit (>= 1.6.1, != 1.7.0) - annotaterb (4.10.2) + annotaterb (4.22.0) + activerecord (>= 6.0.0) + activesupport (>= 6.0.0) appsignal (3.13.1) rack ast (2.4.3) @@ -252,11 +254,11 @@ GEM et-orbi (1.4.0) tzinfo execjs (2.10.1) - factory_bot (5.2.0) - activesupport (>= 4.2.0) - factory_bot_rails (5.2.0) - factory_bot (~> 5.2.0) - railties (>= 4.2.0) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) ffi (1.17.4) file_exists (0.2.0) fugit (1.12.1) @@ -368,7 +370,7 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.1) net-protocol - net-ssh (7.0.0.beta1) + net-ssh (7.3.2) nio4r (2.7.5) nokogiri (1.19.2) mini_portile2 (~> 2.8.2) @@ -379,8 +381,8 @@ GEM ostruct (>= 0.2) orm_adapter (0.5.0) ostruct (0.6.3) - paper_trail (15.1.0) - activerecord (>= 6.1) + paper_trail (17.0.0) + activerecord (>= 7.1) request_store (~> 1.4) parallel (1.28.0) parser (3.3.11.1) @@ -417,7 +419,7 @@ GEM rack (2.2.23) rack-cors (2.0.2) rack (>= 2.0.0) - rack-mini-profiler (2.3.4) + rack-mini-profiler (4.0.1) rack (>= 1.2.0) rack-session (1.0.2) rack (< 3) @@ -500,10 +502,10 @@ GEM rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.5) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (7.1.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) @@ -558,12 +560,16 @@ GEM rubyzip (2.4.1) safely_block (0.5.0) sass (3.4.25) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt securerandom (0.4.1) selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) @@ -598,7 +604,7 @@ GEM capistrano (>= 3.0.1) json spring (4.4.2) - sprockets (3.7.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.5.2) @@ -672,7 +678,7 @@ DEPENDENCIES active_storage_validations (~> 2.0) acts-as-taggable-on (~> 12.0) ahoy_matey (~> 5.0, >= 5.0.2) - annotaterb (~> 4.10.2) + annotaterb (~> 4.22.0) appsignal (~> 3.13.1) aws-sdk-s3 (~> 1.143) base64 (= 0.1.1) @@ -702,7 +708,8 @@ DEPENDENCIES ember-data-source (= 1.13.0) ember-rails (~> 0.21.0) ember-source (= 1.8.0) - factory_bot_rails (= 5.2.0) + erb (~> 6.0.2) + factory_bot_rails (~> 6.5.1) file_exists (~> 0.2.0) geoip (= 1.3.5) gon (~> 6.4) @@ -714,14 +721,14 @@ DEPENDENCIES json_spec (~> 1.1, >= 1.1.5) kaminari (~> 1.2, >= 1.2.2) launchy (= 2.4.3) - listen (~> 3.3) + listen (~> 3.10.0) mobility (~> 1.2, >= 1.2.9) nested-hstore (~> 0.1.2) nested_form (~> 0.3.2) - net-ssh (= 7.0.0.beta1) + net-ssh (= 7.3.2) nokogiri (~> 1.18) oj (~> 3.16, >= 3.16.3) - paper_trail (= 15.1.0) + paper_trail (~> 17.0.0) pdfkit (~> 0.8.7.3) pg (~> 1.5, >= 1.5.4) pg_array_parser (~> 0.0.9) @@ -729,7 +736,7 @@ DEPENDENCIES prawn (= 0.13.2) puma (~> 5.0) rack-cors - rack-mini-profiler (~> 2.0) + rack-mini-profiler (~> 4.0.1) rails (= 7.2.3.1) rails-controller-testing rbnacl (= 4.0.2) @@ -738,7 +745,7 @@ DEPENDENCIES request_store (~> 1.5, >= 1.5.1) responders (~> 3.1, >= 3.1.1) rspec-collection_matchers (~> 1.2, >= 1.2.1) - rspec-rails (~> 6.1, >= 6.1.1) + rspec-rails (~> 7.1) rubocop rubocop-capybara rubocop-factory_bot @@ -747,7 +754,7 @@ DEPENDENCIES rubocop-rspec rubocop-rspec_rails rubyzip (~> 2.3, >= 2.3.2) - sass-rails (~> 5.0) + sass-rails (~> 6) selenium-webdriver (>= 4.0.0.rc1) sidekiq (< 7) sidekiq-cron (~> 1.12) @@ -757,13 +764,13 @@ DEPENDENCIES sitemap_generator (~> 6.3) slackistrano (= 0.1.9) spring - sprockets (= 3.7.2) + sprockets (~> 4) sprockets-rails strong_migrations (~> 1.7) susy (~> 2.2, >= 2.2.14) terser (~> 1.2.3) uuidtools (~> 2.2) - web-console (>= 4.1.0) + web-console webdrivers wicked (= 2.0.0) wkhtmltopdf-binary (~> 0.12.6.6) diff --git a/app/views/layouts/species.html.erb b/app/views/layouts/species.html.erb index 054f63979..eccb413dc 100644 --- a/app/views/layouts/species.html.erb +++ b/app/views/layouts/species.html.erb @@ -8,7 +8,7 @@ - <%= stylesheet_link_tag "species" %> + <%= stylesheet_link_tag "species" %> From 5fdd94d373e1e2e08f31c22512562919f80068ce Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 15:50:02 +0100 Subject: [PATCH 061/106] chore: upgrade target version in .rubocop.yml --- .rubocop.yml | 4 ++-- config/initializers/0_new_framework_defaults_7_2.rb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0d729d038..e0992f781 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,8 +12,8 @@ require: Rails: Enabled: true AllCops: - TargetRubyVersion: 3.2.5 - TargetRailsVersion: 7.1 + TargetRubyVersion: 3.2 + TargetRailsVersion: 7.2 Style/StringLiterals: EnforcedStyle: single_quotes diff --git a/config/initializers/0_new_framework_defaults_7_2.rb b/config/initializers/0_new_framework_defaults_7_2.rb index b549c4a25..683cc7b63 100644 --- a/config/initializers/0_new_framework_defaults_7_2.rb +++ b/config/initializers/0_new_framework_defaults_7_2.rb @@ -32,7 +32,7 @@ # backends that use the same database as Active Record as a queue, hence they # don't need this feature. #++ -# Rails.application.config.active_job.enqueue_after_transaction_commit = :default +Rails.application.config.active_job.enqueue_after_transaction_commit = :default ### # Adds image/webp to the list of content types Active Storage considers as an image @@ -40,7 +40,7 @@ # This is possible due to broad browser support for WebP, but older browsers and email clients may still not support # WebP. Requires imagemagick/libvips built with WebP support. #++ -# Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp] +Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp] ### # Enable validation of migration timestamps. When set, an ActiveRecord::InvalidMigrationTimestampError @@ -51,7 +51,7 @@ # Applications with existing timestamped migrations that do not adhere to the # expected format can disable validation by setting this config to `false`. #++ -# Rails.application.config.active_record.validate_migration_timestamps = true +Rails.application.config.active_record.validate_migration_timestamps = true ### # Controls whether the PostgresqlAdapter should decode dates automatically with manual queries. @@ -61,10 +61,10 @@ # # This query used to return a `String`. #++ -# Rails.application.config.active_record.postgresql_adapter_decode_dates = true +Rails.application.config.active_record.postgresql_adapter_decode_dates = true ### # Enables YJIT as of Ruby 3.3, to bring sizeable performance improvements. If you are # deploying to a memory constrained environment you may want to set this to `false`. #++ -# Rails.application.config.yjit = true +Rails.application.config.yjit = true From 3dddf49a9ed57ed79baa15a57c20979e4c51cab0 Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Wed, 8 Apr 2026 15:55:48 +0100 Subject: [PATCH 062/106] chore: add Rails 7.2 upgrade to changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c88f2f20..9eff57692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### 1.22.0 + +**Rails 7.2 Upgrade** +The primary goal of this release is to upgrade the Rails version without causing +any breaking changes to functionality. + +* Upgrades Rails 7.1.3.4 to 7.2.3.1 +* Some dependency updates/changes allowed or required by the above: + * Upgraded `sprockets` from 3.7.2 to 4.0.3 + * Upgraded `papertrail` from 15.1.0 to 17.0.0 + * Upgraded `acts-as-taggable-on` from 10.0.0 to 12.0.0 + ### 1.21.2 **Species+** From bad866e4c4ebb3180bfa338e140f80a5c60d798b Mon Sep 17 00:00:00 2001 From: Daniel Perrett Date: Thu, 9 Apr 2026 11:32:25 +0100 Subject: [PATCH 063/106] fix: Open Sans is not loading after sprockets upgrade https://github.com/rails/sprockets/issues/785 --- app/assets/stylesheets/mobile/mobile.scss | 2 -- app/assets/stylesheets/species/all.scss | 1 - app/views/layouts/mobile.html.erb | 4 ++++ app/views/layouts/species.html.erb | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/mobile/mobile.scss b/app/assets/stylesheets/mobile/mobile.scss index b4a7ddb10..efff1051d 100644 --- a/app/assets/stylesheets/mobile/mobile.scss +++ b/app/assets/stylesheets/mobile/mobile.scss @@ -1,6 +1,4 @@ // Settings (mobile-first) -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap'); - $navy: #253848; $black: #2D2D2D; $medium-grey: #dddddd; diff --git a/app/assets/stylesheets/species/all.scss b/app/assets/stylesheets/species/all.scss index b28ec7f5e..a18e844fb 100755 --- a/app/assets/stylesheets/species/all.scss +++ b/app/assets/stylesheets/species/all.scss @@ -3,7 +3,6 @@ License: none (public domain) */ -@import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,600,700); @import './variables'; html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { diff --git a/app/views/layouts/mobile.html.erb b/app/views/layouts/mobile.html.erb index f37c2e414..eef38bf48 100644 --- a/app/views/layouts/mobile.html.erb +++ b/app/views/layouts/mobile.html.erb @@ -7,6 +7,10 @@ + + <%= stylesheet_link_tag "mobile" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> diff --git a/app/views/layouts/species.html.erb b/app/views/layouts/species.html.erb index eccb413dc..10a4f6590 100644 --- a/app/views/layouts/species.html.erb +++ b/app/views/layouts/species.html.erb @@ -8,6 +8,10 @@ + + <%= stylesheet_link_tag "species" %>