GitLab adapter for the Vercel Chat SDK. Maps GitLab issues and merge requests to Chat SDK threads using webhooks for inbound events and the GitLab REST API for outbound operations.
Supports both gitlab.com and self-hosted/enterprise GitLab instances.
pnpm add chat-adapter-gitlab chat @chat-adapter/sharedimport { Chat } from "chat";
import { createGitLabAdapter } from "chat-adapter-gitlab";
const adapter = createGitLabAdapter({
token: process.env.GITLAB_TOKEN,
webhookSecret: process.env.GITLAB_WEBHOOK_SECRET,
// For self-hosted instances:
// baseUrl: "https://gitlab.example.com",
});
const chat = new Chat({
adapter,
async onMessage({ message, thread, reply }) {
await reply(`Thanks for your comment, ${message.author.userName}!`);
},
});
// Mount the webhook handler on your HTTP server
// POST /api/webhooks/gitlab → chat.handleWebhook(request)| Variable | Required | Description |
|---|---|---|
GITLAB_TOKEN |
Yes | Access token with api scope |
GITLAB_WEBHOOK_SECRET |
Yes | Webhook verification token |
GITLAB_BASE_URL |
No | Instance URL (default: https://gitlab.com) |
GITLAB_BOT_USERNAME |
No | Bot username (auto-detected if omitted) |
GITLAB_BOT_USER_ID |
No | Bot user ID (auto-detected if omitted) |
The only difference from gitlab.com is the baseUrl. All API paths, authentication, and webhook formats are identical.
const adapter = createGitLabAdapter({
baseUrl: "https://gitlab.example.com",
token: "glpat-...",
webhookSecret: "my-secret",
});In your GitLab project, go to Settings → Webhooks and add:
- URL: Your webhook endpoint (e.g.,
https://your-app.com/api/webhooks/gitlab) - Secret token: Same value as
GITLAB_WEBHOOK_SECRET - Trigger events: Check "Comments" (and optionally "Issues events", "Merge request events")
Inbound comments on issues and merge requests are delivered as Chat SDK messages. The adapter handles both top-level comments and threaded discussions.
async onMessage({ message, reply }) {
// message.threadId encodes the project, type (issue/MR), and IID
await reply("Got it, looking into this now.");
}Line-level comments on merge request diffs are tracked as separate discussion threads. Replies are posted within the correct thread.
// Edit a comment
await chat.editMessage(threadId, messageId, "Updated response");
// Delete a comment
await chat.deleteMessage(threadId, messageId);Add and remove emoji reactions on comments:
await chat.addReaction(threadId, messageId, "thumbsup");
await chat.removeReaction(threadId, messageId, "thumbsup");Supports standard gemojione names: thumbsup, thumbsdown, heart, tada, rocket, eyes, fire, 100, thinking, etc.
Retrieve comment history for an issue or merge request:
const { messages, nextCursor } = await chat.fetchMessages(threadId, { limit: 50 });const { threads } = await chat.listThreads(channelId);Messages are rendered as GitLab-flavored markdown. Cards are converted to formatted markdown with bold titles, field labels, links, and GFM tables.
await reply({ markdown: "**Status:** Deployed to production :rocket:" });Thread IDs encode the project path, type, and IID:
| Type | Format | Example |
|---|---|---|
| Issue | gitlab:{project}:issue:{iid} |
gitlab:my-group/my-project:issue:42 |
| Merge Request | gitlab:{project}:mr:{iid} |
gitlab:my-group/my-project:mr:100 |
| Discussion | gitlab:{project}:mr:{iid}:d:{id} |
gitlab:my-group/my-project:mr:100:d:abc123 |
These methods throw NotImplementedError as they have no GitLab equivalent:
startTyping()— no GitLab equivalentstream()— GitLab rejects rapid editsopenModal()/postEphemeral()/scheduleMessage()— no GitLab equivalent
The adapter automatically skips:
- Comments from the bot's own user ID (resolved on
initialize()) - System-generated notes (assignments, label changes, etc.)
MIT