Skip to content

Commit 65b4f55

Browse files
authored
CRAVEX Jira workflow integration #350 (#375)
Signed-off-by: tdruez <[email protected]>
1 parent 4851d28 commit 65b4f55

File tree

14 files changed

+778
-144
lines changed

14 files changed

+778
-144
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ Release notes
5050
Documentation: https://dejacode.readthedocs.io/en/latest/integrations-github.html
5151
https://github.com/aboutcode-org/dejacode/issues/346
5252

53+
- Add Jira workflow Request integration.
54+
https://github.com/aboutcode-org/dejacode/issues/350
55+
5356
### Version 5.3.0
5457

5558
- Rename ProductDependency is_resolved to is_pinned.

dje/admin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,7 @@ class DataspaceConfigurationForm(forms.ModelForm):
10591059
"purldb_api_key",
10601060
"github_token",
10611061
"gitlab_token",
1062+
"jira_token",
10621063
]
10631064

10641065
def __init__(self, *args, **kwargs):
@@ -1115,6 +1116,15 @@ class DataspaceConfigurationInline(DataspacedFKMixin, admin.StackedInline):
11151116
]
11161117
},
11171118
),
1119+
(
1120+
"Jira Integration",
1121+
{
1122+
"fields": [
1123+
"jira_user",
1124+
"jira_token",
1125+
]
1126+
},
1127+
),
11181128
]
11191129
can_delete = False
11201130

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.4 on 2025-08-01 12:33
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dje', '0009_dataspaceconfiguration_gitlab_token'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='dataspaceconfiguration',
15+
name='jira_token',
16+
field=models.CharField(blank=True, help_text='API token generated from your Atlassian account, used to authenticate API requests to Jira Cloud. Keep this token secure.', max_length=255, verbose_name='Jira API token'),
17+
),
18+
migrations.AddField(
19+
model_name='dataspaceconfiguration',
20+
name='jira_user',
21+
field=models.CharField(blank=True, help_text='The email address associated with your Jira account. Used together with the API token to authenticate API requests.', max_length=255, verbose_name='Jira user email'),
22+
),
23+
]

dje/models.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,26 @@ class DataspaceConfiguration(models.Model):
539539
),
540540
)
541541

542+
jira_user = models.CharField(
543+
_("Jira user email"),
544+
max_length=255,
545+
blank=True,
546+
help_text=_(
547+
"The email address associated with your Jira account. "
548+
"Used together with the API token to authenticate API requests."
549+
),
550+
)
551+
552+
jira_token = models.CharField(
553+
_("Jira API token"),
554+
max_length=255,
555+
blank=True,
556+
help_text=_(
557+
"API token generated from your Atlassian account, used to authenticate "
558+
"API requests to Jira Cloud. Keep this token secure."
559+
),
560+
)
561+
542562
def __str__(self):
543563
return f"Configuration for {self.dataspace}"
544564

@@ -926,7 +946,7 @@ def update(self, **kwargs):
926946

