Skip to content

Commit 98b7e37

Browse files
authored
Bug 1997384 - Create a mapping table in the JiraWebhookSync extension the map jira ids to bug ids
1 parent 91e6fc3 commit 98b7e37

File tree

8 files changed

+409
-33
lines changed

8 files changed

+409
-33
lines changed

extensions/JiraWebhookSync/Extension.pm

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use warnings;
1313

1414
use base qw(Bugzilla::Extension);
1515

16+
use Bugzilla::Bug;
17+
use Bugzilla::BugUrl;
18+
use Bugzilla::Extension::JiraWebhookSync::JiraBugMap;
1619
use Bugzilla::Logging;
1720
use Bugzilla::Util qw(trim);
1821

@@ -21,6 +24,27 @@ use List::Util qw(uniq);
2124
use Mojo::URL;
2225
use Mojo::Util qw(dumper);
2326

27+
# Creates/updates database schema for the extension
28+
sub db_schema_abstract_schema {
29+
my ($self, $args) = @_;
30+
$args->{schema}->{jira_bug_map} = {
31+
FIELDS => [
32+
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,},
33+
bug_id => {
34+
TYPE => 'INT3',
35+
NOTNULL => 1,
36+
REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}
37+
},
38+
jira_id => {TYPE => 'VARCHAR(255)', NOTNULL => 1,},
39+
jira_project_key => {TYPE => 'VARCHAR(100)', NOTNULL => 1,},
40+
],
41+
INDEXES => [
42+
jira_bug_map_bug_id_idx => {FIELDS => ['bug_id', 'jira_id'], TYPE => 'UNIQUE',},
43+
jira_bug_map_project_idx => ['jira_project_key'],
44+
],
45+
};
46+
}
47+
2448
# Adds the JiraWebhookSync configuration panel to the admin interface.
2549
# This hook allows administrators to configure Jira webhook synchronization settings.
2650
sub config_add_panels {
@@ -29,9 +53,46 @@ sub config_add_panels {
2953
$modules->{JiraWebhookSync} = 'Bugzilla::Extension::JiraWebhookSync::Config';
3054
}
3155

32-
# Modifies webhook payload before sending by adding configured whiteboard tags.
56+
# Intercepts see_also additions to check if they should be stored in the mapping table
57+
# instead of as regular see_also links. This is used to work around JBI design choices
58+
# that require the see_also value to not exist to prevent duplicate jira tickets.
59+
sub bug_start_of_update {
60+
my ($self, $args) = @_;
61+
my $new_bug = $args->{bug};
62+
63+
foreach my $see_also (@{$new_bug->see_also}) {
64+
65+
# Check if this see_also URL corresponds to a Jira ticket
66+
my ($jira_id, $project_key)
67+
= Bugzilla::Extension::JiraWebhookSync::JiraBugMap->extract_jira_info(
68+
$see_also->name);
69+
70+
next unless $jira_id && $project_key;
71+
72+
INFO("Intercepting see_also for Jira ticket: $jira_id (project: $project_key)");
73+
74+
# Add the jira id and project key to the jira_bug_map table unless it already exists
75+
my $existing_map
76+
= Bugzilla::Extension::JiraWebhookSync::JiraBugMap->get_by_bug_id($new_bug->id);
77+
if (!$existing_map) {
78+
INFO('Creating new Jira mapping for bug ' . $new_bug->id);
79+
Bugzilla::Extension::JiraWebhookSync::JiraBugMap->create({
80+
bug_id => $new_bug->id,
81+
jira_id => $jira_id,
82+
jira_project_key => $project_key,
83+
});
84+
}
85+
86+
# Remove the see_also entry from the new bug object
87+
$new_bug->remove_see_also($see_also);
88+
}
89+
}
90+
91+
# Modifies webhook payload before sending by adding configured whiteboard tags
92+
# and see_also values from the mapping table.
3393
# Checks if the bug's product/component matches any configuration rules and
3494
# automatically adds the corresponding whiteboard tag to the payload if matched.
95+
# Also checks if there's a Jira mapping for this bug and adds the Jira URL to see_also.
3596
sub webhook_before_send {
3697
my ($self, $args) = @_;
3798
my $webhook = $args->{webhook};
@@ -45,6 +106,34 @@ sub webhook_before_send {
45106
my $uri = Mojo::URL->new($webhook->url);
46107
return if $uri->host ne $hostname;
47108

109+
# Get the bug object from the payload
110+
my $bug_id = $payload->{bug}->{id};
111+
112+
INFO("Processing webhook for bug $bug_id to Jira host $hostname");
113+
114+
# Check if there's a Jira mapping for this bug
115+
my $jira_map
116+
= Bugzilla::Extension::JiraWebhookSync::JiraBugMap->get_by_bug_id($bug_id);
117+
118+
if ($jira_map) {
119+
120+
# Construct the Jira URL from the mapping
121+
my $jira_url = "https://$hostname/browse/" . $jira_map->jira_id;
122+
123+
INFO("Adding Jira see_also to webhook payload: $jira_url");
124+
125+
# Add the Jira URL to the see_also array in the payload
126+
if (!exists $payload->{bug}->{see_also}) {
127+
$payload->{bug}->{see_also} = [];
128+
}
129+
130+
# Only add if not already present
131+
my %existing_see_also = map { $_ => 1 } @{$payload->{bug}->{see_also}};
132+
if (!$existing_see_also{$jira_url}) {
133+
push @{$payload->{bug}->{see_also}}, $jira_url;
134+
}
135+
}
136+
48137
# Make copy of the current whiteboard value
49138
my $whiteboard = $payload->{bug}->{whiteboard};
50139

@@ -60,8 +149,7 @@ sub webhook_before_send {
60149
}
61150
}
62151

63-
$payload->{bug}->{whiteboard}
64-
= _add_whiteboard_tags($whiteboard, \@new_tags);
152+
$payload->{bug}->{whiteboard} = _add_whiteboard_tags($whiteboard, \@new_tags);
65153
}
66154

67155
# Adds a whiteboard tag to the whiteboard string if it doesn't already exist.
@@ -78,7 +166,7 @@ sub _add_whiteboard_tags {
78166
$whiteboard .= " [$new_tag]"; # Append new tag to the end
79167
}
80168

81-
return trim($whiteboard); # Trim whitespace before returning
169+
return trim($whiteboard); # Trim whitespace before returning
82170
}
83171

