@ddtcorex/dsh-maestro-review 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/git-worktree-tool.js +1 -1
- package/lib/git-worktree-tool.js.map +1 -1
- package/lib/govard-audit-lint-tool.d.ts.map +1 -1
- package/lib/govard-audit-lint-tool.js +29 -5
- package/lib/govard-audit-lint-tool.js.map +1 -1
- package/lib/notify.d.ts +9 -0
- package/lib/notify.d.ts.map +1 -1
- package/lib/notify.js +50 -3
- package/lib/notify.js.map +1 -1
- package/lib/orchestrator.d.ts +28 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +194 -21
- package/lib/orchestrator.js.map +1 -1
- package/lib/providers/gitlab.d.ts +1 -1
- package/lib/providers/gitlab.d.ts.map +1 -1
- package/lib/providers/gitlab.js +24 -28
- package/lib/providers/gitlab.js.map +1 -1
- package/lib/skills-tool.d.ts +2 -2
- package/lib/skills-tool.d.ts.map +1 -1
- package/lib/skills-tool.js +17 -2
- package/lib/skills-tool.js.map +1 -1
- package/package.json +9 -9
- package/src/host/git-worktree-tool.ts +1 -1
- package/src/host/govard-audit-lint-tool.ts +24 -4
- package/src/host/notify.ts +55 -3
- package/src/host/orchestrator.ts +200 -23
- package/src/host/providers/gitlab.ts +26 -74
- package/src/host/skills-tool.ts +18 -3
- package/lib/gitlab-webhook.d.ts +0 -17
- package/lib/gitlab-webhook.d.ts.map +0 -1
- package/lib/gitlab-webhook.js +0 -144
- package/lib/gitlab-webhook.js.map +0 -1
- package/src/host/gitlab-webhook.ts +0 -166
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis'
|
|
3
3
|
import z from '@deepseek-ai/schemastery'
|
|
4
4
|
import { loadUserConfig } from '../config-store.js'
|
|
@@ -8,15 +8,13 @@ import { routeGitlabReviewRequest } from '../review-intake.js'
|
|
|
8
8
|
import type { ReviewProvider, ReviewRequest } from './interface.js'
|
|
9
9
|
|
|
10
10
|
export const name = 'maestro-review-webhook'
|
|
11
|
+
export const inject = ['webServer'] as const
|
|
11
12
|
|
|
12
13
|
export interface Config {
|
|
13
|
-
port: number
|
|
14
14
|
botUsername?: string
|
|
15
15
|
secret?: string
|
|
16
16
|
}
|
|
17
|
-
|
|
18
17
|
export const Config: z<Config> = z.object({
|
|
19
|
-
port: z.natural().max(65535).required(),
|
|
20
18
|
botUsername: z.string(),
|
|
21
19
|
secret: z.string().role('secret'),
|
|
22
20
|
})
|
|
@@ -108,83 +106,37 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
108
106
|
const userConfig = await loadUserConfig()
|
|
109
107
|
const expected = userConfig.webhookSecret ?? config.secret
|
|
110
108
|
if (expected === undefined) {
|
|
111
|
-
if (!warnedUnconfigured) {
|
|
112
|
-
warnedUnconfigured = true
|
|
113
|
-
console.error('maestro-review-webhook: no webhook secret configured — set one in Maestro Settings or MAESTRO_GITLAB_WEBHOOK_SECRET; rejecting all requests until then')
|
|
114
|
-
}
|
|
109
|
+
if (!warnedUnconfigured) { warnedUnconfigured = true; console.error('maestro-review-webhook: no webhook secret configured — set one in Maestro Settings or MAESTRO_GITLAB_WEBHOOK_SECRET; rejecting all requests until then') }
|
|
115
110
|
return false
|
|
116
111
|
}
|
|
117
112
|
return secretsMatch(headerValue, expected)
|
|
118
113
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
res.writeHead(404).end()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
res.writeHead(413).end()
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
if (req.url === '/hooks/gitlab-mr/trigger') {
|
|
136
|
-
let parsed: unknown
|
|
137
|
-
try {
|
|
138
|
-
parsed = JSON.parse(raw)
|
|
139
|
-
} catch {
|
|
140
|
-
res.writeHead(400).end()
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
if (!isValidMrOpenedPayload(parsed)) {
|
|
144
|
-
res.writeHead(400).end()
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
const request: OrchestratorReviewRequest = {
|
|
148
|
-
...parsed,
|
|
149
|
-
trigger: 'mention',
|
|
150
|
-
mode: 'quick',
|
|
151
|
-
scope: { kind: 'mr' },
|
|
152
|
-
}
|
|
153
|
-
ctx.emit('maestro/review-request', request)
|
|
154
|
-
res.writeHead(200).end()
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (req.url !== '/hooks/gitlab-mr') {
|
|
159
|
-
res.writeHead(404).end()
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
let body: GitlabMrWebhookBody
|
|
163
|
-
try {
|
|
164
|
-
body = JSON.parse(raw)
|
|
165
|
-
} catch {
|
|
166
|
-
res.writeHead(400).end()
|
|
167
|
-
return
|
|
114
|
+
function makeHandler(expectedPath: string) {
|
|
115
|
+
return (req: IncomingMessage, res: ServerResponse) => {
|
|
116
|
+
if (req.method !== 'POST') { res.writeHead(404).end(); return }
|
|
117
|
+
if (req.url !== expectedPath) { res.writeHead(404).end(); return }
|
|
118
|
+
void requestAuthorized(req.headers['x-gitlab-token']).then(async (authorized) => {
|
|
119
|
+
if (!authorized) { res.writeHead(401).end(); return }
|
|
120
|
+
const raw = await readBody(req)
|
|
121
|
+
if (raw === undefined) { res.writeHead(413).end(); return }
|
|
122
|
+
if (expectedPath === '/hooks/gitlab-mr/trigger') {
|
|
123
|
+
let parsed: unknown; try { parsed = JSON.parse(raw) } catch { res.writeHead(400).end(); return }
|
|
124
|
+
if (!isValidMrOpenedPayload(parsed)) { res.writeHead(400).end(); return }
|
|
125
|
+
const request: OrchestratorReviewRequest = { ...(parsed as any), trigger: 'mention', mode: 'quick', scope: { kind: 'mr' } }
|
|
126
|
+
ctx.emit('maestro/review-request', request); res.writeHead(200).end(); return
|
|
168
127
|
}
|
|
128
|
+
let body: GitlabMrWebhookBody; try { body = JSON.parse(raw) } catch { res.writeHead(400).end(); return }
|
|
169
129
|
const userConfig = await loadUserConfig()
|
|
170
|
-
if ((body.object_kind === 'merge_request' || body.object_kind === 'note') && !hasValidGitlabMrIdentity(body)) {
|
|
171
|
-
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
const request = routeGitlabReviewRequest(
|
|
175
|
-
body,
|
|
176
|
-
userConfig.botUsername ?? config.botUsername ?? 'maestro',
|
|
177
|
-
{ pushEnabled: userConfig.autoRereviewOnPush === true },
|
|
178
|
-
)
|
|
130
|
+
if ((body.object_kind === 'merge_request' || body.object_kind === 'note') && !hasValidGitlabMrIdentity(body)) { res.writeHead(400).end(); return }
|
|
131
|
+
const request = routeGitlabReviewRequest(body, userConfig.botUsername ?? config.botUsername ?? 'maestro', { pushEnabled: userConfig.autoRereviewOnPush === true })
|
|
179
132
|
if (request !== undefined) ctx.emit('maestro/review-request', request)
|
|
180
133
|
res.writeHead(200).end()
|
|
181
134
|
})
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
ctx.
|
|
188
|
-
|
|
189
|
-
}, 'gitlab-webhook teardown')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const h1 = makeHandler('/hooks/gitlab-mr')
|
|
138
|
+
const h2 = makeHandler('/hooks/gitlab-mr/trigger')
|
|
139
|
+
const dispose1 = (ctx as any).webServer.register({ kind: 'exact', path: '/hooks/gitlab-mr', handler: h1 })
|
|
140
|
+
const dispose2 = (ctx as any).webServer.register({ kind: 'exact', path: '/hooks/gitlab-mr/trigger', handler: h2 })
|
|
141
|
+
ctx.effect(() => () => { dispose1(); dispose2() }, 'gitlab-webhook teardown')
|
|
190
142
|
}
|
package/src/host/skills-tool.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {} from '@deepseek-ai/dsh-agent'
|
|
|
5
5
|
export const name = 'maestro-skills-tool'
|
|
6
6
|
export const inject = ['skills', 'tools']
|
|
7
7
|
|
|
8
|
-
export type ReviewSkillProfile = 'magento2' | 'generic'
|
|
8
|
+
export type ReviewSkillProfile = 'magento2' | 'laravel' | 'symfony' | 'wordpress' | 'generic'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Exact skill names rather than a fuzzy search: a review must not silently
|
|
@@ -25,11 +25,26 @@ export const REVIEW_PROFILE_SKILLS: Record<ReviewSkillProfile, readonly string[]
|
|
|
25
25
|
'magento2-security-scan',
|
|
26
26
|
'magento2-performance-audit',
|
|
27
27
|
],
|
|
28
|
+
laravel: [
|
|
29
|
+
'govard-toolbox',
|
|
30
|
+
'govard-laravel',
|
|
31
|
+
'php-dev-core',
|
|
32
|
+
],
|
|
33
|
+
symfony: [
|
|
34
|
+
'govard-toolbox',
|
|
35
|
+
'govard-symfony',
|
|
36
|
+
'php-dev-core',
|
|
37
|
+
],
|
|
38
|
+
wordpress: [
|
|
39
|
+
'govard-toolbox',
|
|
40
|
+
'govard-wordpress',
|
|
41
|
+
'php-dev-core',
|
|
42
|
+
],
|
|
28
43
|
/** Diff review against general best practices; no project skill set required. */
|
|
29
44
|
generic: [],
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
export const MAESTRO_SKILLS_INSTALL_COMMAND = 'curl -fsSL https://raw.githubusercontent.com/ddtcorex/maestro-skills/master/install.sh | bash -s -- --scope personal --target dsh --skills govard-toolbox,govard-magento,magento2-dev-core,magento2-frontend-dev,magento2-hyva-dev,magento2-code-review,magento2-linter,magento2-security-scan,magento2-performance-audit -y'
|
|
47
|
+
export const MAESTRO_SKILLS_INSTALL_COMMAND = 'curl -fsSL https://raw.githubusercontent.com/ddtcorex/maestro-skills/master/install.sh | bash -s -- --scope personal --target dsh --skills govard-toolbox,govard-magento,govard-laravel,govard-symfony,govard-wordpress,php-dev-core,magento2-dev-core,magento2-frontend-dev,magento2-hyva-dev,magento2-code-review,magento2-linter,magento2-security-scan,magento2-performance-audit -y'
|
|
33
48
|
|
|
34
49
|
// This is deliberately process-local and keyed by the reviewer Agent object,
|
|
35
50
|
// which every Cordis child context inherits. Preset mounting inserts child
|
|
@@ -139,7 +154,7 @@ export function apply(ctx: Context): void {
|
|
|
139
154
|
name: 'maestro_load_review_profile',
|
|
140
155
|
description: 'Load the complete, exact skill set required for a configured review profile. Fails if any required skill is unavailable.',
|
|
141
156
|
parameters: {
|
|
142
|
-
profile: { type: 'string', required: true, description: 'Review profile configured for the project. Supported: "magento2" (full Magento skill set), "generic" (no skills required).' },
|
|
157
|
+
profile: { type: 'string', required: true, description: 'Review profile configured for the project. Supported: "magento2" (full Magento skill set), "laravel" (Govard Laravel + PHP), "symfony" (Govard Symfony + PHP), "wordpress" (Govard WordPress + PHP), "generic" (no skills required).' },
|
|
143
158
|
},
|
|
144
159
|
output: {
|
|
145
160
|
schema: {
|
package/lib/gitlab-webhook.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import z from '@deepseek-ai/schemastery';
|
|
3
|
-
export declare const name = "maestro-gitlab-webhook";
|
|
4
|
-
export interface Config {
|
|
5
|
-
port: number;
|
|
6
|
-
/** Username of the GitLab service account used for reviewer assignments. */
|
|
7
|
-
botUsername?: string;
|
|
8
|
-
/**
|
|
9
|
-
* Fallback when Maestro Settings has no webhook secret; optional so a
|
|
10
|
-
* deployment that configures everything through Settings boots without env
|
|
11
|
-
* vars. With neither source set, every request is rejected (fail closed).
|
|
12
|
-
*/
|
|
13
|
-
secret?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare const Config: z<Config>;
|
|
16
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
17
|
-
//# sourceMappingURL=gitlab-webhook.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gitlab-webhook.d.ts","sourceRoot":"","sources":["../src/host/gitlab-webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAMxC,eAAO,MAAM,IAAI,2BAA2B,CAAA;AAE5C,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAI3B,CAAA;AAkDF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAyFxD"}
|
package/lib/gitlab-webhook.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import z from '@deepseek-ai/schemastery';
|
|
3
|
-
import { loadUserConfig } from './config-store.js';
|
|
4
|
-
import { secretsMatch } from './secure-compare.js';
|
|
5
|
-
import { routeGitlabReviewRequest } from './review-intake.js';
|
|
6
|
-
export const name = 'maestro-gitlab-webhook';
|
|
7
|
-
export const Config = z.object({
|
|
8
|
-
port: z.natural().max(65535).required(),
|
|
9
|
-
botUsername: z.string(),
|
|
10
|
-
secret: z.string().role('secret'),
|
|
11
|
-
});
|
|
12
|
-
/** GitLab payloads are small; 5 MB is generous and stops unbounded buffering. */
|
|
13
|
-
const MAX_WEBHOOK_BODY_BYTES = 5 * 1024 * 1024;
|
|
14
|
-
function readBody(req, limit = MAX_WEBHOOK_BODY_BYTES) {
|
|
15
|
-
return new Promise((resolvePromise) => {
|
|
16
|
-
let raw = '';
|
|
17
|
-
let settled = false;
|
|
18
|
-
req.on('data', (chunk) => {
|
|
19
|
-
if (settled)
|
|
20
|
-
return;
|
|
21
|
-
if (raw.length + chunk.length > limit) {
|
|
22
|
-
settled = true;
|
|
23
|
-
req.resume();
|
|
24
|
-
resolvePromise(undefined);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
raw += chunk.toString();
|
|
28
|
-
});
|
|
29
|
-
req.on('end', () => { if (!settled) {
|
|
30
|
-
settled = true;
|
|
31
|
-
resolvePromise(raw);
|
|
32
|
-
} });
|
|
33
|
-
req.on('error', () => { if (!settled) {
|
|
34
|
-
settled = true;
|
|
35
|
-
resolvePromise(undefined);
|
|
36
|
-
} });
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function isValidMrOpenedPayload(value) {
|
|
40
|
-
if (typeof value !== 'object' || value === null)
|
|
41
|
-
return false;
|
|
42
|
-
const v = value;
|
|
43
|
-
return typeof v.projectPath === 'string'
|
|
44
|
-
&& typeof v.projectId === 'number'
|
|
45
|
-
&& typeof v.mrIid === 'number'
|
|
46
|
-
&& typeof v.sourceBranch === 'string';
|
|
47
|
-
}
|
|
48
|
-
function hasValidGitlabMrIdentity(value) {
|
|
49
|
-
if (typeof value !== 'object' || value === null)
|
|
50
|
-
return false;
|
|
51
|
-
const body = value;
|
|
52
|
-
const project = body.project;
|
|
53
|
-
const attributes = body.object_attributes;
|
|
54
|
-
const mergeRequest = body.merge_request;
|
|
55
|
-
const source = body.object_kind === 'note' ? mergeRequest : attributes;
|
|
56
|
-
return typeof project?.id === 'number' && typeof project.path_with_namespace === 'string'
|
|
57
|
-
&& typeof source?.iid === 'number' && typeof source.source_branch === 'string';
|
|
58
|
-
}
|
|
59
|
-
export function apply(ctx, config) {
|
|
60
|
-
// Settings wins over the boot secret, re-read per request so a change in the
|
|
61
|
-
// UI takes effect without a restart. With neither source set, every token
|
|
62
|
-
// fails the comparison (a header string never equals `undefined`), so the
|
|
63
|
-
// server stays closed until a secret is configured.
|
|
64
|
-
let warnedUnconfigured = false;
|
|
65
|
-
async function requestAuthorized(headerValue) {
|
|
66
|
-
const userConfig = await loadUserConfig();
|
|
67
|
-
const expected = userConfig.webhookSecret ?? config.secret;
|
|
68
|
-
if (expected === undefined) {
|
|
69
|
-
if (!warnedUnconfigured) {
|
|
70
|
-
warnedUnconfigured = true;
|
|
71
|
-
console.error('maestro-gitlab-webhook: no webhook secret configured — set one in Maestro Settings or MAESTRO_GITLAB_WEBHOOK_SECRET; rejecting all requests until then');
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
return secretsMatch(headerValue, expected);
|
|
76
|
-
}
|
|
77
|
-
const server = createServer((req, res) => {
|
|
78
|
-
if (req.method !== 'POST') {
|
|
79
|
-
res.writeHead(404).end();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
void requestAuthorized(req.headers['x-gitlab-token']).then(async (authorized) => {
|
|
83
|
-
if (!authorized) {
|
|
84
|
-
res.writeHead(401).end();
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
readBody(req).then(async (raw) => {
|
|
88
|
-
if (raw === undefined) {
|
|
89
|
-
res.writeHead(413).end();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (req.url === '/hooks/gitlab-mr/trigger') {
|
|
93
|
-
let parsed;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(raw);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
res.writeHead(400).end();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (!isValidMrOpenedPayload(parsed)) {
|
|
102
|
-
res.writeHead(400).end();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const request = {
|
|
106
|
-
...parsed,
|
|
107
|
-
trigger: 'mention',
|
|
108
|
-
mode: 'quick',
|
|
109
|
-
scope: { kind: 'mr' },
|
|
110
|
-
};
|
|
111
|
-
ctx.emit('maestro/review-request', request);
|
|
112
|
-
res.writeHead(200).end();
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (req.url !== '/hooks/gitlab-mr') {
|
|
116
|
-
res.writeHead(404).end();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
let body;
|
|
120
|
-
try {
|
|
121
|
-
body = JSON.parse(raw);
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
res.writeHead(400).end();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const userConfig = await loadUserConfig();
|
|
128
|
-
if ((body.object_kind === 'merge_request' || body.object_kind === 'note') && !hasValidGitlabMrIdentity(body)) {
|
|
129
|
-
res.writeHead(400).end();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const request = routeGitlabReviewRequest(body, userConfig.botUsername ?? config.botUsername ?? 'maestro', { pushEnabled: userConfig.autoRereviewOnPush === true });
|
|
133
|
-
if (request !== undefined)
|
|
134
|
-
ctx.emit('maestro/review-request', request);
|
|
135
|
-
res.writeHead(200).end();
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
server.listen(config.port);
|
|
140
|
-
ctx.effect(() => async () => {
|
|
141
|
-
await new Promise((resolve) => { server.close(() => resolve()); });
|
|
142
|
-
}, 'gitlab-webhook teardown');
|
|
143
|
-
}
|
|
144
|
-
//# sourceMappingURL=gitlab-webhook.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gitlab-webhook.js","sourceRoot":"","sources":["../src/host/gitlab-webhook.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage } from 'node:http'
|
|
2
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
-
import z from '@deepseek-ai/schemastery'
|
|
4
|
-
import { loadUserConfig } from './config-store.js'
|
|
5
|
-
import { secretsMatch } from './secure-compare.js'
|
|
6
|
-
import type { MrOpenedPayload, ReviewRequest } from './events.ts'
|
|
7
|
-
import { routeGitlabReviewRequest } from './review-intake.js'
|
|
8
|
-
|
|
9
|
-
export const name = 'maestro-gitlab-webhook'
|
|
10
|
-
|
|
11
|
-
export interface Config {
|
|
12
|
-
port: number
|
|
13
|
-
/** Username of the GitLab service account used for reviewer assignments. */
|
|
14
|
-
botUsername?: string
|
|
15
|
-
/**
|
|
16
|
-
* Fallback when Maestro Settings has no webhook secret; optional so a
|
|
17
|
-
* deployment that configures everything through Settings boots without env
|
|
18
|
-
* vars. With neither source set, every request is rejected (fail closed).
|
|
19
|
-
*/
|
|
20
|
-
secret?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const Config: z<Config> = z.object({
|
|
24
|
-
port: z.natural().max(65535).required(),
|
|
25
|
-
botUsername: z.string(),
|
|
26
|
-
secret: z.string().role('secret'),
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
interface GitlabMrWebhookBody {
|
|
30
|
-
object_kind: string
|
|
31
|
-
project: { id: number; path_with_namespace: string }
|
|
32
|
-
object_attributes: { iid: number; action: string; source_branch: string }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** GitLab payloads are small; 5 MB is generous and stops unbounded buffering. */
|
|
36
|
-
const MAX_WEBHOOK_BODY_BYTES = 5 * 1024 * 1024
|
|
37
|
-
|
|
38
|
-
function readBody(req: IncomingMessage, limit = MAX_WEBHOOK_BODY_BYTES): Promise<string | undefined> {
|
|
39
|
-
return new Promise((resolvePromise) => {
|
|
40
|
-
let raw = ''
|
|
41
|
-
let settled = false
|
|
42
|
-
req.on('data', (chunk: Buffer) => {
|
|
43
|
-
if (settled) return
|
|
44
|
-
if (raw.length + chunk.length > limit) {
|
|
45
|
-
settled = true
|
|
46
|
-
req.resume()
|
|
47
|
-
resolvePromise(undefined)
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
raw += chunk.toString()
|
|
51
|
-
})
|
|
52
|
-
req.on('end', () => { if (!settled) { settled = true; resolvePromise(raw) } })
|
|
53
|
-
req.on('error', () => { if (!settled) { settled = true; resolvePromise(undefined) } })
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isValidMrOpenedPayload(value: unknown): value is MrOpenedPayload {
|
|
58
|
-
if (typeof value !== 'object' || value === null) return false
|
|
59
|
-
const v = value as Record<string, unknown>
|
|
60
|
-
return typeof v.projectPath === 'string'
|
|
61
|
-
&& typeof v.projectId === 'number'
|
|
62
|
-
&& typeof v.mrIid === 'number'
|
|
63
|
-
&& typeof v.sourceBranch === 'string'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function hasValidGitlabMrIdentity(value: unknown): boolean {
|
|
67
|
-
if (typeof value !== 'object' || value === null) return false
|
|
68
|
-
const body = value as Record<string, unknown>
|
|
69
|
-
const project = body.project as Record<string, unknown> | undefined
|
|
70
|
-
const attributes = body.object_attributes as Record<string, unknown> | undefined
|
|
71
|
-
const mergeRequest = body.merge_request as Record<string, unknown> | undefined
|
|
72
|
-
const source = body.object_kind === 'note' ? mergeRequest : attributes
|
|
73
|
-
return typeof project?.id === 'number' && typeof project.path_with_namespace === 'string'
|
|
74
|
-
&& typeof source?.iid === 'number' && typeof source.source_branch === 'string'
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function apply(ctx: Context, config: Config): void {
|
|
78
|
-
// Settings wins over the boot secret, re-read per request so a change in the
|
|
79
|
-
// UI takes effect without a restart. With neither source set, every token
|
|
80
|
-
// fails the comparison (a header string never equals `undefined`), so the
|
|
81
|
-
// server stays closed until a secret is configured.
|
|
82
|
-
let warnedUnconfigured = false
|
|
83
|
-
async function requestAuthorized(headerValue: string | string[] | undefined): Promise<boolean> {
|
|
84
|
-
const userConfig = await loadUserConfig()
|
|
85
|
-
const expected = userConfig.webhookSecret ?? config.secret
|
|
86
|
-
if (expected === undefined) {
|
|
87
|
-
if (!warnedUnconfigured) {
|
|
88
|
-
warnedUnconfigured = true
|
|
89
|
-
console.error('maestro-gitlab-webhook: no webhook secret configured — set one in Maestro Settings or MAESTRO_GITLAB_WEBHOOK_SECRET; rejecting all requests until then')
|
|
90
|
-
}
|
|
91
|
-
return false
|
|
92
|
-
}
|
|
93
|
-
return secretsMatch(headerValue, expected)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const server = createServer((req, res) => {
|
|
97
|
-
if (req.method !== 'POST') {
|
|
98
|
-
res.writeHead(404).end()
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
void requestAuthorized(req.headers['x-gitlab-token']).then(async (authorized) => {
|
|
102
|
-
if (!authorized) {
|
|
103
|
-
res.writeHead(401).end()
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
readBody(req).then(async (raw) => {
|
|
107
|
-
if (raw === undefined) {
|
|
108
|
-
res.writeHead(413).end()
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
if (req.url === '/hooks/gitlab-mr/trigger') {
|
|
112
|
-
let parsed: unknown
|
|
113
|
-
try {
|
|
114
|
-
parsed = JSON.parse(raw)
|
|
115
|
-
} catch {
|
|
116
|
-
res.writeHead(400).end()
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
if (!isValidMrOpenedPayload(parsed)) {
|
|
120
|
-
res.writeHead(400).end()
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
const request: ReviewRequest = {
|
|
124
|
-
...parsed,
|
|
125
|
-
trigger: 'mention',
|
|
126
|
-
mode: 'quick',
|
|
127
|
-
scope: { kind: 'mr' },
|
|
128
|
-
}
|
|
129
|
-
ctx.emit('maestro/review-request', request)
|
|
130
|
-
res.writeHead(200).end()
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (req.url !== '/hooks/gitlab-mr') {
|
|
135
|
-
res.writeHead(404).end()
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
let body: GitlabMrWebhookBody
|
|
139
|
-
try {
|
|
140
|
-
body = JSON.parse(raw)
|
|
141
|
-
} catch {
|
|
142
|
-
res.writeHead(400).end()
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
const userConfig = await loadUserConfig()
|
|
146
|
-
if ((body.object_kind === 'merge_request' || body.object_kind === 'note') && !hasValidGitlabMrIdentity(body)) {
|
|
147
|
-
res.writeHead(400).end()
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
const request = routeGitlabReviewRequest(
|
|
151
|
-
body,
|
|
152
|
-
userConfig.botUsername ?? config.botUsername ?? 'maestro',
|
|
153
|
-
{ pushEnabled: userConfig.autoRereviewOnPush === true },
|
|
154
|
-
)
|
|
155
|
-
if (request !== undefined) ctx.emit('maestro/review-request', request)
|
|
156
|
-
res.writeHead(200).end()
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
server.listen(config.port)
|
|
162
|
-
|
|
163
|
-
ctx.effect(() => async () => {
|
|
164
|
-
await new Promise<void>((resolve) => { server.close(() => resolve()) })
|
|
165
|
-
}, 'gitlab-webhook teardown')
|
|
166
|
-
}
|