From 933031f8652a070f3571cee0ce9614234fa041bc Mon Sep 17 00:00:00 2001 From: ximhan Date: Thu, 6 Nov 2025 16:44:30 +0800 Subject: [PATCH 1/3] support check gitlab mr status --- artbotlib/constants.py | 4 ++ artbotlib/gitlab_mr_status.py | 119 ++++++++++++++++++++++++++++++++++ artbotlib/regex_mapping.py | 8 +++ requirements.txt | 1 + tests/test_regex_mapping.py | 37 +++++++++++ 5 files changed, 169 insertions(+) create mode 100644 artbotlib/gitlab_mr_status.py diff --git a/artbotlib/constants.py b/artbotlib/constants.py index 97b9d43..1410d94 100644 --- a/artbotlib/constants.py +++ b/artbotlib/constants.py @@ -37,6 +37,10 @@ SLACK_SUMMARIZER = 'https://slack-summarizer-hackspace-dpaolell.apps.artc2023.pc3z.p1.openshiftapps.com' +GITLAB_INSTANCE_URL = "https://gitlab.cee.redhat.com" + +GITLAB_PROJECT_PATH = "hybrid-platforms/art/ocp-shipment-data" + # Release Controller and RHCOS browser call arches in different ways; # these two dictionaries easily map names from/to one namespace to the other RC_ARCH_TO_RHCOS_ARCH = { diff --git a/artbotlib/gitlab_mr_status.py b/artbotlib/gitlab_mr_status.py new file mode 100644 index 0000000..11f95f9 --- /dev/null +++ b/artbotlib/gitlab_mr_status.py @@ -0,0 +1,119 @@ +import logging +import os +import gitlab + +from artbotlib import variables +from artbotlib.constants import GITLAB_INSTANCE_URL, GITLAB_PROJECT_PATH +logger = logging.getLogger(__name__) + + +def gitlab_mr_status(so, mr_url): + """ + Fetches and displays GitLab MR pipeline job statuses. + + This command queries the GitLab API to retrieve the downstream pipeline jobs + for a given merge request and displays their statuses. + + Args: + so: SlackOutput instance for sending messages + mr_url: Full GitLab MR URL (e.g., https://gitlab.cee.redhat.com/hybrid-platforms/art/ocp-shipment-data/-/merge_requests/207) + """ + + # Extract MR ID from URL + mr_id = mr_url.rstrip('/').split('/')[-1] + + # Check for auth token + token = os.environ.get('GITLAB_PRIVATE_TOKEN') + if not token: + so.say("Error: GITLAB_PRIVATE_TOKEN environment variable is not set. Please contact the bot administrator.") + logger.error("GITLAB_PRIVATE_TOKEN environment variable not found") + return + + so.say(f"Fetching pipeline job statuses for MR {mr_id}...") + + variables.active_slack_objects.add(so) + + try: + # Connect to GitLab + gl = gitlab.Gitlab(GITLAB_INSTANCE_URL, private_token=token) + gl.auth() + + # Get the project + project = gl.projects.get(GITLAB_PROJECT_PATH) + + # Get the Merge Request + mr = project.mergerequests.get(mr_id) + + # Get the pipeline from the MR + pipeline_info = mr.pipeline + if not pipeline_info: + so.say(f"No pipeline found for MR {mr_id}") + return + + pipeline_id = pipeline_info['id'] + logger.info(f"Found pipeline {pipeline_id} for MR {mr_id}") + + # Get the main pipeline + main_pipeline = project.pipelines.get(pipeline_id) + + # Get downstream pipelines via bridges + bridges = main_pipeline.bridges.list(all=True) + + downstream_pipelines = [] + for bridge in bridges: + if bridge.downstream_pipeline: + downstream_pipelines.append(bridge.downstream_pipeline) + + if not downstream_pipelines: + so.say(f"No downstream pipelines found for MR {mr_id} (Pipeline: {pipeline_id})") + return + + # Format and display results + result_lines = [f"*Pipeline Job Status for MR {mr_id}* (Pipeline: {pipeline_id})"] + result_lines.append("") + + # Loop through downstream pipelines and collect job statuses + for ds_pipeline_info in downstream_pipelines: + ds_id = ds_pipeline_info['id'] + result_lines.append(f"*Downstream Pipeline {ds_id}:*") + + try: + ds_pipeline = project.pipelines.get(ds_id) + jobs = ds_pipeline.jobs.list(all=True) + + if not jobs: + result_lines.append(" (No jobs found for this pipeline)") + result_lines.append("") + continue + + # Group jobs by status for better readability + status_groups = {} + for job in jobs: + status = job.status.upper() + if status not in status_groups: + status_groups[status] = [] + status_groups[status].append((job.name, job.id)) + + # Display jobs grouped by status + for status in sorted(status_groups.keys()): + jobs_list = status_groups[status] + result_lines.append(f" *{status}* ({len(jobs_list)} jobs):") + for job_name, job_id in jobs_list: + result_lines.append(f" • {job_name} (ID: {job_id})") + + result_lines.append("") + + except gitlab.exceptions.GitlabError as e: + logger.error(f"Error fetching jobs for pipeline {ds_id}: {e}") + result_lines.append(f" Error fetching jobs: {e}") + result_lines.append("") + + # Send the formatted message + so.say("\n".join(result_lines)) + + except Exception as e: + so.say(f"Error fetching GitLab MR status: {e}") + logger.exception(f"Unexpected error in gitlab_mr_status for MR {mr_id}") + + finally: + variables.active_slack_objects.remove(so) diff --git a/artbotlib/regex_mapping.py b/artbotlib/regex_mapping.py index 733273c..882d7f6 100644 --- a/artbotlib/regex_mapping.py +++ b/artbotlib/regex_mapping.py @@ -15,6 +15,7 @@ from artbotlib.pr_in_build import pr_info from artbotlib.prow import prow_job_status, first_prow_job_succeeds from artbotlib.translation import translate_names +from artbotlib.gitlab_mr_status import gitlab_mr_status from fuzzywuzzy import process @@ -349,6 +350,13 @@ def map_command_to_regex(so, plain_text, user_id): "user_id": True, "example": "Watch https://github.com/openshift-eng/art-bot/pull/157" }, + { + "regex": r"^gitlab pr status (?Phttps://gitlab\.cee\.redhat\.com/[\w/-]+/-/merge_requests/\d+)$", + "flag": re.I, + "function": gitlab_mr_status, + "user_id": False, + "example": "gitlab pr status https://gitlab.cee.redhat.com/hybrid-platforms/art/ocp-shipment-data/-/merge_requests/207" + }, { "regex": r"^Summarize (?Phttps\://[\w\-]+\.slack\.com/archives/[A-Z0-9]+/p\d+)$", "flag": re.I, diff --git a/requirements.txt b/requirements.txt index cb27727..00f2a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ cachetools slack_bolt fuzzywuzzy python-Levenshtein +python-gitlab # Pin google-* and protobuf to avoid extremely long pip backtracking process google-auth>=2.40.3 google-api-core>=2.25.1 diff --git a/tests/test_regex_mapping.py b/tests/test_regex_mapping.py index b51efb6..863e3cf 100644 --- a/tests/test_regex_mapping.py +++ b/tests/test_regex_mapping.py @@ -931,3 +931,40 @@ def test_brew_event_ts(pipeline_mock): expected_message = generate_expected_message('timestamp for brew event 55331468') so_mock.should_receive('say').once().with_args(expected_message) map_command_to_regex(so_mock, query, None) + + +@patch('artbotlib.regex_mapping.gitlab_mr_status') +def test_gitlab_mr_status(gitlab_mock): + """ + Test valid/invalid queries for gitlab_mr_status.gitlab_mr_status() + """ + + gitlab_mock.side_effect = lambda outputter, *_, **__: outputter.say('mock called') + so_mock = flexmock(so) + + # Valid + query = 'gitlab pr status https://gitlab.cee.redhat.com/hybrid-platforms/art/ocp-shipment-data/-/merge_requests/207' + so_mock.should_receive('say').once() + map_command_to_regex(so_mock, query, None) + + # Valid - different project path + query = 'gitlab pr status https://gitlab.cee.redhat.com/some/other/project/-/merge_requests/123' + so_mock.should_receive('say').once() + map_command_to_regex(so_mock, query, None) + + # Invalid - missing 'gitlab' + query = 'pr status https://gitlab.cee.redhat.com/hybrid-platforms/art/ocp-shipment-data/-/merge_requests/207' + example_command_valid = 'gitlab pr status https://gitlab.cee.redhat.com/hybrid-platforms/art/ocp-shipment-data/-/merge_requests/207' + expected_message = generate_expected_message(example_command_valid) + so_mock.should_receive('say').once().with_args(expected_message) + map_command_to_regex(so_mock, query, None) + + # Invalid - wrong GitLab instance + query = 'gitlab pr status https://gitlab.com/some/project/-/merge_requests/123' + so_mock.should_receive('say').once().with_args(expected_message) + map_command_to_regex(so_mock, query, None) + + # Invalid - GitHub URL instead + query = 'gitlab pr status https://github.com/openshift/art-bot/pull/123' + so_mock.should_receive('say').once().with_args(expected_message) + map_command_to_regex(so_mock, query, None) From d4e09a6ace616ec9c1d3ea06d01f684239b246db Mon Sep 17 00:00:00 2001 From: ximhan Date: Thu, 6 Nov 2025 18:44:11 +0800 Subject: [PATCH 2/3] support check gitlab mr status --- tests/test_regex_mapping.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_regex_mapping.py b/tests/test_regex_mapping.py index 863e3cf..72953c6 100644 --- a/tests/test_regex_mapping.py +++ b/tests/test_regex_mapping.py @@ -958,13 +958,3 @@ def test_gitlab_mr_status(gitlab_mock): expected_message = generate_expected_message(example_command_valid) so_mock.should_receive('say').once().with_args(expected_message) map_command_to_regex(so_mock, query, None) - - # Invalid - wrong GitLab instance - query = 'gitlab pr status https://gitlab.com/some/project/-/merge_requests/123' - so_mock.should_receive('say').once().with_args(expected_message) - map_command_to_regex(so_mock, query, None) - - # Invalid - GitHub URL instead - query = 'gitlab pr status https://github.com/openshift/art-bot/pull/123' - so_mock.should_receive('say').once().with_args(expected_message) - map_command_to_regex(so_mock, query, None) From 03ceebb8195d32090ab4b869bbe9e524faae61fe Mon Sep 17 00:00:00 2001 From: Ximin han Date: Wed, 12 Nov 2025 17:43:56 +0800 Subject: [PATCH 3/3] Update artbotlib/regex_mapping.py Co-authored-by: Adam Dobes <124796824+adobes1@users.noreply.github.com> --- artbotlib/regex_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artbotlib/regex_mapping.py b/artbotlib/regex_mapping.py index 882d7f6..e75c788 100644 --- a/artbotlib/regex_mapping.py +++ b/artbotlib/regex_mapping.py @@ -351,7 +351,7 @@ def map_command_to_regex(so, plain_text, user_id): "example": "Watch https://github.com/openshift-eng/art-bot/pull/157" }, { - "regex": r"^gitlab pr status (?Phttps://gitlab\.cee\.redhat\.com/[\w/-]+/-/merge_requests/\d+)$", + "regex": r"^gitlab mr status (?Phttps://gitlab\.cee\.redhat\.com/[\w/-]+/-/merge_requests/\d+)$", "flag": re.I, "function": gitlab_mr_status, "user_id": False,