Automated code review for GitLab Merge Requests, powered by the GitHub Copilot SDK.
- Webhook-driven reviews: GitLab MR webhooks trigger Copilot-powered code review with inline suggestions
- GitLab polling (
GITLAB_POLL=true): Polls GitLab API for open MRs and/copilot <instruction>comments — no webhook required - Jira integration: Polls Jira for issues in a trigger status, creates branches + MRs in GitLab, triggers agent review
/copilotcommands: MR comments starting with/copilottrigger the agent with custom instructions (e.g.,/copilot add unit tests)- Repo-level configuration: Loads project-specific skills, agents, and instructions from
.github/,.claude/,AGENTS.md - Task execution: Local subprocess or Kubernetes Job isolation per review
- K8s executor: Can run review jobs as Kubernetes Jobs instead of local processes
- OTEL observability: Full OpenTelemetry traces, metrics, and logs when endpoint is configured
📖 Developer Wiki — comprehensive architecture docs, module reference, security model, deployment guides, and more.
docker build -t gitlab-copilot-agent .
docker run -p 8000:8000 \
-e GITLAB_URL=https://gitlab.com \
-e GITLAB_TOKEN=glpat-... \
-e GITLAB_WEBHOOK_SECRET=secret \
-e GITHUB_TOKEN=gho_... \
gitlab-copilot-agent# 1. Start devcontainer
devcontainer up --workspace-folder .
# 2. Configure environment
cp .env.k3d.example .env.k3d # fill in real values
# 3. Deploy to k3d
make k3d-up # create k3d cluster
make k3d-build # build & import image
make k3d-deploy # deploy via Helm| Variable | Default | Description |
|---|---|---|
GITLAB_URL |
— | GitLab instance URL |
GITLAB_TOKEN |
— | GitLab API private token (needs api scope) |
GITLAB_WEBHOOK_SECRET |
— | Secret for validating webhook payloads |
| Variable | Default | Description |
|---|---|---|
GITHUB_TOKEN |
— | GitHub token for Copilot auth |
COPILOT_PROVIDER_TYPE |
None |
BYOK provider: azure, openai, or omit for Copilot |
COPILOT_PROVIDER_BASE_URL |
None |
BYOK provider endpoint |
COPILOT_PROVIDER_API_KEY |
None |
BYOK provider API key |
| Variable | Default | Description |
|---|---|---|
COPILOT_MODEL |
gpt-4 |
Model for reviews |
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server bind host |
PORT |
8000 |
Server bind port |
LOG_LEVEL |
info |
Log level |
AGENT_GITLAB_USERNAME |
None |
Agent's GitLab username for loop prevention |
CLONE_DIR |
system temp | Base directory for repo clones |
| Variable | Default | Description |
|---|---|---|
GITLAB_POLL |
false |
Enable GitLab API polling for MR/note discovery |
GITLAB_POLL_INTERVAL |
30 |
Polling interval in seconds |
GITLAB_POLL_LOOKBACK |
60 |
Minutes to look back on startup for recent MRs |
GITLAB_REVIEW_ON_PUSH |
true |
Re-review MRs on each new commit. Set false to review once per MR |
GITLAB_PROJECTS |
None |
Comma-separated project paths/IDs (required when polling) |
| Variable | Default | Description |
|---|---|---|
TASK_EXECUTOR |
local |
local, kubernetes, or container_apps |
DISPATCH_BACKEND |
azure_storage |
Dispatch backend: azure_storage (Queue + Blob via Claim Check) |
K8S_NAMESPACE |
default |
Kubernetes namespace for Jobs |
K8S_JOB_IMAGE |
— | Docker image for Job pods |
K8S_JOB_CPU_LIMIT |
1 |
CPU limit |
K8S_JOB_MEMORY_LIMIT |
1Gi |
Memory limit |
K8S_JOB_TIMEOUT |
600 |
Job timeout in seconds |
K8S_SECRET_NAME |
None |
K8s Secret for Job pod credentials (auto-set by Helm) |
K8S_CONFIGMAP_NAME |
None |
K8s ConfigMap for Job pod config (auto-set by Helm) |
K8S_JOB_INSTANCE_LABEL |
"" |
Helm release label for NetworkPolicy scoping (auto-set by Helm) |
| Variable | Default | Description |
|---|---|---|
AZURE_STORAGE_CONNECTION_STRING |
None |
Azure Storage connection string (for Azurite/K8s). Overrides URL-based auth. |
AZURE_STORAGE_ACCOUNT_URL |
None |
Azure Blob Storage endpoint for managed identity auth (ACA). |
AZURE_STORAGE_QUEUE_URL |
None |
Azure Queue Storage endpoint for managed identity auth (ACA). |
TASK_QUEUE_NAME |
task-queue |
Azure Storage Queue name for task dispatch |
TASK_BLOB_CONTAINER |
task-data |
Azure Blob container for params and results |
| Variable | Default | Description |
|---|---|---|
JIRA_URL |
None |
Jira instance URL |
JIRA_EMAIL |
None |
Jira user email |
JIRA_API_TOKEN |
None |
Jira API token / PAT |
JIRA_TRIGGER_STATUS |
AI Ready |
Status that triggers agent |
JIRA_IN_PROGRESS_STATUS |
In Progress |
Status after pickup |
JIRA_POLL_INTERVAL |
30 |
Poll interval seconds |
JIRA_PROJECT_MAP |
None |
JSON mapping Jira project keys to GitLab projects |
| Variable | Default | Description |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
None |
OTLP endpoint — enables traces, metrics, logs export |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
Protocol: grpc (port 4317) or http/protobuf (port 4318) |
OTEL_SERVICE_NAME |
gitlab-copilot-agent |
Service name in OTEL resource attributes |
DEPLOYMENT_ENV |
— | Deployment environment label (e.g., production, staging) |
SERVICE_VERSION |
0.1.0 |
Service version in OTEL resource attributes |
- Go to your GitLab project → Settings → Webhooks
- Set the URL to
https://your-host/webhook - Set the secret token to match
GITLAB_WEBHOOK_SECRET - Check Merge request events
- Save
The service needs a publicly reachable URL. For local dev, use ngrok: ngrok http 8000.
The service can optionally poll Jira for issues and automatically create branches + MRs for agent review. See the Jira section in Environment Variables above for configuration.
The JSON must use the {"mappings": {...}} schema with numeric gitlab_project_id and clone_url:
{
"mappings": {
"PROJ": {
"gitlab_project_id": 42,
"clone_url": "https://gitlab.com/myorg/myrepo.git",
"target_branch": "main"
},
"DEMO": {
"gitlab_project_id": 87,
"clone_url": "https://gitlab.com/demos/example.git",
"target_branch": "develop"
}
}
}- Polls JQL: Every
JIRA_POLL_INTERVALseconds, queries Jira for issues inJIRA_TRIGGER_STATUS - Transitions: Moves picked-up issues to
JIRA_IN_PROGRESS_STATUS - Creates branches: Creates a new branch from
target_branch(named{project-key}-{issue-number}-{sanitized-title}) - Creates MRs: Opens an MR from the new branch, targeting
target_branch - Triggers review: The MR triggers the normal webhook review flow
The agent automatically loads project-specific config from the reviewed repo:
Skills and agents (from .github/ and .claude/):
| Path | What | How |
|---|---|---|
.github/skills/*/SKILL.md |
Skills | SDK-native skill_directories |
.claude/skills/*/SKILL.md |
Skills | SDK-native skill_directories |
.github/agents/*.agent.md |
Custom agents | SDK-native custom_agents (YAML frontmatter) |
.claude/agents/*.agent.md |
Custom agents | SDK-native custom_agents (YAML frontmatter) |
Instructions (all discovered, concatenated into system message):
| Path | Standard | Scope |
|---|---|---|
.github/copilot-instructions.md |
GitHub Copilot | Project-wide |
.github/instructions/*.instructions.md |
GitHub Copilot | Per-language |
.claude/CLAUDE.md |
Claude Code | Project-wide |
AGENTS.md |
Universal (Copilot, Claude, Codex, Cursor, GitLab Duo) | Project root + subdirectories |
CLAUDE.md |
Claude Code | Project root |
Symlinked files (e.g., ln -s AGENTS.md CLAUDE.md) are deduplicated automatically.
Each review comment includes:
- Severity tag:
[ERROR],[WARNING], or[INFO] - Description of the issue
- Inline code suggestion (when a concrete fix exists) — click "Apply suggestion" in the GitLab UI to commit the fix
The service uses in-memory structures that are bounded to prevent growth during long uptimes:
| Structure | Purpose | Default Limit | Eviction Strategy |
|---|---|---|---|
RepoLockManager |
Serializes concurrent operations on the same repo | 1,024 entries | LRU — evicts oldest idle (unlocked) lock |
ProcessedIssueTracker |
Prevents re-processing Jira issues within a run | 10,000 entries | Drops oldest 50% when limit is reached |
Active locks are never evicted — the lock manager allows temporary over-capacity rather than dropping in-use locks. Both limits are configurable via constructor arguments but not currently exposed as environment variables.
The service supports three execution backends controlled by TASK_EXECUTOR:
| Backend | How It Works | Use Case |
|---|---|---|
local (default) |
Runs Copilot CLI as a local subprocess | Development, single-node |
kubernetes |
Enqueues to Azure Storage Queue; KEDA ScaledJob triggers task runner pods | Production, self-hosted K8s |
container_apps |
Enqueues to Azure Storage Queue; KEDA event trigger starts ACA Job executions | Production, Azure-managed |
All remote executors use the same Claim Check dispatch pattern: params blob + queue message → KEDA triggers job → task runner dequeues, executes, writes result blob → controller polls result.
Coding task reliability:
- Agent outputs structured JSON listing files intentionally changed (prevents accidental artifact commits)
- In-session retry if agent doesn't return required format
- Branch name collision detection appends
-2,-3, etc. on retry - Stale completed K8s Jobs are automatically replaced on re-execution
Webhook not triggering
- Check that the webhook URL is publicly reachable (test with
curl https://your-host/webhook) - Verify
GITLAB_WEBHOOK_SECRETmatches the secret configured in GitLab - Ensure "Merge request events" is enabled in the webhook settings
- Check the GitLab webhook event log (Settings → Webhooks → Recent Deliveries)
Review posts no inline comments (only a summary)
- Check logs for diff position validation errors — GitLab rejects positions that don't match the diff
- Ensure the MR has actual file changes (empty MRs won't have reviewable diffs)
- Verify the agent is analyzing the correct commit range
Jira poller not processing issues
- Verify
JIRA_PROJECT_MAPis valid JSON and contains the Jira project key - Check that issues are in the exact status name from
JIRA_TRIGGER_STATUS(case-sensitive) - Confirm the Jira user has permission to query and transition issues
- Check logs for JQL query errors or API authentication failures
Git clone timeout or authentication failures
- Verify
GITLAB_TOKENhasapiscope and read access to the target project - Check network connectivity from the container to the GitLab instance
- Ensure the GitLab project URL is valid and accessible
- For self-hosted GitLab, verify SSL certificates are trusted
Enable detailed debug logs:
export LOG_LEVEL=debugThis shows:
- Full webhook payloads
- Git clone/checkout commands
- Copilot SDK interactions
- Diff position calculations
- API request/response details
# Install pre-commit hook (runs ruff + mypy before each commit)
ln -sf ../../scripts/pre-commit .git/hooks/pre-commit
# Run tests
devcontainer exec --workspace-folder . uv run pytest
# Lint
devcontainer exec --workspace-folder . uv run ruff check src/ tests/
# Type check
devcontainer exec --workspace-folder . uv run mypy src/End-to-end tests deploy the agent to k3d and test three flows against host-side mock services. Prerequisites: Docker, Make, k3d, kubectl.
- Webhook MR review — sends MR webhook → verifies review comments posted
- Jira polling — agent polls mock Jira for "AI Ready" issues → verifies transitions, MR creation, comments
- /copilot command — sends note webhook with
/copilot <instruction>→ verifies agent response comment
# Create the E2E cluster
make e2e-up
# Run the full E2E test (builds, deploys, sends webhook, verifies comments)
make e2e-test
# Tear down
make e2e-downSee docs/DEMO.md for automated demo environment setup. One command provisions a GitLab repo + Jira project showcasing all agent capabilities.
This project is licensed under the MIT License.
See the Developer Wiki for full architecture documentation, including:
- Architecture Overview — system diagrams, trust boundaries, deployment topology
- Module Reference — all 29 modules documented
- Request Flows — sequence diagrams for webhook, poller, and Jira flows
- Security Model — trust boundaries, auth, sandboxing
- ADRs — architecture decision records
GitLab Webhook/Poller → FastAPI → Clone repo → Copilot agent review → Parse output → Post inline comments + summary
Run the full stack locally using k3d (k3s-in-Docker). All tooling runs inside the devcontainer — no host-side k8s tools required.
The devcontainer includes everything needed for local k8s development:
| Tool | Installed via | Purpose |
|---|---|---|
| Docker | docker-in-docker devcontainer feature |
Container runtime for k3d nodes |
| kubectl | kubectl-helm-minikube devcontainer feature |
Cluster interaction |
| Helm 3 | kubectl-helm-minikube devcontainer feature |
Chart deployment |
| k3d v5.7.5 | postCreateCommand install script |
Local k3s cluster management |
Docker-in-Docker (not Docker-outside-of-Docker) is required because k3d creates its own Docker containers, networks, and port bindings. DooD would cause path and networking mismatches between the devcontainer and host.
┌─────────────────────────────────────────────────┐
│ Devcontainer │
│ │
│ 1. Start devcontainer (tools auto-installed) │
│ 2. make k3d-up → k3d cluster (~30s) │
│ 3. make k3d-build → docker build + import │
│ 4. make k3d-deploy → helm install │
│ 5. Edit code → make k3d-redeploy (iterate) │
│ 6. make k3d-down → teardown when done │
│ │
│ Docker-in-Docker daemon (persists across sleep) │
│ └── k3d cluster (k3s nodes as containers) │
│ └── Controller pod + Azurite pod + KEDA ScaledJob pods │
└─────────────────────────────────────────────────┘
Cluster lifecycle:
make k3d-upcreates a fresh cluster (~30-40s first time).- Devcontainer sleep/restart: cluster recovers in ~5-10s (DinD volume persists).
- Devcontainer rebuild: full cold start (DinD volume destroyed, re-run
make k3d-up).
cp .env.k3d.example .env.k3d # fill in real values
make k3d-up # create k3d cluster
make k3d-build # build & import image
make k3d-deploy # deploy via Helm| Command | Description |
|---|---|
make k3d-up |
Create k3d cluster |
make k3d-down |
Delete k3d cluster |
make k3d-build |
Build image & import into cluster |
make k3d-deploy |
Deploy/upgrade via Helm |
make k3d-redeploy |
Rebuild + redeploy (one step) |
make k3d-logs |
Tail controller logs |
make k3d-status |
Show pods, jobs, and services |
The controller is exposed on localhost:8080 via k3d port mapping + ServiceLB (override with K3D_HOST_PORT=9000 make k3d-up).
curl http://localhost:8080/health works immediately after deploy — no kubectl port-forward needed.
For manual port-forward (if not using ServiceLB):
kubectl port-forward svc/copilot-agent 8000:8000After deploying to the k3d cluster, verify the full system end-to-end:
# 1. Check all pods are running
make k3d-status
# 2. Verify webhook endpoint responds
curl -s http://localhost:8080/health
# 3. Send a test webhook payload (dry run)
curl -X POST http://localhost:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Gitlab-Token: $(grep GITLAB_WEBHOOK_SECRET .env.k3d | cut -d= -f2)" \
-d '{"object_kind": "merge_request", "event_type": "merge_request"}'
# 4. Watch logs during a live MR event
make k3d-logsFull live E2E (requires GitLab + ngrok/tunnel):
- Start tunnel:
ngrok http 8080(or use VS Code port forwarding) - Configure webhook in GitLab project pointing to tunnel URL
- Open/update an MR → observe agent review in
make k3d-logs - Verify inline comments appear on the MR
Jira live E2E (requires Jira credentials in .env.k3d):
- Transition a Jira issue to the trigger status
- Watch
make k3d-logsfor poller pickup - Verify branch + MR creation in GitLab
- Verify agent self-review on the new MR