+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ Release notes
Documentation: https://dejacode.readthedocs.io/en/latest/integrations-github.html
https://github.com/aboutcode-org/dejacode/issues/346

- Add Jira workflow Request integration.
https://github.com/aboutcode-org/dejacode/issues/350

### Version 5.3.0

- Rename ProductDependency is_resolved to is_pinned.
Expand Down
10 changes: 10 additions & 0 deletions dje/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ class DataspaceConfigurationForm(forms.ModelForm):
"purldb_api_key",
"github_token",
"gitlab_token",
"jira_token",
]

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -1115,6 +1116,15 @@ class DataspaceConfigurationInline(DataspacedFKMixin, admin.StackedInline):
]
},
),
(
"Jira Integration",
{
"fields": [
"jira_user",
"jira_token",
]
},
),
]
can_delete = False

Expand Down
23 changes: 23 additions & 0 deletions dje/migrations/0010_dataspaceconfiguration_jira_token_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-08-01 12:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dje', '0009_dataspaceconfiguration_gitlab_token'),
]

operations = [
migrations.AddField(
model_name='dataspaceconfiguration',
name='jira_token',
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'),
),
migrations.AddField(
model_name='dataspaceconfiguration',
name='jira_user',
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 changes: 21 additions & 1 deletion dje/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,26 @@ class DataspaceConfiguration(models.Model):
),
)

jira_user = models.CharField(
_("Jira user email"),
max_length=255,
blank=True,
help_text=_(
"The email address associated with your Jira account. "
"Used together with the API token to authenticate API requests."
),
)

jira_token = models.CharField(
_("Jira API token"),
max_length=255,
blank=True,
help_text=_(
"API token generated from your Atlassian account, used to authenticate "
"API requests to Jira Cloud. Keep this token secure."
),
)

def __str__(self):
return f"Configuration for {self.dataspace}"

Expand Down Expand Up @@ -926,7 +946,7 @@ def update(self, **kwargs):

