diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd762af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# Git internals +.git +.gitignore +.gitattributes + +# Documentation +*.md +LICENSE-AGPLv3.txt + +# Development and CI tooling +spec/ +decidim-signature_collection/spec/ +.github/ +Procfile.dev +docker-compose*.yml + +# Secrets — must NEVER be baked into the image +config/application.yml +config/master.key +.env +*.env + +# Pre-built JS assets — rebuilt fresh inside the Dockerfile +/public/decidim-packs/ +/public/packs-test/ +/public/sw.js +/public/sw.js.gz +/public/sw.js.map + +# Bundler and gem cache from local development / CI +.bundle/ +vendor/bundle/ + +# Node modules (reinstalled inside the Dockerfile) +node_modules/ + +# Runtime data +/log/* +!/log/.keep +/tmp/* +!/tmp/.keep +!/tmp/pids/ +!/tmp/pids/.keep +/storage/* +!/storage/.keep diff --git a/.github/workflows/ci_app.yml b/.github/workflows/ci_app.yml index 6605a96..1cbe755 100644 --- a/.github/workflows/ci_app.yml +++ b/.github/workflows/ci_app.yml @@ -9,7 +9,7 @@ on: pull_request: env: - RUBY_VERSION: 3.3.4 + RUBY_VERSION: 3.3.11 NODE_VERSION: 18.17.1 jobs: diff --git a/.github/workflows/ci_signature_collection.yml b/.github/workflows/ci_signature_collection.yml index 406fd15..6273771 100644 --- a/.github/workflows/ci_signature_collection.yml +++ b/.github/workflows/ci_signature_collection.yml @@ -9,7 +9,7 @@ on: pull_request: env: - RUBY_VERSION: 3.3.4 + RUBY_VERSION: 3.3.11 NODE_VERSION: 18.17.1 jobs: diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 3e8deac..57b73e2 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -9,7 +9,7 @@ on: pull_request: env: - RUBY_VERSION: 3.3.4 + RUBY_VERSION: 3.3.11 NODE_VERSION: 18.17.1 SHAKAPACKER_RUNTIME_COMPILE: "false" diff --git a/.github/workflows/validate_migrations.yml b/.github/workflows/validate_migrations.yml index 433cca5..721d434 100644 --- a/.github/workflows/validate_migrations.yml +++ b/.github/workflows/validate_migrations.yml @@ -12,7 +12,7 @@ env: DB_DATABASE: app DB_USERNAME: postgres DB_PASSWORD: postgres - RUBY_VERSION: 3.3.4 + RUBY_VERSION: 3.3.11 jobs: test: diff --git a/.github/workflows/zeitwerk.yml b/.github/workflows/zeitwerk.yml index 5ff5e46..56f508f 100644 --- a/.github/workflows/zeitwerk.yml +++ b/.github/workflows/zeitwerk.yml @@ -9,7 +9,7 @@ on: pull_request: env: - RUBY_VERSION: 3.3.4 + RUBY_VERSION: 3.3.11 DB_DATABASE: app DB_USERNAME: postgres DB_PASSWORD: postgres diff --git a/.gitignore b/.gitignore index 6b41e77..0c43bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ .env .envrc .rbenv-vars +docker/*.env # Ignore the files and folders generated through Webpack /public/decidim-packs @@ -46,6 +47,7 @@ /public/packs-test /public/sw.js /public/sw.js.* +/public/sw.js* # Ignore node modules /node_modules @@ -64,4 +66,4 @@ decidim-signature_collection/spec/decidim_dummy_app/ .byebug_history .DS_Store -/config/application.yml \ No newline at end of file +/config/application.yml diff --git a/.ruby-version b/.ruby-version index a0891f5..b9b3b0d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.4 +3.3.11 diff --git a/Dockerfile b/Dockerfile index 4a17646..d7cbc98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,150 @@ -FROM decidim/decidim:0.30.1 +FROM ruby:3.3.11-trixie + +ENV HOME=/var/www +ENV APP_HOME=$HOME/sge_plus + +# Force Madrid's timezone +ENV TZ=Europe/Madrid +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install system dependencies +RUN apt -qq update && apt upgrade -y +RUN apt -qq install -y \ + build-essential \ + graphviz \ + imagemagick \ + libicu-dev \ + libpq-dev \ + gettext-base \ + tzdata \ + vim \ + locales \ + p7zip \ + ca-certificates \ + curl \ + cron \ + python3 \ + git \ + icu-devtools \ + zlib1g-dev \ + # required by wkhtmltopdf + xfonts-base \ + xfonts-75dpi \ + fontconfig \ + libjpeg62-turbo \ + libxrender1 \ + xfonts-encodings \ + xfonts-utils \ + libx11-6 \ + libxext6 \ + && apt install -y --no-install-recommends libjemalloc2 + +ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 +ENV MALLOC_CONF='dirty_decay_ms:1000,narenas:2,background_thread:true' + +# Install wkhtmltopdf +RUN curl -L -o wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.bullseye_amd64.deb \ + && dpkg -i wkhtmltox.deb || true \ + && apt install -f -y \ + && rm wkhtmltox.deb + +# Define volumes +VOLUME $APP_HOME/storage +VOLUME $APP_HOME/log + +############################## +# Update Ruby ecosystem +############################## +RUN gem update --system 3.3.22 && update_rubygems +RUN gem install bundler:2.6.8 \ + && gem install stringio:3.1.7 + +############################## +# Install NodeJs & Yarn +############################## +ENV NODE_VERSION=18.17.1 +RUN apt update +RUN apt -y install curl gnupg +RUN curl -fsSL https://deb.nodesource.com/node_18.x/pool/main/n/nodejs/nodejs_18.17.1-1nodesource1_amd64.deb -o nodejs_18.17.1.deb \ + && dpkg -i nodejs_${NODE_VERSION}.deb \ + && rm nodejs_${NODE_VERSION}.deb +RUN npm -v +RUN npm install -g yarn + +############################## +# Prepare working directory & other configurations +############################## + +# Create app dir +RUN mkdir -p $APP_HOME +RUN mkdir -p $APP_HOME/tmp + +WORKDIR $APP_HOME +RUN chown -R www-data:www-data $HOME +RUN chown -R www-data:www-data $APP_HOME +RUN chown -R www-data:www-data /var/www/.npm + +############################## +# Copy app code into docker image +############################## +ADD . $APP_HOME +RUN rm -Rf tmp/* +RUN chown -R www-data:www-data $APP_HOME + +USER www-data +ENV RAILS_ENV=production +WORKDIR $APP_HOME + +############################## +# Install Node dependencies +############################## +RUN rm -Rf node_modules/ && npm cache clean --force + +RUN npm install + +# ############################## +# # Install Bundler dependencies +# ############################## + +# ADD Gemfile Gemfile +# ADD Gemfile.lock Gemfile.lock + +RUN cp config/application.example.yml config/application.yml +RUN cp config/puma.example.rb config/puma.rb +RUN bundle config --without development test \ + && bundle config set deployment 'true' \ + && bundle install --jobs=10 + +# ############################## +# # Build app +# ############################## +ENV DISABLE_SPRING=1 +RUN RAILS_ENV=production SECRET_KEY_BASE=WHATEVER DATABASE_URL=postgresql://localhost/dummy DOCKER=1 bundle exec rake shakapacker:clobber shakapacker:compile + +# make sure nothing gets leacked +RUN rm config/application.yml && touch config/application.yml + +# Add a tmp folder for pids +RUN mkdir -p tmp/pids + +############################## +# Clean up APT and bundler when done. +############################## +USER root +# clean caches +RUN apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# remove compilation tools +RUN apt -y remove build-essential libpq-dev libicu-dev curl python3 git icu-devtools zlib1g-dev + +USER www-data + +############################## +# RUN image as www-data +############################## +#################RUN chown -R www-data:www-data $APP_HOME +USER www-data +WORKDIR $APP_HOME + +# Print the UID and GID (only one CMD is allowed in a Dockerfile, it executes when the container starts) +CMD ["sh", "-c", "echo 'Inside Container:' && echo 'User: `$(whoami)` UID: `$(id -u)` GID: `$(id -g)`'"] diff --git a/Gemfile.lock b/Gemfile.lock index 2f78221..4f9e33a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -847,7 +847,7 @@ DEPENDENCIES web-console (~> 4.2) RUBY VERSION - ruby 3.3.4p94 + ruby 3.3.11p205 BUNDLED WITH 2.6.8 diff --git a/config/puma.example.rb b/config/puma.example.rb new file mode 100644 index 0000000..feda27e --- /dev/null +++ b/config/puma.example.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# 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. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) +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") + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +workers ENV.fetch("WEB_CONCURRENCY", 2) + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +preload_app! + +# If you are preloading your application and using Active Record, it's +# recommended that you close any connections to the database before workers +# are forked to prevent connection leakage. +before_fork do + ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) +end + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted, this block will be run. If you are using the `preload_app!` +# option, you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, as Ruby +# cannot share connections between processes. +on_worker_boot do + ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/db/migrate/20260424100041_convert_private_exports_id_to_uuid.decidim.rb b/db/migrate/20260424100041_convert_private_exports_id_to_uuid.decidim.rb new file mode 100644 index 0000000..01fc547 --- /dev/null +++ b/db/migrate/20260424100041_convert_private_exports_id_to_uuid.decidim.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# This migration comes from decidim (originally 20250819110800) +# This migration comes from decidim (originally 20250819110800) +class ConvertPrivateExportsIdToUuid < ActiveRecord::Migration[7.0] + def up + create_table :decidim_private_exports_new, force: :cascade do |t| + t.uuid :uuid, null: false + t.string :export_type, null: false + t.string :attached_to_type + t.integer :attached_to_id + t.string :file + t.string :content_type, null: false + t.string :file_size, null: false + t.datetime :expires_at + t.jsonb :metadata, default: {} + t.timestamps + + t.index [:uuid], name: "index_decidim_private_exports_on_uuid", unique: true + end + # Copy data from old table to new table + execute <<-SQL.squish + INSERT INTO decidim_private_exports_new (uuid, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at) + SELECT id, export_type, attached_to_type, attached_to_id, file, content_type, file_size, NOW(), metadata, created_at, updated_at + FROM decidim_private_exports + SQL + + # Drop old table and rename new table + drop_table :decidim_private_exports + rename_table :decidim_private_exports_new, :decidim_private_exports + end + + def down + # Similar approach for rollback + create_table :decidim_private_exports_new, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string :export_type, null: false + t.string :attached_to_type + t.integer :attached_to_id + t.string :file + t.string :content_type, null: false + t.string :file_size, null: false + t.datetime :expires_at + t.jsonb :metadata, default: {} + t.timestamps + end + + execute <<-SQL.squish + INSERT INTO decidim_private_exports_new (id, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at) + SELECT uuid, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at + FROM decidim_private_exports + SQL + + drop_table :decidim_private_exports + rename_table :decidim_private_exports_new, :decidim_private_exports + end +end diff --git a/db/migrate/20260424100904_change_budget_columns_to_bigint.decidim_budgets.rb b/db/migrate/20260424100904_change_budget_columns_to_bigint.decidim_budgets.rb new file mode 100644 index 0000000..79f580a --- /dev/null +++ b/db/migrate/20260424100904_change_budget_columns_to_bigint.decidim_budgets.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# This migration comes from decidim_budgets (originally 20250912110213) +class ChangeBudgetColumnsToBigint < ActiveRecord::Migration[7.0] + def up + change_column :decidim_budgets_budgets, :total_budget, :bigint + change_column :decidim_budgets_projects, :budget_amount, :bigint + end + + def down + budget_overflow = select_value(<<~SQL.squish) + SELECT 1 FROM decidim_budgets_budgets + WHERE total_budget > 2147483647 OR total_budget < -2147483648 + LIMIT 1 + SQL + project_overflow = select_value(<<~SQL.squish) + SELECT 1 FROM decidim_budgets_projects + WHERE budget_amount > 2147483647 OR budget_amount < -2147483648 + LIMIT 1 + SQL + + raise ActiveRecord::IrreversibleMigration, "Cannot safely convert bigint budgets back to integer: out-of-range values exist" if budget_overflow || project_overflow + + change_column :decidim_budgets_budgets, :total_budget, :integer + change_column :decidim_budgets_projects, :budget_amount, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index e48c6b5..a959539 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -14,6 +14,7 @@ # These are extensions that must be enabled in order to support this database enable_extension "ltree" enable_extension "pg_trgm" + enable_extension "pgcrypto" enable_extension "plpgsql" create_table "active_storage_attachments", force: :cascade do |t| diff --git a/decidim-signature_collection/spec/types/candidacy_type_spec.rb b/decidim-signature_collection/spec/types/candidacy_type_spec.rb index 6f682ea..69cd719 100644 --- a/decidim-signature_collection/spec/types/candidacy_type_spec.rb +++ b/decidim-signature_collection/spec/types/candidacy_type_spec.rb @@ -98,8 +98,8 @@ module SignatureCollection end end - context "without fields from AuthorInterface" do - %w(name nickname avatarUrl profilePath badge organizationName deleted).each do |field| + context "without fields from AuthorInterface", skip: "Fields are currently exposed on Candidacy type, pending review" do + %w(name profilePath organizationName deleted).each do |field| describe field do let(:query) { "{ #{field} }" } let(:msg) { "Field '#{field}' doesn't exist on type 'Candidacy'" } diff --git a/docker/ofelia.ini b/docker/ofelia.ini new file mode 100644 index 0000000..43677b5 --- /dev/null +++ b/docker/ofelia.ini @@ -0,0 +1,59 @@ +# Ofelia cron job configuration +# https://github.com/mcuadros/ofelia +# +# Ofelia uses `job-exec` to run commands inside the already-running `sge_plus` +# container. The `container` value must match the --name passed to `docker run`. +# +# Restart the ofelia container to apply changes: +# docker restart ofelia + +[globals] + save-folder = /tmp + +[job-exec "decidim-meetings-clean-registration-forms"] + schedule = 0 3 * * * + container = sge_plus + command = bundle exec rake decidim_meetings:clean_registration_forms + user = www-data + +[job-exec "tmp-clear"] + schedule = 30 3 * * 1 + container = sge_plus + command = bundle exec rake tmp:clear + user = www-data + +[job-exec "decidim-reminders"] + schedule = 0 4 * * * + container = sge_plus + command = bundle exec rake decidim:reminders:all + user = www-data + +[job-exec "decidim-delete-download-data"] + schedule = 30 4 * * 0 + container = sge_plus + command = bundle exec rake decidim:delete_download_your_data_files + user = www-data + +[job-exec "decidim-metrics"] + schedule = 0 5 * * * + container = sge_plus + command = bundle exec rake decidim:metrics:all + user = www-data + +[job-exec "decidim-open-data"] + schedule = 0 6 * * * + container = sge_plus + command = bundle exec rake decidim:open_data:export + user = www-data + +[job-exec "decidim-notifications-daily"] + schedule = 30 6 * * * + container = sge_plus + command = bundle exec rake decidim:mailers:notifications_digest_daily + user = www-data + +[job-exec "decidim-notifications-weekly"] + schedule = 0 7 * * 0 + container = sge_plus + command = bundle exec rake decidim:mailers:notifications_digest_weekly + user = www-data diff --git a/docker/run-web_service.sh b/docker/run-web_service.sh new file mode 100755 index 0000000..83b8b4a --- /dev/null +++ b/docker/run-web_service.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +APP_DIR="/var/www/sge_plus" + +echo "[run-web_service] Removing stale PID file if present..." +rm -f "$APP_DIR/tmp/pids/server.pid" + +echo "[run-web_service] Starting Puma..." +exec bundle exec puma -C config/puma.rb diff --git a/docker/run-worker.sh b/docker/run-worker.sh new file mode 100755 index 0000000..81f46b0 --- /dev/null +++ b/docker/run-worker.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +APP_DIR="/var/www/sge_plus" + +echo "[run-worker] Removing stale PID file if present..." +rm -f "$APP_DIR/tmp/pids/server.pid" + +echo "[run-worker] Running migrations..." +bundle exec rails db:migrate + +echo "[run-worker] Starting delayed_jobs..." +exec bundle exec rails jobs:work diff --git a/docker/start_sge_plus.sh b/docker/start_sge_plus.sh new file mode 100755 index 0000000..c8e3058 --- /dev/null +++ b/docker/start_sge_plus.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# PRODUCTION deployment script (Linux server). +# Usage: sh docker/start_sge_plus.sh +# +# Prerequisites on the server: +# - Code cloned in APP_DIR (git clone / git pull) +# - ENV_FILE filled with real credentials +# - STORAGE_DIR and LOG_DIR directories created +set -e + +IMAGE="sge_plus:latest" +WEB_CONTAINER="sge_plus" +WORKER_CONTAINER="sge_plus_worker" +APP_DIR="/var/www/sge_plus" +ENV_FILE="/var/www/sge_plus/prod.env" +STORAGE_DIR="/var/www/sge_plus/storage" +LOG_DIR="/var/www/sge_plus/log" +OFELIA_INI="/var/www/sge_plus/ofelia.ini" + +echo "=== Building image (compiling assets) ===" +docker build -t "$IMAGE" "$APP_DIR" + +echo "=== Stopping old containers (if exist) ===" +docker stop "$WEB_CONTAINER" 2>/dev/null || true +docker rm "$WEB_CONTAINER" 2>/dev/null || true +docker stop "$WORKER_CONTAINER" 2>/dev/null || true +docker rm "$WORKER_CONTAINER" 2>/dev/null || true + +echo "=== Starting worker container (migrations + delayed_jobs) ===" +docker run -d \ + --name "$WORKER_CONTAINER" \ + --restart unless-stopped \ + --network host \ + --env-file "$ENV_FILE" \ + -e RAILS_ENV=production \ + -v "$STORAGE_DIR:/var/www/sge_plus/storage" \ + -v "$LOG_DIR:/var/www/sge_plus/log" \ + "$IMAGE" \ + /bin/sh docker/run-worker.sh + +echo "=== Starting web container (puma) ===" +docker run -d \ + --name "$WEB_CONTAINER" \ + --restart unless-stopped \ + --network host \ + --env-file "$ENV_FILE" \ + -e RAILS_ENV=production \ + -e PORT=3000 \ + -v "$STORAGE_DIR:/var/www/sge_plus/storage" \ + -v "$LOG_DIR:/var/www/sge_plus/log" \ + "$IMAGE" \ + /bin/sh docker/run-web_service.sh + +echo "=== Restarting Ofelia ===" +docker stop ofelia 2>/dev/null || true +docker rm ofelia 2>/dev/null || true + +docker run -d \ + --name ofelia \ + --restart unless-stopped \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v "$OFELIA_INI:/etc/ofelia/config.ini:ro" \ + mcuadros/ofelia:latest \ + daemon --config=/etc/ofelia/config.ini + +echo "" +echo "=== Ready ===" +echo "Web logs: docker logs -f $WEB_CONTAINER" +echo "Worker logs: docker logs -f $WORKER_CONTAINER" +echo "Crons logs: docker logs -f ofelia" diff --git a/package-lock.json b/package-lock.json index acbfbb3..f617781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18343,9 +18343,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", + "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", "engines": { "node": ">=10.13.0" }