@zereight/mcp-gitlab 2.1.41 → 2.1.42
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/build/index.js +36 -4
- package/build/schemas.js +24 -7
- package/build/test/draft-note-schema.test.js +44 -0
- package/build/test/utils/bulk-publish-options.test.js +32 -0
- package/build/test/utils/gitlab-version-gate.test.js +74 -0
- package/build/tools/registry.js +1 -1
- package/build/utils/bulk-publish-options.js +21 -0
- package/build/utils/gitlab-version-gate.js +47 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
|
|
|
80
80
|
|
|
81
81
|
예시는 기존 `mcp-gitlab`보다 충돌 가능성이 낮은 `zereight-mcp-gitlab` 별칭을 사용합니다. MCP 클라이언트가 찾지 못하면 `which zereight-mcp-gitlab`의 절대 경로를 사용하세요.
|
|
82
82
|
|
|
83
|
-
전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.
|
|
83
|
+
전역 설치를 쓰지 않으려면 `npx -y @zereight/mcp-gitlab@2.1.41`처럼 직전 안정 버전(문서가 권장하는 버전)으로 고정하세요. 항상 최신 버전을 원하면 `npx -y @zereight/mcp-gitlab@latest`를 사용하세요. 새 버전이 나오면 서버가 시작 시 stderr로 알려줍니다(`GITLAB_DISABLE_VERSION_CHECK=true`로 비활성화 가능).
|
|
84
84
|
|
|
85
85
|
#### CLI 인자 사용하기(환경 변수 문제가 있는 클라이언트용)
|
|
86
86
|
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
|
|
|
80
80
|
|
|
81
81
|
The examples use `zereight-mcp-gitlab`, a less collision-prone alias for the legacy `mcp-gitlab` binary. If your MCP client cannot find it, use the absolute path from `which zereight-mcp-gitlab`.
|
|
82
82
|
|
|
83
|
-
No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.
|
|
83
|
+
No global install? Pin `npx` to the previous stable release (the version these docs recommend), for example `npx -y @zereight/mcp-gitlab@2.1.41`. If you always want the newest release, use `npx -y @zereight/mcp-gitlab@latest` instead. The server prints a notice to stderr on startup when a newer version is available (disable with `GITLAB_DISABLE_VERSION_CHECK=true`).
|
|
84
84
|
|
|
85
85
|
#### Using CLI Arguments (for clients with env var issues)
|
|
86
86
|
|
package/README.zh-CN.md
CHANGED
|
@@ -80,7 +80,7 @@ npm install -g @zereight/mcp-gitlab
|
|
|
80
80
|
|
|
81
81
|
示例使用 `zereight-mcp-gitlab`,这是比旧的 `mcp-gitlab` 更不容易冲突的别名。如果 MCP 客户端找不到它,请使用 `which zereight-mcp-gitlab` 输出的绝对路径。
|
|
82
82
|
|
|
83
|
-
如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.
|
|
83
|
+
如果不想全局安装,请将 `npx` 固定到上一个稳定版本(即文档推荐的版本),例如 `npx -y @zereight/mcp-gitlab@2.1.41`。如果始终想使用最新版本,请改用 `npx -y @zereight/mcp-gitlab@latest`。有新版本发布时,服务器会在启动时通过 stderr 提示(可用 `GITLAB_DISABLE_VERSION_CHECK=true` 关闭)。
|
|
84
84
|
|
|
85
85
|
#### 使用 CLI 参数(适用于环境变量有问题的客户端)
|
|
86
86
|
|
package/build/index.js
CHANGED
|
@@ -90,6 +90,8 @@ import { graphqlQueryContainsWriteOperation, graphqlQueryContainsDeleteOperation
|
|
|
90
90
|
import { resolveNestedWikiUpdateTitle } from "./utils/wiki-title.js";
|
|
91
91
|
import { redactSensitiveGitLabFields } from "./utils/redact-sensitive.js";
|
|
92
92
|
import { checkForNewVersion } from "./utils/version-check.js";
|
|
93
|
+
import { assertGitLabVersionAtLeast } from "./utils/gitlab-version-gate.js";
|
|
94
|
+
import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "./utils/bulk-publish-options.js";
|
|
93
95
|
import { cleanMutuallyExclusiveIdUsernameOptions, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS, sanitizeToolArguments, } from "./utils/tool-args.js";
|
|
94
96
|
import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from "./utils/patch-helper.js";
|
|
95
97
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
@@ -4424,19 +4426,49 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
4424
4426
|
};
|
|
4425
4427
|
}
|
|
4426
4428
|
}
|
|
4429
|
+
async function fetchGitLabInstanceVersion() {
|
|
4430
|
+
try {
|
|
4431
|
+
const response = await fetch(`${getEffectiveApiUrl()}/version`, {
|
|
4432
|
+
...getFetchConfig(),
|
|
4433
|
+
});
|
|
4434
|
+
if (!response.ok)
|
|
4435
|
+
return null;
|
|
4436
|
+
const data = await response.json();
|
|
4437
|
+
if (typeof data === "object" &&
|
|
4438
|
+
data !== null &&
|
|
4439
|
+
"version" in data &&
|
|
4440
|
+
typeof data.version === "string") {
|
|
4441
|
+
return data.version;
|
|
4442
|
+
}
|
|
4443
|
+
return null;
|
|
4444
|
+
}
|
|
4445
|
+
catch {
|
|
4446
|
+
return null;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4427
4449
|
/**
|
|
4428
4450
|
* Publish all draft notes for a merge request
|
|
4429
4451
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4430
4452
|
* @param {number|string} mergeRequestIid - The internal ID of the merge request
|
|
4453
|
+
* @param {BulkPublishDraftNotesBody} options - Optional GitLab 19.2+ bulk_publish body fields
|
|
4431
4454
|
* @returns {Promise<GitLabDiscussionNote[]>} Array of published notes
|
|
4432
4455
|
*/
|
|
4433
|
-
async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
|
|
4456
|
+
async function bulkPublishDraftNotes(projectId, mergeRequestIid, options = {}) {
|
|
4434
4457
|
projectId = decodeURIComponent(projectId);
|
|
4435
4458
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${encodeGitLabPathSegment(mergeRequestIid)}/draft_notes/bulk_publish`);
|
|
4459
|
+
const body = buildBulkPublishDraftNotesBody(options);
|
|
4460
|
+
if (needsGitLab19_2BulkPublish(body)) {
|
|
4461
|
+
await assertGitLabVersionAtLeast({
|
|
4462
|
+
major: 19,
|
|
4463
|
+
minor: 2,
|
|
4464
|
+
feature: "reviewer_state, note, and internal on bulk_publish_draft_notes",
|
|
4465
|
+
retryHint: "Omit reviewer_state, note, and internal, then retry.",
|
|
4466
|
+
}, fetchGitLabInstanceVersion);
|
|
4467
|
+
}
|
|
4436
4468
|
const response = await fetch(url.toString(), {
|
|
4437
4469
|
...getFetchConfig(),
|
|
4438
4470
|
method: "POST", // Changed from PUT to POST
|
|
4439
|
-
body: JSON.stringify(
|
|
4471
|
+
body: JSON.stringify(body),
|
|
4440
4472
|
});
|
|
4441
4473
|
if (!response.ok) {
|
|
4442
4474
|
const errorText = await response.text();
|
|
@@ -7612,8 +7644,8 @@ async function handleToolCall(params) {
|
|
|
7612
7644
|
}
|
|
7613
7645
|
case "bulk_publish_draft_notes": {
|
|
7614
7646
|
const args = BulkPublishDraftNotesSchema.parse(params.arguments);
|
|
7615
|
-
const { project_id, merge_request_iid } = args;
|
|
7616
|
-
const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid);
|
|
7647
|
+
const { project_id, merge_request_iid, ...options } = args;
|
|
7648
|
+
const publishedNotes = await bulkPublishDraftNotes(project_id, merge_request_iid, options);
|
|
7617
7649
|
return {
|
|
7618
7650
|
content: [{ type: "text", text: JSON.stringify(publishedNotes) }],
|
|
7619
7651
|
};
|
package/build/schemas.js
CHANGED
|
@@ -2547,26 +2547,31 @@ export const MergeRequestThreadPositionSchema = z.object({
|
|
|
2547
2547
|
});
|
|
2548
2548
|
const optionalMergeRequestThreadPosition = z.preprocess(omitIncompleteMergeRequestPosition, MergeRequestThreadPositionSchema.optional());
|
|
2549
2549
|
// Draft Notes API schemas
|
|
2550
|
+
// Response shape follows Draft Notes API (not Notes API): note, author_id,
|
|
2551
|
+
// line_code, merge_request_id, commit_id — no created_at/updated_at.
|
|
2550
2552
|
export const GitLabDraftNoteSchema = z
|
|
2551
2553
|
.object({
|
|
2552
2554
|
id: z.coerce.string(),
|
|
2555
|
+
author_id: z.coerce.number().nullable().optional(),
|
|
2553
2556
|
author: GitLabUserSchema.optional(),
|
|
2554
2557
|
body: z.string().optional(),
|
|
2555
|
-
note: z.string().optional(), //
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
+
note: z.string().optional(), // Draft Notes API uses 'note'; keep 'body' for callers
|
|
2559
|
+
line_code: z.string().nullable().optional(),
|
|
2560
|
+
merge_request_id: z.coerce.number().nullable().optional(),
|
|
2561
|
+
commit_id: z.string().nullable().optional(),
|
|
2558
2562
|
discussion_id: z.string().nullable().optional(),
|
|
2559
2563
|
position: z.record(z.unknown()).nullable().optional(),
|
|
2560
2564
|
resolve_discussion: z.coerce.boolean().optional(),
|
|
2561
2565
|
})
|
|
2562
2566
|
.transform(data => ({
|
|
2563
|
-
// Normalize the response to always have consistent field names
|
|
2564
2567
|
id: data.id,
|
|
2568
|
+
author_id: data.author_id ?? null,
|
|
2565
2569
|
author: data.author,
|
|
2566
2570
|
body: data.body || data.note || "",
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2571
|
+
line_code: data.line_code ?? null,
|
|
2572
|
+
merge_request_id: data.merge_request_id ?? null,
|
|
2573
|
+
commit_id: data.commit_id ?? null,
|
|
2574
|
+
discussion_id: data.discussion_id ?? null,
|
|
2570
2575
|
position: data.position,
|
|
2571
2576
|
resolve_discussion: data.resolve_discussion,
|
|
2572
2577
|
}));
|
|
@@ -2617,6 +2622,18 @@ export const PublishDraftNoteSchema = ProjectParamsSchema.extend({
|
|
|
2617
2622
|
// Bulk publish draft notes schema
|
|
2618
2623
|
export const BulkPublishDraftNotesSchema = ProjectParamsSchema.extend({
|
|
2619
2624
|
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2625
|
+
reviewer_state: z
|
|
2626
|
+
.enum(["requested_changes", "reviewed"])
|
|
2627
|
+
.optional()
|
|
2628
|
+
.describe("Set reviewer review state after publishing (GitLab 19.2+). Does not record a formal approval. Works even with no draft notes."),
|
|
2629
|
+
note: z
|
|
2630
|
+
.string()
|
|
2631
|
+
.optional()
|
|
2632
|
+
.describe("Summary note body to post on the merge request (GitLab 19.2+)"),
|
|
2633
|
+
internal: z.coerce
|
|
2634
|
+
.boolean()
|
|
2635
|
+
.optional()
|
|
2636
|
+
.describe("If true, the summary note is internal (GitLab 19.2+, default false)"),
|
|
2620
2637
|
});
|
|
2621
2638
|
// Schema for creating a new merge request thread
|
|
2622
2639
|
export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { GitLabDraftNoteSchema } from "../schemas.js";
|
|
4
|
+
test("draft note schema keeps Draft Notes API fields and drops fake timestamps", () => {
|
|
5
|
+
const parsed = GitLabDraftNoteSchema.parse({
|
|
6
|
+
id: 42,
|
|
7
|
+
author_id: 7,
|
|
8
|
+
note: "looks wrong on this line",
|
|
9
|
+
line_code: "a1b2c3d4e5f6_12_17",
|
|
10
|
+
merge_request_id: 99,
|
|
11
|
+
commit_id: "abc123",
|
|
12
|
+
discussion_id: null,
|
|
13
|
+
position: { new_path: "src/app.ts", new_line: 17 },
|
|
14
|
+
resolve_discussion: false,
|
|
15
|
+
// Notes-API leftovers that Draft Notes API never returns — must be stripped
|
|
16
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
17
|
+
updated_at: "2024-01-02T00:00:00Z",
|
|
18
|
+
});
|
|
19
|
+
assert.equal(parsed.id, "42");
|
|
20
|
+
assert.equal(parsed.author_id, 7);
|
|
21
|
+
assert.equal(parsed.body, "looks wrong on this line");
|
|
22
|
+
assert.equal(parsed.line_code, "a1b2c3d4e5f6_12_17");
|
|
23
|
+
assert.equal(parsed.merge_request_id, 99);
|
|
24
|
+
assert.equal(parsed.commit_id, "abc123");
|
|
25
|
+
assert.equal(parsed.discussion_id, null);
|
|
26
|
+
assert.equal(parsed.resolve_discussion, false);
|
|
27
|
+
assert.equal("created_at" in parsed, false);
|
|
28
|
+
assert.equal("updated_at" in parsed, false);
|
|
29
|
+
});
|
|
30
|
+
test("draft note schema preserves null line_code for unresolved positions", () => {
|
|
31
|
+
const parsed = GitLabDraftNoteSchema.parse({
|
|
32
|
+
id: "43",
|
|
33
|
+
author_id: 7,
|
|
34
|
+
note: "orphan draft",
|
|
35
|
+
line_code: null,
|
|
36
|
+
merge_request_id: 99,
|
|
37
|
+
commit_id: null,
|
|
38
|
+
discussion_id: null,
|
|
39
|
+
position: null,
|
|
40
|
+
resolve_discussion: false,
|
|
41
|
+
});
|
|
42
|
+
assert.equal(parsed.line_code, null);
|
|
43
|
+
assert.equal(parsed.commit_id, null);
|
|
44
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { buildBulkPublishDraftNotesBody, needsGitLab19_2BulkPublish, } from "../../utils/bulk-publish-options.js";
|
|
4
|
+
describe("When buildBulkPublishDraftNotesBody runs", () => {
|
|
5
|
+
describe("with no-op defaults", () => {
|
|
6
|
+
test("should omit internal false and empty note", () => {
|
|
7
|
+
assert.deepEqual(buildBulkPublishDraftNotesBody({ internal: false, note: "" }), {});
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
describe("with meaningful 19.2 fields", () => {
|
|
11
|
+
test("should keep reviewer_state, note, and internal true", () => {
|
|
12
|
+
assert.deepEqual(buildBulkPublishDraftNotesBody({
|
|
13
|
+
reviewer_state: "reviewed",
|
|
14
|
+
note: "LGTM",
|
|
15
|
+
internal: true,
|
|
16
|
+
}), { reviewer_state: "reviewed", note: "LGTM", internal: true });
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("When needsGitLab19_2BulkPublish runs", () => {
|
|
21
|
+
describe("with an empty body", () => {
|
|
22
|
+
test("should return false", () => {
|
|
23
|
+
assert.equal(needsGitLab19_2BulkPublish({}), false);
|
|
24
|
+
assert.equal(needsGitLab19_2BulkPublish(buildBulkPublishDraftNotesBody({ internal: false })), false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("with a non-empty body", () => {
|
|
28
|
+
test("should return true", () => {
|
|
29
|
+
assert.equal(needsGitLab19_2BulkPublish({ reviewer_state: "reviewed" }), true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { assertGitLabVersionAtLeast, isGitLabVersionAtLeast, parseGitLabVersion, } from "../../utils/gitlab-version-gate.js";
|
|
4
|
+
function versionFetcher(version) {
|
|
5
|
+
return async () => version;
|
|
6
|
+
}
|
|
7
|
+
function bulkPublishRequirement() {
|
|
8
|
+
return {
|
|
9
|
+
major: 19,
|
|
10
|
+
minor: 2,
|
|
11
|
+
feature: "reviewer_state, note, and internal on bulk_publish_draft_notes",
|
|
12
|
+
retryHint: "Omit reviewer_state, note, and internal, then retry.",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe("When parseGitLabVersion runs", () => {
|
|
16
|
+
describe("with a release suffix", () => {
|
|
17
|
+
test("should parse major.minor.patch from ee builds", () => {
|
|
18
|
+
assert.deepEqual(parseGitLabVersion("19.2.0-ee"), {
|
|
19
|
+
major: 19,
|
|
20
|
+
minor: 2,
|
|
21
|
+
patch: 0,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("with a malformed string", () => {
|
|
26
|
+
test("should return null", () => {
|
|
27
|
+
assert.equal(parseGitLabVersion("unknown"), null);
|
|
28
|
+
assert.equal(parseGitLabVersion(""), null);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("When isGitLabVersionAtLeast compares versions", () => {
|
|
33
|
+
describe("with a version at or above the floor", () => {
|
|
34
|
+
test("should return true", () => {
|
|
35
|
+
assert.equal(isGitLabVersionAtLeast("19.2.0-ee", 19, 2), true);
|
|
36
|
+
assert.equal(isGitLabVersionAtLeast("19.3.1", 19, 2), true);
|
|
37
|
+
assert.equal(isGitLabVersionAtLeast("20.0.0", 19, 2), true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("with a version below the floor", () => {
|
|
41
|
+
test("should return false", () => {
|
|
42
|
+
assert.equal(isGitLabVersionAtLeast("19.1.9-ee", 19, 2), false);
|
|
43
|
+
assert.equal(isGitLabVersionAtLeast("17.5.0", 19, 2), false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("with an unparseable version", () => {
|
|
47
|
+
test("should return null", () => {
|
|
48
|
+
assert.equal(isGitLabVersionAtLeast("not-a-version", 19, 2), null);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("When assertGitLabVersionAtLeast runs", () => {
|
|
53
|
+
describe("with an older instance version", () => {
|
|
54
|
+
test("should throw a retryable error", async () => {
|
|
55
|
+
await assert.rejects(() => assertGitLabVersionAtLeast(bulkPublishRequirement(), versionFetcher("17.5.0-ee")), (error) => {
|
|
56
|
+
assert.ok(error instanceof Error);
|
|
57
|
+
assert.match(error.message, /GitLab 19\.2\+/);
|
|
58
|
+
assert.match(error.message, /17\.5\.0-ee/);
|
|
59
|
+
assert.match(error.message, /Omit reviewer_state/);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("with a new enough instance version", () => {
|
|
65
|
+
test("should resolve without throwing", async () => {
|
|
66
|
+
await assertGitLabVersionAtLeast(bulkPublishRequirement(), versionFetcher("19.2.0-ee"));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("with an unknown instance version", () => {
|
|
70
|
+
test("should fail open and resolve", async () => {
|
|
71
|
+
await assertGitLabVersionAtLeast(bulkPublishRequirement(), versionFetcher(null));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
package/build/tools/registry.js
CHANGED
|
@@ -267,7 +267,7 @@ export const allTools = [
|
|
|
267
267
|
},
|
|
268
268
|
{
|
|
269
269
|
name: "bulk_publish_draft_notes",
|
|
270
|
-
description: "Publish all draft notes for a merge request",
|
|
270
|
+
description: "Publish all draft notes for a merge request. Optionally sets reviewer_state and posts a summary note (GitLab 19.2+). Can set reviewer_state even with no drafts.",
|
|
271
271
|
inputSchema: toJSONSchema(BulkPublishDraftNotesSchema),
|
|
272
272
|
},
|
|
273
273
|
// --- Merge request emoji reaction tools ---
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the POST body. Omit no-op defaults (`internal: false`, empty `note`)
|
|
3
|
+
* so older GitLab instances are not sent unknown fields.
|
|
4
|
+
*/
|
|
5
|
+
export function buildBulkPublishDraftNotesBody(options) {
|
|
6
|
+
const body = {};
|
|
7
|
+
if (options.reviewer_state !== undefined) {
|
|
8
|
+
body.reviewer_state = options.reviewer_state;
|
|
9
|
+
}
|
|
10
|
+
if (options.note) {
|
|
11
|
+
body.note = options.note;
|
|
12
|
+
}
|
|
13
|
+
if (options.internal === true) {
|
|
14
|
+
body.internal = true;
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
/** True when the body needs GitLab 19.2+ bulk_publish fields. */
|
|
19
|
+
export function needsGitLab19_2BulkPublish(body) {
|
|
20
|
+
return Object.keys(body).length > 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call-time GitLab instance version gate.
|
|
3
|
+
*
|
|
4
|
+
* Use when a tool exposes optional params that only exist on newer GitLab.
|
|
5
|
+
* Schemas stay static; check before the API call and throw a retryable error.
|
|
6
|
+
* Fail-open when the instance version cannot be fetched or parsed.
|
|
7
|
+
*/
|
|
8
|
+
const VERSION_CORE = /^(\d+)\.(\d+)(?:\.(\d+))?/;
|
|
9
|
+
/** Parse strings like "19.2.0-ee" / "17.5.3". Returns null if unparseable. */
|
|
10
|
+
export function parseGitLabVersion(version) {
|
|
11
|
+
const match = VERSION_CORE.exec(version.trim());
|
|
12
|
+
if (!match)
|
|
13
|
+
return null;
|
|
14
|
+
const major = Number.parseInt(match[1], 10);
|
|
15
|
+
const minor = Number.parseInt(match[2], 10);
|
|
16
|
+
const patch = match[3] ? Number.parseInt(match[3], 10) : 0;
|
|
17
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return { major, minor, patch };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare a GitLab version string against a major.minor floor.
|
|
24
|
+
* Returns null when the version string cannot be parsed.
|
|
25
|
+
*/
|
|
26
|
+
export function isGitLabVersionAtLeast(version, major, minor) {
|
|
27
|
+
const parsed = parseGitLabVersion(version);
|
|
28
|
+
if (!parsed)
|
|
29
|
+
return null;
|
|
30
|
+
if (parsed.major !== major)
|
|
31
|
+
return parsed.major > major;
|
|
32
|
+
return parsed.minor >= minor;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Throws when the instance is known to be older than `requirement`.
|
|
36
|
+
* Does nothing when the version is unknown (fail-open).
|
|
37
|
+
*/
|
|
38
|
+
export async function assertGitLabVersionAtLeast(requirement, fetchVersion) {
|
|
39
|
+
const current = await fetchVersion();
|
|
40
|
+
if (current === null)
|
|
41
|
+
return;
|
|
42
|
+
const ok = isGitLabVersionAtLeast(current, requirement.major, requirement.minor);
|
|
43
|
+
if (ok === null || ok)
|
|
44
|
+
return;
|
|
45
|
+
throw new Error(`GitLab ${requirement.major}.${requirement.minor}+ required for ${requirement.feature} ` +
|
|
46
|
+
`(instance reports ${current}). ${requirement.retryHint}`);
|
|
47
|
+
}
|
package/package.json
CHANGED