84172
# Checks if a bug matches the criteria defined in a rule.

extensions/JiraWebhookSync/lib/Config.pm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ sub get_param_list {
2727
default => '{}',
2828
checker => \&check_config,
2929
},
30+
{
31+
name => 'jira_webhook_sync_project_keys',
32+
type => 'l',
33+
default => '[]',
34+
checker => \&check_project_keys,
35+
},
3036
);
3137

3238
return @params;
@@ -40,4 +46,12 @@ sub check_config {
4046
return '';
4147
}
4248

49+
sub check_project_keys {
50+
my $config = shift;
51+
my $val = eval { decode_json($config) };
52+
return 'failed to parse JSON' unless defined $val;
53+
return 'value is not ARRAY' unless ref $val && ref $val eq 'ARRAY';
54+
return '';
55+
}
56+
4357
1;
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::Extension::JiraWebhookSync::JiraBugMap;
9+
10+
use 5.10.1;
11+
use strict;
12+
use warnings;
13+
14+
use base qw(Bugzilla::Object);
15+
16+
use Bugzilla;
17+
use Bugzilla::Bug;
18+
use Bugzilla::Error;
19+
use Bugzilla::Logging;
20+
use Bugzilla::Util qw(detaint_natural trim);
21+
22+
use JSON::MaybeXS qw(decode_json);
23+
use List::Util qw(none);
24+
use Mojo::URL;
25+
use Scalar::Util qw(blessed);
26+
27+
###############################
28+
#### Initialization ####
29+
###############################
30+
31+
use constant DB_TABLE => 'jira_bug_map';
32+
33+
use constant DB_COLUMNS => qw(
34+
id
35+
bug_id
36+
jira_id
37+
jira_project_key
38+
);
39+
40+
use constant UPDATE_COLUMNS => qw(
41+
jira_id
42+
jira_project_key
43+
);
44+
45+
use constant VALIDATORS => {
46+
bug_id => \&_check_bug_id,
47+
jira_id => \&_check_jira_id,
48+
jira_project_key => \&_check_jira_project_key,
49+
};
50+
51+
use constant VALIDATOR_DEPENDENCIES => {
52+
jira_id => ['jira_project_key'],
53+
};
54+
55+
###############################
56+
#### Accessors ######
57+
###############################
58+
59+
sub bug_id { return $_[0]->{bug_id}; }
60+
sub jira_id { return $_[0]->{jira_id}; }
61+
sub jira_project_key { return $_[0]->{jira_project_key}; }
62+
63+
sub bug {
64+
my ($self) = @_;
65+
return $self->{bug} //= Bugzilla::Bug->new($self->bug_id);
66+
}
67+
68+
###############################
69+
#### Mutators #####
70+
###############################
71+
72+
sub set_jira_id { $_[0]->set('jira_id', $_[1]); }
73+
sub set_jira_project_key { $_[0]->set('jira_project_key', $_[1]); }
74+
75+
###############################
76+
#### Validators #####
77+
###############################
78+
79+
sub _check_bug_id {
80+
my ($invocant, $bug_id) = @_;
81+
82+
$bug_id = trim($bug_id);
83+
detaint_natural($bug_id) || ThrowUserError('jira_bug_id_required');
84+
85+
return $bug_id;
86+
}
87+
88+
sub _check_jira_id {
89+
my ($invocant, $jira_id) = @_;
90+
91+
$jira_id = trim($jira_id);
92+
$jira_id || ThrowUserError('jira_id_required');
93+
94+
return $jira_id;
95+
}
96+
97+
sub _check_jira_project_key {
98+
my ($invocant, $project_key) = @_;
99+
100+
$project_key = trim($project_key);
101+
$project_key || ThrowUserError('jira_project_key_required');
102+
103+
return $project_key;
104+
}
105+
106+
###############################
107+
#### Methods #####
108+
###############################
109+
110+
# Get mapping by bug_id
111+
sub get_by_bug_id {
112+
my ($class, $bug_id) = @_;
113+
114+
return $class->new({
115+
condition => 'bug_id = ?',
116+
values => [$bug_id],
117+
});
118+
}
119+
120+
# Get mapping by jira_id
121+
sub get_by_jira_id {
122+
my ($class, $jira_id) = @_;
123+
124+
return $class->new({
125+
condition => 'jira_id = ?',
126+
values => [$jira_id],
127+
});
128+
}
129+
130+
# Extract Jira project key from a Jira URL or ID
131+
sub extract_jira_info {
132+
my ($class, $see_also) = @_;
133+
my $params = Bugzilla->params;
134+
135+
# Only return values if the hostname matches
136+
my $url = Mojo::URL->new($see_also);
137+
return (undef, undef) if $url->host ne $params->{jira_webhook_sync_hostname};
138+
139+
# Match pattern
140+
# https://jira.example.com/browse/PROJ-123
141+
my ($project_key, $jira_id);
142+
if ($url->path =~ m{^/browse/([[:upper:]]+-\d+)$}) {
143+
$jira_id = $1;
144+
($project_key) = $jira_id =~ /^([[:upper:]]+)-/;
145+
146+
# Return undef if project key is not in configured list
147+
if (none { $_ eq $project_key }
148+
@{decode_json($params->{jira_webhook_sync_project_keys} || '[]')})
149+
{
150+
return (undef, undef);
151+
}
152+
}
153+
154+
return ($jira_id, $project_key);
155+
}
156+
157+
1;

0 commit comments

Comments
 (0)