Code Review and Merge Requests Summarization on GitLab
Tutorial Goals
- Enable automatic code review in GitLab. Whenever a new merge request is created, the system will automatically add a comment with the analysis results;
- Enable automatic publishing of a summary of code changes in GitLab. This way, when a new merge request is created, a comment with a summary of the changes will be published automatically;
- Create Quick Commands to perform these actions.
In this tutorial, you will split the code review and merge request summary features into three Remote Quick Commands (RQC). Follow the steps below:
Step 1. Create Quick Commands
Quick Command 1: Code Review
This RQC performs automatic code analysis and checks for programming best practices.
Step 1. Access the StackSpot AI Portal and go to the Quick Commands section;

Step 2. Click the ‘Create Quick Command’ button. Then, select the Remote option and fill in the following fields:
- Name: code-review-pt
- Execution command: cr-code
- Description: Automatic code review focused on best practices;
Click the ‘Next’ button;
Select the ‘Hello World’ template and click the ‘Create’ button;
Step 3. Click on the prompt box and update the prompt text:
Act as: a Code Reviewer
Rules:
- Always provide comments in plain text, without code formatting.
- Respond only in Brazilian Portuguese.
- Be objective and clear in your observations.
- Point out possible improvements, best practices, security risks, readability, performance, and maintainability.
- Consider that this code will be deployed to production.
Task: Your task is to critically review the following code and state whether it is suitable for approval in production.
Output_format: plain_text
code: {{input_data}}

Step 4. Click on the other prompt box and add the following name:
- Name: code-fix
- And the following prompt:
Act as: a Code Fixer
Rules:
- Always generate functional code based on the corrections suggested in the code review.
- Respond only in Brazilian Portuguese.
- If any code review suggestion is unclear or subjective, use your best judgment to apply the improvement.
- Preserve the original logic of the code whenever possible.
Task: Based on the original code and the code review comments, generate a new version of the corrected code, ready for production.
Input:
Original code: {{input_data}}
Code review: {{cr-code.answer}}
Output_format: plain_text
Expected output: Updated and corrected code.
Step 5. Click on the ‘Finish’ box. In the ‘Quick Command Utilization’ tab, add the following text in Final Result:
Analysis:
{{cr-code.answer}}
Fix:
{{code-fix.answer}}
Click the ‘Ok’ button to confirm.
Step 6. Click the ‘Finish’ button;

Quick Command 2: Partial file summary
This RQC generates a file summary:
Step 1. Click the ‘Create Quick Command’ button. Select the Remote option and fill in the following fields:
- Name: partial-summary
- Execution command: explain-code
- Description: generates a partial summary of the diff file in a Pull Request. This RQC should be used together with the Pull Request / Merge Request summary solution.
Click the ‘Next’ button.
Select the ‘Hello World’ template and click the ‘Create’ button.
Step 2. Click on the prompt box and update the prompt text to:
Summarize the changes implemented in this GitLab diff.
Return the output in Portuguese, in JSON format, as a JSON array following the schema below:
[
{
"file_path": "The file path in the repository",
"changes_summary": "A description of the changes made to the file, explaining what changed, how it changed, and, when possible, why it changed",
"security_concerns": "Optional field with possible security issues related to the changes, if any"
}
]
This is the diff:
{{ input_data }}
Step 3. Click on the ‘Finish’ box. In the ‘Quick Command Utilization’ tab, add the following text in Final Result:
{{explain-code.answer}}
Click the ‘Ok’ button to confirm.

Step 4. Click the ‘Finish’ button;
Quick Command 3: Complete Merge Request summary
This RQC generates a complete summary of the Merge Request:
Step 1. Click the ‘Create Quick Command’ button. Select the Remote option and fill in the following fields:
- Name: total-summary
- Execution command: explain-code
- Description: This RQC should be used together with the partial summary RQC as part of the Pull Request / Merge Request summary solution.
Click the ‘Next’ button;
Select the ‘Hello World’ template and click the ‘Create’ button.
Step 2. Click the prompt box and update the prompt text to:
You are a bot writing a comment on a GitLab Pull Request.
Below is a JSON object with summaries of the changes applied to each file in the Pull Request. Use this object to generate a human-readable summary of all the changes made in this PR.
At the beginning of your response, include a general summary of all the changes made. Then, provide a list of the modified files, detailing the changes made in each one.
If there are relevant security recommendations, possible important issues, or specific points of attention during the review, include these observations.
Answer in Markdown format.
This is the JSON object:
{{ input_data }}
Step 3. Click the ‘Finish’ box. In the ‘Quick Command Utilization’ tab, add the following text to Final Result:
{{explain-code.answer}}
Click the ‘Ok’ button to confirm.

