Skip to content

Commit 4851d28

Browse files
authored
CRAVEX GitLab workflow integration #346 (#374)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent e4a7d5f commit 4851d28

15 files changed

Lines changed: 507 additions & 76 deletions

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ Release notes
4242
* `/products/{uuid}/imports/`
4343
https://github.com/aboutcode-org/dejacode/issues/273
4444

45+
- Add GitHub workflow Request integration.
46+
Documentation: https://dejacode.readthedocs.io/en/latest/integrations-github.html
47+
https://github.com/aboutcode-org/dejacode/issues/349
48+
49+
- Add GitLab workflow Request integration.
50+
Documentation: https://dejacode.readthedocs.io/en/latest/integrations-github.html
51+
https://github.com/aboutcode-org/dejacode/issues/346
52+
4553
### Version 5.3.0
4654

4755
- Rename ProductDependency is_resolved to is_pinned.

dje/admin.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from django.contrib.admin.utils import unquote
2525
from django.contrib.admin.views.main import ChangeList
2626
from django.contrib.admin.widgets import AdminDateWidget
27+
from django.contrib.admin.widgets import AdminTextInputWidget
2728
from django.contrib.auth.admin import GroupAdmin
2829
from django.contrib.auth.admin import UserAdmin
2930
from django.contrib.auth.models import Group
@@ -1034,7 +1035,7 @@ def has_activity_log(self):
10341035
return True
10351036

10361037

1037-
class HiddenValueWidget(forms.TextInput):
1038+
class HiddenValueWidget(AdminTextInputWidget):
10381039
"""Render a hidden value in the UI."""
10391040

10401041
HIDDEN_VALUE = "*******"
@@ -1057,6 +1058,7 @@ class DataspaceConfigurationForm(forms.ModelForm):
10571058
"vulnerablecode_api_key",
10581059
"purldb_api_key",
10591060
"github_token",
1061+
"gitlab_token",
10601062
]
10611063

10621064
def __init__(self, *args, **kwargs):
@@ -1105,6 +1107,14 @@ class DataspaceConfigurationInline(DataspacedFKMixin, admin.StackedInline):
11051107
]
11061108
},
11071109
),
1110+
(
1111+
"GitLab Integration",
1112+
{
1113+
"fields": [
1114+
"gitlab_token",
1115+
]
1116+
},
1117+
),
11081118
]
11091119
can_delete = False
11101120

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.4 on 2025-07-31 10:43
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dje', '0008_dataspaceconfiguration_github_token'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='dataspaceconfiguration',
15+
name='gitlab_token',
16+
field=models.CharField(blank=True, help_text="Personal access token (PAT) used to authenticate API requests for the GitLab integration. This token must have 'api' scope. Keep this token secure.", max_length=255, verbose_name='GitLab token'),
17+
),
18+
]

dje/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,17 @@ class DataspaceConfiguration(models.Model):
528528
),
529529
)
530530

531+
gitlab_token = models.CharField(
532+
_("GitLab token"),
533+
max_length=255,
534+
blank=True,
535+
help_text=_(
536+
"Personal access token (PAT) used to authenticate API requests for the "
537+
"GitLab integration. This token must have 'api' scope. "
538+
"Keep this token secure."
539+
),
540+
)
541+
531542
def __str__(self):
532543
return f"Configuration for {self.dataspace}"
533544

docs/index.rst

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

5151
integrations-github
52+
integrations-gitlab
5253

5354
.. toctree::
5455
:maxdepth: 1

docs/integrations-github.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ DejaCode's integration with GitHub allows you to automatically forward
77
**Workflow Requests** to GitHub repository **Issues**.
88
This behavior can be selectively applied to any **Request Template** of your choice.
99

10-
GitHub Account and Personal Access Token
11-
----------------------------------------
10+
Prerequisites
11+
-------------
12+
13+
- A **GitHub repository** that you want to integrate with DejaCode.
14+
- A **GitHub user account** with sufficient permissions (at least write access) to
15+
create and manage issues in that repository.
16+
17+
GitHub Personal Access Token
18+
----------------------------
1219