def raw_update(self, **kwargs):
"""
Perform a direct SQL UPDATE on this instance.
Perform a direct SQL UPDATE on this instance.

This method updates the specified fields in the database without triggering
the ``save()`` lifecycle or related signals. It bypasses field validation and
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Welcome to the very start of your DejaCode journey!

integrations-github
integrations-gitlab
integrations-jira

.. toctree::
:maxdepth: 1
Expand Down
111 changes: 111 additions & 0 deletions docs/integrations-jira.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
.. _integrations_jira:

Jira Cloud Integration
======================

DejaCode's integration with Jira allows you to automatically forward
**Workflow Requests** to Jira **Issues**.
This behavior can be selectively applied to any **Request Template** of your choice.

Prerequisites
-------------

- A **Jira Cloud project** that you want to integrate with DejaCode.
- A **Jira user account** with sufficient permissions
(at least *Create Issues* and *Edit Issues*) in that project.

Create Custom "DejaCode Request" Work Type
------------------------------------------

.. warning::

This is required for the integration to function properly.

To create the custom work type in Jira:

1. Navigate to your **Project settings** → **Work types**
2. Click **+ Add work type**
3. Set the name to: ``DejaCode Request``
4. Click **Create**

Create "Closed" Status
----------------------

.. warning::

This is required for the integration to function properly.

This status will be set on the Jira issue when the DejaCode Request is closed.

To create the **Closed** status in Jira:

1. Navigate to **Project settings** → **Work types**
2. Select the ``DejaCode Request`` work type
3. Click **Edit workflow**
4. Click **Add status**
5. Click **Create new status** tab
6. Enter the name: ``Closed``
7. Choose a category: ``Done``
8. Click **Add**
9. Click **Update workflow**

Jira API Token
--------------

To enable integration, you need a Jira Cloud **API token** and the associated
**user email**.

1. **Generate a Jira API Token**:

- Go to: https://id.atlassian.com/manage-profile/security/api-tokens
- Click **"Create API token"**
- Enter a descriptive label (e.g., ``DejaCode Integration``)
- Click **Create** and then **Copy** the token

2. **Store Your Credentials Securely**:

- You will need both:

- Your **Jira user email** (the one used to log into Jira)
- The **API token** you just generated

.. note::

The API token is required for authenticating to the Jira Cloud REST API.
If your Jira instance is hosted on-prem (Jira Server/Data Center), the integration
may not be supported without further customization.

DejaCode Dataspace Configuration
--------------------------------

To use your Jira credentials in DejaCode:

1. Go to the **Administration dashboard**
2. Navigate to **Dataspaces**, and select your Dataspace
3. Scroll to the **Jira Integration** section under **Configuration**
4. Enter:

- Your **Jira user email**
- The **API token** you generated

5. Save the form

Activate Jira Integration on Request Templates
----------------------------------------------

1. Go to the **Administration dashboard**
2. Navigate to **Workflow** > **Request templates**
3. Create or edit a Request Template in your Dataspace
4. Set the **Issue Tracker ID** field to your Jira base URL with project key, e.g.::

https://YOUR-DOMAIN.atlassian.net/projects/PROJECTKEY
https://YOUR-DOMAIN.atlassian.net/jira/software/projects/PROJECTKEY/summary

- This URL must point to your Jira Cloud instance

Once the integration is configured:

- New **Requests** using this template will be automatically pushed to Jira
- Field updates (like title or priority) and **status changes** (e.g. closed) will be
synced
- New **Comments** on a DejaCode Request will be propagated to the Jira Issue
18 changes: 18 additions & 0 deletions workflow/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
from django.contrib import messages
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.core.exceptions import ValidationError
from django.template.defaultfilters import pluralize
from django.utils.translation import gettext as _

from dje.admin import DataspacedAdmin
from dje.admin import dejacode_site
from dje.forms import DataspacedAdminForm
from workflow.inlines import QuestionInline
from workflow.integrations import is_valid_issue_tracker_id
from workflow.models import Priority
from workflow.models import RequestTemplate

Expand Down Expand Up @@ -56,6 +58,21 @@ class PriorityAdmin(DataspacedAdmin):
save_as = False


class RequestTemplateAdminForm(DataspacedAdminForm):
def clean_issue_tracker_id(self):
issue_tracker_id = self.cleaned_data.get("issue_tracker_id")
if issue_tracker_id and not is_valid_issue_tracker_id(issue_tracker_id):
raise ValidationError(
[
"Invalid issue tracker URL format. Supported formats include:",
"• GitHub: https://github.com/ORG/REPO_NAME",
"• GitLab: https://gitlab.com/GROUP/PROJECT_NAME",
"• Jira: https://YOUR_DOMAIN.atlassian.net/projects/PROJECTKEY",
]
)
return issue_tracker_id


@admin.register(RequestTemplate, site=dejacode_site)
class RequestTemplateAdmin(DataspacedAdmin):
list_display = (
Expand All @@ -75,6 +92,7 @@ class RequestTemplateAdmin(DataspacedAdmin):
"include_applies_to",
"include_product",
)
form = RequestTemplateAdminForm
inlines = (QuestionInline,)
actions = [
"copy_to",
Expand Down
93 changes: 40 additions & 53 deletions workflow/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,56 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from django.conf import settings
import re

import requests
from workflow.integrations.base import BaseIntegration
from workflow.integrations.github import GitHubIntegration
from workflow.integrations.gitlab import GitLabIntegration
from workflow.integrations.jira import JiraIntegration

DEJACODE_SITE_URL = settings.SITE_URL.rstrip("/")
__all__ = [
"BaseIntegration",
"GitHubIntegration",
"GitLabIntegration",
"JiraIntegration",
"is_valid_issue_tracker_id",
"get_class_for_tracker",
"get_class_for_platform",
]


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

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

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

def get_session(self):
session = requests.Session()
session.headers.update(self.get_headers())
return session
ISSUE_TRACKER_PATTERNS = [
GITHUB_PATTERN,
GITLAB_PATTERN,
JIRA_PATTERN,
]

def get_headers(self):
"""
Return authentication headers specific to the integration.
Must be implemented in subclasses.
"""
raise NotImplementedError

@staticmethod
def make_issue_title(request):
return f"[DEJACODE] {request.title}"
def is_valid_issue_tracker_id(issue_tracker_id):
return any(pattern.match(issue_tracker_id) for pattern in ISSUE_TRACKER_PATTERNS)

@staticmethod
def make_issue_body(request):
request_url = f"{DEJACODE_SITE_URL}{request.get_absolute_url()}"
label_fields = [
("📝 Request Template", request.request_template),
("📦 Product Context", request.product_context),
("📌 Applies To", request.content_object),
("🙋 Submitted By", request.requester),
("👤 Assigned To", request.assignee),
("🚨 Priority", request.priority),
("🗒️ Notes", request.notes),
("🔗️ DejaCode URL", request_url),
]

lines = []
for label, value in label_fields:
if value:
lines.append(f"### {label}\n{value}")
def get_class_for_tracker(issue_tracker_id):
if "github.com" in issue_tracker_id:
return GitHubIntegration
elif "gitlab.com" in issue_tracker_id:
return GitLabIntegration
elif "atlassian.net" in issue_tracker_id:
return JiraIntegration

lines.append("----")

for question in request.get_serialized_data_as_list():
label = question.get("label")
value = question.get("value")
input_type = question.get("input_type")

if input_type == "BooleanField":
value = "Yes" if str(value).lower() in ("1", "true", "yes") else "No"

lines.append(f"### {label}\n{value}")

return "\n\n".join(lines)
def get_class_for_platform(platform):
return {
"github": GitHubIntegration,
"gitlab": GitLabIntegration,
"jira": JiraIntegration,
}.get(platform)
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载