Step 4. Click the ‘Finish’ button;
Step 2. Create a PAT (Personal Access Token) in StackSpot
Generate and save the Access Token.
- Access the StackSpot Account Portal;
- Click on ‘Access Token’ and then on ‘Generate client key’;
- Copy and save the generated token;

Step 3. Register variables in GitLab
- In your GitLab project, register the following variables:
GENAI_CODE_BUDDY_URL: https://genai-code-buddy-api.stackspot.com/v1/quick-commandsCLIENT_ID: value of the Client ID generated in StackSpot;CLIENT_KEY: value of the Client Key generated in StackSpot;REALM: stackspot-freemium

You can register them directly in the YAML file or in the UI. For more information, see the GitLab documentation.
- Create the
.gitlab-ci.ymlfile at the root of your repository.
#!/usr/bin/env python3
from gitlab_ci_summarizer import *
import os
import sys
import logging
# Import all utility functions from your script (place the function code above here or in an imported module)
# from your_utils import (
# get_gitlab_mr_diff, run_rqc, post_gitlab_mr_comment, validate_encoding, ...
# )
QC_SLUG = "code-review-pt" # Change to your Quick Command slug
def main():
try:
# 1. Get the MR diff
diff = get_gitlab_mr_diff()
if not diff.strip():
logging.warning("⚠️ No changes detected in the MR.")
post_gitlab_mr_comment("📝 **AI Summary**\n\nNo significant changes detected for summary generation.")
return
# 2. Send the diff to the StackSpot API (RQC)
logging.info("Sending diff to StackSpot RQC...")
rqc_result = run_rqc(QC_SLUG, diff)
rqc_result = validate_encoding(rqc_result)
# 3. Post result to the MR
post_gitlab_mr_comment(rqc_result)
logging.info("✅ Summary successfully posted to the MR.")
except Exception as e:
logging.error(f"❌ Failed to process MR: {e}")
try:
post_gitlab_mr_comment(f"❌ **AI Summary Failed**\n\nError: `{str(e)}`")
except Exception:
logging.error("❌ Failed to post error comment to the MR.")
sys.exit(1)
if __name__ == "__main__":
main()
Step 4. Create the automation script for the GitLab pipeline
- Create the
script.pyfile with the desired automation;
#!/usr/bin/env python3
"""GitLab CI/CD MR Summarizer"""
import os
import sys
import requests
from json import dumps, loads
from time import sleep
from urllib.parse import quote_plus
from fnmatch import fnmatch
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
STK_AUTH_BASE_URL = "https://idm.stackspot.com"
STK_AI_API_BASE_URL = "https://genai-code-buddy-api.stackspot.com/v1"
RQC_PARTIAL_SUMMARY_SLUG = "partial-summary"
RQC_TOTAL_SUMMARY_SLUG = "total-summary"
MAX_RQC_INPUT_SIZE_BYTES = 50000 # 50KB limit for RQC inputs
RQC_STATUS_COMPLETED = "COMPLETED"
RQC_STATUS_FAILED = "FAILED"
RQC_TIMEOUT_MINUTES = 15
RQC_SECONDS_TO_WAIT = 10
RQC_TIMEOUT_LIMIT = int((RQC_TIMEOUT_MINUTES * 60) / RQC_SECONDS_TO_WAIT)
class StackSpotAIError(Exception):
"""Custom exception for StackSpot AI errors."""
pass
class RQCExecutionTimeoutError(Exception):
"""Custom exception for RQC execution timeouts."""
pass
def get_gitlab_mr_diff():
"""Get the GitLab MR diff using git commands - reliable approach."""
import subprocess
import os
source_branch = os.environ.get('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME')
target_branch = os.environ.get('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'main')
mr_iid = os.environ.get('CI_MERGE_REQUEST_IID')
logger.info(f"Fetching diff for MR {mr_iid} using git commands")
logger.info(f"Source branch: {source_branch}")
logger.info(f"Target branch: {target_branch}")
logger.info(f"Method: Git commands (reliable, no API limitations)")
if not source_branch:
logger.error("❌ CI_MERGE_REQUEST_SOURCE_BRANCH_NAME not available")
raise ValueError("Source branch name required for git diff")
commit_sha = os.environ.get('CI_COMMIT_SHA')
commit_before_sha = os.environ.get('CI_COMMIT_BEFORE_SHA')
logger.info(f"CI_COMMIT_SHA: {commit_sha}")
logger.info(f"CI_COMMIT_BEFORE_SHA: {commit_before_sha}")
try:
# Unshallow the repository if it's shallow
logger.info("Checking if repository is shallow...")
if os.path.exists('.git/shallow'):
logger.info("Repository is shallow, converting to full clone...")
subprocess.run(['git', 'fetch', '--unshallow'], check=True, timeout=120)
# Fetch both branches with sufficient depth
logger.info(f"Fetching {target_branch} from origin...")
subprocess.run(['git', 'fetch', 'origin', target_branch, '--depth=100'], check=True)
logger.info(f"Fetching {source_branch} from origin...")
subprocess.run(['git', 'fetch', 'origin', source_branch, '--depth=100'], check=True)
# Get the full diff between target and source branch
result = subprocess.run(
['git', 'diff', f'origin/{target_branch}...origin/{source_branch}'],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=60,
cwd=os.getcwd()
)
if result.returncode != 0:
logger.error(
f"❌ Git command failed (code {result.returncode}): {result.stderr[:200]}"
)
raise ValueError("Git diff command failed in CI environment")
diff_content = result.stdout
diff_content = validate_encoding(diff_content)
logger.info(f"Retrieved diff with {len(diff_content)} characters")
logger.info(f"Diff encoding validated, size: {len(diff_content)} chars")
if not diff_content.strip():
logger.warning("⚠️ Empty diff - no changes between branches")
return ""
lines = diff_content.count('\n')
logger.info(f"Diff contains {lines} lines")
if commit_sha:
result_files = subprocess.run(
['git', 'diff', '--name-only', f"origin/{target_branch}...origin/{source_branch}"],
capture_output=True,
text=True,
timeout=60
)
logger.info(
f"Files vs target branch (origin/{target_branch}...origin/{source_branch}):\n{result_files.stdout}"
)
return diff_content.strip()
except FileNotFoundError:
logger.error("❌ Git command not found. Ensure git is installed in CI environment.")
logger.error(
"❌ Check .gitlab-ci.yml before_script includes: apt-get update && apt-get install -y git"
)
raise ValueError("Git not available in CI environment")
except subprocess.TimeoutExpired:
logger.error("❌ Git command timed out")
raise ValueError("Git diff command timed out")
except Exception as e:
logger.error(f"❌ Git command exception: {e}")
raise ValueError(f"Git diff failed with exception: {e}")
def post_gitlab_mr_comment(comment_body):
"""Post a comment to the GitLab MR using personal access token."""
project_id = os.environ['CI_PROJECT_ID']
mr_iid = os.environ['CI_MERGE_REQUEST_IID']
gitlab_token = os.environ['GITLAB_PERSONAL_TOKEN']
gitlab_api_url = os.environ['CI_API_V4_URL']
logger.info(f"Posting comment to MR {mr_iid} in project {project_id}")
comment_body = validate_encoding(comment_body)
comment_body = sanitize_for_json(comment_body)
comment_body = validate_comment_size(comment_body)
logger.info(f"Comment length: {len(comment_body)} characters")
logger.info(f"Comment size: {len(comment_body.encode('utf-8'))} bytes")
logger.info(f"Using GITLAB_PERSONAL_TOKEN: {gitlab_token[:8]}...{gitlab_token[-4:]}")
logger.info(f"Authentication method: Personal Access Token (full API access)")
notes_url = f"{gitlab_api_url}/projects/{project_id}/merge_requests/{mr_iid}/notes"
headers = {"PRIVATE-TOKEN": gitlab_token, "Content-Type": "application/json"}
data = {"body": comment_body}
logger.info(f"Making request to: {notes_url}")
logger.info(f"Request headers: {{'PRIVATE-TOKEN': '{gitlab_token[:8]}...{gitlab_token[-4:]}', 'Content-Type': 'application/json'}}")
logger.info(f"Expected: HTTP 200/201 response for successful comment posting")
response = requests.post(notes_url, headers=headers, json=data)
logger.info(f"Response status: {response.status_code}")
logger.info(f"Response headers: {dict(response.headers)}")
if response.status_code not in [200, 201]:
logger.error(f"❌ Failed to post MR comment: {response.status_code}")
logger.error(f"Response body: {response.text[:500]}")
if response.status_code == 401:
logger.error("❌ Diagnosis: 401 = Authentication failed with GITLAB_PERSONAL_TOKEN")
elif response.status_code == 403:
logger.error("❌ Diagnosis: 403 = GITLAB_PERSONAL_TOKEN lacks permissions to post comments")
elif response.status_code == 404:
logger.error("❌ Diagnosis: 404 = Merge request not found or project access denied")
response.raise_for_status()
logger.info("✅ Successfully posted comment to MR")
return response.json()
def get_stackspot_access_token():
"""Get StackSpot AI access token."""
client_id = os.environ['STACKSPOT_CLIENT_ID']
client_secret = os.environ['STACKSPOT_CLIENT_SECRET']
client_realm = os.environ['STACKSPOT_CLIENT_REALM']
url = f"{STK_AUTH_BASE_URL}/{client_realm}/oidc/oauth/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "client_credentials"
}
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
logger.info("✅ Successfully obtained StackSpot access token")
return token_data["access_token"]
def stackspot_make_request(method, url, body=None, retries=3):
"""Make authenticated request to StackSpot AI API."""
for attempt in range(retries + 1):
try:
access_token = get_stackspot_access_token()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
if method == "GET":
response = requests.get(url, headers=headers)
elif method == "POST":
response = requests.post(url, headers=headers, json=body or {})
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if attempt < retries and e.response.status_code in [401, 403, 500, 503]:
logger.warning(f"⚠️ Got status code {e.response.status_code} on attempt {attempt}, retrying...")
sleep(2**(attempt+1))
continue
raise
def create_rqc_execution(qc_slug, input_data):
"""Create a StackSpot RQC execution."""
url = f"{STK_AI_API_BASE_URL}/quick-commands/create-execution/{qc_slug}"
body = {"input_data": input_data}
logger.info(f"Creating RQC execution for {qc_slug}")
response_data = stackspot_make_request("POST", url, body)
execution_id = response_data
logger.info(f"✅ Created RQC execution with ID: {execution_id}")
return execution_id
def poll_rqc_execution(execution_id):
"""Poll for RQC execution result."""
url = f"{STK_AI_API_BASE_URL}/quick-commands/callback/{execution_id}"
execution_time = 0
for attempt in range(RQC_TIMEOUT_LIMIT):
response_data = stackspot_make_request("GET", url)
if response_data and "progress" in response_data:
status = response_data["progress"]["status"]
else:
logger.error(f"❌ Invalid response data: {response_data}")
return None
logger.debug(f"Polling attempt {attempt}: status = {status}")
if status == RQC_STATUS_COMPLETED:
logger.info(f"✅ RQC execution completed in ~{execution_time} seconds")
if "result" in response_data:
return response_data["result"]
else:
logger.error(f"❌ No result in completed response: {response_data}")
return None
if status == RQC_STATUS_FAILED:
logger.error(f"❌ RQC execution failed: {response_data}")
raise StackSpotAIError(f"RQC execution failed: {response_data}")
execution_time += RQC_SECONDS_TO_WAIT
sleep(RQC_SECONDS_TO_WAIT)
raise RQCExecutionTimeoutError(f"RQC execution timed out after {RQC_TIMEOUT_MINUTES} minutes")
def run_rqc(qc_slug, input_data, retries=1):
"""Execute a StackSpot RQC and get result."""
for attempt in range(retries + 1):
try:
execution_id = create_rqc_execution(qc_slug, input_data)
result = poll_rqc_execution(execution_id)
return result
except RQCExecutionTimeoutError:
if attempt < retries:
logger.warning(f"⚠️ RQC execution timed out, retrying... (attempt {attempt + 1})")
continue
logger.error("❌ RQC execution failed due to timeout")
raise
except StackSpotAIError:
logger.error("❌ RQC execution failed due to StackSpot AI error")
raise
def split_diff(diff):
"""Split complete diff into per-file diffs."""
import re
diff_blocks = re.split(r'(?=^diff --git)', diff, flags=re.MULTILINE)
diff_blocks = [block.strip() for block in diff_blocks if block.strip()]
logger.info(f"Split diff into {len(diff_blocks)} file diffs")
for block in diff_blocks:
first_line = block.splitlines()[0]
logger.info(f"Detected file diff: {first_line}")
return diff_blocks
def string_size_in_bytes(string):
"""Get UTF-8 byte size of string."""
return len(string.encode('utf-8'))
def string_is_too_large(string):
"""Check if string exceeds RQC input size limit."""
return string_size_in_bytes(string) > MAX_RQC_INPUT_SIZE_BYTES
def validate_encoding(text, encoding='utf-8'):
"""Validate and normalize text encoding."""
try:
if isinstance(text, bytes):
return text.decode(encoding, errors='replace')
return text.encode(encoding, errors='replace').decode(encoding)
except Exception as e:
logger.warning(f"⚠️ Encoding validation failed: {e}")
return text.encode('ascii', errors='replace').decode('ascii')
def sanitize_for_json(text):
"""Sanitize text for safe JSON serialization."""
import json
try:
json.dumps(text)
return text
except (UnicodeDecodeError, TypeError):
logger.warning("⚠️ Text contains invalid unicode, sanitizing...")
return text.encode('utf-8', errors='replace').decode('utf-8')
def validate_comment_size(comment_body):
"""Validate GitLab comment size and truncate if needed."""
comment_size_bytes = len(comment_body.encode('utf-8'))
if comment_size_bytes > 1048576: # 1MB GitLab limit
logger.warning(f"⚠️ Comment too large ({comment_size_bytes} bytes), truncating...")
return comment_body[:1048000] + "\n\n... (truncated due to size limit)"
return comment_body
def simplify_file_diff(file_diff):
"""Simplify large file diff by removing details."""
import re
simplified_diff = re.sub(
r'(@@.*?@@)(.*?)(?=(^diff --git|\Z))',
r'\1\n{Changes are too large to process and have been omitted}\n',
file_diff,
flags=re.DOTALL | re.MULTILINE
)
return simplified_diff
def prepare_file_diffs(file_diffs):
"""Batch file diffs optimally for RQC processing."""
joint_diffs = []
current_joint_diff = ""
for index, file_diff in enumerate(file_diffs):
first_line = file_diff.splitlines()[0] if file_diff else ""
file_diff_simplified = file_diff
if string_is_too_large(file_diff):
file_diff_simplified = simplify_file_diff(file_diff)
logger.warning(
f"⚠️ Simplified file diff for {first_line} (size {string_size_in_bytes(file_diff)} bytes)"
)
logger.info(
f"File diff {index + 1}/{len(file_diffs)}: {first_line} ({string_size_in_bytes(file_diff_simplified)} bytes)"
)
if string_is_too_large(current_joint_diff + file_diff_simplified):
if current_joint_diff: # Don't append empty string
joint_diffs.append(current_joint_diff)
current_joint_diff = file_diff_simplified
else:
current_joint_diff += file_diff_simplified
if index == len(file_diffs) - 1:
joint_diffs.append(current_joint_diff)
joint_diffs_sizes = [string_size_in_bytes(diff) for diff in joint_diffs]
logger.info(f"✅ Created {len(joint_diffs)} batched diffs with sizes: {joint_diffs_sizes} bytes")
for i, diff in enumerate(joint_diffs):
batch_file_headers = [blk.splitlines()[0] for blk in split_diff(diff)]
logger.info(
f"Batch {i + 1}: {string_size_in_bytes(diff)} bytes, files: {batch_file_headers}"
)
return joint_diffs
def get_partial_summary_inputs(diff):
"""Get batched inputs for partial summary RQCs."""
file_diffs = split_diff(diff)
inputs = prepare_file_diffs(file_diffs)
return inputs
def strip_response(response):
"""Strip code block formatting from response."""
import re
response = response.strip()
if response.startswith("```"):
response = re.sub(r'^```[a-zA-Z0-9{}]*\s*\n?', '', response)
if response.endswith("```"):
response = response[:-3]
return response
def parse_json_response(response):
"""Parse JSON response from StackSpot AI."""
if not (response := strip_response(response)):
return {}
response = validate_encoding(response)
response = sanitize_for_json(response)
try:
parsed_response = loads(response)
logger.info(f"✅ Parsed StackSpot response: {parsed_response}")
return parsed_response
except Exception as e:
logger.error(f"❌ Failed to parse JSON response: {e}")
return {}
def get_partial_summaries(diff):
"""Get partial summaries for all files in diff."""
inputs = get_partial_summary_inputs(diff)
partial_summaries = []
for i, input_data in enumerate(inputs):
logger.info(f"Processing partial summary batch {i + 1}/{len(inputs)}")
batch_preview = "\n".join(input_data.splitlines()[0:5])
logger.info(f"Batch {i + 1} includes:\n{batch_preview}")
try:
partial_summary_response = run_rqc(RQC_PARTIAL_SUMMARY_SLUG, input_data)
partial_summary = parse_json_response(partial_summary_response)
if partial_summary:
logger.info(f"✅ Partial summary for batch {i + 1}: {partial_summary}")
if isinstance(partial_summary, list):
partial_summaries.extend(partial_summary)
else:
partial_summaries.append(partial_summary)
except Exception as e:
logger.error(f"❌ Failed to get partial summary for batch {i + 1}: {e}")
continue
logger.info(f"✅ Generated {len(partial_summaries)} partial summaries")
logger.info(f"📄 Files summarized: {[ps.get('file') for ps in partial_summaries]}")
return partial_summaries
def get_total_summary(partial_summaries):
"""Get total summary from partial summaries."""
logger.info("🧮 Generating total summary from partial summaries")
try:
comment_response = run_rqc(RQC_TOTAL_SUMMARY_SLUG, dumps(partial_summaries))
comment = strip_response(comment_response)
return comment
except Exception as e:
logger.error(f"❌ Failed to generate total summary: {e}")
return "❌ **Failed to generate AI summary**\n\nThere was an error processing the merge request changes with StackSpot AI. Please review the changes manually."
def main():
"""Main function to process GitLab MR and generate AI summary."""
try:
required_vars = [
'CI_PROJECT_ID', 'CI_MERGE_REQUEST_IID', 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME',
'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'GITLAB_PERSONAL_TOKEN', 'CI_API_V4_URL',
'STACKSPOT_CLIENT_ID', 'STACKSPOT_CLIENT_SECRET', 'STACKSPOT_CLIENT_REALM'
]
missing_vars = [var for var in required_vars if not os.environ.get(var)]
if missing_vars:
logger.error(f"❌ Missing required environment variables: {missing_vars}")
logger.error("❌ Expected: All variables should be set in GitLab CI/CD settings")
for var in missing_vars:
logger.error(f" {var}: {'SET' if os.environ.get(var) else 'NOT SET'}")
sys.exit(1)
logger.info("Environment variables check:")
logger.info("Note: Using git commands + GITLAB_PERSONAL_TOKEN (GitLab.com Free tier compatible)")
for var in required_vars:
value = os.environ.get(var, '')
if 'TOKEN' in var or 'SECRET' in var:
if value:
logger.info(f" {var}: SET ({value[:8]}...{value[-4:]}) - {len(value)} chars")
if value.startswith('$'):
logger.error(f" ⚠️ WARNING: {var} appears to be unresolved variable: {value}")
else:
logger.info(f" {var}: NOT SET")
else:
logger.info(f" {var}: {value if value else 'NOT SET'}")
if value and value.startswith('$'):
logger.error(f" ⚠️ WARNING: {var} appears to be unresolved variable: {value}")
logger.info("Starting GitLab MR summarizer")
diff = get_gitlab_mr_diff()
if not diff.strip():
logger.warning("⚠️ No changes found in MR, skipping summary generation")
return
partial_summaries = get_partial_summaries(diff)
if not partial_summaries:
logger.warning("⚠️ No partial summaries generated, posting fallback comment")
comment = "📝 **AI Summary**\n\nNo significant changes detected for summary generation."
else:
comment = get_total_summary(partial_summaries)
post_gitlab_mr_comment(comment)
logger.info("✅ Successfully completed MR summarization")
except Exception as e:
logger.error(f"❌ Failed to process MR: {e}")
try:
error_comment = f"❌ **AI Summary Failed**\n\nThere was an error generating the AI summary: `{str(e)}`\n\nPlease review the changes manually."
post_gitlab_mr_comment(error_comment)
except:
logger.error("❌ Failed to post error comment to MR")
sys.exit(1)
if __name__ == "__main__":
main()

Step 5. Validate the result
When you open a new merge request, a comment will be published automatically with the analysis result, as shown in the image below:

