Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:41:32 +08:00
commit c0fc36eda5
5 changed files with 378 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "gitlab-ci-debugger",
"description": "Debug and monitor GitLab CI pipelines with automatic log retrieval and analysis",
"version": "0.0.0-2025.11.28",
"author": {
"name": "Martin Prpič",
"email": "mprpic@redhat.com"
},
"skills": [
"./skills/gitlab-ci-debugger"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# gitlab-ci-debugger
Debug and monitor GitLab CI pipelines with automatic log retrieval and analysis

48
plugin.lock.json Normal file
View File

@@ -0,0 +1,48 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:mprpic/claude-gitlab-tools:gitlab-ci-debugger",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "ba92cd6613b4c29891ca583a0decd294ca448c64",
"treeHash": "1afdcf41fa996c604d693d864661ddc0100bbde686bff2e05086efd9cc12475e",
"generatedAt": "2025-11-28T10:27:12.009954Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "gitlab-ci-debugger",
"description": "Debug and monitor GitLab CI pipelines with automatic log retrieval and analysis"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "b68fb76be85c75a20ea94907f1652bd1e7cf9669671731918e6dbf45b71c70c4"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "fdeebcde7875b12d9addd3fa92502dcd35de8527ca66ef0d27c8638e20252094"
},
{
"path": "skills/gitlab-ci-debugger/SKILL.md",
"sha256": "77c47dbc681bc2dbff760e30a94f26815f9338ec69cdd09a739d5922a742758c"
},
{
"path": "skills/gitlab-ci-debugger/scripts/check_mr_pipeline.py",
"sha256": "0697b0ca873df6acf34bf6fa3a3445c42ee689c5a16070dea8dbb346efc8879b"
}
],
"dirSha256": "1afdcf41fa996c604d693d864661ddc0100bbde686bff2e05086efd9cc12475e"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,63 @@
---
name: gitlab-ci-debugger
description: Debug and monitor GitLab CI/CD pipelines for merge requests. Check pipeline status, view job logs, and troubleshoot CI failures. Use this when the user needs to investigate GitLab CI pipeline issues, check job statuses, or view specific job logs.
allowed-tools: Bash, Read
---
# GitLab CI Debugger
This skill helps debug and monitor GitLab CI/CD pipelines associated with merge requests. It can check pipeline status,
display job information grouped by stage, and retrieve logs for specific jobs.
This skill enables Claude to investigate GitLab CI pipeline failures by:
1. Checking the current branch's MR pipeline status
2. Identifying failed jobs
3. Retrieving failed job logs
4. Analyzing error messages and suggesting fixes
5.
## Prerequisites
Required tools (verify that these exist):
- `uv` package manager
- If not installed, instruct the user to run `pip install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh`
The script also requires the following configuration:
- Git repository with GitLab remote configured
- GitLab authentication via GITLAB_TOKEN env var or .netrc file
The script will fail if it detects any missing configuration. Interpret the error message and provide instructions
for setting up the required configuration.
## Instructions
When the user asks to check CI status, debug pipeline failures, or view job logs:
1. **Check Current Pipeline Status**
- Use the `check_mr_pipeline.py` script without arguments to check the current branch's MR pipeline status
- The script will display all jobs grouped by stage with status indicators
2. **Check Specific Branch**
- If the user asks to check on the pipeline status for a different branch than the current one, use the `-b` or
`--branch` option to specify that branch.
- Example: `./check_mr_pipeline.py -b feature-branch`
3. **View Job Logs**
- Use the `-j` or `--job` option to retrieve and display logs for a specific job
- Example: `./check_mr_pipeline.py -j "test-job-name"`
- The script will show the job's metadata and full log output
4. **Troubleshoot CI Failures**
- If the user asks to troubleshoot a CI failure, use the full log output of a job to identify the error and suggest
fixes.
## Troubleshooting
- **No open merge request found**: Ensure there's an open MR for the branch
- **Authentication errors when running the script**: Instruct user to set up GITLAB_TOKEN or .netrc file
- **Job not found**: Use the script without `-j` option first to see available job names
- **Script requires uv**: The script uses `uv run --script` to install dependencies and run the script. Ensure that
`uv` is installed.

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "python-gitlab>=4.0.0",
# ]
# ///
"""
Check CI pipeline status for the merge request associated with a git branch.
Authentication (checked in order):
1. GITLAB_TOKEN environment variable
2. NETRC environment variable (path to .netrc file)
3. ~/.netrc file (default location)
Environment variables:
GITLAB_TOKEN - GitLab personal access token (preferred)
NETRC - Path to .netrc file (default: ~/.netrc)
GITLAB_PROJECT_PATH - Override project path (by default "origin" from .git/config)
GITLAB_DOMAIN - Override default GitLab domain ("gitlab.com")
"""
import argparse
import netrc
import os
import re
import subprocess
import sys
from collections import defaultdict
from typing import Any
import gitlab
from gitlab.v4.objects import Project, ProjectMergeRequest, ProjectPipeline
STATUS_EMOJIS = {
"success": "",
"failed": "",
"running": "",
"pending": "",
"canceled": "",
"skipped": "",
"manual": "",
"created": "",
}
def display_pipeline_status(pipeline: ProjectPipeline) -> None:
"""Display the status of all jobs in a pipeline."""
project = pipeline.manager.gitlab.projects.get(pipeline.project_id)
jobs = project.pipelines.get(pipeline.id).jobs.list(get_all=True)
if not jobs:
print("No jobs found in this pipeline.")
return
# Group jobs by stage; sort by when they started so stages display in order
jobs_by_stage: dict[str, list[Any]] = defaultdict(list)
max_job_name_len = 0
for job in sorted(jobs, key=lambda j: j.started_at):
jobs_by_stage[job.stage].append(job)
max_job_name_len = max(max_job_name_len, len(job.name))
# Display jobs grouped by stage (retain order from above)
for stage in jobs_by_stage.keys():
print(stage)
for job in sorted(jobs_by_stage[stage], key=lambda j: j.name):
status = job.status
emoji = STATUS_EMOJIS.get(status, "?")
print(f" {job.name:<{max_job_name_len}} {emoji} {status}")
def view_job_log(pipeline: ProjectPipeline, job_name: str) -> None:
"""Download and print the log for a specific job (by name)."""
project = pipeline.manager.gitlab.projects.get(pipeline.project_id)
jobs = project.pipelines.get(pipeline.id).jobs.list(get_all=True)
matching_jobs = [job for job in jobs if job.name == job_name]
if not matching_jobs:
print(f"ERROR: Job '{job_name}' not found.", file=sys.stderr)
print("\nAvailable jobs:", file=sys.stderr)
for job in sorted(jobs, key=lambda j: j.name):
print(f" {job.name}", file=sys.stderr)
sys.exit(1)
job = matching_jobs[0]
# Get the full job object (list() returns partial objects without trace())
full_job = project.jobs.get(job.id)
print(f"\n{'=' * 80}")
print(f"Job: {full_job.name}")
print(f"Status: {full_job.status}")
print(f"Stage: {full_job.stage}")
print(f"Web URL: {full_job.web_url}")
print(f"{'=' * 80}\n")
log = full_job.trace().decode("utf-8")
print(log)
def find_mr_for_branch(
project: Project, branch_name: str
) -> ProjectMergeRequest | None:
"""Find an open merge request associated with a given branch."""
mrs = project.mergerequests.list(
source_branch=branch_name, state="opened", get_all=True
)
if not mrs:
return None
# Return the first (most recent) MR
return mrs[0]
def get_latest_pipeline(mr: ProjectMergeRequest) -> ProjectPipeline | None:
"""Get the latest pipeline for a merge request."""
pipelines = mr.pipelines.list(get_all=True)
if not pipelines:
return None
# Pipelines are returned in newest to oldest
return pipelines[0]
def get_current_branch() -> str:
"""Get the current git branch name."""
result = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
text=True,
)
return result.strip()
def get_gitlab_token(domain: str) -> str:
"""Get GitLab authentication token from environment variable or netrc file."""
if token := os.environ.get("GITLAB_TOKEN"):
return token
# Check for NETRC environment variable, otherwise use default ~/.netrc
netrc_file_path = os.environ.get("NETRC") or "~/.netrc"
try:
n = netrc.netrc(netrc_file_path)
authenticator = n.authenticators(domain)
if authenticator:
token = authenticator[-1]
if token:
return token
else:
raise ValueError(
f"ERROR: No applicable token found for {domain} in {netrc_file_path}"
)
except FileNotFoundError:
pass
print(
f"ERROR: No GitLab token found; set GITLAB_TOKEN environment variable or "
f"add {domain} credentials to a .netrc file",
file=sys.stderr,
)
sys.exit(1)
def get_gitlab_info() -> tuple[str, str]:
"""Get GitLab domain and project path from environment or git remote origin."""
if proj_path := os.environ.get("GITLAB_PROJECT_PATH"):
# If only project path is provided, assume gitlab.com as default domain
gitlab_domain = os.environ.get("GITLAB_DOMAIN", "gitlab.com")
return gitlab_domain, proj_path
try:
remote_url = subprocess.check_output(
["git", "remote", "get-url", "origin"],
text=True,
).strip()
except subprocess.CalledProcessError as e:
print(
f"ERROR: Failed to get git remote origin: {e}",
file=sys.stderr,
)
sys.exit(1)
# Parse HTTPS format: https://[gitlab-domain]/group/subgroup/project.git
if match := re.match(r"https://([^/]+)/(.+?)(?:\.git)?$", remote_url):
gitlab_domain = match.group(1)
project_path = match.group(2)
return gitlab_domain, project_path
# Parse SSH format: git@[gitlab-domain]:group/subgroup/project.git
if match := re.match(r"git@([^:]+):(.+?)(?:\.git)?$", remote_url):
gitlab_domain = match.group(1)
project_path = match.group(2)
return gitlab_domain, project_path
print(
f"ERROR: Could not parse GitLab domain and project path from remote URL: {remote_url}",
file=sys.stderr,
)
sys.exit(1)
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"-b",
"--branch",
help="Git branch name (default: current branch)",
default=None,
)
parser.add_argument(
"-j",
"--job",
help="Download and print the log for the specified job name",
default=None,
)
args = parser.parse_args()
branch_name = args.branch if args.branch else get_current_branch()
if branch_name == "main":
print(
"WARNING: You are on the 'main' branch; this will display the latest CI run on main",
file=sys.stderr,
)
gitlab_domain, project_path = get_gitlab_info()
gitlab_url = f"https://{gitlab_domain}"
token = get_gitlab_token(gitlab_domain)
gl = gitlab.Gitlab(url=gitlab_url, private_token=token)
gl.auth()
project = gl.projects.get(project_path)
mr = find_mr_for_branch(project, branch_name)
if not mr:
print(
f"ERROR: No open merge request found for branch '{branch_name}'",
file=sys.stderr,
)
sys.exit(1)
print(f"Found MR !{mr.iid}: {mr.title}")
print(f"MR URL: {mr.web_url}")
pipeline = get_latest_pipeline(mr)
if not pipeline:
print(f"ERROR: No pipeline found for merge request !{mr.iid}", file=sys.stderr)
sys.exit(1)
if args.job:
view_job_log(pipeline, args.job)
else:
print(f"{'=' * 80}")
display_pipeline_status(pipeline)
if __name__ == "__main__":
main()