lfx-gitlab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lfx_gitlab/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """GitLab personal-token actions and signed project triggers."""
lfx_gitlab/base.py ADDED
@@ -0,0 +1,102 @@
1
+ """Connection and telemetry boundary for GitLab action components."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from lfx.custom.custom_component.component import Component
6
+ from lfx.integrations.errors import AuthExpiredError, InvalidRequestError, ProviderUnavailableError
7
+ from lfx.integrations.telemetry import integration_action
8
+ from lfx.io import ConnectionRefInput, IntInput
9
+ from lfx.schema.data import Data
10
+
11
+ from lfx_gitlab.client import GitLabAuthExpiredError, GitLabClient, bounded_text, positive_id
12
+
13
+
14
+ def connection_input(capability):
15
+ return ConnectionRefInput(
16
+ name="connection",
17
+ display_name="GitLab Connection",
18
+ provider="gitlab",
19
+ auth_profile_id="gitlab-personal-token",
20
+ identity_kind="user",
21
+ ownership_mode="user",
22
+ required_scopes=["api"],
23
+ capabilities=[capability],
24
+ required=True,
25
+ info="Use a verified personal access token for this deployment's configured GitLab instance.",
26
+ )
27
+
28
+
29
+ def project_input(*, connectable=True):
30
+ return IntInput(
31
+ name="project_id",
32
+ display_name="Project ID",
33
+ required=True,
34
+ input_types=["Message"] if connectable else [],
35
+ info="Numeric GitLab project ID. Project paths and URLs are unavailable.",
36
+ )
37
+
38
+
39
+ class GitLabActionComponent(Component):
40
+ icon = "Gitlab"
41
+ capability_id: ClassVar[str] = ""
42
+ operation: ClassVar[str] = ""
43
+ resource: ClassVar[str] = ""
44
+
45
+ def _pre_run_setup(self):
46
+ self.__dict__.pop("_gitlab_result", None)
47
+
48
+ async def build_result(self) -> Data:
49
+ if "_gitlab_result" in self.__dict__:
50
+ return Data(data=self.__dict__["_gitlab_result"])
51
+ path = f"/projects/{positive_id(self.project_id, field='Project ID')}"
52
+ operation = self.operation
53
+ if self.resource != "project":
54
+ path += "/" + {"issue": "issues", "merge_request": "merge_requests"}[self.resource]
55
+ if self.resource != "project" and operation in {"get", "update", "comment"}:
56
+ number_field = "Issue Number" if self.resource == "issue" else "Merge Request Number"
57
+ path += "/" + str(positive_id(self.number, field=number_field))
58
+ body = None
59
+ if operation == "comment":
60
+ path += "/notes"
61
+ body = {"body": bounded_text(self.body, required=True, field="Comment")}
62
+ elif operation == "create":
63
+ body = {
64
+ "title": bounded_text(self.title, required=True, maximum=255, field="Title"),
65
+ "description": bounded_text(self.body_content, field="Description"),
66
+ }
67
+ if self.resource == "merge_request":
68
+ body.update(
69
+ source_branch=bounded_text(self.source_branch, required=True, maximum=255, field="Source Branch"),
70
+ target_branch=bounded_text(self.target_branch, required=True, maximum=255, field="Target Branch"),
71
+ )
72
+ elif operation == "update":
73
+ body = {}
74
+ if self.title not in (None, ""):
75
+ body["title"] = bounded_text(self.title, required=True, maximum=255, field="Title")
76
+ if self.body_content not in (None, ""):
77
+ body["description"] = bounded_text(self.body_content, field="Description")
78
+ if self.state_event not in {"unchanged", "close", "reopen"}:
79
+ msg = "Choose a supported state change."
80
+ raise InvalidRequestError(msg, provider="gitlab")
81
+ if self.state_event != "unchanged":
82
+ body["state_event"] = self.state_event
83
+ if not body:
84
+ msg = "Provide a new title, description, or state change."
85
+ raise InvalidRequestError(msg, provider="gitlab")
86
+ lease = self.resolve_connection("connection")
87
+ try:
88
+ credential = await lease.get_credential()
89
+ except AuthExpiredError:
90
+ raise GitLabAuthExpiredError from None
91
+ method = "POST" if operation in {"create", "comment"} else "PUT" if operation == "update" else "GET"
92
+ async with (
93
+ integration_action(
94
+ self, provider="gitlab", capability=self.capability_id, owner_kind=credential.owner_kind
95
+ ),
96
+ GitLabClient(lease) as client,
97
+ ):
98
+ result = await client.request(method, path, body=body)
99
+ if not isinstance(result, dict):
100
+ raise ProviderUnavailableError(provider="gitlab")
101
+ self.__dict__["_gitlab_result"] = result
102
+ return Data(data=result)
lfx_gitlab/client.py ADDED
@@ -0,0 +1,244 @@
1
+ """Bounded GitLab API requests using a trusted deployment origin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import ipaddress
7
+ import json
8
+ import math
9
+ import os
10
+ import re
11
+ from datetime import datetime, timezone
12
+ from urllib.parse import urlsplit
13
+
14
+ import httpx
15
+ from lfx.integrations.errors import (
16
+ AuthExpiredError,
17
+ ConnectionNotAuthorizedError,
18
+ IntegrationNetworkError,
19
+ InvalidRequestError,
20
+ ProviderUnavailableError,
21
+ RateLimitedError,
22
+ ResourceNotFoundError,
23
+ )
24
+ from lfx.utils.ssrf_httpx import ssrf_protected_strict_httpx_client_kwargs_for_url
25
+ from lfx.utils.ssrf_protection import SSRFProtectionError
26
+ from lfx.utils.url_redaction import suppress_sensitive_http_logs
27
+
28
+ _PATH = re.compile(
29
+ r"/(?:user|users/[1-9][0-9]*|version|personal_access_tokens/self|projects/[1-9][0-9]*(?:/(?:hooks|issues|merge_requests)(?:/[1-9][0-9]*(?:/notes)?)?)?)\Z"
30
+ )
31
+ MAX_RESPONSE_BYTES = 2 * 1024 * 1024
32
+ MAX_REQUEST_BYTES = 64 * 1024
33
+
34
+
35
+ class GitLabAuthExpiredError(AuthExpiredError):
36
+ """Keep reconnect guidance visible in canvas errors as well as structured hints."""
37
+
38
+ def __init__(self):
39
+ super().__init__(provider="gitlab")
40
+ self.message = (
41
+ "The GitLab credential is expired or was rejected. Reconnect the GitLab connection and try again."
42
+ )
43
+ self.safe_message = self.message
44
+ self.args = (self.safe_message,)
45
+
46
+
47
+ def api_origin() -> str:
48
+ """Read operator configuration rather than any flow or provider URL."""
49
+ value = os.environ.get("LANGFLOW_GITLAB_ORIGIN", "https://gitlab.com")
50
+ parts = urlsplit(value)
51
+ host = parts.hostname or ""
52
+ if (
53
+ parts.scheme != "https"
54
+ or parts.username is not None
55
+ or parts.password is not None
56
+ or parts.query
57
+ or parts.fragment
58
+ or parts.path not in {"", "/"}
59
+ or parts.port not in {None, 443}
60
+ or not re.fullmatch(r"[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?", host)
61
+ or "." not in host
62
+ or host.endswith((".localhost", ".local"))
63
+ ):
64
+ msg = "Configure one public HTTPS GitLab origin without a path or port."
65
+ raise InvalidRequestError(msg, provider="gitlab")
66
+ try:
67
+ ipaddress.ip_address(host)
68
+ except ValueError:
69
+ pass
70
+ else:
71
+ msg = "Configure a GitLab DNS hostname rather than an IP address."
72
+ raise InvalidRequestError(msg, provider="gitlab")
73
+ return "https://" + host.lower()
74
+
75
+
76
+ def positive_id(value, *, field="ID") -> int:
77
+ msg = f"{field} must be an integer from 1 to 9223372036854775807 (the signed 64-bit range)."
78
+ if isinstance(value, bool):
79
+ raise InvalidRequestError(msg, provider="gitlab")
80
+ try:
81
+ number = int(value)
82
+ except (TypeError, ValueError, OverflowError):
83
+ raise InvalidRequestError(msg, provider="gitlab") from None
84
+ if str(number) != str(value) or not 1 <= number <= 2**63 - 1:
85
+ raise InvalidRequestError(msg, provider="gitlab")
86
+ return number
87
+
88
+
89
+ def bounded_text(value, *, required: bool = False, maximum: int = 60000, field="Text") -> str:
90
+ if required and isinstance(value, str) and not value.strip():
91
+ msg = f"{field} must contain non-whitespace text."
92
+ raise InvalidRequestError(msg, provider="gitlab")
93
+ try:
94
+ valid = isinstance(value, str) and len(value.encode()) <= maximum and (not required or bool(value.strip()))
95
+ except UnicodeEncodeError:
96
+ valid = False
97
+ if not valid:
98
+ msg = f"{field} must be text containing at most {maximum} UTF-8 bytes."
99
+ raise InvalidRequestError(msg, provider="gitlab")
100
+ return value
101
+
102
+
103
+ def _safe_output(value):
104
+ if isinstance(value, dict):
105
+ return {
106
+ key: _safe_output(item)
107
+ for key, item in value.items()
108
+ if not any(
109
+ term in key.lower()
110
+ for term in (
111
+ "token",
112
+ "secret",
113
+ "password",
114
+ "private_key",
115
+ "clone_url",
116
+ "http_url_to_repo",
117
+ "ssh_url_to_repo",
118
+ )
119
+ )
120
+ }
121
+ if isinstance(value, list):
122
+ return [_safe_output(item) for item in value]
123
+ return value
124
+
125
+
126
+ def response_error(response: httpx.Response):
127
+ status = response.status_code
128
+ if status == httpx.codes.UNAUTHORIZED:
129
+ return GitLabAuthExpiredError()
130
+ if status == httpx.codes.TOO_MANY_REQUESTS:
131
+ try:
132
+ delay = float(response.headers.get("retry-after", "60"))
133
+ except (ValueError, OverflowError):
134
+ delay = 60.0
135
+ return RateLimitedError(provider="gitlab", retry_after=max(0, delay) if math.isfinite(delay) else 60.0)
136
+ if status == httpx.codes.FORBIDDEN:
137
+ return ConnectionNotAuthorizedError(provider="gitlab", reason="provider")
138
+ if status == httpx.codes.NOT_FOUND:
139
+ return ResourceNotFoundError(provider="gitlab")
140
+ if httpx.codes.BAD_REQUEST <= status < httpx.codes.INTERNAL_SERVER_ERROR:
141
+ return InvalidRequestError(provider="gitlab", http_status=status)
142
+ return ProviderUnavailableError(provider="gitlab", http_status=status)
143
+
144
+
145
+ class GitLabClient:
146
+ """Reject redirects and arbitrary paths before resolving credentials."""
147
+
148
+ def __init__(self, lease, *, transport: httpx.AsyncBaseTransport | None = None):
149
+ self.lease = lease
150
+ self.origin = api_origin()
151
+ try:
152
+ kwargs = (
153
+ ssrf_protected_strict_httpx_client_kwargs_for_url(self.origin)[1]
154
+ if transport is None
155
+ else {"transport": transport}
156
+ )
157
+ except SSRFProtectionError:
158
+ raise IntegrationNetworkError(
159
+ reason="connection", provider="gitlab", host=urlsplit(self.origin).hostname
160
+ ) from None
161
+ kwargs["follow_redirects"] = False
162
+ self._client = httpx.AsyncClient(timeout=20, trust_env=False, **kwargs)
163
+
164
+ async def __aenter__(self):
165
+ return self
166
+
167
+ async def __aexit__(self, *_args):
168
+ await self._client.aclose()
169
+
170
+ async def request(self, method, path, *, body=None, params=None):
171
+ if method not in {"GET", "POST", "PUT", "DELETE"} or not _PATH.fullmatch(path):
172
+ msg = "The GitLab REST request is invalid."
173
+ raise InvalidRequestError(msg, provider="gitlab")
174
+ content = json.dumps(body, ensure_ascii=False).encode() if body is not None else None
175
+ if content is not None and len(content) > MAX_REQUEST_BYTES:
176
+ msg = "The GitLab request exceeds 64 KiB."
177
+ raise InvalidRequestError(msg, provider="gitlab")
178
+ try:
179
+ credential = await self.lease.get_credential()
180
+ except AuthExpiredError:
181
+ raise GitLabAuthExpiredError from None
182
+ if (
183
+ credential.provider != "gitlab"
184
+ or credential.identity != "user_delegated"
185
+ or credential.account is None
186
+ or credential.account.tenant_id != self.origin
187
+ or not credential.scopes_verified
188
+ or "api" not in credential.granted_scopes
189
+ ):
190
+ raise ConnectionNotAuthorizedError(
191
+ provider="gitlab", hint="Choose a verified GitLab personal access token connection for this deployment."
192
+ )
193
+ expiry = credential.expires_at
194
+ if expiry is None or expiry.tzinfo is None or expiry <= datetime.now(timezone.utc):
195
+ raise GitLabAuthExpiredError
196
+ token = credential.access_token.get_secret_value()
197
+ try:
198
+ try:
199
+ return await asyncio.wait_for(self._request(method, path, content, params, token), timeout=25)
200
+ except AuthExpiredError as error:
201
+ try:
202
+ replacement = await self.lease.get_token_after_auth_error(error, rejected_token=token)
203
+ except AuthExpiredError:
204
+ raise GitLabAuthExpiredError from None
205
+ if replacement == token:
206
+ raise
207
+ return await self.request(method, path, body=body, params=params)
208
+ except (asyncio.TimeoutError, httpx.TimeoutException):
209
+ raise IntegrationNetworkError(
210
+ reason="timeout", provider="gitlab", host=urlsplit(self.origin).hostname
211
+ ) from None
212
+ except httpx.TransportError:
213
+ raise IntegrationNetworkError(
214
+ reason="connection", provider="gitlab", host=urlsplit(self.origin).hostname
215
+ ) from None
216
+
217
+ async def _request(self, method, path, content, params, token):
218
+ async with (
219
+ suppress_sensitive_http_logs(),
220
+ self._client.stream(
221
+ method,
222
+ self.origin + "/api/v4" + path,
223
+ content=content,
224
+ params=params,
225
+ headers={"PRIVATE-TOKEN": token, "Content-Type": "application/json", "Accept": "application/json"},
226
+ ) as response,
227
+ ):
228
+ chunks = bytearray()
229
+ async for chunk in response.aiter_bytes():
230
+ chunks.extend(chunk)
231
+ if len(chunks) > MAX_RESPONSE_BYTES:
232
+ msg = "The GitLab response exceeds 2 MiB."
233
+ raise InvalidRequestError(msg, provider="gitlab")
234
+ if not httpx.codes.OK <= response.status_code < httpx.codes.MULTIPLE_CHOICES:
235
+ raise response_error(response)
236
+ if not chunks:
237
+ return {}
238
+ try:
239
+ payload = json.loads(chunks)
240
+ except (ValueError, UnicodeDecodeError):
241
+ raise ProviderUnavailableError(provider="gitlab") from None
242
+ if not isinstance(payload, (dict, list)):
243
+ raise ProviderUnavailableError(provider="gitlab")
244
+ return _safe_output(payload)
@@ -0,0 +1 @@
1
+ """GitLab component categories."""
@@ -0,0 +1,27 @@
1
+ """Stable GitLab component identities."""
2
+
3
+ from .issue_comment_create import GitLabCreateIssueCommentComponent
4
+ from .issue_create import GitLabCreateIssueComponent
5
+ from .issue_get import GitLabGetIssueComponent
6
+ from .issue_trigger import GitLabOnIssueTriggerComponent
7
+ from .issue_update import GitLabUpdateIssueComponent
8
+ from .merge_request_comment_create import GitLabCreateMergeRequestCommentComponent
9
+ from .merge_request_create import GitLabCreateMergeRequestComponent
10
+ from .merge_request_get import GitLabGetMergeRequestComponent
11
+ from .merge_request_trigger import GitLabOnMergeRequestTriggerComponent
12
+ from .merge_request_update import GitLabUpdateMergeRequestComponent
13
+ from .project_get import GitLabGetProjectComponent
14
+
15
+ __all__ = [
16
+ "GitLabCreateIssueCommentComponent",
17
+ "GitLabCreateIssueComponent",
18
+ "GitLabCreateMergeRequestCommentComponent",
19
+ "GitLabCreateMergeRequestComponent",
20
+ "GitLabGetIssueComponent",
21
+ "GitLabGetMergeRequestComponent",
22
+ "GitLabGetProjectComponent",
23
+ "GitLabOnIssueTriggerComponent",
24
+ "GitLabOnMergeRequestTriggerComponent",
25
+ "GitLabUpdateIssueComponent",
26
+ "GitLabUpdateMergeRequestComponent",
27
+ ]
@@ -0,0 +1,284 @@
1
+ {
2
+ "schema_version": 1,
3
+ "provider_id": "gitlab",
4
+ "display_name": "GitLab",
5
+ "icon": "Gitlab",
6
+ "docs_url": "https://docs.langflow.org/next/bundles-gitlab",
7
+ "auth_profiles": [
8
+ {
9
+ "id": "gitlab-personal-token",
10
+ "kind": "api_key",
11
+ "identity": "user_delegated",
12
+ "supports_pkce": false,
13
+ "supports_refresh": false,
14
+ "default_scopes": [
15
+ "api"
16
+ ],
17
+ "client_type_by_context": {
18
+ "hosted": "external",
19
+ "self_managed": "external",
20
+ "desktop": "external",
21
+ "headless": "external"
22
+ },
23
+ "owner_by_context": {
24
+ "hosted": "customer",
25
+ "self_managed": "customer",
26
+ "desktop": "customer",
27
+ "headless": "customer"
28
+ }
29
+ }
30
+ ],
31
+ "capabilities": [
32
+ {
33
+ "id": "gitlab.project_read",
34
+ "display_name": "GitLab: Get Project",
35
+ "auth_profile_id": "gitlab-personal-token",
36
+ "identity": "user_delegated",
37
+ "required_scopes": [
38
+ "api"
39
+ ],
40
+ "conditional_scopes": [],
41
+ "policy_keys": [
42
+ "integrations.gitlab.project_read"
43
+ ],
44
+ "substrate": "rest",
45
+ "maturity": "preview",
46
+ "deployment_contexts": [
47
+ "hosted",
48
+ "self_managed",
49
+ "desktop",
50
+ "headless"
51
+ ],
52
+ "risk": "read",
53
+ "component_ref": "GitLabGetProjectComponent"
54
+ },
55
+ {
56
+ "id": "gitlab.issue_read",
57
+ "display_name": "GitLab: Get Issue",
58
+ "auth_profile_id": "gitlab-personal-token",
59
+ "identity": "user_delegated",
60
+ "required_scopes": [
61
+ "api"
62
+ ],
63
+ "conditional_scopes": [],
64
+ "policy_keys": [
65
+ "integrations.gitlab.issue_read"
66
+ ],
67
+ "substrate": "rest",
68
+ "maturity": "preview",
69
+ "deployment_contexts": [
70
+ "hosted",
71
+ "self_managed",
72
+ "desktop",
73
+ "headless"
74
+ ],
75
+ "risk": "read",
76
+ "component_ref": "GitLabGetIssueComponent"
77
+ },
78
+ {
79
+ "id": "gitlab.issue_create",
80
+ "display_name": "GitLab: Create Issue",
81
+ "auth_profile_id": "gitlab-personal-token",
82
+ "identity": "user_delegated",
83
+ "required_scopes": [
84
+ "api"
85
+ ],
86
+ "conditional_scopes": [],
87
+ "policy_keys": [
88
+ "integrations.gitlab.issue_create"
89
+ ],
90
+ "substrate": "rest",
91
+ "maturity": "preview",
92
+ "deployment_contexts": [
93
+ "hosted",
94
+ "self_managed",
95
+ "desktop",
96
+ "headless"
97
+ ],
98
+ "risk": "write",
99
+ "component_ref": "GitLabCreateIssueComponent"
100
+ },
101
+ {
102
+ "id": "gitlab.issue_update",
103
+ "display_name": "GitLab: Update Issue",
104
+ "auth_profile_id": "gitlab-personal-token",
105
+ "identity": "user_delegated",
106
+ "required_scopes": [
107
+ "api"
108
+ ],
109
+ "conditional_scopes": [],
110
+ "policy_keys": [
111
+ "integrations.gitlab.issue_update"
112
+ ],
113
+ "substrate": "rest",
114
+ "maturity": "preview",
115
+ "deployment_contexts": [
116
+ "hosted",
117
+ "self_managed",
118
+ "desktop",
119
+ "headless"
120
+ ],
121
+ "risk": "write",
122
+ "component_ref": "GitLabUpdateIssueComponent"
123
+ },
124
+ {
125
+ "id": "gitlab.merge_request_read",
126
+ "display_name": "GitLab: Get Merge Request",
127
+ "auth_profile_id": "gitlab-personal-token",
128
+ "identity": "user_delegated",
129
+ "required_scopes": [
130
+ "api"
131
+ ],
132
+ "conditional_scopes": [],
133
+ "policy_keys": [
134
+ "integrations.gitlab.merge_request_read"
135
+ ],
136
+ "substrate": "rest",
137
+ "maturity": "preview",
138
+ "deployment_contexts": [
139
+ "hosted",
140
+ "self_managed",
141
+ "desktop",
142
+ "headless"
143
+ ],
144
+ "risk": "read",
145
+ "component_ref": "GitLabGetMergeRequestComponent"
146
+ },
147
+ {
148
+ "id": "gitlab.merge_request_create",
149
+ "display_name": "GitLab: Create Merge Request",
150
+ "auth_profile_id": "gitlab-personal-token",
151
+ "identity": "user_delegated",
152
+ "required_scopes": [
153
+ "api"
154
+ ],
155
+ "conditional_scopes": [],
156
+ "policy_keys": [
157
+ "integrations.gitlab.merge_request_create"
158
+ ],
159
+ "substrate": "rest",
160
+ "maturity": "preview",
161
+ "deployment_contexts": [
162
+ "hosted",
163
+ "self_managed",
164
+ "desktop",
165
+ "headless"
166
+ ],
167
+ "risk": "write",
168
+ "component_ref": "GitLabCreateMergeRequestComponent"
169
+ },
170
+ {
171
+ "id": "gitlab.merge_request_update",
172
+ "display_name": "GitLab: Update Merge Request",
173
+ "auth_profile_id": "gitlab-personal-token",
174
+ "identity": "user_delegated",
175
+ "required_scopes": [
176
+ "api"
177
+ ],
178
+ "conditional_scopes": [],
179
+ "policy_keys": [
180
+ "integrations.gitlab.merge_request_update"
181
+ ],
182
+ "substrate": "rest",
183
+ "maturity": "preview",
184
+ "deployment_contexts": [
185
+ "hosted",
186
+ "self_managed",
187
+ "desktop",
188
+ "headless"
189
+ ],
190
+ "risk": "write",
191
+ "component_ref": "GitLabUpdateMergeRequestComponent"
192
+ },
193
+ {
194
+ "id": "gitlab.issue_comment_create",
195
+ "display_name": "GitLab: Create Issue Comment",
196
+ "auth_profile_id": "gitlab-personal-token",
197
+ "identity": "user_delegated",
198
+ "required_scopes": [
199
+ "api"
200
+ ],
201
+ "conditional_scopes": [],
202
+ "policy_keys": [
203
+ "integrations.gitlab.issue_comment_create"
204
+ ],
205
+ "substrate": "rest",
206
+ "maturity": "preview",
207
+ "deployment_contexts": [
208
+ "hosted",
209
+ "self_managed",
210
+ "desktop",
211
+ "headless"
212
+ ],
213
+ "risk": "write",
214
+ "component_ref": "GitLabCreateIssueCommentComponent"
215
+ },
216
+ {
217
+ "id": "gitlab.merge_request_comment_create",
218
+ "display_name": "GitLab: Create Merge Request Comment",
219
+ "auth_profile_id": "gitlab-personal-token",
220
+ "identity": "user_delegated",
221
+ "required_scopes": [
222
+ "api"
223
+ ],
224
+ "conditional_scopes": [],
225
+ "policy_keys": [
226
+ "integrations.gitlab.merge_request_comment_create"
227
+ ],
228
+ "substrate": "rest",
229
+ "maturity": "preview",
230
+ "deployment_contexts": [
231
+ "hosted",
232
+ "self_managed",
233
+ "desktop",
234
+ "headless"
235
+ ],
236
+ "risk": "write",
237
+ "component_ref": "GitLabCreateMergeRequestCommentComponent"
238
+ },
239
+ {
240
+ "id": "gitlab.trigger.issue",
241
+ "display_name": "GitLab: On Issue",
242
+ "auth_profile_id": "gitlab-personal-token",
243
+ "identity": "user_delegated",
244
+ "required_scopes": [
245
+ "api"
246
+ ],
247
+ "conditional_scopes": [],
248
+ "policy_keys": [
249
+ "integrations.gitlab.trigger.issue"
250
+ ],
251
+ "substrate": "rest",
252
+ "maturity": "preview",
253
+ "deployment_contexts": [
254
+ "hosted",
255
+ "self_managed",
256
+ "headless"
257
+ ],
258
+ "risk": "read",
259
+ "component_ref": "GitLabOnIssueTriggerComponent"
260
+ },
261
+ {
262
+ "id": "gitlab.trigger.merge_request",
263
+ "display_name": "GitLab: On Merge Request",
264
+ "auth_profile_id": "gitlab-personal-token",
265
+ "identity": "user_delegated",
266
+ "required_scopes": [
267
+ "api"
268
+ ],
269
+ "conditional_scopes": [],
270
+ "policy_keys": [
271
+ "integrations.gitlab.trigger.merge_request"
272
+ ],
273
+ "substrate": "rest",
274
+ "maturity": "preview",
275
+ "deployment_contexts": [
276
+ "hosted",
277
+ "self_managed",
278
+ "headless"
279
+ ],
280
+ "risk": "read",
281
+ "component_ref": "GitLabOnMergeRequestTriggerComponent"
282
+ }
283
+ ]
284
+ }
@@ -0,0 +1,21 @@
1
+ """Create a GitLab comment using a connection."""
2
+
3
+ from lfx.io import IntInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabCreateIssueCommentComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Create Issue Comment"
10
+ name = "GitLabCreateIssueCommentComponent"
11
+ description = "Create a comment as the connection owner."
12
+ capability_id = "gitlab.issue_comment_create"
13
+ operation = "comment"
14
+ resource = "issue"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Issue Number", required=True, input_types=["Message"]),
19
+ MultilineInput(name="body", display_name="Comment", required=True),
20
+ ]
21
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,21 @@
1
+ """Create one GitLab issue using a connection."""
2
+
3
+ from lfx.io import MessageTextInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabCreateIssueComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Create Issue"
10
+ name = "GitLabCreateIssueComponent"
11
+ description = "Create a GitLab issue as the connection owner."
12
+ capability_id = "gitlab.issue_create"
13
+ operation = "create"
14
+ resource = "issue"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ MessageTextInput(name="title", display_name="Title", required=True),
19
+ MultilineInput(name="body_content", display_name="Description", value=""),
20
+ ]
21
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,20 @@
1
+ """Get one GitLab issue using a connection."""
2
+
3
+ from lfx.io import IntInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabGetIssueComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Get Issue"
10
+ name = "GitLabGetIssueComponent"
11
+ description = "Get a GitLab issue as the connection owner."
12
+ capability_id = "gitlab.issue_read"
13
+ operation = "get"
14
+ resource = "issue"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Issue Number", required=True, input_types=["Message"]),
19
+ ]
20
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,11 @@
1
+ """Declare a signed GitLab project webhook trigger."""
2
+
3
+ from lfx_gitlab.triggers import GitLabProjectTriggerComponent, trigger_inputs
4
+
5
+
6
+ class GitLabOnIssueTriggerComponent(GitLabProjectTriggerComponent):
7
+ display_name = "GitLab: On Issue"
8
+ name = "GitLabOnIssueTriggerComponent"
9
+ description = "Start the flow for selected signed project events."
10
+ trigger_kind = "gitlab.issue"
11
+ inputs = trigger_inputs("issue")
@@ -0,0 +1,30 @@
1
+ """Update one GitLab issue using a connection."""
2
+
3
+ from lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabUpdateIssueComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Update Issue"
10
+ name = "GitLabUpdateIssueComponent"
11
+ description = "Update a GitLab issue as the connection owner."
12
+ capability_id = "gitlab.issue_update"
13
+ operation = "update"
14
+ resource = "issue"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Issue Number", required=True, input_types=["Message"]),
19
+ MessageTextInput(name="title", display_name="Title", value="", info="Leave blank to keep the existing title."),
20
+ MultilineInput(
21
+ name="body_content",
22
+ display_name="Description",
23
+ value="",
24
+ info="Leave blank to keep the existing description.",
25
+ ),
26
+ DropdownInput(
27
+ name="state_event", display_name="State Change", options=["unchanged", "close", "reopen"], value="unchanged"
28
+ ),
29
+ ]
30
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,21 @@
1
+ """Create a GitLab comment using a connection."""
2
+
3
+ from lfx.io import IntInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabCreateMergeRequestCommentComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Create Merge Request Comment"
10
+ name = "GitLabCreateMergeRequestCommentComponent"
11
+ description = "Create a comment as the connection owner."
12
+ capability_id = "gitlab.merge_request_comment_create"
13
+ operation = "comment"
14
+ resource = "merge_request"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Merge Request Number", required=True, input_types=["Message"]),
19
+ MultilineInput(name="body", display_name="Comment", required=True),
20
+ ]
21
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,23 @@
1
+ """Create one GitLab merge request using a connection."""
2
+
3
+ from lfx.io import MessageTextInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabCreateMergeRequestComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Create Merge Request"
10
+ name = "GitLabCreateMergeRequestComponent"
11
+ description = "Create a GitLab merge request as the connection owner."
12
+ capability_id = "gitlab.merge_request_create"
13
+ operation = "create"
14
+ resource = "merge_request"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ MessageTextInput(name="title", display_name="Title", required=True),
19
+ MultilineInput(name="body_content", display_name="Description", value=""),
20
+ MessageTextInput(name="source_branch", display_name="Source Branch", required=True),
21
+ MessageTextInput(name="target_branch", display_name="Target Branch", required=True),
22
+ ]
23
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,20 @@
1
+ """Get one GitLab merge request using a connection."""
2
+
3
+ from lfx.io import IntInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabGetMergeRequestComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Get Merge Request"
10
+ name = "GitLabGetMergeRequestComponent"
11
+ description = "Get a GitLab merge request as the connection owner."
12
+ capability_id = "gitlab.merge_request_read"
13
+ operation = "get"
14
+ resource = "merge_request"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Merge Request Number", required=True, input_types=["Message"]),
19
+ ]
20
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,11 @@
1
+ """Declare a signed GitLab project webhook trigger."""
2
+
3
+ from lfx_gitlab.triggers import GitLabProjectTriggerComponent, trigger_inputs
4
+
5
+
6
+ class GitLabOnMergeRequestTriggerComponent(GitLabProjectTriggerComponent):
7
+ display_name = "GitLab: On Merge Request"
8
+ name = "GitLabOnMergeRequestTriggerComponent"
9
+ description = "Start the flow for selected signed project events."
10
+ trigger_kind = "gitlab.merge_request"
11
+ inputs = trigger_inputs("merge_request")
@@ -0,0 +1,30 @@
1
+ """Update one GitLab merge request using a connection."""
2
+
3
+ from lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabUpdateMergeRequestComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Update Merge Request"
10
+ name = "GitLabUpdateMergeRequestComponent"
11
+ description = "Update a GitLab merge request as the connection owner."
12
+ capability_id = "gitlab.merge_request_update"
13
+ operation = "update"
14
+ resource = "merge_request"
15
+ inputs = [
16
+ connection_input(capability_id),
17
+ project_input(),
18
+ IntInput(name="number", display_name="Merge Request Number", required=True, input_types=["Message"]),
19
+ MessageTextInput(name="title", display_name="Title", value="", info="Leave blank to keep the existing title."),
20
+ MultilineInput(
21
+ name="body_content",
22
+ display_name="Description",
23
+ value="",
24
+ info="Leave blank to keep the existing description.",
25
+ ),
26
+ DropdownInput(
27
+ name="state_event", display_name="State Change", options=["unchanged", "close", "reopen"], value="unchanged"
28
+ ),
29
+ ]
30
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,16 @@
1
+ """Get one GitLab project using a connection."""
2
+
3
+ from lfx.io import Output
4
+
5
+ from lfx_gitlab.base import GitLabActionComponent, connection_input, project_input
6
+
7
+
8
+ class GitLabGetProjectComponent(GitLabActionComponent):
9
+ display_name = "GitLab: Get Project"
10
+ name = "GitLabGetProjectComponent"
11
+ description = "Get a GitLab project as the connection owner."
12
+ capability_id = "gitlab.project_read"
13
+ operation = "get"
14
+ resource = "project"
15
+ inputs = [connection_input(capability_id), project_input()]
16
+ outputs = [Output(display_name="Result", name="result", method="build_result")]
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://schemas.langflow.org/extension/v1.json",
3
+ "id": "lfx-gitlab",
4
+ "version": "0.1.0",
5
+ "name": "GitLab",
6
+ "description": "GitLab project, issue, merge request and comment actions with signed project event triggers.",
7
+ "lfx": {
8
+ "compat": [
9
+ "1"
10
+ ]
11
+ },
12
+ "bundles": [
13
+ {
14
+ "name": "gitlab",
15
+ "path": "components/gitlab"
16
+ }
17
+ ],
18
+ "integrations": [
19
+ {
20
+ "provider_id": "gitlab",
21
+ "bundle": "gitlab",
22
+ "path": "capabilities.v1.json"
23
+ }
24
+ ]
25
+ }
lfx_gitlab/triggers.py ADDED
@@ -0,0 +1,58 @@
1
+ """Declarative GitLab project triggers owned by the server runtime."""
2
+
3
+ from lfx.base.triggers.base import BaseTriggerComponent
4
+ from lfx.io import BoolInput, DropdownInput, MultiselectInput
5
+
6
+ from lfx_gitlab.base import connection_input, project_input
7
+
8
+ ISSUE_ACTIONS = ["open", "update", "close", "reopen"]
9
+ MR_ACTIONS = ["open", "update", "close", "reopen", "merge", "approved", "unapproved", "approval", "unapproval"]
10
+
11
+
12
+ def trigger_inputs(resource):
13
+ return [
14
+ connection_input(f"gitlab.trigger.{resource}"),
15
+ project_input(connectable=False),
16
+ MultiselectInput(
17
+ name="actions",
18
+ display_name="Events",
19
+ options=ISSUE_ACTIONS if resource == "issue" else MR_ACTIONS,
20
+ value=["open", "close", "reopen"],
21
+ ),
22
+ BoolInput(
23
+ name="include_self_events",
24
+ display_name="Include Own Events",
25
+ value=False,
26
+ advanced=True,
27
+ info="Enable only when the flow cannot write back into a trigger loop.",
28
+ ),
29
+ BoolInput(name="include_bot_events", display_name="Include Bot Events", value=False, advanced=True),
30
+ DropdownInput(
31
+ name="delivery_mode",
32
+ display_name="Delivery Mode",
33
+ options=["push"],
34
+ value="push",
35
+ advanced=True,
36
+ info="Requires public HTTPS ingress and GitLab 19.1 or newer signing tokens.",
37
+ ),
38
+ ]
39
+
40
+
41
+ class GitLabProjectTriggerComponent(BaseTriggerComponent):
42
+ provider = "gitlab"
43
+ icon = "Gitlab"
44
+ needs_connection = True
45
+
46
+ def trigger_config(self):
47
+ return {
48
+ "connection": getattr(self, "connection", None),
49
+ "project_id": getattr(self, "project_id", None),
50
+ "actions": getattr(self, "actions", []),
51
+ "delivery_mode": "push",
52
+ "include_self_events": getattr(self, "include_self_events", False),
53
+ "include_bot_events": getattr(self, "include_bot_events", False),
54
+ }
55
+
56
+
57
+ # Preserve imports of the original helper while component source uses the loader-compatible name.
58
+ GitLabProjectTrigger = GitLabProjectTriggerComponent
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: lfx-gitlab
3
+ Version: 0.1.0
4
+ Summary: GitLab actions and signed project webhook triggers.
5
+ License: MIT
6
+ Requires-Python: <3.16,>=3.10
7
+ Requires-Dist: httpx<1.0,>=0.27
8
+ Requires-Dist: lfx<2.0.0,>=1.14.0.dev0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # GitLab integrations
12
+
13
+ This preview bundle provides nine actions and two signed project webhook triggers. Actions run as the verified personal access token owner.
14
+
15
+ Follow the [GitLab setup guide](https://docs.langflow.org/next/bundles-gitlab) for personal access token setup, deployment configuration, connections, background runs, event identity, limits, and recovery. The maintained [guide source](../../../docs/docs/Components/bundles-gitlab.mdx) ships with the next-version documentation.
16
+
17
+ The [provider contract](../../../design/provider-integrations/contracts/gitlab.v1.json) records capabilities and acceptance evidence. Live tenant authorization and production delivery acceptance remain unverified.
@@ -0,0 +1,23 @@
1
+ lfx_gitlab/__init__.py,sha256=7KFL1joUnl8LcUsAfB_Oi0JaoOeLgkLsUg-eR9yyeYs,65
2
+ lfx_gitlab/base.py,sha256=G8DycRfLRxqzU2BR4qtG5yagndW6qLFxUBj4nqRmjeM,4524
3
+ lfx_gitlab/client.py,sha256=xbWmQM9BlKMa_G7sEuWSMjvyUAHHoE_dePUrQG7Zo60,9799
4
+ lfx_gitlab/extension.json,sha256=ofRKzuWrsTc2un8_3j1JxuYBIn0s4q30s_P7u-1fBWg,512
5
+ lfx_gitlab/triggers.py,sha256=iVQdReJwgUNrFFzs1WJhc4JVT78iS9PzqtKo63Nz8DM,2149
6
+ lfx_gitlab/components/__init__.py,sha256=emLso-Zy9TdiRC52KtESwi7Ao8MQAV-Z7KhBG6BkK0Q,35
7
+ lfx_gitlab/components/gitlab/__init__.py,sha256=pzAPpinipS15G_tjHrGpqlkRJPoeq932p0wBXFb3lzo,1162
8
+ lfx_gitlab/components/gitlab/capabilities.v1.json,sha256=ZlSp0pcwJoPXLmCJK7CqU2ySywd5RF_Cy_NBhjJnADE,7393
9
+ lfx_gitlab/components/gitlab/issue_comment_create.py,sha256=KCLG4XeMHs3C_m70Q8XL3ZE0qrUnizcD0z-jNjioQyU,860
10
+ lfx_gitlab/components/gitlab/issue_create.py,sha256=zPZCDib2va-ja-sn3zyi2cWL9w4uwHJT7AErOFnRDPE,824
11
+ lfx_gitlab/components/gitlab/issue_get.py,sha256=iu9CJzKK2cOdVBWFPWYJMl_V_nNXlI3Pys8xJrONrHY,722
12
+ lfx_gitlab/components/gitlab/issue_trigger.py,sha256=STfzgODfeIFVAvocLR5bw8kXKRa8XSdfRcv4Bebtr9Y,427
13
+ lfx_gitlab/components/gitlab/issue_update.py,sha256=nq-XmsWvOl19Kipl9o6-UeNxl7LbgMA0bII0DYEi9YQ,1262
14
+ lfx_gitlab/components/gitlab/merge_request_comment_create.py,sha256=2hlQppfDnhM_PBnDE8UbRQoJrl5y0HrEmi2nd-DzO6Q,906
15
+ lfx_gitlab/components/gitlab/merge_request_create.py,sha256=mTF2-UAHUcKGZjZa246ssbkk9VHwCCpk2QSLB3sY-Ko,1064
16
+ lfx_gitlab/components/gitlab/merge_request_get.py,sha256=1ZO9UZO8F8z1XgThI3cb4lY_DrLYVbgFTuA8q8k_fIg,784
17
+ lfx_gitlab/components/gitlab/merge_request_trigger.py,sha256=HI_AsRmv2ilMIoTtUJl-GHtcYOl-1RmWinDweUYx39k,465
18
+ lfx_gitlab/components/gitlab/merge_request_update.py,sha256=zlQcqbE41cbum4AGAfYajsTeX8V3grJWxoLwUJNUzKU,1324
19
+ lfx_gitlab/components/gitlab/project_get.py,sha256=Ad_i7umjquGms7miZn8kgcTmegJMN0Vqmo9Y8gh4Kww,601
20
+ lfx_gitlab-0.1.0.dist-info/METADATA,sha256=eTE67_T49Tba6BAsJZSxxRleFRV4dFHuHMv05aBO6w0,978
21
+ lfx_gitlab-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
22
+ lfx_gitlab-0.1.0.dist-info/entry_points.txt,sha256=N8vyQMhI8xJkcDJZzek6KuU4h44RB143_utxcMlxRcU,146
23
+ lfx_gitlab-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [langflow.extensions]
2
+ lfx-gitlab = lfx_gitlab
3
+
4
+ [langflow.trigger_providers]
5
+ gitlab = langflow.services.triggers.providers.gitlab.runtime:PROVIDER