927947
def raw_update(self, **kwargs):
928948
"""
929-
Perform a direct SQL UPDATE on this instance.
949+
Perform a direct SQL UPDATE on this instance.
930950
931951
This method updates the specified fields in the database without triggering
932952
the ``save()`` lifecycle or related signals. It bypasses field validation and

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Welcome to the very start of your DejaCode journey!
5050

5151
integrations-github
5252
integrations-gitlab
53+
integrations-jira
5354

5455
.. toctree::
5556
:maxdepth: 1

docs/integrations-jira.rst

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
.. _integrations_jira:
2+
3+
Jira Cloud Integration
4+
======================
5+
6+
DejaCode's integration with Jira allows you to automatically forward
7+
**Workflow Requests** to Jira **Issues**.
8+
This behavior can be selectively applied to any **Request Template** of your choice.
9+
10+
Prerequisites
11+
-------------
12+
13+
- A **Jira Cloud project** that you want to integrate with DejaCode.
14+
- A **Jira user account** with sufficient permissions
15+
(at least *Create Issues* and *Edit Issues*) in that project.
16+
17+
Create Custom "DejaCode Request" Work Type
18+
------------------------------------------
19+
20+
.. warning::
21+
22+
This is required for the integration to function properly.
23+
24+
To create the custom work type in Jira:
25+
26+
1. Navigate to your **Project settings** → **Work types**
27+
2. Click **+ Add work type**
28+
3. Set the name to: ``DejaCode Request``
29+
4. Click **Create**
30+
31+
Create "Closed" Status
32+
----------------------
33+
34+
.. warning::
35+
36+
This is required for the integration to function properly.
37+
38+
This status will be set on the Jira issue when the DejaCode Request is closed.
39+
40+
To create the **Closed** status in Jira:
41+
42+
1. Navigate to **Project settings** → **Work types**
43+
2. Select the ``DejaCode Request`` work type
44+
3. Click **Edit workflow**
45+
4. Click **Add status**
46+
5. Click **Create new status** tab
47+
6. Enter the name: ``Closed``
48+
7. Choose a category: ``Done``
49+
8. Click **Add**
50+
9. Click **Update workflow**
51+
52+
Jira API Token
53+
--------------
54+
55+
To enable integration, you need a Jira Cloud **API token** and the associated
56+
**user email**.
57+
58+
1. **Generate a Jira API Token**:
59+
60+
- Go to: https://id.atlassian.com/manage-profile/security/api-tokens
61+
- Click **"Create API token"**
62+
- Enter a descriptive label (e.g., ``DejaCode Integration``)
63+
- Click **Create** and then **Copy** the token
64+
65+
2. **Store Your Credentials Securely**:
66+
67+
- You will need both:
68+
69+
- Your **Jira user email** (the one used to log into Jira)
70+
- The **API token** you just generated
71+
72+
.. note::
73+
74+
The API token is required for authenticating to the Jira Cloud REST API.
75+
If your Jira instance is hosted on-prem (Jira Server/Data Center), the integration
76+
may not be supported without further customization.
77+
78+
DejaCode Dataspace Configuration
79+
--------------------------------
80+
81+
To use your Jira credentials in DejaCode:
82+
83+
1. Go to the **Administration dashboard**
84+
2. Navigate to **Dataspaces**, and select your Dataspace
85+
3. Scroll to the **Jira Integration** section under **Configuration**
86+
4. Enter:
87+
88+
- Your **Jira user email**
89+
- The **API token** you generated
90+
91+
5. Save the form
92+
93+
Activate Jira Integration on Request Templates
94+
----------------------------------------------
95+
96+
1. Go to the **Administration dashboard**
97+
2. Navigate to **Workflow** > **Request templates**
98+
3. Create or edit a Request Template in your Dataspace
99+
4. Set the **Issue Tracker ID** field to your Jira base URL with project key, e.g.::
100+
101+
https://YOUR-DOMAIN.atlassian.net/projects/PROJECTKEY
102+
https://YOUR-DOMAIN.atlassian.net/jira/software/projects/PROJECTKEY/summary
103+
104+
- This URL must point to your Jira Cloud instance
105+
106+
Once the integration is configured:
107+
108+
- New **Requests** using this template will be automatically pushed to Jira
109+
- Field updates (like title or priority) and **status changes** (e.g. closed) will be
110+
synced
111+
- New **Comments** on a DejaCode Request will be propagated to the Jira Issue

workflow/admin.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from django.contrib import messages
1212
from django.contrib.admin.utils import unquote
1313
from django.core.exceptions import PermissionDenied
14+
from django.core.exceptions import ValidationError
1415
from django.template.defaultfilters import pluralize
1516
from django.utils.translation import gettext as _
1617

1718
from dje.admin import DataspacedAdmin
1819
from dje.admin import dejacode_site
1920
from dje.forms import DataspacedAdminForm
2021
from workflow.inlines import QuestionInline
22+
from workflow.integrations import is_valid_issue_tracker_id
2123
from workflow.models import Priority
2224
from workflow.models import RequestTemplate
2325

@@ -56,6 +58,21 @@ class PriorityAdmin(DataspacedAdmin):
5658
save_as = False
5759

5860

61+
class RequestTemplateAdminForm(DataspacedAdminForm):
62+
def clean_issue_tracker_id(self):
63+
issue_tracker_id = self.cleaned_data.get("issue_tracker_id")
64+
if issue_tracker_id and not is_valid_issue_tracker_id(issue_tracker_id):
65+
raise ValidationError(
66+
[
67+
"Invalid issue tracker URL format. Supported formats include:",
68+
"• GitHub: https://github.com/ORG/REPO_NAME",
69+
"• GitLab: https://gitlab.com/GROUP/PROJECT_NAME",
70+
"• Jira: https://YOUR_DOMAIN.atlassian.net/projects/PROJECTKEY",
71+
]
72+
)
73+
return issue_tracker_id
74+
75+
5976
@admin.register(RequestTemplate, site=dejacode_site)
6077
class RequestTemplateAdmin(DataspacedAdmin):
6178
list_display = (
@@ -75,6 +92,7 @@ class RequestTemplateAdmin(DataspacedAdmin):
7592
"include_applies_to",
7693
"include_product",
7794
)
95+
form = RequestTemplateAdminForm
7896
inlines = (QuestionInline,)
7997
actions = [
8098
"copy_to",

workflow/integrations/__init__.py

Lines changed: 40 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,69 +6,56 @@
66
# See https://aboutcode.org for more information about AboutCode FOSS projects.
77
#
88

9-
from django.conf import settings
9+
import re
1010

11-
import requests
11+
from workflow.integrations.base import BaseIntegration
12+
from workflow.integrations.github import GitHubIntegration
13+
from workflow.integrations.gitlab import GitLabIntegration
14+
from workflow.integrations.jira import JiraIntegration
1215

13-
DEJACODE_SITE_URL = settings.SITE_URL.rstrip("/")
16+
__all__ = [
17+
"BaseIntegration",
18+
"GitHubIntegration",
19+
"GitLabIntegration",
20+
"JiraIntegration",
21+
"is_valid_issue_tracker_id",
22+
"get_class_for_tracker",
23+
"get_class_for_platform",
24+
]
1425

1526

16-
class BaseIntegration:
17-
"""Base class for managing issue tracker integrations from DejaCode requests."""
27+
GITHUB_PATTERN = re.compile(r"^https://github\.com/[^/]+/[^/]+/?$")
1828

19-
default_timeout = 10
29+
GITLAB_PATTERN = re.compile(r"^https://gitlab\.com/[^/]+/[^/]+/?$")
2030

21-
def __init__(self, dataspace):
22-
if not dataspace:
23-
raise ValueError("Dataspace must be provided.")
24-
self.dataspace = dataspace
25-
self.session = self.get_session()
31+
JIRA_PATTERN = re.compile(
32+
r"^https://[a-zA-Z0-9.-]+\.atlassian\.net(?:/[^/]+)*"
33+
r"/(?:projects|browse)/[A-Z][A-Z0-9]+(?:/[^/]*)*/*$"
34+
)
2635

27-
def get_session(self):
28-
session = requests.Session()
29-
session.headers.update(self.get_headers())
30-
return session
36+
ISSUE_TRACKER_PATTERNS = [
37+
GITHUB_PATTERN,
38+
GITLAB_PATTERN,
39+
JIRA_PATTERN,
40+
]
3141

32-
def get_headers(self):
33-
"""
34-
Return authentication headers specific to the integration.
35-
Must be implemented in subclasses.
36-
"""
37-
raise NotImplementedError
3842

39-
@staticmethod
40-
def make_issue_title(request):
41-
return f"[DEJACODE] {request.title}"
43+
def is_valid_issue_tracker_id(issue_tracker_id):
44+
return any(pattern.match(issue_tracker_id) for pattern in ISSUE_TRACKER_PATTERNS)
4245

43-
@staticmethod
44-
def make_issue_body(request):
45-
request_url = f"{DEJACODE_SITE_URL}{request.get_absolute_url()}"
46-
label_fields = [
47-
("📝 Request Template", request.request_template),
48-
("📦 Product Context", request.product_context),
49-
("📌 Applies To", request.content_object),
50-
("🙋 Submitted By", request.requester),
51-
("👤 Assigned To", request.assignee),
52-
("🚨 Priority", request.priority),
53-
("🗒️ Notes", request.notes),
54-
("🔗️ DejaCode URL", request_url),
55-
]
5646

57-
lines = []
58-
for label, value in label_fields:
59-
if value:
60-
lines.append(f"### {label}\n{value}")
47+
def get_class_for_tracker(issue_tracker_id):
48+
if "github.com" in issue_tracker_id:
49+
return GitHubIntegration
50+
elif "gitlab.com" in issue_tracker_id:
51+
return GitLabIntegration
52+
elif "atlassian.net" in issue_tracker_id:
53+
return JiraIntegration
6154

62-
lines.append("----")
6355

64-
for question in request.get_serialized_data_as_list():
65-
label = question.get("label")
66-
value = question.get("value")
67-
input_type = question.get("input_type")
68-
69-
if input_type == "BooleanField":
70-
value = "Yes" if str(value).lower() in ("1", "true", "yes") else "No"
71-
72-
lines.append(f"### {label}\n{value}")
73-
74-
return "\n\n".join(lines)
56+
def get_class_for_platform(platform):
57+
return {
58+
"github": GitHubIntegration,
59+
"gitlab": GitLabIntegration,
60+
"jira": JiraIntegration,
61+
}.get(platform)

0 commit comments

Comments
 (0)