diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml
index 99fa6de..a166fb6 100644
--- a/config/locales/js-en.yml
+++ b/config/locales/js-en.yml
@@ -46,7 +46,7 @@ en:
title: Quick snippets for Git
title: "Pull requests"
tab_prs:
- empty: There are no pull requests linked yet. Link an existing PR by using the code OP#%{wp_id} in the PR title/description or create a new PR.
+ empty: There are no pull requests linked yet. Reference this work package in the PR title or description using its ID (e.g. %{wp_id}) or a full OpenProject URL.
updated_on: Updated on
work_packages:
tab_name: "Gitea"
diff --git a/lib/open_project/gitea_integration/notification_handler/helper.rb b/lib/open_project/gitea_integration/notification_handler/helper.rb
index 61f9e7f..b27bd10 100644
--- a/lib/open_project/gitea_integration/notification_handler/helper.rb
+++ b/lib/open_project/gitea_integration/notification_handler/helper.rb
@@ -30,14 +30,24 @@ module OpenProject::GiteaIntegration
module NotificationHandler
module Helper
def extract_work_package_ids(text)
- host_name = Regexp.escape(Setting.host_name)
- wp_id = WorkPackage::SemanticIdentifier::ID_ROUTE_CONSTRAINT
- url_part = /http(?:s?):\/\/#{host_name}\/(?:\S+?\/)*(?:work_packages|wp)\/(#{wp_id})/
+ host_name = Regexp.escape(Setting.host_name)
+ numeric_id = /\d+/
+ semantic_id = WorkPackage::SemanticIdentifier::SEMANTIC_ID_PATTERN
+ any_id = WorkPackage::SemanticIdentifier::ID_ROUTE_CONSTRAINT
- wp_regex = /OP#(#{wp_id})|#{url_part}/
+ # Full OpenProject URL — matches both numeric and semantic IDs
+ url_part = /http(?:s?):\/\/#{host_name}\/(?:\S+?\/)*(?:work_packages|wp)\/(#{any_id})/
+
+ # OP# prefix — required for bare numeric IDs to avoid collision with
+ # Gitea issue numbers (e.g. "Fix #10" refers to a Gitea issue, not an OP WP)
+ op_prefix_numeric = /OP#(#{numeric_id})/
+
+ # Semantic identifiers (e.g. KS-10, PROJ-42) are unambiguous enough to
+ # match without any prefix, both with and without OP#
+ op_prefix_semantic = /(?:OP#)?(#{semantic_id})/
String(text)
- .scan(wp_regex)
+ .scan(/#{url_part}|#{op_prefix_numeric}|#{op_prefix_semantic}/)
.filter_map { |groups| groups.compact.first }
.uniq
end