Skip to content

Commit 9452d21

Browse files
sudaclaude
andcommitted
feat(integrations): add GitLab as alternative SCM provider (mongrel-intelligence#1087)
Add GitLab as a second SCM integration alongside GitHub, following the existing IntegrationModule/SCMIntegration architecture. This enables CASCADE to process GitLab merge request webhooks and run agents against GitLab repositories. Key additions: - Core GitLab module (client via @gitbeaker/rest, dual-persona model, SCMIntegration implementation) - Router layer (webhook route, signature verification, adapter, queue types) - 8 trigger handlers (MR opened, pipeline success/failure, approval, comment mention, merged, conflict detected, ready to merge) - 10 GitLab gadgets for agent MR operations - Frontend SCM tab with GitHub/GitLab provider selector - CLI webhook commands with --gitlab-only support - Worker entry GitLab job dispatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d643527 commit 9452d21

80 files changed

Lines changed: 4102 additions & 41 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"license": "MIT",
5656
"dependencies": {
5757
"@anthropic-ai/claude-agent-sdk": "^0.2.91",
58+
"@gitbeaker/rest": "^43.8.0",
5859
"@hono/node-server": "^1.13.7",
5960
"@hono/trpc-server": "^0.4.2",
6061
"@llmist/cli": "^16.0.3",

src/agents/definitions/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CAPABILITIES } from '../capabilities/registry.js';
99
export const IntegrationCategorySchema = z.enum(['pm', 'scm', 'alerting']);
1010

1111
// Known providers for validation
12-
export const KnownProviderSchema = z.enum(['trello', 'jira', 'github', 'sentry']);
12+
export const KnownProviderSchema = z.enum(['trello', 'jira', 'github', 'gitlab', 'sentry']);
1313

1414
// Trigger event format validation: {category}:{event-name}
1515
// Categories: pm, scm (integration-bound), alerting (monitoring), internal (orchestration chaining)

src/api/routers/_shared/triggerTypes.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,63 +168,63 @@ export const TRIGGER_REGISTRY: Record<TriggerCategory, KnownTriggerEvent[]> = {
168168
label: 'CI Passed',
169169
description: 'CI check suite passed',
170170
contextPipeline: ['prContext'],
171-
providers: ['github'],
171+
providers: ['github', 'gitlab'],
172172
},
173173
{
174174
event: 'scm:check-suite-failure',
175175
label: 'CI Failed',
176176
description: 'CI check suite failed',
177177
contextPipeline: ['prContext'],
178-
providers: ['github'],
178+
providers: ['github', 'gitlab'],
179179
},
180180
{
181181
event: 'scm:pr-review-submitted',
182182
label: 'PR Review Submitted',
183183
description: 'Review submitted on PR',
184184
contextPipeline: ['prContext', 'prConversation'],
185-
providers: ['github'],
185+
providers: ['github', 'gitlab'],
186186
},
187187
{
188188
event: 'scm:review-requested',
189189
label: 'Review Requested',
190190
description: 'Review requested on PR',
191191
contextPipeline: ['prContext'],
192-
providers: ['github'],
192+
providers: ['github', 'gitlab'],
193193
},
194194
{
195195
event: 'scm:pr-opened',
196196
label: 'PR Opened',
197197
description: 'PR opened',
198198
contextPipeline: ['prContext'],
199-
providers: ['github'],
199+
providers: ['github', 'gitlab'],
200200
},
201201
{
202202
event: 'scm:pr-comment-mention',
203203
label: 'PR Comment Mention',
204204
description: 'Bot @mentioned in PR comment',
205205
contextPipeline: ['prContext', 'prConversation'],
206-
providers: ['github'],
206+
providers: ['github', 'gitlab'],
207207
},
208208
{
209209
event: 'scm:pr-merged',
210210
label: 'PR Merged',
211211
description: 'PR merged to base branch',
212212
contextPipeline: ['prContext'],
213-
providers: ['github'],
213+
providers: ['github', 'gitlab'],
214214
},
215215
{
216216
event: 'scm:pr-ready-to-merge',
217217
label: 'PR Ready to Merge',
218218
description: 'PR approved and CI passed',
219219
contextPipeline: ['prContext'],
220-
providers: ['github'],
220+
providers: ['github', 'gitlab'],
221221
},
222222
{
223223
event: 'scm:pr-conflict-detected',
224224
label: 'PR Conflict Detected',
225225
description: 'PR has merge conflicts with the base branch',
226226
contextPipeline: ['prContext'],
227-
providers: ['github'],
227+
providers: ['github', 'gitlab'],
228228
},
229229
],
230230
internal: [

src/api/routers/webhooks.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import {
1515
import { trelloCreateWebhook, trelloDeleteWebhook, trelloListWebhooks } from './webhooks/trello.js';
1616
import type {
1717
GitHubWebhook,
18+
GitLabWebhookInfo,
1819
JiraWebhookInfo,
1920
SentryWebhookInfo,
2021
TrelloWebhook,
2122
} from './webhooks/types.js';
2223

23-
export type { GitHubWebhook, JiraWebhookInfo, SentryWebhookInfo, TrelloWebhook };
24+
export type { GitHubWebhook, GitLabWebhookInfo, JiraWebhookInfo, SentryWebhookInfo, TrelloWebhook };
2425

2526
export const webhooksRouter = router({
2627
list: adminProcedure
@@ -52,9 +53,16 @@ export const webhooksRouter = router({
5253
};
5354
}
5455

56+
// GitLab — informational only (webhooks must be configured in GitLab UI)
57+
let gitlab: GitLabWebhookInfo[] | null = null;
58+
if (pctx.scmProvider === 'gitlab') {
59+
gitlab = [];
60+
}
61+
5562
return {
5663
trello: trelloResult.status === 'fulfilled' ? trelloResult.value : [],
5764
github: githubResult.status === 'fulfilled' ? githubResult.value : [],
65+
gitlab,
5866
jira: jiraResult.status === 'fulfilled' ? jiraResult.value : [],
5967
sentry,
6068
errors: {
@@ -72,6 +80,7 @@ export const webhooksRouter = router({
7280
callbackBaseUrl: z.string().url(),
7381
trelloOnly: z.boolean().optional(),
7482
githubOnly: z.boolean().optional(),
83+
gitlabOnly: z.boolean().optional(),
7584
jiraOnly: z.boolean().optional(),
7685
oneTimeTokens: oneTimeTokensSchema,
7786
}),
@@ -83,6 +92,7 @@ export const webhooksRouter = router({
8392
const results: {
8493
trello?: TrelloWebhook | string;
8594
github?: GitHubWebhook | string;
95+
gitlab?: { url: string; webhookSecretSet: boolean } | string;
8696
jira?: JiraWebhookInfo | string;
8797
sentry?: SentryWebhookInfo;
8898
labelsEnsured?: string[];
@@ -134,8 +144,22 @@ export const webhooksRouter = router({
134144
results.labelsEnsured = await jiraEnsureLabels(pctx);
135145
}
136146

147+
// GitLab — display-only (webhooks must be configured in GitLab UI)
148+
if (
149+
!input.trelloOnly &&
150+
!input.githubOnly &&
151+
!input.jiraOnly &&
152+
pctx.scmProvider === 'gitlab' &&
153+
pctx.gitlabToken
154+
) {
155+
results.gitlab = {
156+
url: `${baseUrl}/gitlab/webhook`,
157+
webhookSecretSet: !!pctx.gitlabWebhookSecret,
158+
};
159+
}
160+
137161
// GitHub webhook
138-
if (!input.trelloOnly && !input.jiraOnly && pctx.githubToken) {
162+
if (!input.trelloOnly && !input.gitlabOnly && !input.jiraOnly && pctx.githubToken) {
139163
const githubCallbackUrl = `${baseUrl}/github/webhook`;
140164
const existing = await githubListWebhooks(pctx);
141165
const duplicate = existing.find(
@@ -168,6 +192,7 @@ export const webhooksRouter = router({
168192
callbackBaseUrl: z.string().url(),
169193
trelloOnly: z.boolean().optional(),
170194
githubOnly: z.boolean().optional(),
195+
gitlabOnly: z.boolean().optional(),
171196
jiraOnly: z.boolean().optional(),
172197
oneTimeTokens: oneTimeTokensSchema,
173198
}),
@@ -176,9 +201,10 @@ export const webhooksRouter = router({
176201
const pctx = await resolveProjectContext(input.projectId, ctx.effectiveOrgId);
177202
applyOneTimeTokens(pctx, input.oneTimeTokens);
178203
const baseUrl = input.callbackBaseUrl.replace(/\/$/, '');
179-
const deleted: { trello: string[]; github: number[]; jira: number[] } = {
204+
const deleted: { trello: string[]; github: number[]; gitlab: number[]; jira: number[] } = {
180205
trello: [],
181206
github: [],
207+
gitlab: [],
182208
jira: [],
183209
};
184210

src/api/routers/webhooks/context.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,27 @@ export async function resolveProjectContext(
3838
const alertingIntegration = await getIntegrationByProjectAndCategory(projectId, 'alerting');
3939
const sentryConfigured = alertingIntegration?.provider === 'sentry' && !!creds.SENTRY_API_TOKEN;
4040

41+
// Determine SCM provider from integration config
42+
const scmIntegration = await getIntegrationByProjectAndCategory(projectId, 'scm');
43+
const scmProvider = (scmIntegration?.provider === 'gitlab' ? 'gitlab' : 'github') as
44+
| 'github'
45+
| 'gitlab';
46+
4147
return {
4248
projectId,
4349
orgId: project.orgId,
4450
repo: project.repo,
4551
pmType: project.pm?.type ?? 'trello',
52+
scmProvider,
4653
boardId: trelloConfig?.boardId,
4754
jiraBaseUrl: jiraConfig?.baseUrl,
4855
jiraProjectKey: jiraConfig?.projectKey,
4956
jiraLabels,
5057
trelloApiKey: creds.TRELLO_API_KEY ?? '',
5158
trelloToken: creds.TRELLO_TOKEN ?? '',
5259
githubToken: creds.GITHUB_TOKEN_IMPLEMENTER ?? '',
60+
gitlabToken: creds.GITLAB_TOKEN_IMPLEMENTER ?? '',
61+
gitlabWebhookSecret: creds.GITLAB_WEBHOOK_SECRET ?? undefined,
5362
jiraEmail: creds.JIRA_EMAIL ?? '',
5463
jiraApiToken: creds.JIRA_API_TOKEN ?? '',
5564
webhookSecret: creds.GITHUB_WEBHOOK_SECRET ?? undefined,

src/api/routers/webhooks/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export interface JiraWebhookInfo {
2626
enabled: boolean;
2727
}
2828

29+
export interface GitLabWebhookInfo {
30+
id: number;
31+
url: string;
32+
enableSslVerification: boolean;
33+
}
34+
2935
export interface SentryWebhookInfo {
3036
url: string;
3137
webhookSecretSet: boolean;
@@ -37,13 +43,16 @@ export interface ProjectContext {
3743
orgId: string;
3844
repo?: string;
3945
pmType: 'trello' | 'jira';
46+
scmProvider: 'github' | 'gitlab';
4047
boardId?: string;
4148
jiraBaseUrl?: string;
4249
jiraProjectKey?: string;
4350
jiraLabels?: string[];
4451
trelloApiKey: string;
4552
trelloToken: string;
4653
githubToken: string;
54+
gitlabToken: string;
55+
gitlabWebhookSecret?: string;
4756
jiraEmail?: string;
4857
jiraApiToken?: string;
4958
webhookSecret?: string;

src/cli/dashboard/webhooks/create.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class WebhooksCreate extends DashboardCommand {
1515
}),
1616
'trello-only': Flags.boolean({ description: 'Only create Trello webhook', default: false }),
1717
'github-only': Flags.boolean({ description: 'Only create GitHub webhook', default: false }),
18+
'gitlab-only': Flags.boolean({ description: 'Only create GitLab webhook', default: false }),
1819
'github-token': Flags.string({
1920
description: 'One-time GitHub PAT with admin:repo_hook scope',
2021
}),
@@ -44,6 +45,7 @@ export default class WebhooksCreate extends DashboardCommand {
4445
callbackBaseUrl,
4546
trelloOnly: flags['trello-only'],
4647
githubOnly: flags['github-only'],
48+
gitlabOnly: flags['gitlab-only'],
4749
oneTimeTokens: Object.keys(oneTimeTokens).length > 0 ? oneTimeTokens : undefined,
4850
}),
4951
);
@@ -71,6 +73,23 @@ export default class WebhooksCreate extends DashboardCommand {
7173
}
7274
}
7375

76+
if (result.gitlab) {
77+
if (typeof result.gitlab === 'string') {
78+
this.log(`GitLab: ${result.gitlab}`);
79+
} else {
80+
this.log('');
81+
this.log('GitLab (manual setup required):');
82+
this.log(` Webhook URL: ${result.gitlab.url}`);
83+
this.log(' Steps:');
84+
this.log(' 1. Go to your GitLab project > Settings > Webhooks');
85+
this.log(' 2. Set the URL to the webhook URL above');
86+
this.log(' 3. Enable triggers: Push events, Merge request events, Pipeline events');
87+
if (result.gitlab.webhookSecretSet) {
88+
this.log(' 4. Set the Secret Token to your configured GITLAB_WEBHOOK_SECRET');
89+
}
90+
}
91+
}
92+
7493
if (result.jira) {
7594
if (typeof result.jira === 'string') {
7695
this.log(`JIRA: ${result.jira}`);

0 commit comments

Comments
 (0)