Gitlab Auto Merge
976
Automated merge request management for GitLab. This service monitors your GitLab project and automatically handles merge requests.
merge_request_event source — see Pipeline Cancellation Scope)main, master or any other) from the GitLab project at startupdocker run -d \
--name gitlab-auto-merge \
--restart unless-stopped \
-p 8711:10200 \
-e EXECUTION_ENVIRONMENT=Docker \
-e GITLAB_SERVER_URL=https://gitlab.example.com \
-e GITLAB_PROJECT=group/project \
-e GITLAB_ACCESS_TOKEN=glpat-xxxx \
neckarit/gitlab-auto-merge:latest \
--mode=Run
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
container_name: gitlab-auto-merge
restart: unless-stopped
ports:
- "8711:10200"
environment:
- EXECUTION_ENVIRONMENT=Docker
- DEPLOYMENT_STAGE=Production
- GITLAB_SERVER_URL=https://gitlab.example.com
- GITLAB_PROJECT=group/project
- GITLAB_ACCESS_TOKEN=${GITLAB_ACCESS_TOKEN}
healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:10200/api/auto-merge/health/liveness || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
command:
- "--mode=Run"
The health check marks the container unhealthy while GitLab rejects the access token or the last completed cycle is older than 5 minutes: /api/auto-merge/health/liveness then answers HTTP 503. After a 401 Auto-Merge retries after 1 minute, doubling up to every 15 minutes, and returns to 15-second cycles once GitLab accepts the token again. A new token value takes effect once the container is recreated with docker compose up -d; docker restart keeps the old value.
Create a .env file next to your docker-compose.yml:
GITLAB_ACCESS_TOKEN=glpat-xxxxxxxxxxxx
Then start with:
docker compose up -d
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
container_name: gitlab-auto-merge
restart: unless-stopped
ports:
- "8711:10200"
volumes:
- /path/to/your/ca.crt:/certs/ca.crt:ro
environment:
- EXECUTION_ENVIRONMENT=Docker
- DEPLOYMENT_STAGE=Production
- GITLAB_SERVER_URL=https://gitlab.example.com
- GITLAB_PROJECT=group/project
- GITLAB_ACCESS_TOKEN=${GITLAB_ACCESS_TOKEN}
- CA_CERT_PATH=/certs/ca.crt
healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:10200/api/auto-merge/health/liveness || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
command:
- "--mode=Run"
If your GitLab instance uses a self-signed certificate or a certificate signed by an internal CA, you can configure the service to trust it.
Pass the certificate directly as a Base64-encoded environment variable. No file mounts needed.
Generate Base64:
# Encode your PEM certificate to Base64
cat /path/to/your/ca.crt | base64 -w0
Docker Run:
docker run -d \
--name gitlab-auto-merge \
--restart unless-stopped \
-p 8711:10200 \
-e EXECUTION_ENVIRONMENT=Docker \
-e GITLAB_SERVER_URL=https://gitlab.example.com \
-e GITLAB_PROJECT=group/project \
-e GITLAB_ACCESS_TOKEN=glpat-xxxx \
-e CA_CERT_BASE64="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..." \
neckarit/gitlab-auto-merge:latest \
--mode=Run
Docker Compose:
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
container_name: gitlab-auto-merge
restart: unless-stopped
ports:
- "8711:10200"
environment:
- EXECUTION_ENVIRONMENT=Docker
- GITLAB_SERVER_URL=https://gitlab.example.com
- GITLAB_PROJECT=group/project
- GITLAB_ACCESS_TOKEN=${GITLAB_ACCESS_TOKEN}
- CA_CERT_BASE64=${CA_CERT_BASE64}
command:
- "--mode=Run"
Mount the certificate file into the container.
Docker Run:
docker run -d \
--name gitlab-auto-merge \
--restart unless-stopped \
-p 8711:10200 \
-v /path/to/your/ca.crt:/certs/ca.crt:ro \
-e EXECUTION_ENVIRONMENT=Docker \
-e GITLAB_SERVER_URL=https://gitlab.example.com \
-e GITLAB_PROJECT=group/project \
-e GITLAB_ACCESS_TOKEN=glpat-xxxx \
-e CA_CERT_PATH=/certs/ca.crt \
neckarit/gitlab-auto-merge:latest \
--mode=Run
Docker Compose:
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
container_name: gitlab-auto-merge
restart: unless-stopped
ports:
- "8711:10200"
volumes:
- /path/to/your/ca.crt:/certs/ca.crt:ro
environment:
- EXECUTION_ENVIRONMENT=Docker
- GITLAB_SERVER_URL=https://gitlab.example.com
- GITLAB_PROJECT=group/project
- GITLAB_ACCESS_TOKEN=${GITLAB_ACCESS_TOKEN}
- CA_CERT_PATH=/certs/ca.crt
command:
- "--mode=Run"
Notes:
CA_CERT_BASE64 takes precedence over CA_CERT_PATH if both are set| Variable | Description | Required | Default |
|---|---|---|---|
GITLAB_SERVER_URL | GitLab instance URL (e.g., https://gitlab.com) | Yes | - |
GITLAB_PROJECT | Project path (e.g., mygroup/myproject) | Yes | - |
GITLAB_ACCESS_TOKEN | GitLab personal access token with api scope | Yes | - |
GITLAB_WEBHOOK_SIGNING_TOKEN | Signing token (whsec_…) of the GitLab webhook. Unset: every webhook delivery gets 401, the service polls | No | - |
MERGE_REQUEST_BADGE_ENABLED | Embed the SVG status badge image in MR descriptions. Set to false when the service host is not reachable from user browsers. | No | true |
REPEATED_FAILURE_THRESHOLD | How many consecutive pipeline failures on the same code take a merge request out of the merge queue. | No | 2 |
CA_CERT_BASE64 | Base64-encoded CA certificate (PEM format). Takes precedence over file path | No | - |
CA_CERT_PATH | Path to custom CA certificate (PEM format) | No | - |
EXECUTION_ENVIRONMENT | Runtime environment type | No | Docker |
DEPLOYMENT_STAGE | Deployment stage | No | - |
SERVICE_HOST | Service host identifier | No | - |
DIAGNOSTICS_ENABLED | Write the diagnostics bundle (false switches it off) | No | true |
DIAGNOSTICS_DIRECTORY | Directory of the diagnostics bundle | No | /var/lib/neckar/diagnostics |
DIAGNOSTICS_RETENTION | Files older than this are deleted, e.g. 7d, 36h | No | 7d |
DIAGNOSTICS_MAX_SIZE | Upper bound of the directory; the oldest files are deleted above it, e.g. 1GiB, 500MiB | No | 1GiB |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector URL. Setting this single variable enables OpenTelemetry export (Ktor server traces + JVM auto-instrumentation). | No | - |
OTEL_EXPORTER_OTLP_PROTOCOL | OTLP wire protocol: grpc (port 4317) or http/protobuf (port 4318). Match it to your collector's listener. | No | grpc |
OTEL_RESOURCE_ATTRIBUTES | Comma-separated resource attributes tagged onto every signal, e.g. deployment.environment=production. | No | - |
OTEL_SDK_DISABLED | Force telemetry off explicitly (true). Otherwise enablement is endpoint-driven: an in-JVM OpenTelemetry customizer disables the SDK while no OTEL_EXPORTER_OTLP_ENDPOINT is set. | No | endpoint-driven |
Telemetry is off until you point it at a collector — without an OTEL_EXPORTER_OTLP_ENDPOINT the image exports nothing and never phones home. Set that one variable to your OTLP collector and export turns on; an in-JVM OpenTelemetry customizer keeps the SDK disabled as long as no endpoint is configured.
The endpoint's port determines the protocol: 4317 is gRPC (the default), 4318 is HTTP/protobuf. If your collector only speaks HTTP, use port 4318 and set OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf.
Docker Run:
docker run -d \
--name gitlab-auto-merge \
--restart unless-stopped \
-p 8711:10200 \
-e EXECUTION_ENVIRONMENT=Docker \
-e GITLAB_SERVER_URL=https://gitlab.example.com \
-e GITLAB_PROJECT=group/project \
-e GITLAB_ACCESS_TOKEN=glpat-xxxx \
-e OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector.example.com:4317 \
neckarit/gitlab-auto-merge:latest \
--mode=Run
Docker Compose:
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
container_name: gitlab-auto-merge
restart: unless-stopped
ports:
- "8711:10200"
environment:
- EXECUTION_ENVIRONMENT=Docker
- GITLAB_SERVER_URL=https://gitlab.example.com
- GITLAB_PROJECT=group/project
- GITLAB_ACCESS_TOKEN=${GITLAB_ACCESS_TOKEN}
- OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector.example.com:4317
- OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
command:
- "--mode=Run"
The service emits Ktor server traces and the standard JVM auto-instrumentation. To force telemetry off explicitly — for example to silence the service in an environment where OTEL_EXPORTER_OTLP_ENDPOINT is set for other containers — set OTEL_SDK_DISABLED=true.
Auto-Merge writes every cycle and every scheduler event as one JSON line into DIAGNOSTICS_DIRECTORY: one file per UTC hour under cycles/, one file per UTC day under scheduler-events/, finished files compressed with xz. Each cycle line holds, per open merge request, the state, the desire, the plan with its hold reason, and every executed command with its result; it holds neither the access token nor description texts.
Mount a volume on /var/lib/neckar to keep the files across container re-creation:
services:
gitlab-auto-merge:
image: neckarit/gitlab-auto-merge:latest
volumes:
- auto-merge-state:/var/lib/neckar
volumes:
auto-merge-state:
Collect everything support needs in one archive, on the Docker host of the container:
curl -fsSL https://auto-merge.dev/auto-merge-diagnostics.sh | bash
The script finds the Auto-Merge container, copies the diagnostics directory, adds the container log, docker inspect and the answers of the status endpoints, replaces the value of every secret environment variable by <redacted>, and writes auto-merge-diagnostics-<timestamp>.tar.gz into the current directory. --container, --since and --output override its defaults (bash -s -- --container gitlab-auto-merge).
Export a time range as tar.xz with manifest.json:
curl -o auto-merge-diagnostics.tar.xz \
"http://localhost:8711/api/auto-merge/diagnostics/bundle?from=2026-09-24T08:00:00Z&to=2026-09-24T14:00:00Z"
Without HTTP access, copy the directory out of the container:
docker cp gitlab-auto-merge:/var/lib/neckar/diagnostics ./auto-merge-diagnostics
Read a finished hour with xzcat cycles/2026-09-24/12.jsonl.xz | jq. GET /api/auto-merge/diagnostics/status returns writeFailures and droppedCycles.
| Value | Description |
|---|---|
LocalDev | Local development (IDE, hot reload) |
Standalone | JAR running directly on server/VM |
Docker | Running inside a Docker container |
CI | Continuous Integration environment |
| Value | Description |
|---|---|
Development | Development environment (unstable, test data) |
Staging | Pre-production environment |
Production | Live production environment |
| Mode | Description |
|---|---|
DryRun | Only log what would be done (default). No changes to GitLab. |
OnlyLabels | Only update MR labels. No rebase, no merge, no pipeline cancellation. |
RebaseOnly | Rebase MRs and update labels, but don't cancel pipelines or merge. |
NoMerge | Rebase MRs, update labels, cancel pipelines, but don't merge. |
NoPipelineCancellation | Merge, rebase, update labels, but don't cancel pipelines. |
Run | Full execution: merge, rebase, update labels, cancel pipelines. |
Usage:
docker run ... neckarit/gitlab-auto-merge:latest --mode=Run
Use Cases:
DryRun - Test the service without making any changesOnlyLabels - Visualize MR status without affecting CI/CDRebaseOnly - Keep MRs up-to-date without interrupting running pipelinesNoMerge - Prepare MRs for merging, but let humans do the final mergeNoPipelineCancellation - Fully automated merging but let all pipelines run to completionRun - Fully automated merge workflow with pipeline optimization| Endpoint | Description |
|---|---|
GET /api/auto-merge/merge-requests | List categorized merge requests |
GET /api/auto-merge/pipelines | Pipeline status overview |
GET /api/auto-merge/log | Execution log |
POST /api/auto-merge/events/from-gitlab | GitLab webhook endpoint |
GET /api/auto-merge/diagnostics/bundle?from=…&to=… | Diagnostics bundle of a time range (tar.xz) |
GET /api/auto-merge/diagnostics/status | Write failures and dropped entries of the diagnostics writer |
Configure a signed webhook in your GitLab project (GitLab 19.0 or newer):
echo "whsec_$(openssl rand -base64 32)"GITLAB_WEBHOOK_SIGNING_TOKENhttps://your-auto-merge-host/api/auto-merge/events/from-gitlabA delivery without a matching signature gets 401; while GITLAB_WEBHOOK_SIGNING_TOKEN is unset, every delivery gets 401 and the service reacts by polling alone.
The service automatically applies these labels to merge requests:
Status labels use the scoped Auto-Merge:: prefix, so GitLab keeps exactly one of them on an MR. Each names one hold that needs human attention; a deliberate wait (pipeline running, rebase needed, rebasing) and a GitLab-native state (draft, approved) carry no label.
| Label | Meaning |
|---|---|
Auto-Merge::Has conflicts | Has merge conflicts, needs manual resolution |
Auto-Merge::Waiting for approval | No approval yet — a reviewer must approve it |
Auto-Merge::Unresolved discussions | Has open blocking discussions |
Auto-Merge::Pipeline failed | Pipeline failed (or was skipped) and needs attention |
Auto-Merge::Blocked by CI | GitLab blocks the merge on CI despite a green pipeline |
Auto-Merge::Last pipeline failed | Pipeline running, but the previous pipeline failed |
Auto-Merge::Repeated pipeline failure | Out of the queue after repeated failures on the same code |
Auto-Merge::Waiting for dependency | A merge request the description declares with blocked-by:, depends-on: or waits-for: is not merged |
GitLab's native UI already displays pipeline status prominently (badge, header, widget), so a running pipeline gets no label of its own.
Standalone labels without a :: scope, so several can be set on one MR at once:
| Label | Meaning |
|---|---|
Merge First | Prioritizes this MR — processed before others |
Merge Last | Only merged when no other MRs are in the queue |
Keep Pipeline | Pipeline will not be cancelled by Auto Merge |
Skip Auto-Merge | Ignored by Auto Merge — no rebase, merge, or pipeline management |
Auto Merge writes none of the user labels. Users set and remove them in the sidebar or with a quick action in a comment. The MR-local part of the Auto-Merge section of an open MR ends with the quick actions for its current labels, before the queue table of a queue member, for example Keep pipeline: `/label ~"Keep Pipeline"` · Skip Auto-Merge: `/label ~"Skip Auto-Merge"` or, on an ignored MR, Resume Auto-Merge: `/unlabel ~"Skip Auto-Merge"` .
The queue position of an MR is shown in its description section, not as a label.
Auto Merge cancels superseded pipelines (every active one but the newest of a merge request, except for a merged merge request or one with Skip Auto-Merge or Keep Pipeline) on a strict positive allowlist:
Pipeline source | Cancelled by Auto Merge? |
|---|---|
merge_request_event | yes — the only source Auto Merge ever replaces |
api | no |
trigger | no |
schedule | no |
web | no |
push | no |
| any other source | no |
Auto Merge starts its own MR pipeline as merge_request_event. It only ever
competes with other merge_request_event pipelines on the same MR. API-triggered
verify pipelines, scheduled jobs, manual web triggers, and external triggers all
have their own purpose and are never cancelled — even if they share a SHA with an
MR pipeline.
The allowlist is positive on purpose: any new GitLab pipeline source defaults to "do not cancel", which is the safe default for a destructive operation.
Draft merge requests receive special handling to save CI resources:
When you mark an MR as draft, its pipeline is cancelled. When you mark it ready, the next pipeline will run normally and the MR will be processed.
Auto Merge retries no failed job. A merge request whose pipeline failed leaves the merge queue, and Auto Merge starts no pipeline for it.
After --repeated-failure-threshold (environment variable REPEATED_FAILURE_THRESHOLD, default 2) consecutive pipeline failures on the same code, the merge request carries the label Auto-Merge::Repeated pipeline failure. It keeps its priority labels and returns to the queue once a push changes the code.
To prioritize an important merge request:
Merge First to the MRThis is useful for hotfixes or time-sensitive changes that need to be merged quickly.
Current Status: Conflict detection labels MRs that have merge conflicts, but automatic conflict resolution is not yet functional (returns 500 server error from GitLab API).
MRs with conflicts will:
Auto-Merge::Has conflicts label| Endpoint | Method | Description |
|---|---|---|
/api/auto-merge/configuration | GET | Service configuration |
/api/auto-merge/merge-requests | GET | List categorized merge requests |
/api/auto-merge/merge-requests/ascii | GET | MRs as plain text |
/api/auto-merge/pipelines | GET | Pipeline status overview |
/api/auto-merge/pipelines/status | GET | Aggregated pipeline status |
/api/auto-merge/pipelines/by-branch/main | GET | Main branch pipelines |
/api/auto-merge/pipelines/scheduled | GET | Scheduled pipeline list |
/api/auto-merge/runs | GET | Auto-merge run history |
/api/auto-merge/runs/latest | GET | Most recent run details |
/api/auto-merge/actions | GET | Actions timeline |
/api/auto-merge/runners | GET | Runner and job status |
/api/auto-merge/log | GET | Execution log messages |
/api/auto-merge/log/actions | GET | Executed actions log |
/api/auto-merge/events/history | GET | Received GitLab events |
/api/auto-merge/events/from-gitlab | POST | GitLab webhook endpoint |
Proprietary - Neckar IT GmbH
Format: Keep a Changelog 1.1.0.
POST /api/auto-merge/events/from-gitlab accepts a delivery only with a valid GitLab
webhook signature (HMAC-SHA256 over webhook-id, webhook-timestamp and the body, Standard Webhooks) from the
signing token in GITLAB_WEBHOOK_SIGNING_TOKEN, and only within 5 minutes of webhook-timestamp. Any other
delivery gets 401, and so does every delivery while the variable is unset; Auto-Merge then reacts through polling.Changelog truncated to fit Docker Hub's 25000-byte description limit. See the full CHANGELOG in the repository.
Content type
Image
Digest
sha256:24c59a9bf…
Size
330.3 MB
Last updated
5 days ago
docker pull neckarit/gitlab-auto-merge