Sign inSign up

neckarit/gitlab-auto-merge

By neckarit

•Updated 5 days ago

Gitlab Auto Merge

Image
Networking
Developer tools
0

976

neckarit/gitlab-auto-merge repository overview

⁠GitLab Auto Merge

Automated merge request management for GitLab. This service monitors your GitLab project and automatically handles merge requests.

⁠Features

  • Automatic Merging - Merges approved MRs when pipeline succeeds
  • Auto Rebase - Rebases MRs that are behind the target branch
  • Pipeline Management - Cancels superseded and obsolete pipelines to save CI resources (only merge_request_event source — see Pipeline Cancellation Scope⁠)
  • Smart Labels - Automatically labels MRs with their current status, and states the queue position in the MR description
  • Draft MR Handling - Cancels pipelines and removes labels from draft MRs to save CI resources
  • Job Retry - Retries failed jobs caused by transient errors (OOM, network issues)
  • Priority Support - Important MRs can be prioritized with a label
  • Stacked MRs - An MR that targets another MR's branch is never merged or rebased; it merges once GitLab retargets it to the project's default branch. Auto-Merge reads the default branch (main, master or any other) from the GitLab project at startup
  • Conflict Detection - Identifies and labels MRs with merge conflicts
  • Web Dashboard - REST API for monitoring and integration

⁠Quick Start

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 \
  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
      - 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
⁠Production Example with All Options
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"

⁠Custom SSL Certificates

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"
⁠Option 2: Certificate File Path

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:

  • The certificate must be in PEM format
  • CA_CERT_BASE64 takes precedence over CA_CERT_PATH if both are set
  • The custom CA will be trusted in addition to system default CAs
  • Hostname verification remains enabled for security

⁠Environment Variables

