
Gitlab
- 269 installs
- 12 repo stars
- Updated August 4, 2026
- odyssey4me/agent-skills
Interact with GitLab repos, merge requests, pipelines, and issues from agents to automate reviews, CI triage, and release coordination.
About
Connects agents to GitLab for repository, merge request, pipeline, and issue operations, enabling automated DevOps assistance, CI triage, and release coordination from within agent-driven development workflows.
- Works with repos, MRs, and CI pipelines
- Automates issue and review lookups
- Hooks agents into GitLab DevOps flows
- Supports release and triage assistance
- Reduces manual GitLab UI context switching
Gitlab by the numbers
- 269 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #352 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/odyssey4me/agent-skills --skill gitlabAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 269 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 4, 2026 |
| Repository | odyssey4me/agent-skills ↗ |
What it does
Interact with GitLab repos, merge requests, pipelines, and issues from agents to automate reviews, CI triage, and release coordination.
Files
GitLab Skill
This skill provides GitLab integration using the official glab CLI tool. A Python wrapper script produces markdown-formatted output for read/view operations. Action commands (create, merge, close, comment) should use glab directly.
Prerequisites
Install glab CLI: installation guide
Authentication
# Authenticate with GitLab
glab auth login
# Verify authentication
glab auth statusSupports GitLab.com, GitLab Dedicated, and GitLab Self-Managed instances. See GitLab CLI Authentication for details.
Script Usage
The wrapper script (scripts/gitlab.py) formats output as markdown. Use it for read/view operations to get agent-consumable output. Use glab directly for action commands (create, merge, close, comment). See permissions.md for read/write classification of each command.
# Check glab CLI is installed and authenticated
$SKILL_DIR/scripts/gitlab.py check
# Issues
$SKILL_DIR/scripts/gitlab.py issues list --repo GROUP/REPO
$SKILL_DIR/scripts/gitlab.py issues view 123 --repo GROUP/REPO
# Merge Requests
$SKILL_DIR/scripts/gitlab.py mrs list --repo GROUP/REPO
$SKILL_DIR/scripts/gitlab.py mrs view 456 --repo GROUP/REPO
# Pipelines
$SKILL_DIR/scripts/gitlab.py pipelines list --repo GROUP/REPO
$SKILL_DIR/scripts/gitlab.py pipelines view 123456 --repo GROUP/REPO
# Repositories
$SKILL_DIR/scripts/gitlab.py repos list
$SKILL_DIR/scripts/gitlab.py repos view GROUP/REPOAll commands support --limit N for list commands (default 30).
Commands (Direct glab Usage)
For action commands, use glab directly:
Issues
glab issue list # List issues
glab issue view 123 # View issue details
glab issue create # Create new issue
glab issue note 123 # Add comment
glab issue close 123 # Close issue
glab issue update 123 --label bug # Edit issueFull reference: glab issue
Merge Requests
glab mr list # List merge requests
glab mr view 456 # View MR details
glab mr create # Create new MR
glab mr approve 456 # Approve MR
glab mr merge 456 # Merge MR
glab mr checkout 456 # Checkout MR branch
glab mr diff 456 # View MR diff
glab mr note 456 # Add comment to MRFull reference: glab mr
Pipelines & CI/CD
glab ci list # List pipelines
glab ci view 123456 # View pipeline details
glab ci run # Trigger pipeline
glab ci trace # Watch pipeline logs
glab ci retry 123456 # Retry failed pipeline
glab ci status # Show pipeline statusFull references:
Repositories
glab repo list # List repositories
glab repo view GROUP/REPO # View repository
glab repo create # Create repository
glab repo clone GROUP/REPO # Clone repository
glab repo fork GROUP/REPO # Fork repositoryFull reference: glab repo
Releases
glab release list # List releases
glab release view v1.0.0 # View release details
glab release create v1.0.0 # Create release
glab release delete v1.0.0 # Delete releaseFull reference: glab release
Examples
Daily MR Review
# List MRs assigned to you
glab mr list --assignee=@me
# Review a specific MR
$SKILL_DIR/scripts/gitlab.py mrs view 456
glab mr diff 456
glab mr approve 456
# Verify approval was recorded
$SKILL_DIR/scripts/gitlab.py mrs view 456 # check approval statusCreate Issue and Link MR
# Create issue
glab issue create --title "Bug: Login fails" --description "Description" --label bug
# Verify: note the issue number from output
# Create MR that closes it (use issue number from above)
glab mr create --title "Fix login bug" --description "Closes #123"
# Verify MR was created and linked
$SKILL_DIR/scripts/gitlab.py mrs view <number>Monitor CI Pipeline
# Check current pipeline status
glab ci status
# Watch pipeline logs in real-time
glab ci trace
# Retry failed jobs
glab ci retry
# Verify pipeline restarted
$SKILL_DIR/scripts/gitlab.py pipelines listSee common-workflows.md for more examples.
Advanced Usage
JSON Output for Scripting
# Get JSON output
glab issue list --output json
# Process with jq
glab mr list --output json | jq '.[] | "\(.iid): \(.title)"'GitLab API Access
For operations not covered by glab commands:
# Make authenticated API request
glab api projects/:id/issues
# POST request
glab api projects/:id/issues -X POST -f title="Issue" -f description="Text"
# Process response
glab api projects/:id | jq '.star_count'Full reference: glab api
Aliases for Frequent Operations
# Create shortcuts
glab alias set mrs 'mr list --assignee=@me'
glab alias set issues 'issue list --assignee=@me'
glab alias set pipelines 'ci list'
# Use them
glab mrs
glab issues
glab pipelinesConfiguration
# View configuration
glab config get
# Set default editor
glab config set editor vim
# Set default Git protocol
glab config set git_protocol sshConfiguration stored in ~/.config/glab-cli/config.yml
Model Guidance
This skill wraps an official CLI. A fast, lightweight model is sufficient.
Troubleshooting
# Check authentication
glab auth status
# Re-authenticate
glab auth login
# Enable debug logging
DEBUG=1 glab issue list
# Check glab version
glab versionOfficial Documentation
- GitLab CLI Manual: <https://docs.gitlab.com/cli/>
- GitLab CLI Repository: <https://gitlab.com/gitlab-org/cli>
- GitLab API Documentation: <https://docs.gitlab.com/ee/api/>
- GitLab CI/CD: <https://docs.gitlab.com/ee/ci/>
Common GitLab Workflows with glab CLI
This document provides practical examples of common GitLab workflows using the glab CLI.
Table of Contents
- Issue Management
- Merge Request Workflow
- CI/CD and Pipelines
- Repository Management
- Team Collaboration
- Automation Examples
Issue Management
Daily Issue Triage
Review and label new issues:
#!/bin/bash
# List unlabeled issues
glab issue list --label "" --output json | jq -r '.[] | "\(.iid) \(.title)"'
# For each issue, view and add labels
glab issue list --label "" --per-page 10 | while read issue; do
issue_num=$(echo $issue | awk '{print $1}' | tr -d '#')
glab issue view $issue_num
echo "Add label (bug/enhancement/question/documentation/skip):"
read label
if [ "$label" != "skip" ]; then
glab issue update $issue_num --label $label
fi
doneClose Stale Issues
Close issues inactive for over 90 days:
# List issues older than 90 days
glab issue list --state opened --output json | \
jq -r --arg date "$(date -d '90 days ago' --iso-8601)" \
'.[] | select(.updated_at < $date) | "\(.iid) \(.title)"'
# Close them with a comment
glab issue list --state opened --output json | \
jq -r --arg date "$(date -d '90 days ago' --iso-8601)" \
'.[] | select(.updated_at < $date) | .iid' | \
while read num; do
glab issue note $num --message "Closing due to inactivity. Please reopen if still relevant."
glab issue close $num
doneCreate Issue from Template
# Create issue with details
glab issue create \
--title "Login fails with OAuth" \
--description "Steps to reproduce: ..." \
--label bug \
--assignee @meBulk Issue Operations
Add milestone to multiple issues:
# Find issues with label "v2.0"
glab issue list --label "v2.0" --output json | \
jq -r '.[].iid' | \
while read num; do
glab issue update $num --milestone "2.0 Release"
doneMerge Request Workflow
Create MR from Feature Branch
# Ensure you're on feature branch
git checkout -b feature/new-login
# Make changes and commit
git add .
git commit -m "feat: implement new login flow"
# Push branch
git push -u origin feature/new-login
# Create MR with auto-filled title/body from commits
glab mr create --fill
# Or with custom details
glab mr create \
--title "Implement new login flow" \
--description "This MR implements OAuth 2.0 login.
## Changes
- Add OAuth provider
- Update login UI
- Add tests
Closes #123" \
--label enhancement \
--assignee @reviewer1Review MRs Assigned to You
#!/bin/bash
# Daily MR review workflow
echo "=== MRs waiting for your review ==="
glab mr list --assignee=@me --output json | jq -r '.[] | "\(.iid) \(.title) (@\(.author.username))"'
# Review each MR
glab mr list --assignee=@me --output json | \
jq -r '.[].iid' | \
while read mr; do
echo -e "\n=== Reviewing MR !$mr ==="
# View MR details
glab mr view $mr
# View diff
glab mr diff $mr
# Checkout locally to test
echo "Checkout and test locally? (y/n)"
read checkout
if [ "$checkout" = "y" ]; then
glab mr checkout $mr
# Run tests
make test
# Switch back
git checkout -
fi
# Submit review
echo "Action: (approve/comment/skip)"
read action
case $action in
approve)
glab mr approve $mr
glab mr note $mr --message "LGTM! ✅"
;;
comment)
echo "Enter comment:"
read comment
glab mr note $mr --message "$comment"
;;
esac
doneAuto-merge When Checks Pass
# Merge MR when pipeline succeeds
glab mr merge 456 --when-pipeline-succeeds --remove-source-branchUpdate MR Based on Review Comments
# View MR with comments
glab mr view 456
# Make changes
git add .
git commit -m "fix: address review comments"
git push
# Add comment to MR
glab mr note 456 --message "Updated per review feedback"CI/CD and Pipelines
Monitor Pipeline Status
#!/bin/bash
# Watch latest CI run for current branch
# Get latest pipeline for current branch
BRANCH=$(git branch --show-current)
PIPELINE_ID=$(glab ci list --branch $BRANCH --per-page 1 --output json | jq -r '.[0].id')
# Watch it in real-time
glab ci trace $PIPELINE_IDTrigger Deployment
# Trigger pipeline with variables
glab ci run --branch main
# Monitor pipeline
glab ci status
glab ci traceRetry Failed CI Jobs
# List recent failed pipelines
glab ci list --status failed --per-page 10
# Retry failed jobs
glab ci retry 123456Download Build Artifacts
# Download artifacts from latest pipeline
glab ci artifact downloadCancel In-Progress Pipelines
# Cancel specific pipeline
glab ci cancel 123456
# Cancel all in-progress pipelines for a branch
glab ci list --branch feature/test --status running --output json | \
jq -r '.[].id' | \
while read pipeline; do
glab ci cancel $pipeline
doneRepository Management
Create New Repository
# Create new repository
glab repo create my-new-project \
--description "My awesome project" \
--public
# Clone it
glab repo clone my-username/my-new-projectSync Fork with Upstream
# Fork a repository
glab repo fork original-owner/repo
# Add upstream if not already added
git remote add upstream https://gitlab.com/original-owner/repo.git
# Fetch upstream changes
git fetch upstream
# Merge upstream changes
git checkout main
git merge upstream/main
git push origin mainArchive Old Repositories
# Archive a repository
glab repo archive my-username/old-projectTeam Collaboration
Assign Code Review to Team
# Create MR and assign reviewer
glab mr create --fill --assignee @teammate1
# Or add reviewers to existing MR
glab mr update 456 --assignee @teammate2Track Team's MR Status
#!/bin/bash
# Team MR dashboard
echo "=== Team MRs Status ==="
# List MRs by team members
for member in alice bob carol; do
echo -e "\n$member's MRs:"
glab mr list --author $member --output json | \
jq -r '.[] | " !\(.iid): \(.title) | Pipeline: \(.pipeline.status)"'
doneCreate Release
# Create a release
glab release create v2.1.0 \
--name "Version 2.1.0" \
--notes "## What's New
- Feature 1
- Feature 2
## Bug Fixes
- Fix 1
- Fix 2" \
--ref main
# Or create release from tag
git tag -a v2.1.0 -m "Release v2.1.0"
git push origin v2.1.0
glab release create v2.1.0 --ref v2.1.0Automation Examples
Daily Standup Report
#!/bin/bash
# Generate daily activity report
TODAY=$(date --iso-8601)
YESTERDAY=$(date -d '1 day ago' --iso-8601)
echo "=== Activity Report for $TODAY ==="
echo -e "\nIssues closed:"
glab issue list --state closed --updated-after $YESTERDAY --output json | \
jq -r '.[] | " #\(.iid): \(.title)"'
echo -e "\nMRs merged:"
glab mr list --state merged --updated-after $YESTERDAY --output json | \
jq -r '.[] | " !\(.iid): \(.title)"'
echo -e "\nNew issues:"
glab issue list --created-after $YESTERDAY --output json | \
jq -r '.[] | " #\(.iid): \(.title) (@\(.author.username))"'
echo -e "\nNew MRs:"
glab mr list --created-after $YESTERDAY --output json | \
jq -r '.[] | " !\(.iid): \(.title) (@\(.author.username))"'Auto-label MRs Based on Files Changed
#!/bin/bash
# Auto-label MRs based on changed files
for mr in $(glab mr list --state opened --output json | jq -r '.[].iid'); do
# Get changed files
FILES=$(glab mr view $mr --output json | jq -r '.changes[].new_path')
# Add labels based on file patterns
if echo "$FILES" | grep -q "^docs/"; then
glab mr update $mr --label documentation
fi
if echo "$FILES" | grep -q "test"; then
glab mr update $mr --label tests
fi
if echo "$FILES" | grep -q "\.py$"; then
glab mr update $mr --label python
fi
if echo "$FILES" | grep -q "\.js$\|\.ts$"; then
glab mr update $mr --label javascript
fi
doneNotify on Failed CI
#!/bin/bash
# Check for failed CI and notify
# Get failed pipelines from last hour
glab ci list --status failed --output json | \
jq -r --arg time "$(date -d '1 hour ago' --iso-8601=seconds)" \
'.[] | select(.created_at > $time) | "❌ \(.ref): \(.web_url)"' | \
while read -r line; do
# Send notification (example using curl to a webhook)
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d "{\"text\": \"$line\"}"
doneBulk MR Cleanup
Close draft MRs older than 30 days:
# List old draft MRs
glab mr list --state opened --draft --output json | \
jq -r --arg date "$(date -d '30 days ago' --iso-8601)" \
'.[] | select(.created_at < $date) | "\(.iid) \(.title)"'
# Close them
glab mr list --state opened --draft --output json | \
jq -r --arg date "$(date -d '30 days ago' --iso-8601)" \
'.[] | select(.created_at < $date) | .iid' | \
while read mr; do
glab mr note $mr --message "Closing stale draft MR. Please reopen if still working on this."
glab mr close $mr
doneAdvanced: Using GitLab API
For operations not covered by glab commands, use the API:
# Get project statistics
glab api projects/:id | jq '{
stars: .star_count,
forks: .forks_count,
issues: .open_issues_count
}'
# List project members
glab api projects/:id/members --paginate | \
jq -r '.[] | "\(.username)\t\(.access_level)"'
# Search code across group
glab api /projects/:id/search \
-f scope=blobs \
-f search='function authenticate' | \
jq -r '.[] | "\(.filename):\(.startline)"'Tips for Efficient Workflows
Use Aliases
Create shortcuts for common operations:
# Save frequently used commands as aliases
glab alias set mrs 'mr list --assignee=@me'
glab alias set issues 'issue list --assignee=@me'
glab alias set pipelines 'ci list'
# Use them
glab mrs
glab issues
glab pipelinesJSON Output for Scripting
Use --output json flag for programmatic processing:
# Get specific fields only
glab mr list --output json | jq '.[] | {iid, title, author}'
# Process with jq
glab issue list --output json | \
jq '.[] | select(.labels[] | contains("bug"))'
# Export to CSV
glab mr list --output json | \
jq -r '.[] | [.iid, .title, .author.username, .created_at] | @csv'Environment Variables
Control glab behavior with environment variables:
# Enable debug logging
DEBUG=1 glab mr list
# Use specific token
GITLAB_TOKEN=glpat_custom_token glab repo list
# Use different host
GITLAB_HOST=gitlab.company.com glab mr listAdditional Resources
Command Permissions
This reference classifies commands by access level to help agents enforce appropriate permission controls.
- read: Safe to execute without user confirmation. These commands
only retrieve or display information.
- write: Requires user confirmation before execution. These
commands create, modify, or delete data.
Note: This skill's script only provides read operations. Write operations use the glab CLI directly and are not covered here.
| Command | Access | Description |
|---|---|---|
| check | read | Verify setup and connectivity |
| issues list | read | List project issues |
| issues view | read | View issue details |
| mrs list | read | List merge requests |
| mrs view | read | View merge request details |
| pipelines list | read | List pipelines |
| pipelines view | read | View pipeline details |
| repos list | read | List projects |
| repos view | read | View project details |
#!/usr/bin/env python3
"""GitLab wrapper skill for AI agents.
Wraps the glab CLI to produce markdown-formatted output for read/view commands.
Action commands (create, merge, close, comment) should use glab directly.
Usage:
python gitlab.py check
python gitlab.py issues list --repo GROUP/REPO
python gitlab.py issues view 123 --repo GROUP/REPO
python gitlab.py mrs list --repo GROUP/REPO
python gitlab.py mrs view 456 --repo GROUP/REPO
python gitlab.py pipelines list --repo GROUP/REPO
python gitlab.py pipelines view 123456 --repo GROUP/REPO
python gitlab.py repos list
python gitlab.py repos view GROUP/REPO
Requirements:
glab CLI (https://gitlab.com/gitlab-org/cli)
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from typing import Any
# ============================================================================
# glab CLI HELPER
# ============================================================================
def run_glab(args: list[str], output_json: bool = False) -> dict[str, Any] | list[Any] | str:
"""Run a glab CLI command and return parsed output.
Args:
args: Arguments to pass to glab (e.g., ["issue", "list"]).
output_json: Whether to request JSON output via --output json.
Returns:
Parsed JSON data (dict or list), or raw string output.
Raises:
SystemExit: If glab command fails.
"""
cmd = ["glab", *args]
if output_json:
cmd.extend(["--output", "json"])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
output = result.stdout.strip()
if output_json and output:
return json.loads(output)
return output
# ============================================================================
# DATE FORMATTING
# ============================================================================
def format_date(iso_date: str | None) -> str:
"""Format ISO 8601 date to YYYY-MM-DD HH:MM.
Args:
iso_date: ISO 8601 date string (e.g., "2024-01-15T10:30:00Z").
Returns:
Formatted date string, or "N/A" if input is None/empty.
"""
if not iso_date:
return "N/A"
# ISO 8601: 2024-01-15T10:30:00Z → 2024-01-15 10:30
return iso_date[:10] + " " + iso_date[11:16] if len(iso_date) >= 16 else iso_date[:10]
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def _get_username(author: dict[str, Any] | None) -> str:
"""Extract username from an author/user dict.
Args:
author: User dictionary with 'username' key.
Returns:
Username string or "Unknown".
"""
if not author:
return "Unknown"
if isinstance(author, dict):
return author.get("username", "Unknown")
return str(author)
def _get_usernames(users: list[dict[str, Any]]) -> str:
"""Extract comma-separated usernames from a list of user dicts.
Args:
users: List of user dictionaries.
Returns:
Comma-separated usernames, or empty string if none.
"""
if not users:
return ""
names = []
for u in users:
if isinstance(u, dict):
names.append(u.get("username", "?"))
else:
names.append(str(u))
return ", ".join(names)
def _get_labels(labels: list[Any]) -> str:
"""Extract comma-separated label names from a list.
GitLab returns labels as plain strings, not dicts.
Args:
labels: List of label strings (or dicts for compatibility).
Returns:
Comma-separated label names, or empty string if none.
"""
if not labels:
return ""
names = []
for label in labels:
if isinstance(label, dict):
names.append(label.get("name", "?"))
else:
names.append(str(label))
return ", ".join(names)
# ============================================================================
# FORMAT FUNCTIONS — one per entity type (markdown output)
# ============================================================================
def format_issue_summary(issue: dict[str, Any]) -> str:
"""Format a GitLab issue for markdown display.
Args:
issue: Issue dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
iid = issue.get("iid", "?")
title = issue.get("title", "(No title)")
state = issue.get("state", "unknown")
author = _get_username(issue.get("author"))
assignees = _get_usernames(issue.get("assignees", []))
labels = _get_labels(issue.get("labels", []))
created = format_date(issue.get("created_at"))
lines = [
f"### #{iid}: {title}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if assignees:
lines.append(f"- **Assignees:** {assignees}")
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
body = issue.get("description")
if body:
lines.append(f"\n{body.strip()}")
url = issue.get("web_url")
if url:
lines.append(f"\n- **URL:** {url}")
return "\n".join(lines)
def format_issue_row(issue: dict[str, Any]) -> str:
"""Format a GitLab issue as a compact markdown entry for lists.
Args:
issue: Issue dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
iid = issue.get("iid", "?")
title = issue.get("title", "(No title)")
state = issue.get("state", "unknown")
author = _get_username(issue.get("author"))
labels = _get_labels(issue.get("labels", []))
created = format_date(issue.get("created_at"))
lines = [
f"### #{iid}: {title}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_mr_summary(mr: dict[str, Any]) -> str:
"""Format a GitLab merge request for markdown display.
Args:
mr: MR dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
iid = mr.get("iid", "?")
title = mr.get("title", "(No title)")
state = mr.get("state", "unknown")
draft = " (Draft)" if mr.get("draft") else ""
author = _get_username(mr.get("author"))
assignees = _get_usernames(mr.get("assignees", []))
labels = _get_labels(mr.get("labels", []))
created = format_date(mr.get("created_at"))
lines = [
f"### !{iid}: {title}{draft}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if assignees:
lines.append(f"- **Assignees:** {assignees}")
if labels:
lines.append(f"- **Labels:** {labels}")
source = mr.get("source_branch")
target = mr.get("target_branch")
if source and target:
lines.append(f"- **Branch:** {source} \u2192 {target}")
merge_status = mr.get("merge_status")
if merge_status:
lines.append(f"- **Merge Status:** {merge_status}")
lines.append(f"- **Created:** {created}")
body = mr.get("description")
if body:
lines.append(f"\n{body.strip()}")
url = mr.get("web_url")
if url:
lines.append(f"\n- **URL:** {url}")
return "\n".join(lines)
def format_mr_row(mr: dict[str, Any]) -> str:
"""Format a GitLab MR as a compact markdown entry for lists.
Args:
mr: MR dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
iid = mr.get("iid", "?")
title = mr.get("title", "(No title)")
state = mr.get("state", "unknown")
draft = " (Draft)" if mr.get("draft") else ""
author = _get_username(mr.get("author"))
labels = _get_labels(mr.get("labels", []))
created = format_date(mr.get("created_at"))
lines = [
f"### !{iid}: {title}{draft}",
f"- **State:** {state}",
f"- **Author:** {author}",
]
if labels:
lines.append(f"- **Labels:** {labels}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_pipeline_summary(pipeline: dict[str, Any]) -> str:
"""Format a GitLab pipeline for markdown display.
Args:
pipeline: Pipeline dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
pid = pipeline.get("id", "?")
status = pipeline.get("status", "unknown")
ref = pipeline.get("ref", "")
sha = pipeline.get("sha", "")
created = format_date(pipeline.get("created_at"))
lines = [
f"### Pipeline #{pid}",
f"- **Status:** {status}",
]
if ref:
lines.append(f"- **Ref:** {ref}")
if sha:
lines.append(f"- **Commit:** {sha[:8]}")
lines.append(f"- **Created:** {created}")
source = pipeline.get("source")
if source:
lines.append(f"- **Source:** {source}")
url = pipeline.get("web_url")
if url:
lines.append(f"- **URL:** {url}")
return "\n".join(lines)
def format_pipeline_row(pipeline: dict[str, Any]) -> str:
"""Format a GitLab pipeline as a compact markdown entry for lists.
Args:
pipeline: Pipeline dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
pid = pipeline.get("id", "?")
status = pipeline.get("status", "unknown")
ref = pipeline.get("ref", "")
created = format_date(pipeline.get("created_at"))
lines = [
f"### Pipeline #{pid}",
f"- **Status:** {status}",
]
if ref:
lines.append(f"- **Ref:** {ref}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_repo_summary(repo: dict[str, Any]) -> str:
"""Format a GitLab repository for markdown display.
Args:
repo: Repository dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
full_name = repo.get("path_with_namespace", "")
if not full_name:
name = repo.get("name", "(Unknown)")
namespace = repo.get("namespace", {})
ns_path = namespace.get("full_path", "") if isinstance(namespace, dict) else ""
full_name = f"{ns_path}/{name}" if ns_path else name
description = repo.get("description") or "(No description)"
visibility = repo.get("visibility", "unknown")
stars = repo.get("star_count", 0)
forks = repo.get("forks_count", 0)
updated = format_date(repo.get("updated_at"))
lines = [
f"### {full_name}",
f"- **Description:** {description}",
f"- **Visibility:** {visibility}",
f"- **Stars:** {stars}",
]
if forks:
lines.append(f"- **Forks:** {forks}")
default_branch = repo.get("default_branch")
if default_branch:
lines.append(f"- **Default Branch:** {default_branch}")
lines.append(f"- **Updated:** {updated}")
url = repo.get("web_url")
if url:
lines.append(f"- **URL:** {url}")
return "\n".join(lines)
def format_repo_row(repo: dict[str, Any]) -> str:
"""Format a GitLab repository as a compact markdown entry for lists.
Args:
repo: Repository dictionary from glab --output json.
Returns:
Markdown-formatted string.
"""
full_name = repo.get("path_with_namespace", "")
if not full_name:
name = repo.get("name", "(Unknown)")
namespace = repo.get("namespace", {})
ns_path = namespace.get("full_path", "") if isinstance(namespace, dict) else ""
full_name = f"{ns_path}/{name}" if ns_path else name
description = repo.get("description") or "(No description)"
visibility = repo.get("visibility", "unknown")
stars = repo.get("star_count", 0)
lines = [
f"### {full_name}",
f"- **Description:** {description}",
f"- **Visibility:** {visibility}",
f"- **Stars:** {stars}",
]
return "\n".join(lines)
# ============================================================================
# COMMAND HANDLERS — one per subcommand, return exit code
# ============================================================================
def cmd_check(_args: argparse.Namespace) -> int:
"""Verify glab CLI is installed and authenticated.
Args:
_args: Parsed arguments (unused).
Returns:
Exit code (0 success, 1 error).
"""
if not shutil.which("glab"):
print(
"Error: glab CLI not found. Install from https://gitlab.com/gitlab-org/cli",
file=sys.stderr,
)
return 1
result = subprocess.run(["glab", "auth", "status"], capture_output=True, text=True)
if result.returncode != 0:
print("Error: glab CLI not authenticated.", file=sys.stderr)
print(result.stderr.strip(), file=sys.stderr)
print("\nRun: glab auth login", file=sys.stderr)
return 1
print("\u2713 glab CLI is installed and authenticated")
# Show auth details (stderr from glab auth status contains the info)
for line in result.stderr.strip().splitlines():
print(f" {line.strip()}")
return 0
def cmd_issues_list(args: argparse.Namespace) -> int:
"""List issues for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
glab_args = ["issue", "list"]
if args.repo:
glab_args.extend(["-R", args.repo])
glab_args.extend(["--per-page", str(args.limit)])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
items = data if isinstance(data, list) else []
if not items:
print("No issues found")
else:
print(f"## Issues\n\nFound {len(items)} issue(s):\n")
print("\n\n".join(format_issue_row(i) for i in items))
return 0
def cmd_issues_view(args: argparse.Namespace) -> int:
"""View a single issue.
Args:
args: Parsed arguments with issue number, repo, json flags.
Returns:
Exit code.
"""
glab_args = ["issue", "view", str(args.number)]
if args.repo:
glab_args.extend(["-R", args.repo])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
if isinstance(data, dict):
print(format_issue_summary(data))
return 0
def cmd_mrs_list(args: argparse.Namespace) -> int:
"""List merge requests for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
glab_args = ["mr", "list"]
if args.repo:
glab_args.extend(["-R", args.repo])
glab_args.extend(["--per-page", str(args.limit)])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
items = data if isinstance(data, list) else []
if not items:
print("No merge requests found")
else:
print(f"## Merge Requests\n\nFound {len(items)} MR(s):\n")
print("\n\n".join(format_mr_row(mr) for mr in items))
return 0
def cmd_mrs_view(args: argparse.Namespace) -> int:
"""View a single merge request.
Args:
args: Parsed arguments with MR number, repo, json flags.
Returns:
Exit code.
"""
glab_args = ["mr", "view", str(args.number)]
if args.repo:
glab_args.extend(["-R", args.repo])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
if isinstance(data, dict):
print(format_mr_summary(data))
return 0
def cmd_pipelines_list(args: argparse.Namespace) -> int:
"""List pipelines for a repository.
Args:
args: Parsed arguments with repo, limit, json flags.
Returns:
Exit code.
"""
glab_args = ["ci", "list"]
if args.repo:
glab_args.extend(["-R", args.repo])
glab_args.extend(["--per-page", str(args.limit)])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
items = data if isinstance(data, list) else []
if not items:
print("No pipelines found")
else:
print(f"## Pipelines\n\nFound {len(items)} pipeline(s):\n")
print("\n\n".join(format_pipeline_row(p) for p in items))
return 0
def cmd_pipelines_view(args: argparse.Namespace) -> int:
"""View a single pipeline.
Args:
args: Parsed arguments with pipeline ID, repo, json flags.
Returns:
Exit code.
"""
glab_args = ["ci", "view", str(args.pipeline_id)]
if args.repo:
glab_args.extend(["-R", args.repo])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
if isinstance(data, dict):
print(format_pipeline_summary(data))
return 0
def cmd_repos_list(args: argparse.Namespace) -> int:
"""List repositories for the authenticated user.
Args:
args: Parsed arguments with limit, json flags.
Returns:
Exit code.
"""
glab_args = ["repo", "list"]
glab_args.extend(["--per-page", str(args.limit)])
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
items = data if isinstance(data, list) else []
if not items:
print("No repositories found")
else:
print(f"## Repositories\n\nFound {len(items)} repository(ies):\n")
print("\n\n".join(format_repo_row(r) for r in items))
return 0
def cmd_repos_view(args: argparse.Namespace) -> int:
"""View a single repository.
Args:
args: Parsed arguments with repo name, json flag.
Returns:
Exit code.
"""
glab_args = ["repo", "view", args.repo]
if args.json:
data = run_glab(glab_args, output_json=True)
print(json.dumps(data, indent=2))
else:
data = run_glab(glab_args, output_json=True)
if isinstance(data, dict):
print(format_repo_summary(data))
return 0
# ============================================================================
# ARGUMENT PARSER
# ============================================================================
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser with nested subcommands.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="GitLab wrapper for AI agents \u2014 markdown-formatted glab output",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# check
subparsers.add_parser("check", help="Verify glab CLI is installed and authenticated")
# issues
issues_parser = subparsers.add_parser("issues", help="Issue operations")
issues_sub = issues_parser.add_subparsers(dest="issues_command")
issues_list = issues_sub.add_parser("list", help="List issues")
issues_list.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
issues_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
issues_list.add_argument("--json", action="store_true", help="Output raw JSON")
issues_view = issues_sub.add_parser("view", help="View issue details")
issues_view.add_argument("number", type=int, help="Issue number")
issues_view.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
issues_view.add_argument("--json", action="store_true", help="Output raw JSON")
# mrs
mrs_parser = subparsers.add_parser("mrs", help="Merge request operations")
mrs_sub = mrs_parser.add_subparsers(dest="mrs_command")
mrs_list = mrs_sub.add_parser("list", help="List merge requests")
mrs_list.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
mrs_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
mrs_list.add_argument("--json", action="store_true", help="Output raw JSON")
mrs_view = mrs_sub.add_parser("view", help="View MR details")
mrs_view.add_argument("number", type=int, help="MR number")
mrs_view.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
mrs_view.add_argument("--json", action="store_true", help="Output raw JSON")
# pipelines
pipelines_parser = subparsers.add_parser("pipelines", help="Pipeline operations")
pipelines_sub = pipelines_parser.add_subparsers(dest="pipelines_command")
pipelines_list = pipelines_sub.add_parser("list", help="List pipelines")
pipelines_list.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
pipelines_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
pipelines_list.add_argument("--json", action="store_true", help="Output raw JSON")
pipelines_view = pipelines_sub.add_parser("view", help="View pipeline details")
pipelines_view.add_argument("pipeline_id", type=int, help="Pipeline ID")
pipelines_view.add_argument("--repo", "-R", help="Repository (GROUP/REPO)")
pipelines_view.add_argument("--json", action="store_true", help="Output raw JSON")
# repos
repos_parser = subparsers.add_parser("repos", help="Repository operations")
repos_sub = repos_parser.add_subparsers(dest="repos_command")
repos_list = repos_sub.add_parser("list", help="List repositories")
repos_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
repos_list.add_argument("--json", action="store_true", help="Output raw JSON")
repos_view = repos_sub.add_parser("view", help="View repository details")
repos_view.add_argument("repo", help="Repository (GROUP/REPO)")
repos_view.add_argument("--json", action="store_true", help="Output raw JSON")
return parser
# ============================================================================
# MAIN
# ============================================================================
def main() -> int:
"""Main entry point.
Returns:
Exit code.
"""
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
if args.command == "check":
return cmd_check(args)
elif args.command == "issues":
if not hasattr(args, "issues_command") or not args.issues_command:
parser.parse_args(["issues", "--help"])
return 1
if args.issues_command == "list":
return cmd_issues_list(args)
elif args.issues_command == "view":
return cmd_issues_view(args)
elif args.command == "mrs":
if not hasattr(args, "mrs_command") or not args.mrs_command:
parser.parse_args(["mrs", "--help"])
return 1
if args.mrs_command == "list":
return cmd_mrs_list(args)
elif args.mrs_command == "view":
return cmd_mrs_view(args)
elif args.command == "pipelines":
if not hasattr(args, "pipelines_command") or not args.pipelines_command:
parser.parse_args(["pipelines", "--help"])
return 1
if args.pipelines_command == "list":
return cmd_pipelines_list(args)
elif args.pipelines_command == "view":
return cmd_pipelines_view(args)
elif args.command == "repos":
if not hasattr(args, "repos_command") or not args.repos_command:
parser.parse_args(["repos", "--help"])
return 1
if args.repos_command == "list":
return cmd_repos_list(args)
elif args.repos_command == "view":
return cmd_repos_view(args)
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())