# Gitlab API

> Integrates with the GitLab REST API v4 and GraphQL API via python-gitlab to automate projects, merge requests, CI/CD pipelines, runners, and container registry management.

- Skill: `paulpas/gitlab-api` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/gitlab-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/gitlab-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/gitlab-api

---





# GitLab API & python-gitlab Integration

Integrates with the GitLab REST API v4 and GraphQL API using the `python-gitlab` library to programmatically manage projects, groups, merge requests, CI/CD pipelines, runners, container registry, and GitLab Pages.

## TL;DR for Code Generation

- [ ] Authenticate with a personal access token (PAT) via `gitlab.Gitlab(url, token)` or config file
- [ ] Use the `gl.projects.get()`, `gl.groups.get()`, and `gl.runners.all()` patterns for resource access
- [ ] Enable pagination with `iterator=True` on list calls to auto-fetch all pages
- [ ] Handle `gitlab.exceptions.GitlabError` with specific subclasses for auth, not-found, and validation errors
- [ ] Use `gl.gitlab_cli()` for command-line style operations or the Python API for programmatic access
- [ ] Target both gitlab.com SaaS and self-managed instances by configuring `url` explicitly

---

## When to Use

Use this skill when:

- Automating project creation, group management, or member access across GitLab namespaces
- Building merge request approval workflows, automated code review bots, or merge train integrations
- Triggering, inspecting, or canceling CI/CD pipelines programmatically
- Managing GitLab CI runners (shared, group, or project-specific)
- Cleaning up container registry tags or managing GitLab Pages deployments
- Implementing GitLab GraphQL queries for complex, nested data extraction

---

## When NOT to Use

Avoid this skill for:

- Direct git operations (clone/push/fetch) — use `gitpython` or the `git` CLI instead
- Infrastructure-as-code for GitLab itself (use the `coding-terraform-sdk` skill with `gitlab terraform provider`)
- Simple webhook receivers that don't need to call the GitLab API back

---

## Core Workflow

1. **Authenticate and Initialize Client** — Create a `gitlab.Gitlab` instance with your GitLab URL and a personal access token. **Checkpoint:** Call `gl.auth()` to verify the token works and retrieve the authenticated user.

2. **Resolve Target Project or Group** — Use `gl.projects.get(namespace/repo)` or `gl.groups.get(group_id)` to obtain the resource handle. **Checkpoint:** Handle `GitlabGetError` (404) immediately if the resource is missing.

3. **Execute the Operation** — Call methods on the project handle: create MRs, list pipelines, trigger CI jobs, manage protected branches. **Checkpoint:** Inspect the returned object's `id`, `web_url`, or `status` fields.

4. **Handle Pagination** — For list endpoints, pass `iterator=True` and optionally `per_page` and `page` parameters. **Checkpoint:** Iterate fully — by default, GitLab returns 20 items per page.

5. **Error Handling** — Catch `gitlab.exceptions.GitlabOperationError` for mutation failures, `GitlabAuthenticationError` for bad tokens, and `GitlabListError` for pagination issues. **Checkpoint:** Log the `response_body` attribute from the exception for debugging.

---

## Implementation Patterns

### Pattern 1: Project Management and Merge Request Automation

```python
import os
import gitlab
from gitlab.exceptions import GitlabCreateError, GitlabGetError

def create_merge_request_with_approvers(
    project_path: str,
    source_branch: str,
    target_branch: str,
    title: str,
    description: str,
    approver_ids: list[int] | None = None,
) -> dict:
    """Create a merge request and optionally assign approvers.

    Args:
        project_path: Full project path (e.g., "my-group/my-project").
        source_branch: Feature branch name.
        target_branch: Destination branch (usually "main" or "master").
        title: MR title.
        description: MR description in Markdown.
        approver_ids: GitLab user IDs for required approvers.

    Returns:
        Dict with MR ID, web URL, and merge status.

    Raises:
        GitlabCreateError: If MR creation fails due to validation.
        GitlabGetError: If the project is not found.
    """
    token = os.environ["GITLAB_TOKEN"]
    gl = gitlab.Gitlab("https://gitlab.com", token)

    try:
        project = gl.projects.get(project_path)
    except GitlabGetError:
        raise RuntimeError(f"Project '{project_path}' not found. Verify the path and your access permissions.")

    mr = project.mergerequests.create({
        "source_branch": source_branch,
        "target_branch": target_branch,
        "title": title,
        "description": description,
        "remove_source_branch": True,
    })

    # Assign approvers if provided (GitLab Premium/Ultimate feature)
    if approver_ids:
        try:
            mr.approvals.set_approvers(approver_ids=approver_ids)
        except GitlabCreateError:
            # Silently continue — approver assignment may not be available
            # on Free tier or self-managed without license
            pass

    return {
        "id": mr.id,
        "web_url": mr.web_url,
        "state": mr.state,
        "merge_status": mr.merge_status,
    }
```