1320
To enable integration, you need a GitHub **fine-grained personal access token (PAT)**.
1421

docs/integrations-gitlab.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.. _integrations_gitlab:
2+
3+
GitLab Integration
4+
==================
5+
6+
DejaCode's integration with GitLab allows you to automatically forward
7+
**Workflow Requests** to GitLab project **Issues**.
8+
This behavior can be selectively applied to any **Request Template** of your choice.
9+
10+
Prerequisites
11+
-------------
12+
13+
- A **GitLab project** that you want to integrate with DejaCode.
14+
- A **GitLab user account** with sufficient permissions to create and manage issues in
15+
the target project.
16+
17+
GitLab Personal Access Token
18+
----------------------------
19+
20+
To enable integration, you need a GitLab **personal access token (PAT)** with
21+
appropriate permissions.
22+
23+
1. **Access GitLab Personal Access Tokens**:
24+
25+
- Go to: https://gitlab.com/-/user_settings/personal_access_tokens
26+
- Click the **"Add new token"** button
27+
- Provide a **name** (e.g., ``DejaCode Integration``) and **expiration date**
28+
(recommended)
29+
30+
2. **Permissions**:
31+
32+
Under **Scopes**, select:
33+
34+
- ``api`` — Full access to create, update, and comment on issues
35+
36+
.. note::
37+
38+
It is recommended to **create a dedicated GitLab user** with a clear, descriptive
39+
name such as ``dejacode-integration``. This ensures that all GitLab issues managed by
40+
the integration are clearly attributed to that user, improving traceability and
41+
auditability.
42+
43+
3. **Generate the Token**:
44+
45+
- Click **Create token**
46+
- Copy the token and store it securely — you’ll need it for the next step
47+
48+
DejaCode Dataspace Configuration
49+
--------------------------------
50+
51+
To use your GitLab token in DejaCode:
52+
53+
1. Go to the **Administration dashboard**
54+
2. Navigate to **Dataspaces**, and select your Dataspace
55+
3. Scroll to the **GitLab Integration** section under **Configuration**
56+
4. Paste your GitLab token in the **GitLab token** field
57+
5. Save the form
58+
59+
Activate GitLab Integration on Request Templates
60+
------------------------------------------------
61+
62+
1. Go to the **Administration dashboard**
63+
2. Navigate to **Workflow** > **Request templates**
64+
3. Create or edit a Request Template in your Dataspace
65+
4. Set the **Issue Tracker ID** field to your GitLab project URL, e.g.::
66+
67+
https://gitlab.com/group/project_name
68+
69+
Once the integration is configured:
70+
71+
- New **Requests** using this template will be automatically pushed to GitLab
72+
- Field updates (like title or priority) and **status changes** (e.g. closed) will be
73+
synced
74+
- New **Comments** on a DejaCode Request will be propagated to the GitLab Issue.

workflow/integrations/__init__.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,70 @@
55
# See https://github.com/aboutcode-org/dejacode for support or download.
66
# See https://aboutcode.org for more information about AboutCode FOSS projects.
77
#
8+
9+
from django.conf import settings
10+
11+
import requests
12+
13+
DEJACODE_SITE_URL = settings.SITE_URL.rstrip("/")
14+
15+
16+
class BaseIntegration:
17+
"""Base class for managing issue tracker integrations from DejaCode requests."""
18+
19+
default_timeout = 10
20+
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()
26+
27+
def get_session(self):
28+
session = requests.Session()
29+
session.headers.update(self.get_headers())
30+
return session
31+
32+
def get_headers(self):
33+
"""
34+
Return authentication headers specific to the integration.
35+
Must be implemented in subclasses.
36+
"""
37+
raise NotImplementedError
38+
39+
@staticmethod
40+
def make_issue_title(request):
41+
return f"[DEJACODE] {request.title}"
42+
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+
]
56+
57+
lines = []
58+
for label, value in label_fields:
59+
if value:
60+
lines.append(f"### {label}\n{value}")
61+
62+
lines.append("----")
63+
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)

