Skip to content

GitLab Integration

Diverge provides first-class GitLab integration for automatic preview environments on every Merge Request. This guide covers webhook setup, CI/CD pipelines with change detection, sticky MR comments, merge gating, and self-hosted GitLab configuration.

Install the Diverge controller with GitLab notifier enabled:

Terminal window
helm repo add diverge https://divergedev.github.io/diverge
helm repo update
helm install diverge diverge/diverge \
--namespace diverge-system \
--create-namespace \
--set notifierProvider=gitlab \
--set controller.env.DIVERGE_NOTIFIER_TOKEN=<gitlab-api-token> \
--set controller.env.DIVERGE_WEBHOOK_SECRET=<webhook-secret>
Variable Description
DIVERGE_NOTIFIER_TOKEN GitLab API token with api scope (personal or project access token)
DIVERGE_WEBHOOK_SECRET Shared secret for webhook signature validation
Flag Description
--notifier-provider=gitlab Enable GitLab MR comments and commit statuses
--gitlab-url=https://gitlab.example.com Base URL for self-hosted GitLab (omit for gitlab.com)

Register webhooks in your GitLab project (or group) to trigger automatic environment lifecycle.

Navigate to Settings → Webhooks → Add new webhook:

Setting Value
URL https://diverge.yourdomain.com/gitlab-webhook
Secret token Your DIVERGE_WEBHOOK_SECRET value
Trigger ✅ Merge request events

Multi-Service Environments (PreviewGroups)

Section titled “Multi-Service Environments (PreviewGroups)”

Add a second webhook for coordinated multi-service deployments:

Setting Value
URL https://diverge.yourdomain.com/gitlab-previewgroup-webhook
Secret token Your DIVERGE_WEBHOOK_SECRET value
Trigger ✅ Merge request events
MR Action Diverge Behavior
open, reopen Creates Environment / PreviewGroup CR
update (new push) Updates the CR with new commit SHA
merge, close Deletes the CR (triggers teardown)

Diverge provides a complete CI pipeline example that mirrors the GitHub Actions workflow. Add this to your .gitlab-ci.yml:

stages:
- build
- analyze
- preview
- test
- cleanup
variables:
REGISTRY: ${CI_REGISTRY_IMAGE}
# All jobs needing the CLI use this image (git, curl, jq included)
default:
image: ghcr.io/divergedev/diverge-cli:v0.8.2

To pin a specific version, replace v0.8.2 with the desired tag. Use latest to always get the newest release.

Use diverge diff to detect which services changed based on git diff and .diverge.yaml path mappings:

analyze:
stage: analyze
script:
- DIFF_JSON=$(diverge diff --output json --base "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}")
- echo "${DIFF_JSON}" | jq .
- SERVICES=$(echo "${DIFF_JSON}" | jq -r '.services // [] | join(", ")')
- echo "Changed services:" ${SERVICES}
# Trace routes for each changed service
- |
for svc in $(echo "${DIFF_JSON}" | jq -r '.services[]?' 2>/dev/null); do
echo "--- Route trace for ${svc} ---"
diverge route "${svc}" || true
done
rules:
- if: $CI_MERGE_REQUEST_IID

Create the preview environment and post a summary note on the Merge Request:

