Bridges GitLab merge request and note hooks to Hermes runs.
1.0K
Bridges GitLab merge request and note webhooks that address the bot to Hermes runs, one thread per merge request or issue, and answers in the merge request or issue: an award, one status note edited in place and a reply in each discussion that asked. Talks directly to a Hermes API server — any deployment that exposes /v1/runs.
docker run --rm -p 8080:8080 cenk1cenk2/hermes-bridge-gitlab:latest
| Tag | Source |
|---|---|
latest | The latest release. |
v<version> | The release @kilic.dev/hermes-bridge-gitlab@<version> created by semantic-release. |
Durations are milliseconds unless noted.
| Variable | Default | Description |
|---|---|---|
PORT | 8080 | Port the HTTP server binds to. |
AGENT_NAME | Hermes | Name the agent goes by in its status notes and replies, e.g. labrat. |
LOG_LEVEL | info | Lowest level logged: error, warn, info, debug or verbose. |
LOG_FORMAT | json | json for one JSON object per line, text for readable local lines. |
REDIS_URL | required | Redis or Valkey holding every queue and all shared state. |
QUEUE_PREFIX | hermes-bridge-gitlab | Prefix of every Redis key and queue. |
QUEUE_RETENTION_COMPLETED | 3600 | Seconds a completed job is kept. |
QUEUE_RETENTION_FAILED | 86400 | Seconds a failed job is kept. |
QUEUE_TRANSACTION_RETRIES | 10 | Attempts of a Redis transaction that keeps conflicting. |
GITLAB_URL | required | Base URL of the GitLab instance, e.g. https://gitlab.example.com. |
GITLAB_TOKEN | required | Personal access token of the bot (api scope) for awards, notes and merge request reads. |
GITLAB_WEBHOOK_TOKEN | required | Secret token of the project hooks, compared with X-Gitlab-Token. |
GITLAB_BOT_ID | required | GitLab user id of the bot; its own notes and merge request events are dropped, and it is the assignee or reviewer that starts a fix or a review. |
GITLAB_BOT_USERNAME | required | Username a note mentions to address the bot, and @<username> stop stops its thread. |
GITLAB_WEBHOOK_TIMEOUT | 4000 | Time a webhook may spend queueing its event before it answers 503. |
GITLAB_WEBHOOK_TTL | 86400000 | Lifetime of seen deliveries and stops. |
GITLAB_WEBHOOK_INSTRUCTIONS_DEFAULT | none | Instructions of an input whose kind has none of its own. |
GITLAB_WEBHOOK_INSTRUCTIONS_REVIEW | none | Instructions of a review input. |
GITLAB_WEBHOOK_INSTRUCTIONS_FIX | none | Instructions of a fix input. |
GITLAB_WEBHOOK_INSTRUCTIONS_MENTION | none | Instructions of a mention input. |
GITLAB_WEBHOOK_ALLOWED_USERS | Comma-separated GitLab usernames allowed to trigger the bot by a mention, an assignment, a review request or @<username> stop. Anyone else is dropped without an award, a note or a run. Empty allows everyone. | |
GITLAB_CLIENT_TIMEOUT | 10000 | Timeout of one GitLab API call. |
GITLAB_CLIENT_PAGES | 10 | Pages of 100 discussions searched for a note whose hook carried no discussion_id. |
GITLAB_NOTE_RATE_LIMIT_MAX | 1 | Awards, notes and edits posted per rate limit window, across replicas. |
GITLAB_NOTE_RATE_LIMIT_DURATION | 1000 | Length of the rate limit window. |
GITLAB_NOTE_DEBOUNCE | 10000 | Delay that collects progress and the streamed answer into one edit of the status note. |
GITLAB_DISCUSSION_TTL | 86400000 | Lifetime of a status note's state, a thread's progress and its last stop note. |
GITLAB_RECOVERY_AGE | 86400000 | Age of the oldest open status note the boot sweep resumes or closes. |
GITLAB_RECOVERY_GRACE | 60000 | Age a status note needs before the boot sweep touches it. |
HERMES_URL | required | Base URL of the Hermes API server, e.g. https://hermes.example.com/v1. |
HERMES_API_KEY | required | Key sent to the Hermes API server. |
HERMES_INSTRUCTIONS | none | Instructions of a run whose input carries none, that is when neither GITLAB_WEBHOOK_INSTRUCTIONS_<KIND> nor GITLAB_WEBHOOK_INSTRUCTIONS_DEFAULT is set. |
HERMES_EVENTS_IDLE_TIMEOUT | 120000 | Silence on a run event stream before it is dropped and the run is polled. |
DRIVER_SEEN_TTL | 86400000 | Lifetime of a seen idempotency key. |
DRIVER_QUEUE_LOCK_TTL | 30000 | Lifetime of a thread lock. |
DRIVER_QUEUE_LOCK_RENEW_INTERVAL | 10000 | Renewal interval of a held thread lock. |
DRIVER_QUEUE_SWEEP_INTERVAL | 1000 | Interval of the sweep for threads with due work. |
DRIVER_BACKOFF_INITIAL | 5000 | First retry delay while a run create gets 429, a 5xx or no connection. |
DRIVER_BACKOFF_MAX | 60000 | Largest retry delay. |
DRIVER_POLICY_BATCH | true | Join the inputs queued behind a run into one run. |
DRIVER_POLICY_STEER | true | Steer a follow-up into the running run instead of queueing it. |
DRIVER_POLICY_RESUBMIT_MAX | 0 | New runs in the same Hermes session for a run that ends interrupted. |
DRIVER_POLICY_BACKOFF_WINDOW | 600000 | Time Hermes may stay busy or unavailable before the thread fails. |
DRIVER_POLICY_APPROVAL | deny-stop | deny-stop stops a run after denying its approval request, deny-continue lets it go on. |
DRIVER_FOLLOW_LEASE_TTL | 30000 | Lifetime of a run lease; another replica takes the run over once it expires. |
DRIVER_FOLLOW_LEASE_RENEW_INTERVAL | 10000 | Renewal interval of a held run lease. |
DRIVER_FOLLOW_SWEEP_INTERVAL | 5000 | Interval of the sweep for runs nobody follows. |
DRIVER_FOLLOW_POLL_INTERVAL | 5000 | Interval between polls of a run without a stream. |
DRIVER_FOLLOW_RETENTION | 86400000 | Time a finished run's record is kept. |
DRIVER_HEARTBEAT_IDLE | 1200000 | Silence in a thread before it reports idle. |
DRIVER_HEARTBEAT_SWEEP_INTERVAL | 60000 | Interval of the idle sweep. |
| Method | Path | Description |
|---|---|---|
GET | /healthz | Liveness: answers 200 ok. |
GET | /readyz | Readiness: answers 200 ok, and 503 once a shutdown starts draining. |
POST | /v1/hooks/gitlab | GitLab project hooks for merge request and note events: 400 on a wrong X-Gitlab-Token or a payload that fails the schema, 503 when the event could not be queued, else 200. |
| any | any other | Answers 404. |
Every event of the bot itself (GITLAB_BOT_ID) is dropped first, so nothing the bot writes starts a run. A delivery is handled once per Idempotency-Key (or webhook-id) header, and each input is sent once per note or merge request update.
| Event | Condition | Kind |
|---|---|---|
| Note | not create, a system note, not on a merge request or an issue, or no @<bot> in it | dropped |
| Note | exactly @<bot> stop | stop, never sent to Hermes |
| Note | mentions @<bot> | mention |
| Merge request | closed or merged | stop, when the thread has a run |
| Merge request | not open, the bot newly an assignee or a reviewer, or its review re-requested | dropped, with a notice |
| Merge request | not open | dropped |
| Merge request | the bot newly an assignee | fix |
| Merge request | the bot newly a reviewer, or its review re-requested | review |
| Merge request | the bot taken off and neither assignee nor reviewer | stop, when the thread has a run |
Each merge request or issue is one thread, gitlab-<project>-mr-<iid> or gitlab-<project>-issue-<iid>, which is also the Hermes session. An input carries the instructions of its kind, GITLAB_WEBHOOK_INSTRUCTIONS_<KIND> or else GITLAB_WEBHOOK_INSTRUCTIONS_DEFAULT; kinds with different instructions are never steered into each other's runs, and its meta {projectId, noteableType, iid, discussionId, noteId, kind} says where to answer.
Every call to GitLab goes through one BullMQ queue with a global concurrency of 1 and a shared rate limit, so replicas never race each other on a merge request.
eyes award at once, a stop note too.This merge request is <state>, so no <kind> run was started, once per update.<agent> has <state> the session through gitlab bridge, with key <key>, running in harness in session <session id> | run <run id>., where <agent> is AGENT_NAME, <key> is the thread key without its gitlab- prefix and <state> is accepted, turning failed or stopped when the run ends so. The answer, as Hermes streams it, follows that sentence and edits the note in place behind GITLAB_NOTE_DEBOUNCE, since edits notify nobody. A queued run, tool calls, todos and heartbeats stay off the note. A run a replica takes over is polled, so its note jumps from the partial answer to the final one.Done. for a run without one; the status note keeps only its sentence. A failure, a denied command and a stop are replies too, ending in the same sentence with failed (a denied command too) or stopped, and leave the partial answer below the sentence of the status note; a stop is also answered in the discussion of the stop note, and a stop of a run whose input has no discussion opens the status note to reply in. A stop note with nothing running is answered Nothing is running..Fixed in <sha>. replies, pushes and the resolution of review threads stay with the agent.Every line carries a message that is a complete sentence fixed per event, its source class as context, and its ids, numbers and reasons as fields at the root of the JSON object ({"level":"log","message":"Created a run for the thread.","context":"DriverThreadService","run":"...","thread":"...","replayed":false}); text prints the same fields inline. Durations are durationMs; tokens and bodies are never logged.
info: one line per HTTP request (method, path, status, duration, remote address; /healthz and /readyz only at debug), webhook verification or rejection with the Idempotency-Key and X-Gitlab-Event headers, each routed event and each dropped one with its reason (and, for a merge request that is not open, the kind it would have started and its state), queued notices, acknowledgement and thread input, skipped redeliveries, stops, opened status notes, posted replies, notices and awards, run creation, busy backoff, steers, follows and takeovers, finished runs with status and duration, Redis connections, the recovery sweep summary, the drain on shutdown and the runs and threads it hands over.debug: every run stream event, Hermes and GitLab call (method, path, status, duration), queued award, note and edit, status note edit, thread lock and run lease acquire, renew and release, and Redis transaction retry.warn and error: anything retried, dropped or failed, with its error.Run from apps/gitlab, tests/post-fixture.ts posts a fixture from tests/testdata to the webhook URL given as its second argument, with the webhook token, a fresh note id or updated_at and a fresh Idempotency-Key header. It runs on Node 26 as is, since Node strips the types:
GITLAB_WEBHOOK_TOKEN=... node tests/post-fixture.ts tests/testdata/note-merge-request.json https://bridge.example.com/v1/hooks/gitlab
The application lives in apps/gitlab of the workspace and runs on the shared core in packages/core; see the repository README.
pnpm install
pnpm lint
pnpm test
pnpm build
pnpm --filter @kilic.dev/hermes-bridge-gitlab start
Content type
Image
Digest
sha256:01e207697…
Size
74.3 MB
Last updated
7 days ago
docker pull cenk1cenk2/hermes-bridge-gitlab