workflow/integrations/github.py

Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,18 @@
88

99
from urllib.parse import urlparse
1010

11-
from django.conf import settings
12-
13-
import requests
11+
from workflow.integrations import BaseIntegration
1412

1513
GITHUB_API_URL = "https://api.github.com"
16-
DEJACODE_SITE_URL = settings.SITE_URL.rstrip("/")
1714

1815

19-
class GitHubIntegration:
16+
class GitHubIntegration(BaseIntegration):
2017
"""
2118
A class for managing GitHub issue creation, updates, and comments
2219
from DejaCode requests.
2320
"""
2421

2522
api_url = GITHUB_API_URL
26-
default_timeout = 10
27-
28-
def __init__(self, dataspace):
29-
if not dataspace:
30-
raise ValueError("Dataspace must be provided.")
31-
self.dataspace = dataspace
32-
self.session = self.get_session()
33-
34-
def get_session(self):
35-
session = requests.Session()
36-
session.headers.update(self.get_headers())
37-
return session
3823

3924
def get_headers(self):
4025
github_token = self.dataspace.get_configuration(field_name="github_token")
@@ -61,6 +46,7 @@ def sync(self, request):
6146
title=self.make_issue_title(request),
6247
body=self.make_issue_body(request),
6348
state="closed" if request.is_closed else "open",
49+
labels=labels,
6450
)
6551
else:
6652
issue = self.create_issue(
@@ -93,7 +79,7 @@ def create_issue(self, repo_id, title, body="", labels=None):
9379
response.raise_for_status()
9480
return response.json()
9581

96-
def update_issue(self, repo_id, issue_id, title=None, body=None, state=None):
82+
def update_issue(self, repo_id, issue_id, title=None, body=None, state=None, labels=None):
9783
"""Update an existing GitHub issue."""
9884
url = f"{self.api_url}/repos/{repo_id}/issues/{issue_id}"
9985
data = {}
@@ -103,6 +89,8 @@ def update_issue(self, repo_id, issue_id, title=None, body=None, state=None):
10389
data["body"] = body
10490
if state:
10591
data["state"] = state
92+
if labels:
93+
data["labels"] = labels
10694

10795
response = self.session.patch(
10896
url,
@@ -137,40 +125,3 @@ def extract_github_repo_path(url):
137125
raise ValueError("Incomplete GitHub repository path.")
138126

139127
return f"{path_parts[0]}/{path_parts[1]}"
140-
141-
@staticmethod
142-
def make_issue_title(request):
143-
return f"[DEJACODE] {request.title}"
144-
145-
@staticmethod
146-
def make_issue_body(request):
147-
request_url = f"{DEJACODE_SITE_URL}{request.get_absolute_url()}"
148-
label_fields = [
149-
("📝 Request Template", request.request_template),
150-
("📦 Product Context", request.product_context),
151-
("📌 Applies To", request.content_object),
152-
("🙋 Submitted By", request.requester),
153-
("👤 Assigned To", request.assignee),
154-
("🚨 Priority", request.priority),
155-
("🗒️ Notes", request.notes),
156-
("🔗️ DejaCode URL", request_url),
157-
]
158-
159-
lines = []
160-
for label, value in label_fields:
161-
if value:
162-
lines.append(f"### {label}\n{value}")
163-
164-
lines.append("----")
165-
166-
for question in request.get_serialized_data_as_list():
167-
label = question.get("label")
168-
value = question.get("value")
169-
input_type = question.get("input_type")
170-
171-
if input_type == "BooleanField":
172-
value = "Yes" if str(value).lower() in ("1", "true", "yes") else "No"
173-
174-
lines.append(f"### {label}\n{value}")
175-
176-
return "\n\n".join(lines)

0 commit comments

Comments
 (0)