### Pattern 2: CI/CD Pipeline Trigger and Status Polling

```python
import time
import os
import gitlab
from gitlab.exceptions import GitlabError

def trigger_pipeline_and_wait(
    project_path: str,
    branch: str = "main",
    variables: dict | None = None,
    poll_interval: int = 10,
    timeout: int = 600,
) -> dict:
    """Trigger a GitLab CI/CD pipeline and poll until completion.

    Args:
        project_path: Full project path.
        branch: Target branch for the pipeline.
        variables: Optional CI/CD variables to pass to the pipeline.
        poll_interval: Seconds between status checks.
        timeout: Maximum seconds to wait for pipeline completion.

    Returns:
        Dict with pipeline ID, status, and web URL.

    Raises:
        TimeoutError: If the pipeline does not complete within timeout.
        GitlabError: If pipeline creation or status retrieval fails.
    """
    gl = gitlab.Gitlab("https://gitlab.com", os.environ["GITLAB_TOKEN"])
    project = gl.projects.get(project_path)

    pipeline = project.pipelines.create({
        "ref": branch,
        "variables": [
            {"key": k, "value": v}
            for k, v in (variables or {}).items()
        ],
    })

    start = time.monotonic()
    while pipeline.status in ("pending", "running", "created"):
        if time.monotonic() - start > timeout:
            raise TimeoutError(
                f"Pipeline {pipeline.id} did not complete within {timeout}s. "
                f"Last status: {pipeline.status}"
            )
        time.sleep(poll_interval)
        pipeline.refresh()

    return {
        "id": pipeline.id,
        "web_url": pipeline.web_url,
        "status": pipeline.status,
        "duration": pipeline.duration,
        "finished_at": pipeline.finished_at,
    }
```

### Pattern 3: Container Registry Tag Cleanup

```python
from datetime import datetime, timedelta, timezone
import os
import gitlab
from gitlab.exceptions import GitlabError

def cleanup_old_registry_tags(
    project_path: str,
    keep_last: int = 10,
    max_age_days: int = 30,
) -> list[str]:
    """Delete container registry tags older than max_age_days, keeping
    at least keep_last recent tags.

    Args:
        project_path: Full repository path.
        keep_last: Minimum number of most recent tags to preserve.
        max_age_days: Delete tags older than this many days.

    Returns:
        List of deleted tag names.
    """
    gl = gitlab.Gitlab("https://gitlab.com", os.environ["GITLAB_TOKEN"])
    project = gl.projects.get(project_path)

    try:
        tags = project.repositories_tags.list(iterator=True)
    except GitlabError as exc:
        raise RuntimeError(
            f"Failed to list registry tags for {project_path}: {exc}"
        ) from exc

    sorted_tags = sorted(
        tags,
        key=lambda t: datetime.fromisoformat(t.created_at),
        reverse=True,
    )

    # Always keep the most recent tags
    keep_set = set(t.name for t in sorted_tags[:keep_last])
    cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days)
    deleted: list[str] = []

    for tag in sorted_tags:
        if tag.name in keep_set:
            continue
        created = datetime.fromisoformat(tag.created_at)
        if created < cutoff:
            try:
                tag.delete()
                deleted.append(tag.name)
            except GitlabError:
                pass  # Tag might be protected or locked

    return deleted
```

