4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
268 lines
9.7 KiB
Python
268 lines
9.7 KiB
Python
"""
|
|
GitLab Integration E2E Tests (cloud-hosted GitLab source).
|
|
|
|
GitLab is a supplementary investigation source, not a primary alert source.
|
|
These tests mirror how GitHub is used: a production alert fires (Grafana/generic),
|
|
and the agent queries GitLab to correlate recent commits, MRs, and pipelines.
|
|
|
|
Required env vars:
|
|
GITLAB_ACCESS_TOKEN - Personal access token with read_api + read_repository scope
|
|
GITLAB_PROJECT_ID - Project path (e.g. "myorg/myrepo")
|
|
|
|
Optional env vars:
|
|
GITLAB_BASE_URL - GitLab instance URL (defaults to https://gitlab.com/api/v4)
|
|
GITLAB_MR_IID - MR IID to post a note on (required only for test_gitlab_post_mr_note)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from integrations.gitlab import (
|
|
DEFAULT_GITLAB_BASE_URL,
|
|
build_gitlab_config,
|
|
get_gitlab_commits,
|
|
get_gitlab_mrs,
|
|
get_gitlab_pipelines,
|
|
post_gitlab_mr_note,
|
|
validate_gitlab_config,
|
|
)
|
|
|
|
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _require_env() -> tuple[str, str, str]:
|
|
"""Return (access_token, base_url, project_id) or skip the test."""
|
|
access_token = os.getenv("GITLAB_ACCESS_TOKEN", "").strip()
|
|
project_id = os.getenv("GITLAB_PROJECT_ID", "").strip()
|
|
base_url = (
|
|
os.getenv("GITLAB_BASE_URL", DEFAULT_GITLAB_BASE_URL).strip() or DEFAULT_GITLAB_BASE_URL
|
|
)
|
|
|
|
missing = []
|
|
if not access_token:
|
|
missing.append("GITLAB_ACCESS_TOKEN")
|
|
if not project_id:
|
|
missing.append("GITLAB_PROJECT_ID")
|
|
|
|
if missing:
|
|
pytest.skip(f"GitLab env vars not set: {', '.join(missing)}")
|
|
|
|
return access_token, base_url, project_id
|
|
|
|
|
|
def _gitlab_config(access_token: str, base_url: str):
|
|
return build_gitlab_config({"base_url": base_url, "auth_token": access_token})
|
|
|
|
|
|
def _gitlab_project_url(base_url: str, project_id: str) -> str:
|
|
"""Build a full GitLab project URL from base_url + project path."""
|
|
instance = base_url.replace("/api/v4", "").rstrip("/")
|
|
return f"{instance}/{project_id}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Connectivity
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_connectivity():
|
|
"""Verify that the token authenticates and /user responds."""
|
|
access_token, base_url, _ = _require_env()
|
|
config = _gitlab_config(access_token, base_url)
|
|
|
|
result = validate_gitlab_config(config)
|
|
|
|
assert result.ok, f"GitLab connectivity failed: {result.detail}"
|
|
assert "@" in result.detail or "Authenticated" in result.detail, (
|
|
f"Expected username in detail, got: {result.detail}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Tool-level: commits
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_list_commits():
|
|
"""Fetch recent commits for the configured project."""
|
|
access_token, base_url, project_id = _require_env()
|
|
config = _gitlab_config(access_token, base_url)
|
|
|
|
since = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
|
|
commits = get_gitlab_commits(
|
|
config=config,
|
|
project_id=project_id,
|
|
since=since,
|
|
per_page=5,
|
|
)
|
|
|
|
assert isinstance(commits, list), "Expected a list of commits"
|
|
if commits:
|
|
first = commits[0]
|
|
assert "id" in first or "short_id" in first, f"Unexpected commit shape: {first.keys()}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Tool-level: merge requests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_list_mrs():
|
|
"""Fetch merge requests for the configured project."""
|
|
access_token, base_url, project_id = _require_env()
|
|
config = _gitlab_config(access_token, base_url)
|
|
|
|
updated_after = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
|
|
mrs = get_gitlab_mrs(
|
|
config=config,
|
|
project_id=project_id,
|
|
state="merged",
|
|
updated_after=updated_after,
|
|
per_page=5,
|
|
)
|
|
|
|
assert isinstance(mrs, list), "Expected a list of merge requests"
|
|
if mrs:
|
|
first = mrs[0]
|
|
assert "iid" in first or "id" in first, f"Unexpected MR shape: {first.keys()}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Tool-level: pipelines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_list_pipelines():
|
|
"""Fetch CI/CD pipelines for the configured project."""
|
|
access_token, base_url, project_id = _require_env()
|
|
config = _gitlab_config(access_token, base_url)
|
|
|
|
updated_after = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
|
|
pipelines = get_gitlab_pipelines(
|
|
config=config,
|
|
project_id=project_id,
|
|
updated_after=updated_after,
|
|
per_page=5,
|
|
)
|
|
|
|
assert isinstance(pipelines, list), "Expected a list of pipelines"
|
|
if pipelines:
|
|
first = pipelines[0]
|
|
assert "id" in first, f"Unexpected pipeline shape: {first.keys()}"
|
|
assert "status" in first, f"Pipeline missing status: {first.keys()}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. End-to-end investigation — GitLab as supplementary evidence source
|
|
#
|
|
# This is the realistic production path: a Grafana-style alert fires (high
|
|
# error rate, service degradation, etc.) and the agent also queries GitLab
|
|
# to correlate recent commits and MRs that may have caused it.
|
|
#
|
|
# GitLab is picked up via repo_url or annotations["repository"], exactly the
|
|
# same way GitHub is detected — not from a "gitlab_ci" alert.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_investigation_e2e():
|
|
"""
|
|
Full investigation flow with GitLab as a supplementary evidence source.
|
|
|
|
Simulates a Grafana-style production alert that includes a repo_url
|
|
pointing at the configured GitLab project. The agent is expected to use
|
|
GitLab tools (commits, MRs) alongside the alert context to produce a
|
|
root cause, mirroring how GitHub is used in kubernetes/datadog e2e tests.
|
|
"""
|
|
access_token, base_url, project_id = _require_env()
|
|
|
|
from surfaces.cli.investigation import run_investigation_cli
|
|
|
|
# Load the shared fixture and patch in the real project URL so the
|
|
# The repo_url hint lets the agent correlate the alert with this project.
|
|
fixture_path = FIXTURES_DIR / "gitlab_high_error_rate_alert.json"
|
|
raw_alert = json.loads(fixture_path.read_text())
|
|
repo_url = _gitlab_project_url(base_url, project_id)
|
|
for alert in raw_alert.get("alerts", []):
|
|
alert.setdefault("annotations", {})["repo_url"] = repo_url
|
|
raw_alert.setdefault("commonAnnotations", {})["repo_url"] = repo_url
|
|
|
|
print(f"\nRunning investigation — primary: Grafana alert, supplementary: GitLab ({project_id})")
|
|
|
|
investigation_result = run_investigation_cli(raw_alert=raw_alert)
|
|
|
|
root_cause = investigation_result.get("root_cause", "")
|
|
remediation_steps = investigation_result.get("remediation_steps", [])
|
|
|
|
print(f"Root cause: {root_cause}")
|
|
print(f"Remediation steps: {remediation_steps}")
|
|
|
|
assert root_cause, (
|
|
"Investigation produced no root cause. "
|
|
"Check that GITLAB_ACCESS_TOKEN has read_api scope, GITLAB_PROJECT_ID is valid, "
|
|
"and an LLM key (ANTHROPIC_API_KEY or OPENAI_API_KEY) is set."
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Tool-level: post MR note
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitlab_post_mr_note():
|
|
"""Post a comment on a real MR and verify the note is created.
|
|
|
|
Reads project and MR context from the fixture annotations, the same way
|
|
the agent receives them from a real CI/CD alert.
|
|
|
|
Required env vars:
|
|
GITLAB_MR_IID - IID (not global ID) of an open MR in the configured project
|
|
"""
|
|
access_token, base_url, project_id = _require_env()
|
|
|
|
mr_iid = os.getenv("GITLAB_MR_IID", "").strip()
|
|
if not mr_iid:
|
|
pytest.skip("GITLAB_MR_IID env var not set")
|
|
|
|
# Load fixture and inject env values into annotations — same pattern as
|
|
# test_gitlab_investigation_e2e, mirroring how a real alert carries this context.
|
|
fixture_path = FIXTURES_DIR / "gitlab_high_error_rate_alert.json"
|
|
raw_alert = json.loads(fixture_path.read_text())
|
|
repo_url = _gitlab_project_url(base_url, project_id)
|
|
for alert in raw_alert.get("alerts", []):
|
|
alert.setdefault("annotations", {})["repo_url"] = repo_url
|
|
alert["annotations"]["mr_iid"] = mr_iid
|
|
raw_alert.setdefault("commonAnnotations", {})["repo_url"] = repo_url
|
|
raw_alert["commonAnnotations"]["mr_iid"] = mr_iid
|
|
|
|
# Read back from annotations — the same way the agent would in production.
|
|
annotations = raw_alert["alerts"][0]["annotations"]
|
|
project_id_from_annotation = project_id
|
|
mr_iid_from_annotation = annotations["mr_iid"]
|
|
|
|
config = _gitlab_config(access_token, base_url)
|
|
body = "[opensre e2e test] Automated RCA comment — safe to ignore."
|
|
result = post_gitlab_mr_note(
|
|
config=config,
|
|
project_id=project_id_from_annotation,
|
|
mr_iid=mr_iid_from_annotation,
|
|
body=body,
|
|
)
|
|
|
|
assert isinstance(result, dict), f"Expected a dict response, got: {type(result)}"
|
|
assert "id" in result, f"Note response missing 'id': {result.keys()}"
|
|
assert result.get("body") == body, f"Posted body mismatch: {result.get('body')!r}"
|
|
|
|
print(
|
|
f"\nPosted note id={result['id']} on MR !{mr_iid_from_annotation} in {project_id_from_annotation}"
|
|
)
|