preview:deploy:
stage: preview
script:
- diverge create --mr "${CI_MERGE_REQUEST_IID}"
# Post sticky MR comment using GitLab Notes API
- |
COMMENT_BODY="<!-- diverge-preview-comment -->
## 🚀 Preview Environment
| Property | Value |
|---|---|
| **Environment** | \`preview-mr-${CI_MERGE_REQUEST_IID}\` |
| **Commit** | \`${CI_COMMIT_SHORT_SHA}\` |
| **Deploy Mode** | \`delta\` |"
EXISTING_NOTE_ID=$(curl -s --header "PRIVATE-TOKEN: ${DIVERGE_GITLAB_TOKEN}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes?per_page=100" \
| jq -r '.[] | select(.body | contains("diverge-preview-comment")) | .id' | head -1)
if [ -n "${EXISTING_NOTE_ID}" ] && [ "${EXISTING_NOTE_ID}" != "null" ]; then
curl -s --request PUT \
--header "PRIVATE-TOKEN: ${DIVERGE_GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data "$(jq -n --arg body "${COMMENT_BODY}" '{body: $body}')" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes/${EXISTING_NOTE_ID}" > /dev/null
else
curl -s --request POST \
--header "PRIVATE-TOKEN: ${DIVERGE_GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data "$(jq -n --arg body "${COMMENT_BODY}" '{body: $body}')" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" > /dev/null
fi
rules:
- if: $CI_MERGE_REQUEST_IID

The <!-- diverge-preview-comment --> HTML marker ensures only one comment is maintained per MR — subsequent pushes update the existing note instead of creating new ones.

Tear down environments when the MR is merged or closed:

cleanup:
stage: cleanup
script:
- diverge delete "preview-mr-${CI_MERGE_REQUEST_IID}" || true
rules:
- if: $CI_MERGE_REQUEST_IID
when: manual
allow_failure: true
environment:
name: preview/mr-${CI_MERGE_REQUEST_IID}
action: stop

Configure these in Settings → CI/CD → Variables:

Variable Type Masked Description
KUBECONFIG File No Kubernetes cluster credentials
DIVERGE_GITLAB_TOKEN Variable Yes GitLab API token with api scope

Diverge posts diverge/preview commit statuses to GitLab, enabling merge gating on preview environment health.

Status Meaning
pending Environment is being provisioned
running Deployment in progress
success Environment is healthy and serving traffic
failed An error occurred during provisioning
canceled Environment was terminated
  1. Go to Settings → Repository → Protected branches
  2. Select your target branch (e.g., main)
  3. Under Status checks, add diverge/preview

Diverge automatically posts and updates Merge Request notes through the environment lifecycle:

Event Comment Content
Created Services being deployed, routing header
Ready Preview URL, curl command with routing header
Failed Error details, failed conditions, controller log hints
Teardown Cleanup confirmation and reason

Comments are deduplicated — Diverge tracks the CommentID and updates the existing note on subsequent events. If a note is deleted (404), Diverge automatically creates a new one.

The GitLab notifier implements automatic retry with exponential backoff for HTTP 429 (rate limit) and 403 responses, respecting the Retry-After header.


For self-hosted GitLab instances:

Pass the --gitlab-url flag to the controller:

# Helm values.yaml
controller:
extraArgs:
- "--gitlab-url=https://gitlab.internal.example.com"

If your instance uses self-signed certificates, mount your CA bundle:

controller:
extraVolumes:
- name: ca-certs
configMap:
name: internal-ca-bundle
extraVolumeMounts:
- name: ca-certs
mountPath: /etc/ssl/certs/internal-ca.pem
subPath: ca.pem

The webhook endpoint must be reachable from your GitLab instance. For internal clusters, configure:

  • GitLab Admin Area → Settings → Network → Outbound requests: Allow requests to the local network
  • Firewall rules: Ensure GitLab can reach the Diverge ingress endpoint

For internal container registries, configure image pull secrets:

controller:
imagePullSecrets:
- name: internal-registry-credentials

Diverge uses standard GitLab CI variables in pipeline configurations:

Variable Used For
CI_MERGE_REQUEST_IID MR number for diverge create --mr
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME Source branch identification
CI_MERGE_REQUEST_TARGET_BRANCH_NAME Base branch for diverge diff --base
CI_COMMIT_SHORT_SHA Container image tagging
CI_MERGE_REQUEST_EVENT_TYPE Cleanup trigger (merged, closed)
CI_PROJECT_ID GitLab API calls for MR comments
CI_API_V4_URL GitLab API base URL
CI_REGISTRY / CI_REGISTRY_IMAGE Container registry authentication

Feature GitHub GitLab
Webhook authentication HMAC-SHA256 Token (constant-time)
MR/PR comments ✅ Sticky comments ✅ Sticky notes
Commit statuses diverge/preview diverge/preview
PreviewGroup support
Change detection (diverge diff)
Route tracing (diverge route)
Merge gating ✅ Branch protection ✅ Protected branches
Config fetching ✅ Contents API ✅ Repository Files API
Pipeline testing ✅ Workflow dispatch ✅ Pipeline trigger API
CLI install + caching setup-diverge@v1 diverge-cli Docker image
Self-hosted support ✅ (--gitlab-url)
Rate limit handling ✅ (429/403 + Retry-After)

Issue Solution
No MR comments Verify DIVERGE_NOTIFIER_TOKEN has api scope
Webhook 404 Check webhook URL matches /gitlab-webhook (not /webhook/gitlab)
Webhook auth failures Ensure DIVERGE_WEBHOOK_SECRET matches the GitLab webhook secret token
Self-hosted cert errors Mount your internal CA bundle into the controller pod
Rate limiting Diverge handles this automatically with exponential backoff
Pipeline not triggering Ensure CI rules use $CI_MERGE_REQUEST_IID (not $CI_PIPELINE_SOURCE)