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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions artbotlib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
119 changes: 119 additions & 0 deletions artbotlib/gitlab_mr_status.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions artbotlib/regex_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 (?P<mr_url>https://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 (?P<thread_url>https\://[\w\-]+\.slack\.com/archives/[A-Z0-9]+/p\d+)$",
"flag": re.I,
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions tests/test_regex_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,3 +931,30 @@ 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)