VariableDescriptionRequiredDefault
GITLAB_SERVER_URLGitLab instance URL (e.g., https://gitlab.com)Yes-
GITLAB_PROJECTProject path (e.g., mygroup/myproject)Yes-
GITLAB_ACCESS_TOKENGitLab personal access token with api scopeYes-
GITLAB_WEBHOOK_SIGNING_TOKENSigning token (whsec_…) of the GitLab webhook. Unset: every webhook delivery gets 401, the service pollsNo-
MERGE_REQUEST_BADGE_ENABLEDEmbed the SVG status badge image in MR descriptions. Set to false when the service host is not reachable from user browsers.Notrue
REPEATED_FAILURE_THRESHOLDHow many consecutive pipeline failures on the same code take a merge request out of the merge queue.No2
CA_CERT_BASE64Base64-encoded CA certificate (PEM format). Takes precedence over file pathNo-
CA_CERT_PATHPath to custom CA certificate (PEM format)No-
EXECUTION_ENVIRONMENTRuntime environment typeNoDocker
DEPLOYMENT_STAGEDeployment stageNo-
SERVICE_HOSTService host identifierNo-
DIAGNOSTICS_ENABLEDWrite the diagnostics bundle (false switches it off)Notrue
DIAGNOSTICS_DIRECTORYDirectory of the diagnostics bundleNo/var/lib/neckar/diagnostics
DIAGNOSTICS_RETENTIONFiles older than this are deleted, e.g. 7d, 36hNo7d
DIAGNOSTICS_MAX_SIZEUpper bound of the directory; the oldest files are deleted above it, e.g. 1GiB, 500MiBNo1GiB
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector URL. Setting this single variable enables OpenTelemetry export (Ktor server traces + JVM auto-instrumentation).No-
OTEL_EXPORTER_OTLP_PROTOCOLOTLP wire protocol: grpc (port 4317) or http/protobuf (port 4318). Match it to your collector's listener.Nogrpc
OTEL_RESOURCE_ATTRIBUTESComma-separated resource attributes tagged onto every signal, e.g. deployment.environment=production.No-
OTEL_SDK_DISABLEDForce 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.Noendpoint-driven
⁠OpenTelemetry

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.

⁠Diagnostics Bundle

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.

⁠Execution Environment Values
ValueDescription
LocalDevLocal development (IDE, hot reload)
StandaloneJAR running directly on server/VM
DockerRunning inside a Docker container
CIContinuous Integration environment
⁠Deployment Stage Values
ValueDescription
DevelopmentDevelopment environment (unstable, test data)
StagingPre-production environment
ProductionLive production environment

⁠Modes

ModeDescription
DryRunOnly log what would be done (default). No changes to GitLab.
OnlyLabelsOnly update MR labels. No rebase, no merge, no pipeline cancellation.
RebaseOnlyRebase MRs and update labels, but don't cancel pipelines or merge.
NoMergeRebase MRs, update labels, cancel pipelines, but don't merge.
NoPipelineCancellationMerge, rebase, update labels, but don't cancel pipelines.
RunFull 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 changes
  • OnlyLabels - Visualize MR status without affecting CI/CD
  • RebaseOnly - Keep MRs up-to-date without interrupting running pipelines
  • NoMerge - Prepare MRs for merging, but let humans do the final merge
  • NoPipelineCancellation - Fully automated merging but let all pipelines run to completion
  • Run - Fully automated merge workflow with pipeline optimization

⁠API Endpoints

EndpointDescription
GET /api/auto-merge/merge-requestsList categorized merge requests
GET /api/auto-merge/pipelinesPipeline status overview
GET /api/auto-merge/logExecution log
POST /api/auto-merge/events/from-gitlabGitLab webhook endpoint
GET /api/auto-merge/diagnostics/bundle?from=…&to=…Diagnostics bundle of a time range (tar.xz)
GET /api/auto-merge/diagnostics/statusWrite failures and dropped entries of the diagnostics writer

⁠GitLab Webhook Integration

Configure a signed webhook in your GitLab project (GitLab 19.0 or newer):

  1. Generate a signing token: echo "whsec_$(openssl rand -base64 32)"
  2. Pass it to the container as GITLAB_WEBHOOK_SIGNING_TOKEN
  3. Go to Settings → Webhooks
  4. URL: https://your-auto-merge-host/api/auto-merge/events/from-gitlab
  5. Signing token: the same value
  6. Trigger: Merge request events, Pipeline events

A 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.

⁠Labels Applied

The service automatically applies these labels to merge requests:

⁠Status Labels

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.

LabelMeaning
Auto-Merge::Has conflictsHas merge conflicts, needs manual resolution
Auto-Merge::Waiting for approvalNo approval yet — a reviewer must approve it
Auto-Merge::Unresolved discussionsHas open blocking discussions
Auto-Merge::Pipeline failedPipeline failed (or was skipped) and needs attention
Auto-Merge::Blocked by CIGitLab blocks the merge on CI despite a green pipeline
Auto-Merge::Last pipeline failedPipeline running, but the previous pipeline failed
Auto-Merge::Repeated pipeline failureOut of the queue after repeated failures on the same code
Auto-Merge::Waiting for dependencyA 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.

⁠User Labels

Standalone labels without a :: scope, so several can be set on one MR at once:

LabelMeaning
Merge FirstPrioritizes this MR — processed before others
Merge LastOnly merged when no other MRs are in the queue
Keep PipelinePipeline will not be cancelled by Auto Merge
Skip Auto-MergeIgnored 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.

⁠Pipeline Cancellation Scope

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 sourceCancelled by Auto Merge?
merge_request_eventyes — the only source Auto Merge ever replaces
apino
triggerno
scheduleno
webno
pushno
any other sourceno

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 MR Handling

Draft merge requests receive special handling to save CI resources:

  1. Pipeline Cancellation - Running pipelines for draft MRs are automatically cancelled
  2. Label Removal - All Auto Merge labels are removed from draft MRs
  3. Exclusion from Queue - Draft MRs are not considered for merging or rebasing

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.

⁠Failed Pipelines

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.

⁠Priority Support

To prioritize an important merge request:

  1. Add the label Merge First to the MR
  2. The MR will be processed before other MRs of the same status
  3. Low-priority MR pipelines may be cancelled when high-priority MRs have running pipelines

This is useful for hotfixes or time-sensitive changes that need to be merged quickly.

⁠Conflict Detection

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:

  • Receive the Auto-Merge::Has conflicts label
  • Have their pipelines cancelled (since they cannot be merged anyway)
  • Require manual conflict resolution

⁠API Endpoints

EndpointMethodDescription
/api/auto-merge/configurationGETService configuration
/api/auto-merge/merge-requestsGETList categorized merge requests
/api/auto-merge/merge-requests/asciiGETMRs as plain text
/api/auto-merge/pipelinesGETPipeline status overview
/api/auto-merge/pipelines/statusGETAggregated pipeline status
/api/auto-merge/pipelines/by-branch/mainGETMain branch pipelines
/api/auto-merge/pipelines/scheduledGETScheduled pipeline list
/api/auto-merge/runsGETAuto-merge run history
/api/auto-merge/runs/latestGETMost recent run details
/api/auto-merge/actionsGETActions timeline
/api/auto-merge/runnersGETRunner and job status
/api/auto-merge/logGETExecution log messages
/api/auto-merge/log/actionsGETExecuted actions log
/api/auto-merge/events/historyGETReceived GitLab events
/api/auto-merge/events/from-gitlabPOSTGitLab webhook endpoint

⁠License

Proprietary - Neckar IT GmbH


⁠Changelog — GitLab Auto-Merge

Format: Keep a Changelog 1.1.0⁠.

⁠[Unreleased]

⁠Changed
  • Signed webhooks only: 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.

Tag summary

Content type

Image

Digest

sha256:24c59a9bf…

Size

330.3 MB

Last updated

5 days ago

docker pull neckarit/gitlab-auto-merge