### BAD vs GOOD: Error Handling and Pagination

```python
# ❌ BAD — no pagination, no error handling, hardcoded values
def list_projects_bad():
    gl = gitlab.Gitlab("https://gitlab.com", "MY_TOKEN")
    projects = gl.projects.list()  # Only gets first 20!
    for p in projects:
        print(p.name)

# ✅ GOOD — iterator for full pagination, typed, descriptive errors
from gitlab.exceptions import GitlabAuthenticationError, GitlabListError

def list_all_project_names(url: str, token: str) -> list[str]:
    """List all project names the authenticated user can access.

    Args:
        url: GitLab instance URL (e.g., "https://gitlab.com").
        token: Personal access token with read_api scope.

    Returns:
        Sorted list of full project paths.

    Raises:
        GitlabAuthenticationError: If the token is invalid.
        GitlabListError: If pagination fails.
    """
    if not url or not token:
        raise ValueError("GitLab URL and token are required.")

    gl = gitlab.Gitlab(url, token)
    try:
        gl.auth()
    except GitlabAuthenticationError:
        raise RuntimeError(
            "GitLab authentication failed. Verify your token has 'read_api' scope."
        )

    try:
        projects = gl.projects.list(iterator=True, per_page=100)
    except GitlabListError as exc:
        raise RuntimeError(
            f"Failed to retrieve project list: {exc}"
        ) from exc

    return sorted([p.path_with_namespace for p in projects])
```

## MUST DO

- Use personal access tokens with the minimum required scopes (read_api, read_repository, write_repository)
- Call `gl.auth()` immediately after creating the Gitlab client to validate credentials
- Always pass `iterator=True` when calling list endpoints to paginate through all results
- Store GitLab URL and token in environment variables (`GITLAB_URL`, `GITLAB_TOKEN`)
- Use project path (namespace/repo) instead of project ID for readability and portability
- Check pipeline job logs via `pipeline.jobs.list()` for failure diagnostics

## MUST NOT DO

- Never embed tokens in code or configuration files committed to version control
- Avoid calling `gl.projects.list()` without `iterator=True` for production workflows
- Do not swallow `GitlabError` exceptions — always log the error details
- Never use admin credentials for routine API operations
- Do not poll pipelines without a timeout — always set a maximum wait duration

---

## Constraints

### MUST DO
- Implement structured error responses with consistent format: {error_code, message, details, request_id}
- Add rate limiting per client/API key with configurable burst and sustained limits using a token bucket algorithm
- Validate all incoming requests against a schema before processing — reject malformed input with clear error messages
- Include correlation/request IDs in all log entries for end-to-end request tracing across service boundaries

### MUST NOT DO
- Do not expose internal implementation details, stack traces, or database queries in error responses
- Avoid accepting unbounded request bodies — set maximum payload sizes and timeout limits
- Never trust client-supplied authentication tokens without validation (signature verification, expiration check)
- Do not log request/response bodies containing PII, API keys, or other sensitive data


## Live References

- [python-gitlab Documentation](https://python-gitlab.readthedocs.io/en/stable/)
- [python-gitlab GitHub Repository](https://github.com/python-gitlab/python-gitlab)
- [GitLab REST API v4 Reference](https://docs.gitlab.com/ee/api/api_resources.html)
- [GitLab GraphQL API](https://docs.gitlab.com/ee/api/graphql/)
- [GitLab CI/CD Pipeline API](https://docs.gitlab.com/ee/api/pipelines.html)
- [GitLab Container Registry API](https://docs.gitlab.com/ee/api/container_registry.html)
- [GitLab Merge Request Approvals API](https://docs.gitlab.com/ee/api/merge_request_approvals.html)

## Related Skills

| Skill | Purpose |
|---|---|
| `coding-github-api` | GitHub API with PyGithub and Octokit |
| `coding-bitbucket-api` | Bitbucket Cloud API with atlassian-python-api |
| `coding-kubernetes-api` | Kubernetes client-python for deploying to GitLab-managed clusters |

