From 684107f8f5cabdd0dc3b1f75e9937d08b068d876 Mon Sep 17 00:00:00 2001 From: Bradley Bishop Date: Tue, 28 Oct 2025 14:26:04 -0400 Subject: [PATCH 1/5] Added all integrations needed to completely manage gitlab issues. --- CHANGELOG.md | 6 +- actions/issue.close.yaml | 27 ++++++ actions/issue.create.yaml | 36 ++++++++ actions/issue.list.yaml | 31 +++++++ actions/issue.note.create.yaml | 31 +++++++ actions/issue.notes.list.yaml | 27 ++++++ actions/issue.reopen.yaml | 30 ++++++ actions/issue.update.yaml | 39 ++++++++ actions/issue_close.py | 26 ++++++ actions/issue_create.py | 49 ++++++++++ actions/issue_list.py | 27 ++++++ actions/issue_note_create.py | 29 ++++++ actions/issue_notes_list.py | 32 +++++++ actions/issue_reopen.py | 26 ++++++ actions/issue_update.py | 56 ++++++++++++ actions/lib/gitlab.py | 162 +++++++++++++++++++++++++++++++-- pack.yaml | 2 +- 17 files changed, 628 insertions(+), 8 deletions(-) create mode 100644 actions/issue.close.yaml create mode 100644 actions/issue.create.yaml create mode 100644 actions/issue.list.yaml create mode 100644 actions/issue.note.create.yaml create mode 100644 actions/issue.notes.list.yaml create mode 100644 actions/issue.reopen.yaml create mode 100644 actions/issue.update.yaml create mode 100644 actions/issue_close.py create mode 100644 actions/issue_create.py create mode 100644 actions/issue_list.py create mode 100644 actions/issue_note_create.py create mode 100644 actions/issue_notes_list.py create mode 100644 actions/issue_reopen.py create mode 100644 actions/issue_update.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9953c8a..c29ee28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## v1.0.1 +## v1.0.2 + +* Added the rest of the integrations needed to completely manage gitlab issues. + +* ## v1.0.1 * Small bug fixes regarding Python 3 support diff --git a/actions/issue.close.yaml b/actions/issue.close.yaml new file mode 100644 index 0000000..1c4cce6 --- /dev/null +++ b/actions/issue.close.yaml @@ -0,0 +1,27 @@ +--- + +name: issue.close +description: "Close a GitLab issue" + +runner_type: python-script +entry_point: issue_close.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + issue_iid: + type: string + required: true + description: the iid of the issue in the project (not global unique id) + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.create.yaml b/actions/issue.create.yaml new file mode 100644 index 0000000..ec73826 --- /dev/null +++ b/actions/issue.create.yaml @@ -0,0 +1,36 @@ +--- + +name: issue.create +description: "Create a new GitLab issue" + +runner_type: python-script +entry_point: issue_create.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + title: + type: string + required: true + description: The issue title + description: + type: string + description: The issue description + assignee_ids: + type: string + description: Comma-separated list of user IDs to assign the issue to + labels: + type: string + description: Comma-separated list of label names + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.list.yaml b/actions/issue.list.yaml new file mode 100644 index 0000000..b4c3527 --- /dev/null +++ b/actions/issue.list.yaml @@ -0,0 +1,31 @@ +--- + +name: issue.list +description: "List all issues for a project with filtering by state" + +runner_type: python-script +entry_point: issue_list.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + state: + type: string + description: Filter issues by state + enum: + - opened + - closed + - all + default: all + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.note.create.yaml b/actions/issue.note.create.yaml new file mode 100644 index 0000000..5717df5 --- /dev/null +++ b/actions/issue.note.create.yaml @@ -0,0 +1,31 @@ +--- + +name: issue.note.create +description: "Add a note (comment) to a GitLab issue" + +runner_type: python-script +entry_point: issue_note_create.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + issue_iid: + type: string + required: true + description: the iid of the issue in the project (not global unique id) + body: + type: string + required: true + description: The content of the note/comment + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.notes.list.yaml b/actions/issue.notes.list.yaml new file mode 100644 index 0000000..0234f96 --- /dev/null +++ b/actions/issue.notes.list.yaml @@ -0,0 +1,27 @@ +--- + +name: issue.notes.list +description: "List all notes (comments) on a GitLab issue" + +runner_type: python-script +entry_point: issue_notes_list.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + issue_iid: + type: string + required: true + description: the iid of the issue in the project (not global unique id) + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.reopen.yaml b/actions/issue.reopen.yaml new file mode 100644 index 0000000..da1bed5 --- /dev/null +++ b/actions/issue.reopen.yaml @@ -0,0 +1,30 @@ +--- + +name: issue.reopen +description: "Reopen a closed GitLab issue with optional description update" + +runner_type: python-script +entry_point: issue_reopen.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + issue_iid: + type: string + required: true + description: the iid of the issue in the project (not global unique id) + description: + type: string + description: Optional new description for the issue when reopening + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue.update.yaml b/actions/issue.update.yaml new file mode 100644 index 0000000..eb56837 --- /dev/null +++ b/actions/issue.update.yaml @@ -0,0 +1,39 @@ +--- + +name: issue.update +description: "Update an existing GitLab issue" + +runner_type: python-script +entry_point: issue_update.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/group/project) + issue_iid: + type: string + required: true + description: the iid of the issue in the project (not global unique id) + title: + type: string + description: The new issue title + description: + type: string + description: The new issue description + assignee_ids: + type: string + description: Comma-separated list of user IDs to assign the issue to (empty string to clear) + labels: + type: string + description: Comma-separated list of label names (empty string to clear) + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/issue_close.py b/actions/issue_close.py new file mode 100644 index 0000000..165a86c --- /dev/null +++ b/actions/issue_close.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueClose(GitlabIssuesAPI): + + def run(self, url, project, issue_iid, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + issue = self.close(self.url, project, issue_iid) + + # Return formatted response + result = { + 'id': issue.get('iid'), + 'global_id': issue.get('id'), + 'title': issue.get('title'), + 'state': issue.get('state'), + 'web_url': issue.get('web_url'), + 'updated_at': issue.get('updated_at'), + 'closed_at': issue.get('closed_at') + } + + return True, result diff --git a/actions/issue_create.py b/actions/issue_create.py new file mode 100644 index 0000000..de4368f --- /dev/null +++ b/actions/issue_create.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueCreate(GitlabIssuesAPI): + + def run(self, url, project, title, description, assignee_ids, labels, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + # Parse assignee_ids if provided as comma-separated string + parsed_assignee_ids = None + if assignee_ids: + if isinstance(assignee_ids, str): + parsed_assignee_ids = [int(aid.strip()) for aid in assignee_ids.split(',')] + elif isinstance(assignee_ids, list): + parsed_assignee_ids = [int(aid) for aid in assignee_ids] + + # Parse labels if provided as comma-separated string + parsed_labels = None + if labels: + if isinstance(labels, str): + parsed_labels = [label.strip() for label in labels.split(',')] + elif isinstance(labels, list): + parsed_labels = labels + + issue = self.create( + self.url, + project, + title, + description=description, + assignee_ids=parsed_assignee_ids, + labels=parsed_labels + ) + + # Return formatted response + result = { + 'id': issue.get('iid'), + 'global_id': issue.get('id'), + 'title': issue.get('title'), + 'description': issue.get('description'), + 'state': issue.get('state'), + 'web_url': issue.get('web_url'), + 'created_at': issue.get('created_at') + } + + return True, result diff --git a/actions/issue_list.py b/actions/issue_list.py new file mode 100644 index 0000000..fd2e8b4 --- /dev/null +++ b/actions/issue_list.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueList(GitlabIssuesAPI): + + def run(self, url, project, state, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + issues = self.list(self.url, project, state=state) + + # Format the response to include name (title), ID (iid), and status (state) + formatted_issues = [] + for issue in issues: + formatted_issues.append({ + 'name': issue.get('title'), + 'id': issue.get('iid'), + 'status': issue.get('state'), + 'web_url': issue.get('web_url'), + 'created_at': issue.get('created_at'), + 'updated_at': issue.get('updated_at') + }) + + return True, formatted_issues diff --git a/actions/issue_note_create.py b/actions/issue_note_create.py new file mode 100644 index 0000000..061dcb2 --- /dev/null +++ b/actions/issue_note_create.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueNoteCreate(GitlabIssuesAPI): + + def run(self, url, project, issue_iid, body, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + note = self.create_note(self.url, project, issue_iid, body) + + # Return formatted response + result = { + 'id': note.get('id'), + 'body': note.get('body'), + 'author': { + 'id': note.get('author', {}).get('id'), + 'username': note.get('author', {}).get('username'), + 'name': note.get('author', {}).get('name') + }, + 'created_at': note.get('created_at'), + 'noteable_type': note.get('noteable_type'), + 'noteable_iid': note.get('noteable_iid') + } + + return True, result diff --git a/actions/issue_notes_list.py b/actions/issue_notes_list.py new file mode 100644 index 0000000..0a5df7b --- /dev/null +++ b/actions/issue_notes_list.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueNotesList(GitlabIssuesAPI): + + def run(self, url, project, issue_iid, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + notes = self.list_notes(self.url, project, issue_iid) + + # Format the response + formatted_notes = [] + for note in notes: + formatted_notes.append({ + 'id': note.get('id'), + 'body': note.get('body'), + 'author': { + 'id': note.get('author', {}).get('id'), + 'username': note.get('author', {}).get('username'), + 'name': note.get('author', {}).get('name') + }, + 'created_at': note.get('created_at'), + 'updated_at': note.get('updated_at'), + 'system': note.get('system', False), + 'noteable_type': note.get('noteable_type') + }) + + return True, formatted_notes diff --git a/actions/issue_reopen.py b/actions/issue_reopen.py new file mode 100644 index 0000000..297c9a4 --- /dev/null +++ b/actions/issue_reopen.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueReopen(GitlabIssuesAPI): + + def run(self, url, project, issue_iid, description, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + issue = self.reopen(self.url, project, issue_iid, description=description) + + # Return formatted response + result = { + 'id': issue.get('iid'), + 'global_id': issue.get('id'), + 'title': issue.get('title'), + 'description': issue.get('description'), + 'state': issue.get('state'), + 'web_url': issue.get('web_url'), + 'updated_at': issue.get('updated_at') + } + + return True, result diff --git a/actions/issue_update.py b/actions/issue_update.py new file mode 100644 index 0000000..436d63f --- /dev/null +++ b/actions/issue_update.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +from lib.gitlab import GitlabIssuesAPI + + +class GitlabIssueUpdate(GitlabIssuesAPI): + + def run(self, url, project, issue_iid, title, description, assignee_ids, labels, token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + # Parse assignee_ids if provided as comma-separated string + parsed_assignee_ids = None + if assignee_ids is not None: + if isinstance(assignee_ids, str): + if assignee_ids.strip(): # Only parse if not empty string + parsed_assignee_ids = [int(aid.strip()) for aid in assignee_ids.split(',')] + else: + parsed_assignee_ids = [] # Empty list clears assignees + elif isinstance(assignee_ids, list): + parsed_assignee_ids = [int(aid) for aid in assignee_ids] + + # Parse labels if provided as comma-separated string + parsed_labels = None + if labels is not None: + if isinstance(labels, str): + if labels.strip(): # Only parse if not empty string + parsed_labels = [label.strip() for label in labels.split(',')] + else: + parsed_labels = [] # Empty list clears labels + elif isinstance(labels, list): + parsed_labels = labels + + issue = self.update( + self.url, + project, + issue_iid, + title=title, + description=description, + assignee_ids=parsed_assignee_ids, + labels=parsed_labels + ) + + # Return formatted response + result = { + 'id': issue.get('iid'), + 'global_id': issue.get('id'), + 'title': issue.get('title'), + 'description': issue.get('description'), + 'state': issue.get('state'), + 'web_url': issue.get('web_url'), + 'updated_at': issue.get('updated_at') + } + + return True, result diff --git a/actions/lib/gitlab.py b/actions/lib/gitlab.py index 64834af..f6a9a24 100644 --- a/actions/lib/gitlab.py +++ b/actions/lib/gitlab.py @@ -27,17 +27,18 @@ def wrap(*args, **kwargs): class RequestsMethod(object): @staticmethod - def method(method, url, verify_ssl=False, headers=None, params=None): + def method(method, url, verify_ssl=False, headers=None, params=None, json_data=None): methods = {'get': requests.get, - 'post': requests.post} + 'post': requests.post, + 'put': requests.put} if not params: params = dict() requests_method = methods.get(method) response = requests_method( - url, headers=headers, params=params, verify=verify_ssl) - + url, headers=headers, params=params, json=json_data, verify=verify_ssl) + if response.status_code: return response.json() @@ -63,9 +64,14 @@ def _get(self, url, endpoint, headers, params=None, *args, **kwargs): return RequestsMethod.method('get', api_url, self.verify_ssl, headers, params) @override_token - def _post(self, url, endpoint, headers, params=None, *args, **kwargs): + def _post(self, url, endpoint, headers, params=None, json_data=None, *args, **kwargs): api_url = '/'.join((url, self._api_ext, endpoint)) - return RequestsMethod.method('post', api_url, self.verify_ssl, headers, params) + return RequestsMethod.method('post', api_url, self.verify_ssl, headers, params, json_data) + + @override_token + def _put(self, url, endpoint, headers, params=None, json_data=None, *args, **kwargs): + api_url = '/'.join((url, self._api_ext, endpoint)) + return RequestsMethod.method('put', api_url, self.verify_ssl, headers, params, json_data) def get(self, *args, **kwargs): return self._get(*args, **kwargs) @@ -73,6 +79,9 @@ def get(self, *args, **kwargs): def post(self, *args, **kwargs): return self._post(*args, **kwargs) + def put(self, *args, **kwargs): + return self._put(*args, **kwargs) + class GitlabProjectsAPI(GitlabRestClient): @@ -98,6 +107,147 @@ def get(self, url, endpoint, issue_id, **kwargs): self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_id) return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) + def list(self, url, endpoint, state=None, **kwargs): + """List all issues for a project. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + state: Filter by state ('opened', 'closed', 'all'). Default is 'all'. + **kwargs: Additional query parameters + """ + real_endpoint = "{0}/{1}/{2}".format( + self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint) + + params = kwargs.get('params', {}) + if state: + params['state'] = state + kwargs['params'] = params + + return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) + + def create(self, url, endpoint, title, description=None, assignee_ids=None, labels=None, **kwargs): + """Create a new issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + title: Issue title (required) + description: Issue description + assignee_ids: List of user IDs to assign + labels: List of label names + **kwargs: Additional issue parameters + """ + real_endpoint = "{0}/{1}/{2}".format( + self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint) + + json_data = {'title': title} + if description: + json_data['description'] = description + if assignee_ids: + json_data['assignee_ids'] = assignee_ids + if labels: + json_data['labels'] = ','.join(labels) if isinstance(labels, list) else labels + + # Merge any additional parameters + json_data.update(kwargs) + + return self._post(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + + def update(self, url, endpoint, issue_iid, title=None, description=None, + assignee_ids=None, labels=None, state_event=None, **kwargs): + """Update an existing issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + issue_iid: Issue IID (project-specific ID) + title: New title + description: New description + assignee_ids: List of user IDs to assign + labels: List of label names + state_event: State action ('close' or 'reopen') + **kwargs: Additional issue parameters + """ + real_endpoint = "{0}/{1}/{2}/{3}".format( + self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid) + + json_data = {} + if title: + json_data['title'] = title + if description is not None: # Allow empty string to clear description + json_data['description'] = description + if assignee_ids is not None: + json_data['assignee_ids'] = assignee_ids + if labels is not None: + json_data['labels'] = ','.join(labels) if isinstance(labels, list) else labels + if state_event: + json_data['state_event'] = state_event + + # Merge any additional parameters + json_data.update(kwargs) + + return self._put(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + + def close(self, url, endpoint, issue_iid, **kwargs): + """Close an issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + issue_iid: Issue IID (project-specific ID) + **kwargs: Additional parameters (e.g., description update) + """ + return self.update(url, endpoint, issue_iid, state_event='close', **kwargs) + + def reopen(self, url, endpoint, issue_iid, description=None, **kwargs): + """Reopen a closed issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + issue_iid: Issue IID (project-specific ID) + description: Optional description update + **kwargs: Additional parameters + """ + update_kwargs = {'state_event': 'reopen'} + if description is not None: + update_kwargs['description'] = description + update_kwargs.update(kwargs) + + return self.update(url, endpoint, issue_iid, **update_kwargs) + + def list_notes(self, url, endpoint, issue_iid, **kwargs): + """List all notes (comments) on an issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + issue_iid: Issue IID (project-specific ID) + **kwargs: Additional query parameters + """ + real_endpoint = "{0}/{1}/{2}/{3}/notes".format( + self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid) + return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) + + def create_note(self, url, endpoint, issue_iid, body, **kwargs): + """Add a note (comment) to an issue. + + Args: + url: GitLab instance URL + endpoint: Project path (e.g., 'group/project') + issue_iid: Issue IID (project-specific ID) + body: The content of the note + **kwargs: Additional note parameters + """ + real_endpoint = "{0}/{1}/{2}/{3}/notes".format( + self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid) + + json_data = {'body': body} + json_data.update(kwargs) + + return self._post(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + class GitlabPipelineAPI(GitlabRestClient): diff --git a/pack.yaml b/pack.yaml index 324c372..46731f5 100644 --- a/pack.yaml +++ b/pack.yaml @@ -3,7 +3,7 @@ name: gitlab description: GitLab Rest API keywords: - gitlab -version: 1.0.1 +version: 1.0.2 author: Daniel Chamot email: daniel@nullkarma.com python_versions: From ae7d3727e9177b8e6ab18dfa40f66a9f097a4f13 Mon Sep 17 00:00:00 2001 From: Bradley Bishop Date: Tue, 28 Oct 2025 14:43:19 -0400 Subject: [PATCH 2/5] Fixing syntax issues. --- actions/issue_create.py | 8 ++++++-- actions/issue_update.py | 11 ++++++++--- actions/lib/gitlab.py | 15 ++++++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/actions/issue_create.py b/actions/issue_create.py index de4368f..ebf2057 100644 --- a/actions/issue_create.py +++ b/actions/issue_create.py @@ -14,7 +14,9 @@ def run(self, url, project, title, description, assignee_ids, labels, token, ver parsed_assignee_ids = None if assignee_ids: if isinstance(assignee_ids, str): - parsed_assignee_ids = [int(aid.strip()) for aid in assignee_ids.split(',')] + parsed_assignee_ids = [ + int(aid.strip()) for aid in assignee_ids.split(',') + ] elif isinstance(assignee_ids, list): parsed_assignee_ids = [int(aid) for aid in assignee_ids] @@ -22,7 +24,9 @@ def run(self, url, project, title, description, assignee_ids, labels, token, ver parsed_labels = None if labels: if isinstance(labels, str): - parsed_labels = [label.strip() for label in labels.split(',')] + parsed_labels = [ + label.strip() for label in labels.split(',') + ] elif isinstance(labels, list): parsed_labels = labels diff --git a/actions/issue_update.py b/actions/issue_update.py index 436d63f..86e7380 100644 --- a/actions/issue_update.py +++ b/actions/issue_update.py @@ -5,7 +5,8 @@ class GitlabIssueUpdate(GitlabIssuesAPI): - def run(self, url, project, issue_iid, title, description, assignee_ids, labels, token, verify_ssl): + def run(self, url, project, issue_iid, title, description, + assignee_ids, labels, token, verify_ssl): self.url = url or self.url self.verify_ssl = verify_ssl or self.verify_ssl self.token = token or self.token @@ -15,7 +16,9 @@ def run(self, url, project, issue_iid, title, description, assignee_ids, labels, if assignee_ids is not None: if isinstance(assignee_ids, str): if assignee_ids.strip(): # Only parse if not empty string - parsed_assignee_ids = [int(aid.strip()) for aid in assignee_ids.split(',')] + parsed_assignee_ids = [ + int(aid.strip()) for aid in assignee_ids.split(',') + ] else: parsed_assignee_ids = [] # Empty list clears assignees elif isinstance(assignee_ids, list): @@ -26,7 +29,9 @@ def run(self, url, project, issue_iid, title, description, assignee_ids, labels, if labels is not None: if isinstance(labels, str): if labels.strip(): # Only parse if not empty string - parsed_labels = [label.strip() for label in labels.split(',')] + parsed_labels = [ + label.strip() for label in labels.split(',') + ] else: parsed_labels = [] # Empty list clears labels elif isinstance(labels, list): diff --git a/actions/lib/gitlab.py b/actions/lib/gitlab.py index f6a9a24..c65ee8c 100644 --- a/actions/lib/gitlab.py +++ b/actions/lib/gitlab.py @@ -124,9 +124,11 @@ def list(self, url, endpoint, state=None, **kwargs): params['state'] = state kwargs['params'] = params - return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) + return self._get(url, real_endpoint, token=self.token, + headers=self._headers, **kwargs) - def create(self, url, endpoint, title, description=None, assignee_ids=None, labels=None, **kwargs): + def create(self, url, endpoint, title, description=None, + assignee_ids=None, labels=None, **kwargs): """Create a new issue. Args: @@ -152,7 +154,8 @@ def create(self, url, endpoint, title, description=None, assignee_ids=None, labe # Merge any additional parameters json_data.update(kwargs) - return self._post(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + return self._post(url, real_endpoint, token=self.token, + headers=self._headers, json_data=json_data) def update(self, url, endpoint, issue_iid, title=None, description=None, assignee_ids=None, labels=None, state_event=None, **kwargs): @@ -187,7 +190,8 @@ def update(self, url, endpoint, issue_iid, title=None, description=None, # Merge any additional parameters json_data.update(kwargs) - return self._put(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + return self._put(url, real_endpoint, token=self.token, + headers=self._headers, json_data=json_data) def close(self, url, endpoint, issue_iid, **kwargs): """Close an issue. @@ -246,7 +250,8 @@ def create_note(self, url, endpoint, issue_iid, body, **kwargs): json_data = {'body': body} json_data.update(kwargs) - return self._post(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) + return self._post(url, real_endpoint, token=self.token, + headers=self._headers, json_data=json_data) class GitlabPipelineAPI(GitlabRestClient): From 104b5676891e062aacd41dbb2ef718738988f061 Mon Sep 17 00:00:00 2001 From: Bradley Bishop Date: Tue, 28 Oct 2025 14:47:45 -0400 Subject: [PATCH 3/5] removing all blank lines and trailing white space. --- actions/lib/gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/lib/gitlab.py b/actions/lib/gitlab.py index c65ee8c..79c540d 100644 --- a/actions/lib/gitlab.py +++ b/actions/lib/gitlab.py @@ -38,7 +38,7 @@ def method(method, url, verify_ssl=False, headers=None, params=None, json_data=N requests_method = methods.get(method) response = requests_method( url, headers=headers, params=params, json=json_data, verify=verify_ssl) - + if response.status_code: return response.json() From 9bdc10a1c0687762af6fa7e6038143819db4e253 Mon Sep 17 00:00:00 2001 From: Bradley Bishop Date: Tue, 28 Oct 2025 22:17:17 -0400 Subject: [PATCH 4/5] Added the ability to get file from project. --- CHANGELOG.md | 1 + actions/lib/gitlab.py | 21 ++++++++++++++++++ actions/project.file.get.yaml | 35 ++++++++++++++++++++++++++++++ actions/project_file_get.py | 41 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 actions/project.file.get.yaml create mode 100644 actions/project_file_get.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c29ee28..518b2ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v1.0.2 * Added the rest of the integrations needed to completely manage gitlab issues. +* Added ability to get a file from a project. * ## v1.0.1 diff --git a/actions/lib/gitlab.py b/actions/lib/gitlab.py index 79c540d..acd5463 100644 --- a/actions/lib/gitlab.py +++ b/actions/lib/gitlab.py @@ -94,6 +94,27 @@ def get(self, url, endpoint, **kwargs): quote_plus(endpoint)) return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) + def get_file(self, url, project, file_path, ref='main', **kwargs): + """Get a file from a project repository. + + Args: + url: GitLab instance URL + project: Project path (e.g., 'group/project') + file_path: Path to file in repository + ref: Branch, tag, or commit SHA (default: 'main') + **kwargs: Additional query parameters + """ + real_endpoint = "{0}/{1}/repository/files/{2}".format( + self._api_endpoint, quote_plus(project), + quote_plus(file_path)) + + params = kwargs.get('params', {}) + params['ref'] = ref + kwargs['params'] = params + + return self._get(url, real_endpoint, token=self.token, + headers=self._headers, **kwargs) + class GitlabIssuesAPI(GitlabRestClient): diff --git a/actions/project.file.get.yaml b/actions/project.file.get.yaml new file mode 100644 index 0000000..09db2f5 --- /dev/null +++ b/actions/project.file.get.yaml @@ -0,0 +1,35 @@ +--- + +name: project.file.get +description: "Get a file/template from a GitLab project repository" + +runner_type: python-script +entry_point: project_file_get.py + +parameters: + url: + type: string + description: Override the configured base url for gitlab + project: + type: string + required: true + description: the 'path_with_namespace' for the project (e.g., group/project) + file_path: + type: string + required: true + description: Path to file in repository (e.g., '.gitlab/issue_templates/bug.md') + ref: + type: string + description: Branch, tag, or commit SHA to get file from + default: main + decode_content: + type: boolean + description: Decode base64 content and include in response + default: true + token: + type: string + description: Override the configured token for the gitlab api + secret: true + verify_ssl: + type: boolean + default: False diff --git a/actions/project_file_get.py b/actions/project_file_get.py new file mode 100644 index 0000000..f536788 --- /dev/null +++ b/actions/project_file_get.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +import base64 +from lib.gitlab import GitlabProjectsAPI + + +class GitlabProjectFileGet(GitlabProjectsAPI): + + def run(self, url, project, file_path, ref, decode_content, + token, verify_ssl): + self.url = url or self.url + self.verify_ssl = verify_ssl or self.verify_ssl + self.token = token or self.token + + file_data = self.get_file(self.url, project, file_path, ref=ref) + + # Return formatted response + result = { + 'file_name': file_data.get('file_name'), + 'file_path': file_data.get('file_path'), + 'size': file_data.get('size'), + 'encoding': file_data.get('encoding'), + 'content': file_data.get('content'), + 'ref': file_data.get('ref'), + 'blob_id': file_data.get('blob_id'), + 'commit_id': file_data.get('commit_id'), + 'last_commit_id': file_data.get('last_commit_id') + } + + # Decode base64 content if requested + if decode_content and file_data.get('encoding') == 'base64': + try: + decoded_content = base64.b64decode( + file_data.get('content', '') + ).decode('utf-8') + result['content_decoded'] = decoded_content + except Exception as e: + result['content_decoded'] = None + result['decode_error'] = str(e) + + return True, result From 833354a7e225854a1eacc4b4d74340b3a4ad9aff Mon Sep 17 00:00:00 2001 From: Bradley Bishop Date: Wed, 29 Oct 2025 09:15:26 -0400 Subject: [PATCH 5/5] Small syntax fixes to conform with rest of pack. --- actions/lib/gitlab.py | 79 +------------------------------------------ 1 file changed, 1 insertion(+), 78 deletions(-) diff --git a/actions/lib/gitlab.py b/actions/lib/gitlab.py index acd5463..1611211 100644 --- a/actions/lib/gitlab.py +++ b/actions/lib/gitlab.py @@ -95,15 +95,6 @@ def get(self, url, endpoint, **kwargs): return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) def get_file(self, url, project, file_path, ref='main', **kwargs): - """Get a file from a project repository. - - Args: - url: GitLab instance URL - project: Project path (e.g., 'group/project') - file_path: Path to file in repository - ref: Branch, tag, or commit SHA (default: 'main') - **kwargs: Additional query parameters - """ real_endpoint = "{0}/{1}/repository/files/{2}".format( self._api_endpoint, quote_plus(project), quote_plus(file_path)) @@ -129,14 +120,6 @@ def get(self, url, endpoint, issue_id, **kwargs): return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) def list(self, url, endpoint, state=None, **kwargs): - """List all issues for a project. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - state: Filter by state ('opened', 'closed', 'all'). Default is 'all'. - **kwargs: Additional query parameters - """ real_endpoint = "{0}/{1}/{2}".format( self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint) @@ -150,17 +133,6 @@ def list(self, url, endpoint, state=None, **kwargs): def create(self, url, endpoint, title, description=None, assignee_ids=None, labels=None, **kwargs): - """Create a new issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - title: Issue title (required) - description: Issue description - assignee_ids: List of user IDs to assign - labels: List of label names - **kwargs: Additional issue parameters - """ real_endpoint = "{0}/{1}/{2}".format( self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint) @@ -172,7 +144,6 @@ def create(self, url, endpoint, title, description=None, if labels: json_data['labels'] = ','.join(labels) if isinstance(labels, list) else labels - # Merge any additional parameters json_data.update(kwargs) return self._post(url, real_endpoint, token=self.token, @@ -180,26 +151,13 @@ def create(self, url, endpoint, title, description=None, def update(self, url, endpoint, issue_iid, title=None, description=None, assignee_ids=None, labels=None, state_event=None, **kwargs): - """Update an existing issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - issue_iid: Issue IID (project-specific ID) - title: New title - description: New description - assignee_ids: List of user IDs to assign - labels: List of label names - state_event: State action ('close' or 'reopen') - **kwargs: Additional issue parameters - """ real_endpoint = "{0}/{1}/{2}/{3}".format( self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid) json_data = {} if title: json_data['title'] = title - if description is not None: # Allow empty string to clear description + if description is not None: json_data['description'] = description if assignee_ids is not None: json_data['assignee_ids'] = assignee_ids @@ -208,33 +166,15 @@ def update(self, url, endpoint, issue_iid, title=None, description=None, if state_event: json_data['state_event'] = state_event - # Merge any additional parameters json_data.update(kwargs) return self._put(url, real_endpoint, token=self.token, headers=self._headers, json_data=json_data) def close(self, url, endpoint, issue_iid, **kwargs): - """Close an issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - issue_iid: Issue IID (project-specific ID) - **kwargs: Additional parameters (e.g., description update) - """ return self.update(url, endpoint, issue_iid, state_event='close', **kwargs) def reopen(self, url, endpoint, issue_iid, description=None, **kwargs): - """Reopen a closed issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - issue_iid: Issue IID (project-specific ID) - description: Optional description update - **kwargs: Additional parameters - """ update_kwargs = {'state_event': 'reopen'} if description is not None: update_kwargs['description'] = description @@ -243,28 +183,11 @@ def reopen(self, url, endpoint, issue_iid, description=None, **kwargs): return self.update(url, endpoint, issue_iid, **update_kwargs) def list_notes(self, url, endpoint, issue_iid, **kwargs): - """List all notes (comments) on an issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - issue_iid: Issue IID (project-specific ID) - **kwargs: Additional query parameters - """ real_endpoint = "{0}/{1}/{2}/{3}/notes".format( self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid) return self._get(url, real_endpoint, token=self.token, headers=self._headers, **kwargs) def create_note(self, url, endpoint, issue_iid, body, **kwargs): - """Add a note (comment) to an issue. - - Args: - url: GitLab instance URL - endpoint: Project path (e.g., 'group/project') - issue_iid: Issue IID (project-specific ID) - body: The content of the note - **kwargs: Additional note parameters - """ real_endpoint = "{0}/{1}/{2}/{3}/notes".format( self._api_endpoint, quote_plus(endpoint), self._api_sub_endpoint, issue_iid)