From 80c150c2aad5450ce5fb55075d1b6a2369989dfa Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Wed, 10 Jun 2026 09:44:49 +0200 Subject: [PATCH 1/4] Fix retry loop in NameError job recovery initializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoodJob's `.discarded` scope is defined as `finished.where.not(error: nil)`, which matches any finished job with an error — including jobs with `error_event: :retried` that already have a pending retry scheduled. On every restart, the initializer was finding these already-retried jobs and calling `retry_job` again. Thankfully good_job has us covered and prevent a new retry job from being queued. This is visible with `ActionForStateMismatchError` or `PG::UniqueViolation` errors on each boot. Not harmful, but far from ideal. Symptoms visible in the logs on every restart: Failed to enqueue job for retry SomeJob (job id: b03926e2-...): PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "good_jobs_pkey" DETAIL: Key (id)=(a798492e-...) already exists. Exclude jobs with `error_event: :retried` so only truly stuck jobs (unhandled, retry_stopped, discarded) are re-queued. --- config/initializers/retry_failed_jobs_with_name_error.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/retry_failed_jobs_with_name_error.rb b/config/initializers/retry_failed_jobs_with_name_error.rb index add24ea1a17..9fbed7789bb 100644 --- a/config/initializers/retry_failed_jobs_with_name_error.rb +++ b/config/initializers/retry_failed_jobs_with_name_error.rb @@ -42,6 +42,7 @@ Rails.application.configure do GoodJob::Job .discarded + .where.not(error_event: GoodJob::ErrorEvents::RETRIED) # reject discarded jobs that were already retried .where("error LIKE ?", "NameError: uninitialized constant %") .filter do |job| # Only retry jobs with NameError related to the job class name. From 36c0c8ae5e1723cfc5f49322f0502cbe5878a5a0 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Thu, 11 Jun 2026 11:13:31 +0200 Subject: [PATCH 2/4] [JIM-112] Capital letters in user email or login break import with error. https://community.openproject.org/wp/JIM-112 --- app/models/import/jira_user.rb | 4 +- .../import/jira_import_projects_job.rb | 12 +++- spec/models/import/jira_user_spec.rb | 63 +++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/app/models/import/jira_user.rb b/app/models/import/jira_user.rb index 258b70fa2b6..fc657c4d4eb 100644 --- a/app/models/import/jira_user.rb +++ b/app/models/import/jira_user.rb @@ -51,8 +51,8 @@ module Import def try_to_find_existing_op_users op_attributes = to_op_attributes - User.where(login: op_attributes[:login]).or( - User.where(mail: op_attributes[:mail]) + User.by_login(op_attributes[:login]).or( + User.where("LOWER(mail) = ?", op_attributes[:mail]&.downcase) ) end diff --git a/app/workers/import/jira_import_projects_job.rb b/app/workers/import/jira_import_projects_job.rb index ee7b0a71ee3..2d1f6b5a4aa 100644 --- a/app/workers/import/jira_import_projects_job.rb +++ b/app/workers/import/jira_import_projects_job.rb @@ -384,10 +384,16 @@ module Import jira_user = Import::JiraUser.find_by(jira_user_key:, jira_import: @jira_import) if jira_user - JiraOpenProjectReference.find_by!( - jira_entity_class: "Import::JiraUser", + reference = JiraOpenProjectReference.find_by( + jira_entity_class: jira_user.class.to_s, jira_entity_id: jira_user.id - ).op_leg + ) + if reference + reference.op_leg + else + raise "Import::JiraOpenProjectReference with jira_entity_class #{jira_user.class} " \ + "and jira_entity_id #{jira_user.id} not found!" + end else raise "Import::JiraUser with jira_user_key #{jira_user_key} not found!" end diff --git a/spec/models/import/jira_user_spec.rb b/spec/models/import/jira_user_spec.rb index e0f2b00acec..8130c12a0a9 100644 --- a/spec/models/import/jira_user_spec.rb +++ b/spec/models/import/jira_user_spec.rb @@ -110,6 +110,69 @@ RSpec.describe Import::JiraUser do end end + describe "#try_to_find_existing_op_users" do + subject(:result) { jira_user.try_to_find_existing_op_users } + + let(:payload) { { "displayName" => "Test User", "name" => "testuser", "emailAddress" => "test@example.com" } } + + context "when no matching user exists" do + it "returns an empty relation" do + expect(result).to be_empty + end + end + + context "when a user with matching login exists (case-insensitive)" do + let!(:existing_user) { create(:user, login: "TestUser", mail: "other@example.com") } + + it "finds the user" do + expect(result).to contain_exactly(existing_user) + end + end + + context "when a user with matching email exists (case-insensitive)" do + let!(:existing_user) { create(:user, login: "otherlogin", mail: "TEST@EXAMPLE.COM") } + + it "finds the user" do + expect(result).to contain_exactly(existing_user) + end + end + + context "when a user matches both login and email" do + let!(:existing_user) { create(:user, login: "TESTUSER", mail: "TEST@example.com") } + + it "finds the user once" do + expect(result).to contain_exactly(existing_user) + end + end + + context "when different users match login and email respectively" do + let!(:user_by_login) { create(:user, login: "TESTUSER", mail: "different@example.com") } + let!(:user_by_email) { create(:user, login: "differentlogin", mail: "TEST@EXAMPLE.COM") } + + it "finds both users" do + expect(result).to contain_exactly(user_by_login, user_by_email) + end + end + + context "when email is nil in payload" do + let(:payload) { { "displayName" => "Test User", "name" => "testuser", "emailAddress" => nil } } + let!(:existing_user) { create(:user, login: "TESTUSER", mail: "any@example.com") } + + it "still finds users by login" do + expect(result).to contain_exactly(existing_user) + end + end + + context "when email separator is used" do + let(:payload) { { "displayName" => "Test User", "name" => "othername", "emailAddress" => "any@example.com" } } + let!(:existing_user) { create(:user, login: "testname", mail: "any+test@example.com") } + + it "considers the email to be different and does not find it this user account" do + expect(result).to be_empty + end + end + end + describe "#sanitize_name (private)" do subject(:jira_user) { described_class.new(payload: {}) } From de7d1c98bbae8e9eb4eca110787e727bd3760878 Mon Sep 17 00:00:00 2001 From: corinnaguenther <131807161+corinnaguenther@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:25:35 +0200 Subject: [PATCH 3/4] Update Release Notes (#23689) Section about releasing project IDs --- docs/release-notes/17-5-0/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/17-5-0/README.md b/docs/release-notes/17-5-0/README.md index 58e864e9df9..045a4986c89 100644 --- a/docs/release-notes/17-5-0/README.md +++ b/docs/release-notes/17-5-0/README.md @@ -49,9 +49,9 @@ Existing integrations such as GitHub and GitLab already support the new identifi [See our system admin guide for detailed information on how to manage work package identifiers](../../system-admin-guide/manage-work-packages/work-package-identifiers/). -#### Releasing unused numerical identifiers +#### Releasing unused project identifiers -When switching from the default numerical sequence to project-based work package identifiers, previously reserved numerical identifiers can be released again if they are no longer needed. This helps administrators avoid unnecessary gaps and keep numerical identifiers available if they later revert to the default sequence. +OpenProject allows administrators to release reserved project identifiers that are no longer needed. Please note that this option is currently only available when numerical work package identifiers are enabled. > [!NOTE] > Releasing an identifier cannot be undone. External links and integrations using it will stop resolving, and the name becomes available for any new project to claim. From 0b0b37b878d61ed071d326eee34c046f895e1604 Mon Sep 17 00:00:00 2001 From: OpenProject Actions CI Date: Fri, 12 Jun 2026 04:36:08 +0000 Subject: [PATCH 4/4] update locales from crowdin [ci skip] --- config/locales/crowdin/fr.yml | 2 +- modules/github_integration/config/locales/crowdin/js-cs.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/crowdin/fr.yml b/config/locales/crowdin/fr.yml index 476758e3e5d..bdba017720e 100644 --- a/config/locales/crowdin/fr.yml +++ b/config/locales/crowdin/fr.yml @@ -3983,7 +3983,7 @@ fr: label_enumerations: Énumérations label_enterprise: Entreprise label_enterprise_active_users: "%{current}/%{limit} utilisateurs actifs inscrits" - label_enterprise_edition: édition Enterprise + label_enterprise_edition: Édition Enterprise label_enterprise_support: Support Enterprise label_environment: Environnement label_estimates_and_progress: Estimations et progression diff --git a/modules/github_integration/config/locales/crowdin/js-cs.yml b/modules/github_integration/config/locales/crowdin/js-cs.yml index 8aac7a792bd..d87a0df5268 100644 --- a/modules/github_integration/config/locales/crowdin/js-cs.yml +++ b/modules/github_integration/config/locales/crowdin/js-cs.yml @@ -31,8 +31,8 @@ cs: label: Git snippets description: Kopírovat úryvky gitu do schránky git_actions: - branch_name: Název pobočky - commit_message: odevzdat zprávu + branch_name: Název větve + commit_message: Zpráva commitu cmd: Vytvořit větev s prázdným commitem title: Rychlé snippety pro Git copy_success: "✅ zkopírováno!"