@zereight/mcp-gitlab 2.0.36 → 2.1.1
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.md +69 -332
- package/build/auth-retry.js +81 -0
- package/build/config.js +75 -0
- package/build/customSchemas.js +0 -12
- package/build/gitlab-client-pool.js +0 -1
- package/build/index.js +472 -1650
- package/build/oauth.js +4 -4
- package/build/schemas.js +160 -48
- package/build/test/mcp-oauth-tests.js +49 -0
- package/build/test/schema-tests.js +59 -3
- package/build/test/test-auth-retry.js +188 -0
- package/build/test/test-search-code.js +7 -4
- package/build/test/test-token-optimizations.js +691 -0
- package/build/test/test-toolset-filtering.js +39 -33
- package/build/tools/registry.js +1295 -0
- package/build/utils/helpers.js +57 -0
- package/build/utils/schema.js +49 -0
- package/build/utils/url.js +19 -0
- package/package.json +24 -5
package/build/oauth.js
CHANGED
|
@@ -459,15 +459,15 @@ export class GitLabOAuth {
|
|
|
459
459
|
/**
|
|
460
460
|
* Get a valid access token, refreshing if necessary
|
|
461
461
|
*/
|
|
462
|
-
async getAccessToken() {
|
|
462
|
+
async getAccessToken(force = false) {
|
|
463
463
|
let tokenData = this.loadToken();
|
|
464
|
-
// If no token or expired, start OAuth flow or refresh
|
|
464
|
+
// If no token or expired (or forced), start OAuth flow or refresh
|
|
465
465
|
if (!tokenData) {
|
|
466
466
|
logger.info("No stored token found. Starting OAuth flow...");
|
|
467
467
|
tokenData = await this.startOAuthFlow();
|
|
468
468
|
}
|
|
469
|
-
else if (this.isTokenExpired(tokenData)) {
|
|
470
|
-
logger.info("Token expired. Refreshing...");
|
|
469
|
+
else if (force || this.isTokenExpired(tokenData)) {
|
|
470
|
+
logger.info(force && !this.isTokenExpired(tokenData) ? "Force-refreshing OAuth token..." : "Token expired. Refreshing...");
|
|
471
471
|
if (tokenData.refresh_token) {
|
|
472
472
|
try {
|
|
473
473
|
tokenData = await this.refreshAccessToken(tokenData.refresh_token);
|
package/build/schemas.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
// Helper: coerce a JSON-stringified array to an actual array.
|
|
3
|
+
// LLMs sometimes send '["a", "b"]' (string) instead of ["a", "b"] (array).
|
|
4
|
+
const coerceStringArray = z.preprocess((val) => {
|
|
5
|
+
if (typeof val === "string") {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(val);
|
|
8
|
+
if (Array.isArray(parsed))
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
catch { /* not JSON, fall through */ }
|
|
12
|
+
}
|
|
13
|
+
return val;
|
|
14
|
+
}, z.array(z.string()));
|
|
2
15
|
// Base schemas for common types
|
|
3
16
|
export const GitLabAuthorSchema = z.object({
|
|
4
17
|
name: z.string(),
|
|
@@ -904,7 +917,7 @@ export const GitLabMergeRequestSchema = z.object({
|
|
|
904
917
|
.optional()
|
|
905
918
|
.describe("Number of commits the source branch is behind the target branch"),
|
|
906
919
|
rebase_in_progress: z
|
|
907
|
-
.boolean()
|
|
920
|
+
.coerce.boolean()
|
|
908
921
|
.optional()
|
|
909
922
|
.describe("Whether rebase is currently in progress for this merge request"),
|
|
910
923
|
merge_when_pipeline_succeeds: z.coerce.boolean().optional(),
|
|
@@ -1135,9 +1148,17 @@ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
|
|
|
1135
1148
|
});
|
|
1136
1149
|
export const SearchRepositoriesSchema = z
|
|
1137
1150
|
.object({
|
|
1138
|
-
search: z.string().describe("Search query"),
|
|
1151
|
+
search: z.string().optional().describe("Search query"),
|
|
1152
|
+
query: z.string().optional().describe("Search query (alias for 'search')"),
|
|
1139
1153
|
})
|
|
1140
|
-
.merge(PaginationOptionsSchema)
|
|
1154
|
+
.merge(PaginationOptionsSchema)
|
|
1155
|
+
.transform((data) => {
|
|
1156
|
+
const search = data.search || data.query;
|
|
1157
|
+
if (!search) {
|
|
1158
|
+
throw new Error("Either 'search' or 'query' must be provided");
|
|
1159
|
+
}
|
|
1160
|
+
return { ...data, search, query: undefined };
|
|
1161
|
+
});
|
|
1141
1162
|
export const CreateRepositorySchema = z.object({
|
|
1142
1163
|
name: z.string().describe("Repository name"),
|
|
1143
1164
|
description: z.string().optional().describe("Repository description"),
|
|
@@ -1222,12 +1243,12 @@ const MergeRequestOptionsSchema = {
|
|
|
1222
1243
|
draft: z.coerce.boolean().optional().describe("Create as draft merge request"),
|
|
1223
1244
|
allow_collaboration: z.coerce.boolean().optional().describe("Allow commits from upstream members"),
|
|
1224
1245
|
remove_source_branch: z
|
|
1225
|
-
.boolean()
|
|
1246
|
+
.coerce.boolean()
|
|
1226
1247
|
.nullable()
|
|
1227
1248
|
.optional()
|
|
1228
1249
|
.describe("Flag indicating if a merge request should remove the source branch when merging."),
|
|
1229
1250
|
squash: z
|
|
1230
|
-
.boolean()
|
|
1251
|
+
.coerce.boolean()
|
|
1231
1252
|
.nullable()
|
|
1232
1253
|
.optional()
|
|
1233
1254
|
.describe("If true, squash all commits into a single commit on merge."),
|
|
@@ -1246,7 +1267,7 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
|
|
|
1246
1267
|
from: z.string().describe("The base branch or commit SHA to compare from"),
|
|
1247
1268
|
to: z.string().describe("The target branch or commit SHA to compare to"),
|
|
1248
1269
|
straight: z
|
|
1249
|
-
.boolean()
|
|
1270
|
+
.coerce.boolean()
|
|
1250
1271
|
.optional()
|
|
1251
1272
|
.describe("Comparison method: false for '...' (default), true for '--'"),
|
|
1252
1273
|
excluded_file_patterns: z
|
|
@@ -1273,7 +1294,7 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
|
1273
1294
|
.optional()
|
|
1274
1295
|
.describe("New state (close/reopen) for the MR"),
|
|
1275
1296
|
remove_source_branch: z
|
|
1276
|
-
.boolean()
|
|
1297
|
+
.coerce.boolean()
|
|
1277
1298
|
.optional()
|
|
1278
1299
|
.describe("Flag indicating if the source branch should be removed"),
|
|
1279
1300
|
squash: z.coerce.boolean().optional().describe("Squash commits into a single commit when merging"),
|
|
@@ -1281,24 +1302,24 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
|
1281
1302
|
});
|
|
1282
1303
|
export const MergeMergeRequestSchema = ProjectParamsSchema.extend({
|
|
1283
1304
|
merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
|
|
1284
|
-
auto_merge: z
|
|
1305
|
+
auto_merge: z.coerce
|
|
1285
1306
|
.boolean()
|
|
1286
1307
|
.optional()
|
|
1287
1308
|
.default(false)
|
|
1288
1309
|
.describe("If true, the merge request merges when the pipeline succeeds."),
|
|
1289
1310
|
merge_commit_message: z.string().optional().describe("Custom merge commit message"),
|
|
1290
|
-
merge_when_pipeline_succeeds: z
|
|
1311
|
+
merge_when_pipeline_succeeds: z.coerce
|
|
1291
1312
|
.boolean()
|
|
1292
1313
|
.optional()
|
|
1293
1314
|
.default(false)
|
|
1294
1315
|
.describe("If true, the merge request merges when the pipeline succeeds.in GitLab 17.11. Use"),
|
|
1295
|
-
should_remove_source_branch: z
|
|
1316
|
+
should_remove_source_branch: z.coerce
|
|
1296
1317
|
.boolean()
|
|
1297
1318
|
.optional()
|
|
1298
1319
|
.default(false)
|
|
1299
1320
|
.describe("Remove source branch after merge"),
|
|
1300
1321
|
squash_commit_message: z.string().optional().describe("Custom squash commit message"),
|
|
1301
|
-
squash: z
|
|
1322
|
+
squash: z.coerce
|
|
1302
1323
|
.boolean()
|
|
1303
1324
|
.optional()
|
|
1304
1325
|
.default(false)
|
|
@@ -1394,7 +1415,7 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
|
1394
1415
|
page: z.coerce.number().optional().describe("Page number for pagination (default: 1)"),
|
|
1395
1416
|
per_page: z.coerce.number().optional().describe("Number of items per page (max: 100, default: 20)"),
|
|
1396
1417
|
unidiff: z
|
|
1397
|
-
.boolean()
|
|
1418
|
+
.coerce.boolean()
|
|
1398
1419
|
.optional()
|
|
1399
1420
|
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1400
1421
|
});
|
|
@@ -1410,7 +1431,7 @@ export const GetMergeRequestFileDiffSchema = GetMergeRequestSchema.extend({
|
|
|
1410
1431
|
.describe("List of file paths to retrieve diffs for (e.g. ['src/api/users.ts', 'src/repo/user.go']). " +
|
|
1411
1432
|
"Call list_merge_request_changed_files first to get the full list of changed paths."),
|
|
1412
1433
|
unidiff: z
|
|
1413
|
-
.boolean()
|
|
1434
|
+
.coerce.boolean()
|
|
1414
1435
|
.optional()
|
|
1415
1436
|
.describe("Present diff in the unified diff format. Default is false."),
|
|
1416
1437
|
});
|
|
@@ -1421,7 +1442,7 @@ export const ListMergeRequestVersionsSchema = ProjectParamsSchema.extend({
|
|
|
1421
1442
|
export const GetMergeRequestVersionSchema = ListMergeRequestVersionsSchema.extend({
|
|
1422
1443
|
version_id: z.coerce.string().describe("The ID of the merge request diff version"),
|
|
1423
1444
|
unidiff: z
|
|
1424
|
-
.boolean()
|
|
1445
|
+
.coerce.boolean()
|
|
1425
1446
|
.optional()
|
|
1426
1447
|
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1427
1448
|
});
|
|
@@ -1579,14 +1600,11 @@ export const UpdateIssueSchema = z.object({
|
|
|
1579
1600
|
confidential: z.coerce.boolean().optional().describe("Set the issue to be confidential"),
|
|
1580
1601
|
discussion_locked: z.coerce.boolean().optional().describe("Flag to lock discussions"),
|
|
1581
1602
|
due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"),
|
|
1582
|
-
labels:
|
|
1603
|
+
labels: coerceStringArray.optional().describe("Array of label names"),
|
|
1583
1604
|
milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
|
|
1584
1605
|
state_event: z.enum(["close", "reopen"]).optional().describe("Update issue state (close/reopen)"),
|
|
1585
1606
|
weight: z.coerce.number().optional().describe("Weight of the issue (numeric, typically hours of work)"),
|
|
1586
|
-
issue_type: z
|
|
1587
|
-
.enum(["issue", "incident", "test_case", "task"])
|
|
1588
|
-
.optional()
|
|
1589
|
-
.describe("The type of issue. One of issue, incident, test_case or task."),
|
|
1607
|
+
issue_type: z.preprocess((val) => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["issue", "incident", "test_case", "task"]).optional()).describe("The type of issue. One of issue, incident, test_case or task."),
|
|
1590
1608
|
});
|
|
1591
1609
|
export const DeleteIssueSchema = z.object({
|
|
1592
1610
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -1645,7 +1663,7 @@ export const ListProjectsSchema = z
|
|
|
1645
1663
|
search_namespaces: z.coerce.boolean().optional().describe("Needs to be true if search is full path"),
|
|
1646
1664
|
owned: z.coerce.boolean().optional().describe("Filter for projects owned by current user"),
|
|
1647
1665
|
membership: z
|
|
1648
|
-
.boolean()
|
|
1666
|
+
.coerce.boolean()
|
|
1649
1667
|
.optional()
|
|
1650
1668
|
.describe("Filter for projects where current user is a member"),
|
|
1651
1669
|
simple: z.coerce.boolean().optional().describe("Return only limited fields"),
|
|
@@ -1663,21 +1681,22 @@ export const ListProjectsSchema = z
|
|
|
1663
1681
|
.optional()
|
|
1664
1682
|
.describe("Return projects sorted in ascending or descending order"),
|
|
1665
1683
|
with_issues_enabled: z
|
|
1666
|
-
.boolean()
|
|
1684
|
+
.coerce.boolean()
|
|
1667
1685
|
.optional()
|
|
1668
1686
|
.describe("Filter projects with issues feature enabled"),
|
|
1669
1687
|
with_merge_requests_enabled: z
|
|
1670
|
-
.boolean()
|
|
1688
|
+
.coerce.boolean()
|
|
1671
1689
|
.optional()
|
|
1672
1690
|
.describe("Filter projects with merge requests feature enabled"),
|
|
1673
1691
|
min_access_level: z.coerce.number().optional().describe("Filter by minimum access level"),
|
|
1692
|
+
topic: z.string().optional().describe("Filter by topic (projects tagged with this topic)"),
|
|
1674
1693
|
})
|
|
1675
1694
|
.merge(PaginationOptionsSchema);
|
|
1676
1695
|
// Label operation schemas
|
|
1677
1696
|
export const ListLabelsSchema = z.object({
|
|
1678
1697
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
1679
1698
|
with_counts: z
|
|
1680
|
-
.boolean()
|
|
1699
|
+
.coerce.boolean()
|
|
1681
1700
|
.optional()
|
|
1682
1701
|
.describe("Whether or not to include issue and merge request counts"),
|
|
1683
1702
|
include_ancestor_groups: z.coerce.boolean().optional().describe("Include ancestor groups"),
|
|
@@ -1729,11 +1748,11 @@ export const ListGroupProjectsSchema = z
|
|
|
1729
1748
|
.optional()
|
|
1730
1749
|
.describe("Filter by project visibility"),
|
|
1731
1750
|
with_issues_enabled: z
|
|
1732
|
-
.boolean()
|
|
1751
|
+
.coerce.boolean()
|
|
1733
1752
|
.optional()
|
|
1734
1753
|
.describe("Filter projects with issues feature enabled"),
|
|
1735
1754
|
with_merge_requests_enabled: z
|
|
1736
|
-
.boolean()
|
|
1755
|
+
.coerce.boolean()
|
|
1737
1756
|
.optional()
|
|
1738
1757
|
.describe("Filter projects with merge requests feature enabled"),
|
|
1739
1758
|
min_access_level: z.coerce.number().optional().describe("Filter by minimum access level"),
|
|
@@ -1742,6 +1761,7 @@ export const ListGroupProjectsSchema = z
|
|
|
1742
1761
|
statistics: z.coerce.boolean().optional().describe("Include project statistics"),
|
|
1743
1762
|
with_custom_attributes: z.coerce.boolean().optional().describe("Include custom attributes"),
|
|
1744
1763
|
with_security_reports: z.coerce.boolean().optional().describe("Include security reports"),
|
|
1764
|
+
topic: z.string().optional().describe("Filter by topic (projects tagged with this topic)"),
|
|
1745
1765
|
})
|
|
1746
1766
|
.merge(PaginationOptionsSchema);
|
|
1747
1767
|
// Add wiki operation schemas
|
|
@@ -1785,7 +1805,7 @@ export const GitLabWikiPageSchema = z.object({
|
|
|
1785
1805
|
export const ListGroupWikiPagesSchema = z
|
|
1786
1806
|
.object({
|
|
1787
1807
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
1788
|
-
with_content: z.boolean().optional().describe("Include content of the wiki pages"),
|
|
1808
|
+
with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
|
|
1789
1809
|
})
|
|
1790
1810
|
.merge(PaginationOptionsSchema);
|
|
1791
1811
|
export const GetGroupWikiPageSchema = z.object({
|
|
@@ -1966,7 +1986,7 @@ export const CreateDraftNoteSchema = ProjectParamsSchema.extend({
|
|
|
1966
1986
|
.describe("The ID of a discussion the draft note replies to"),
|
|
1967
1987
|
position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
|
|
1968
1988
|
resolve_discussion: z
|
|
1969
|
-
.boolean()
|
|
1989
|
+
.coerce.boolean()
|
|
1970
1990
|
.optional()
|
|
1971
1991
|
.describe("Whether to resolve the discussion when publishing"),
|
|
1972
1992
|
});
|
|
@@ -1977,7 +1997,7 @@ export const UpdateDraftNoteSchema = ProjectParamsSchema.extend({
|
|
|
1977
1997
|
body: z.string().optional().describe("The content of the draft note"),
|
|
1978
1998
|
position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
|
|
1979
1999
|
resolve_discussion: z
|
|
1980
|
-
.boolean()
|
|
2000
|
+
.coerce.boolean()
|
|
1981
2001
|
.optional()
|
|
1982
2002
|
.describe("Whether to resolve the discussion when publishing"),
|
|
1983
2003
|
});
|
|
@@ -2085,7 +2105,7 @@ export const ListCommitsSchema = z.object({
|
|
|
2085
2105
|
all: z.coerce.boolean().optional().describe("Retrieve every commit from the repository"),
|
|
2086
2106
|
with_stats: z.coerce.boolean().optional().describe("Stats about each commit are added to the response"),
|
|
2087
2107
|
first_parent: z
|
|
2088
|
-
.boolean()
|
|
2108
|
+
.coerce.boolean()
|
|
2089
2109
|
.optional()
|
|
2090
2110
|
.describe("Follow only the first parent commit upon seeing a merge commit"),
|
|
2091
2111
|
order: z.enum(["default", "topo"]).optional().describe("List commits in order"),
|
|
@@ -2102,7 +2122,7 @@ export const GetCommitDiffSchema = z.object({
|
|
|
2102
2122
|
project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
|
|
2103
2123
|
sha: z.string().describe("The commit hash or name of a repository branch or tag"),
|
|
2104
2124
|
full_diff: z
|
|
2105
|
-
.boolean()
|
|
2125
|
+
.coerce.boolean()
|
|
2106
2126
|
.optional()
|
|
2107
2127
|
.describe("Whether to return the full diff or only first page (default: false)"),
|
|
2108
2128
|
});
|
|
@@ -2145,7 +2165,7 @@ export const ListProjectMembersSchema = z.object({
|
|
|
2145
2165
|
user_ids: z.array(z.coerce.number()).optional().describe("Filter by user IDs"),
|
|
2146
2166
|
skip_users: z.array(z.coerce.number()).optional().describe("User IDs to exclude"),
|
|
2147
2167
|
include_inheritance: z
|
|
2148
|
-
.boolean()
|
|
2168
|
+
.coerce.boolean()
|
|
2149
2169
|
.optional()
|
|
2150
2170
|
.describe("Include inherited members. Defaults to false."),
|
|
2151
2171
|
per_page: z.coerce.number().optional().describe("Number of items per page (default: 20, max: 100)"),
|
|
@@ -2216,11 +2236,11 @@ export const ListGroupIterationsSchema = z
|
|
|
2216
2236
|
.optional()
|
|
2217
2237
|
.describe("Fields in which fuzzy search should be performed with the query given in the argument search. The available options are title and cadence_title. Default is [title]."),
|
|
2218
2238
|
include_ancestors: z
|
|
2219
|
-
.boolean()
|
|
2239
|
+
.coerce.boolean()
|
|
2220
2240
|
.optional()
|
|
2221
2241
|
.describe("Include iterations for group and its ancestors. Defaults to true."),
|
|
2222
2242
|
include_descendants: z
|
|
2223
|
-
.boolean()
|
|
2243
|
+
.coerce.boolean()
|
|
2224
2244
|
.optional()
|
|
2225
2245
|
.describe("Include iterations for group and its descendants. Defaults to false."),
|
|
2226
2246
|
updated_before: z
|
|
@@ -2255,8 +2275,8 @@ export const GitLabEventSchema = z
|
|
|
2255
2275
|
created_at: z.string(),
|
|
2256
2276
|
author: GitLabEventAuthorSchema,
|
|
2257
2277
|
author_username: z.string(),
|
|
2258
|
-
imported: z.coerce.boolean(),
|
|
2259
|
-
imported_from: z.string(),
|
|
2278
|
+
imported: z.coerce.boolean().optional(),
|
|
2279
|
+
imported_from: z.string().nullable().optional(),
|
|
2260
2280
|
})
|
|
2261
2281
|
.passthrough(); // Allow additional fields
|
|
2262
2282
|
// List events schema
|
|
@@ -2419,7 +2439,7 @@ export const ListReleasesSchema = z
|
|
|
2419
2439
|
.optional()
|
|
2420
2440
|
.describe("The direction of the order. Either desc (default) for descending order or asc for ascending order."),
|
|
2421
2441
|
include_html_description: z
|
|
2422
|
-
.boolean()
|
|
2442
|
+
.coerce.boolean()
|
|
2423
2443
|
.optional()
|
|
2424
2444
|
.describe("If true, a response includes HTML rendered Markdown of the release description."),
|
|
2425
2445
|
})
|
|
@@ -2428,7 +2448,7 @@ export const GetReleaseSchema = z.object({
|
|
|
2428
2448
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2429
2449
|
tag_name: z.string().describe("The Git tag the release is associated with"),
|
|
2430
2450
|
include_html_description: z
|
|
2431
|
-
.boolean()
|
|
2451
|
+
.coerce.boolean()
|
|
2432
2452
|
.optional()
|
|
2433
2453
|
.describe("If true, a response includes HTML rendered Markdown of the release description."),
|
|
2434
2454
|
});
|
|
@@ -2511,7 +2531,7 @@ export const ListJobArtifactsSchema = z.object({
|
|
|
2511
2531
|
.optional()
|
|
2512
2532
|
.describe("Directory path within the artifacts archive (defaults to root)"),
|
|
2513
2533
|
recursive: z
|
|
2514
|
-
.boolean()
|
|
2534
|
+
.coerce.boolean()
|
|
2515
2535
|
.optional()
|
|
2516
2536
|
.describe("Whether to list artifacts recursively"),
|
|
2517
2537
|
});
|
|
@@ -2593,8 +2613,8 @@ export const CreateWorkItemSchema = z.object({
|
|
|
2593
2613
|
.default("issue")
|
|
2594
2614
|
.describe("Type of work item to create. Defaults to 'issue'."),
|
|
2595
2615
|
description: z.string().optional().describe("Description of the work item (Markdown supported)"),
|
|
2596
|
-
labels:
|
|
2597
|
-
assignee_usernames:
|
|
2616
|
+
labels: coerceStringArray.optional().describe("Array of label names to assign"),
|
|
2617
|
+
assignee_usernames: coerceStringArray.optional().describe("Array of usernames to assign"),
|
|
2598
2618
|
parent_iid: z.coerce.number().optional().describe("IID of the parent work item to set hierarchy"),
|
|
2599
2619
|
weight: z.coerce.number().optional().describe("Weight of the work item"),
|
|
2600
2620
|
health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Set health status"),
|
|
@@ -2607,9 +2627,9 @@ export const CreateWorkItemSchema = z.object({
|
|
|
2607
2627
|
export const UpdateWorkItemSchema = WorkItemParamsSchema.extend({
|
|
2608
2628
|
title: z.string().optional().describe("New title"),
|
|
2609
2629
|
description: z.string().optional().describe("New description (Markdown supported)"),
|
|
2610
|
-
add_labels:
|
|
2611
|
-
remove_labels:
|
|
2612
|
-
assignee_usernames:
|
|
2630
|
+
add_labels: coerceStringArray.optional().describe("Label names to add"),
|
|
2631
|
+
remove_labels: coerceStringArray.optional().describe("Label names to remove"),
|
|
2632
|
+
assignee_usernames: coerceStringArray.optional().describe("Set assignees by username (replaces existing)"),
|
|
2613
2633
|
state_event: z.enum(["close", "reopen"]).optional().describe("Close or reopen the work item"),
|
|
2614
2634
|
weight: z.coerce.number().optional().describe("Set weight (issues, tasks, epics only)"),
|
|
2615
2635
|
status: z.string().optional().describe("Set status by ID. Use list_work_item_statuses to get available status IDs."),
|
|
@@ -2617,11 +2637,11 @@ export const UpdateWorkItemSchema = WorkItemParamsSchema.extend({
|
|
|
2617
2637
|
parent_project_id: z.coerce.string().optional().describe("Project ID or path of the parent work item (defaults to same project as the work item)"),
|
|
2618
2638
|
remove_parent: z.coerce.boolean().optional().describe("Set to true to remove the parent from hierarchy"),
|
|
2619
2639
|
children_to_add: z.array(z.object({
|
|
2620
|
-
project_id: z.coerce.string().describe("Project ID or path of the child work item"),
|
|
2640
|
+
project_id: z.coerce.string().optional().describe("Project ID or path of the child work item. Defaults to the parent work item's project if omitted."),
|
|
2621
2641
|
iid: z.coerce.number().describe("IID of the child work item"),
|
|
2622
2642
|
})).optional().describe("Array of children to add to this work item's hierarchy"),
|
|
2623
2643
|
children_to_remove: z.array(z.object({
|
|
2624
|
-
project_id: z.coerce.string().describe("Project ID or path of the child work item"),
|
|
2644
|
+
project_id: z.coerce.string().optional().describe("Project ID or path of the child work item. Defaults to the parent work item's project if omitted."),
|
|
2625
2645
|
iid: z.coerce.number().describe("IID of the child work item"),
|
|
2626
2646
|
})).optional().describe("Array of children to remove from this work item's hierarchy"),
|
|
2627
2647
|
health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional().describe("Set health status on issues and epics"),
|
|
@@ -2631,12 +2651,12 @@ export const UpdateWorkItemSchema = WorkItemParamsSchema.extend({
|
|
|
2631
2651
|
iteration_id: z.string().optional().describe("Iteration ID (e.g. 'gid://gitlab/Iteration/123' or numeric ID). Use list_group_iterations to find available iterations."),
|
|
2632
2652
|
confidential: z.coerce.boolean().optional().describe("Set confidentiality"),
|
|
2633
2653
|
linked_items_to_add: z.array(z.object({
|
|
2634
|
-
project_id: z.coerce.string().describe("Project ID or path of the work item to link"),
|
|
2654
|
+
project_id: z.coerce.string().optional().describe("Project ID or path of the work item to link. Defaults to the same project if omitted."),
|
|
2635
2655
|
iid: z.coerce.number().describe("IID of the work item to link"),
|
|
2636
2656
|
link_type: z.enum(["RELATED", "BLOCKED_BY", "BLOCKS"]).optional().default("RELATED").describe("Link type: RELATED, BLOCKED_BY, or BLOCKS. Defaults to RELATED."),
|
|
2637
2657
|
})).optional().describe("Work items to link"),
|
|
2638
2658
|
linked_items_to_remove: z.array(z.object({
|
|
2639
|
-
project_id: z.coerce.string().describe("Project ID or path of the linked work item to remove"),
|
|
2659
|
+
project_id: z.coerce.string().optional().describe("Project ID or path of the linked work item to remove. Defaults to the same project if omitted."),
|
|
2640
2660
|
iid: z.coerce.number().describe("IID of the linked work item to remove"),
|
|
2641
2661
|
})).optional().describe("Linked work items to remove"),
|
|
2642
2662
|
custom_fields: z.array(z.object({
|
|
@@ -2693,6 +2713,98 @@ export const ListCustomFieldDefinitionsSchema = z.object({
|
|
|
2693
2713
|
.default("issue")
|
|
2694
2714
|
.describe("The work item type to list custom field definitions for. Defaults to 'issue'."),
|
|
2695
2715
|
});
|
|
2716
|
+
// --- Emoji Reaction schemas (REST: MRs and Issues) ---
|
|
2717
|
+
const emojiNameField = z.string().describe("Name of the emoji without colons (e.g. 'thumbsup', 'rocket', 'eyes')");
|
|
2718
|
+
const awardIdField = z.coerce.string().describe("The ID of the emoji reaction to delete");
|
|
2719
|
+
const noteEmojiDiscussionField = z.coerce.string().optional().describe("The ID of a discussion thread. Required for notes that are discussion replies; omit for top-level notes.");
|
|
2720
|
+
export const CreateMergeRequestEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2721
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2722
|
+
name: emojiNameField,
|
|
2723
|
+
});
|
|
2724
|
+
export const DeleteMergeRequestEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2725
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2726
|
+
award_id: awardIdField,
|
|
2727
|
+
});
|
|
2728
|
+
export const CreateMergeRequestNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2729
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2730
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2731
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2732
|
+
name: emojiNameField,
|
|
2733
|
+
});
|
|
2734
|
+
export const DeleteMergeRequestNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2735
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2736
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2737
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2738
|
+
award_id: awardIdField,
|
|
2739
|
+
});
|
|
2740
|
+
export const CreateIssueEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2741
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2742
|
+
name: emojiNameField,
|
|
2743
|
+
});
|
|
2744
|
+
export const DeleteIssueEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2745
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2746
|
+
award_id: awardIdField,
|
|
2747
|
+
});
|
|
2748
|
+
export const CreateIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2749
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2750
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2751
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2752
|
+
name: emojiNameField,
|
|
2753
|
+
});
|
|
2754
|
+
export const DeleteIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2755
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2756
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2757
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2758
|
+
award_id: awardIdField,
|
|
2759
|
+
});
|
|
2760
|
+
// --- Emoji Reaction schemas (GraphQL: Work Items) ---
|
|
2761
|
+
export const CreateWorkItemEmojiReactionSchema = z.object({
|
|
2762
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2763
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2764
|
+
name: emojiNameField,
|
|
2765
|
+
});
|
|
2766
|
+
export const DeleteWorkItemEmojiReactionSchema = z.object({
|
|
2767
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2768
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2769
|
+
name: emojiNameField,
|
|
2770
|
+
});
|
|
2771
|
+
export const CreateWorkItemNoteEmojiReactionSchema = z.object({
|
|
2772
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2773
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2774
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2775
|
+
name: emojiNameField,
|
|
2776
|
+
});
|
|
2777
|
+
export const DeleteWorkItemNoteEmojiReactionSchema = z.object({
|
|
2778
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2779
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2780
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2781
|
+
name: emojiNameField,
|
|
2782
|
+
});
|
|
2783
|
+
export const ListMergeRequestEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2784
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2785
|
+
});
|
|
2786
|
+
export const ListMergeRequestNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2787
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2788
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2789
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2790
|
+
});
|
|
2791
|
+
export const ListIssueEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2792
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2793
|
+
});
|
|
2794
|
+
export const ListIssueNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2795
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2796
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2797
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2798
|
+
});
|
|
2799
|
+
export const ListWorkItemEmojiReactionsSchema = z.object({
|
|
2800
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2801
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2802
|
+
});
|
|
2803
|
+
export const ListWorkItemNoteEmojiReactionsSchema = z.object({
|
|
2804
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2805
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2806
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2807
|
+
});
|
|
2696
2808
|
// --- Incident Timeline Event schemas ---
|
|
2697
2809
|
export const GetTimelineEventsSchema = z.object({
|
|
2698
2810
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2740,7 +2852,7 @@ export const ListWebhookEventsSchema = z
|
|
|
2740
2852
|
.optional()
|
|
2741
2853
|
.describe("Filter by response status code (e.g. 200, 500) or category: successful, client_failure, server_failure"),
|
|
2742
2854
|
summary: z
|
|
2743
|
-
.boolean()
|
|
2855
|
+
.coerce.boolean()
|
|
2744
2856
|
.optional()
|
|
2745
2857
|
.describe("If true, return only summary fields (id, url, trigger, response_status, execution_duration) without full request/response payloads. Recommended for overview queries to avoid huge responses."),
|
|
2746
2858
|
per_page: z
|
|
@@ -139,6 +139,55 @@ describe("MCP OAuth — Discovery Endpoints", () => {
|
|
|
139
139
|
assert.ok(body.resource, "Should have resource field");
|
|
140
140
|
console.log(" ✓ Protected resource metadata returned");
|
|
141
141
|
});
|
|
142
|
+
test("path-prefixed MCP_SERVER_URL serves path-aware discovery metadata", async () => {
|
|
143
|
+
const mockPort = await findMockServerPort(MOCK_GITLAB_PORT_BASE + 25);
|
|
144
|
+
const prefixedMockGitLab = new MockGitLabServer({
|
|
145
|
+
port: mockPort,
|
|
146
|
+
validTokens: [MOCK_OAUTH_TOKEN],
|
|
147
|
+
});
|
|
148
|
+
await prefixedMockGitLab.start();
|
|
149
|
+
const scopedServers = [];
|
|
150
|
+
try {
|
|
151
|
+
const mockGitLabUrl = prefixedMockGitLab.getUrl();
|
|
152
|
+
addOAuthEndpoints(prefixedMockGitLab, MOCK_OAUTH_TOKEN, MOCK_CLIENT_ID, mockGitLabUrl);
|
|
153
|
+
const mcpPort = await findAvailablePort(MCP_SERVER_PORT_BASE + 25);
|
|
154
|
+
const mcpBaseUrl = `http://${HOST}:${mcpPort}`;
|
|
155
|
+
const issuerPath = "/gitlab-mcp";
|
|
156
|
+
const prefixedServerUrl = `${mcpBaseUrl}${issuerPath}`;
|
|
157
|
+
const server = await launchServer({
|
|
158
|
+
mode: TransportMode.STREAMABLE_HTTP,
|
|
159
|
+
port: mcpPort,
|
|
160
|
+
timeout: 5000,
|
|
161
|
+
env: {
|
|
162
|
+
STREAMABLE_HTTP: "true",
|
|
163
|
+
GITLAB_MCP_OAUTH: "true",
|
|
164
|
+
GITLAB_OAUTH_APP_ID: "test-oauth-app-id",
|
|
165
|
+
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
166
|
+
MCP_SERVER_URL: prefixedServerUrl,
|
|
167
|
+
MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL: "true",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
scopedServers.push(server);
|
|
171
|
+
const authMetadataRes = await fetch(`${mcpBaseUrl}/.well-known/oauth-authorization-server${issuerPath}`);
|
|
172
|
+
assert.strictEqual(authMetadataRes.status, 200, "Should return 200");
|
|
173
|
+
const authMetadata = (await authMetadataRes.json());
|
|
174
|
+
assert.strictEqual(authMetadata.issuer, prefixedServerUrl);
|
|
175
|
+
assert.strictEqual(authMetadata.authorization_endpoint, `${prefixedServerUrl}/authorize`);
|
|
176
|
+
assert.strictEqual(authMetadata.token_endpoint, `${prefixedServerUrl}/token`);
|
|
177
|
+
assert.strictEqual(authMetadata.registration_endpoint, `${prefixedServerUrl}/register`);
|
|
178
|
+
assert.strictEqual(authMetadata.revocation_endpoint, `${prefixedServerUrl}/revoke`);
|
|
179
|
+
const resourceMetadataRes = await fetch(`${mcpBaseUrl}/.well-known/oauth-protected-resource${issuerPath}/mcp`);
|
|
180
|
+
assert.strictEqual(resourceMetadataRes.status, 200, "Should return 200");
|
|
181
|
+
const resourceMetadata = (await resourceMetadataRes.json());
|
|
182
|
+
assert.strictEqual(resourceMetadata.resource, prefixedServerUrl);
|
|
183
|
+
assert.deepStrictEqual(resourceMetadata.authorization_servers, [prefixedServerUrl]);
|
|
184
|
+
console.log(" ✓ Path-prefixed discovery metadata returned at RFC well-known URLs");
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
cleanupServers(scopedServers);
|
|
188
|
+
await prefixedMockGitLab.stop();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
142
191
|
});
|
|
143
192
|
// ---------------------------------------------------------------------------
|
|
144
193
|
// Test suite: /mcp auth enforcement
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ts-node
|
|
2
|
-
import { GetFileContentsSchema, GitLabFileContentSchema, CreatePipelineSchema, CreateIssueNoteSchema } from '../schemas.js';
|
|
2
|
+
import { GetFileContentsSchema, GitLabFileContentSchema, CreatePipelineSchema, CreateIssueNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateIssueEmojiReactionSchema, DeleteMergeRequestEmojiReactionSchema, DeleteIssueEmojiReactionSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema } from '../schemas.js';
|
|
3
3
|
function runGetFileContentsSchemaTests() {
|
|
4
4
|
console.log('🧪 Testing GetFileContentsSchema...');
|
|
5
5
|
const cases = [
|
|
@@ -371,13 +371,69 @@ function runCreateIssueNoteSchemaTests() {
|
|
|
371
371
|
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
372
372
|
return { passed, failed };
|
|
373
373
|
}
|
|
374
|
+
function runEmojiReactionSchemaTests() {
|
|
375
|
+
console.log('\n🧪 Testing Emoji Reaction Schemas...');
|
|
376
|
+
const cases = [
|
|
377
|
+
{ name: 'schema:create_mr_emoji:valid', schema: CreateMergeRequestEmojiReactionSchema, input: { project_id: 'my/project', merge_request_iid: '42', name: 'thumbsup' }, expected: { project_id: 'my/project', merge_request_iid: '42', name: 'thumbsup' } },
|
|
378
|
+
{ name: 'schema:create_mr_emoji:numeric-iid-coerced', schema: CreateMergeRequestEmojiReactionSchema, input: { project_id: 'my/project', merge_request_iid: 42, name: 'rocket' }, expected: { merge_request_iid: '42', name: 'rocket' } },
|
|
379
|
+
{ name: 'schema:create_mr_emoji:reject-missing-name', schema: CreateMergeRequestEmojiReactionSchema, input: { project_id: 'my/project', merge_request_iid: '42' }, shouldFail: true },
|
|
380
|
+
{ name: 'schema:delete_mr_emoji:valid', schema: DeleteMergeRequestEmojiReactionSchema, input: { project_id: 'my/project', merge_request_iid: '42', award_id: '123' }, expected: { merge_request_iid: '42', award_id: '123' } },
|
|
381
|
+
{ name: 'schema:create_issue_emoji:valid', schema: CreateIssueEmojiReactionSchema, input: { project_id: 'my/project', issue_iid: '10', name: 'thumbsdown' }, expected: { issue_iid: '10', name: 'thumbsdown' } },
|
|
382
|
+
{ name: 'schema:create_issue_emoji:reject-missing-name', schema: CreateIssueEmojiReactionSchema, input: { project_id: 'my/project', issue_iid: '10' }, shouldFail: true },
|
|
383
|
+
{ name: 'schema:delete_issue_emoji:valid', schema: DeleteIssueEmojiReactionSchema, input: { project_id: 'my/project', issue_iid: '10', award_id: '99' }, expected: { issue_iid: '10', award_id: '99' } },
|
|
384
|
+
{ name: 'schema:create_work_item_emoji:valid', schema: CreateWorkItemEmojiReactionSchema, input: { project_id: 'my/project', iid: 5, name: 'rocket' }, expected: { iid: 5, name: 'rocket' } },
|
|
385
|
+
{ name: 'schema:create_work_item_emoji:reject-missing-name', schema: CreateWorkItemEmojiReactionSchema, input: { project_id: 'my/project', iid: 5 }, shouldFail: true },
|
|
386
|
+
{ name: 'schema:create_work_item_note_emoji:valid', schema: CreateWorkItemNoteEmojiReactionSchema, input: { project_id: 'my/project', iid: 5, note_id: 'gid://gitlab/Note/123', name: 'thumbsup' }, expected: { iid: 5, note_id: 'gid://gitlab/Note/123', name: 'thumbsup' } },
|
|
387
|
+
{ name: 'schema:create_work_item_note_emoji:reject-missing-note-id', schema: CreateWorkItemNoteEmojiReactionSchema, input: { project_id: 'my/project', iid: 5, name: 'thumbsup' }, shouldFail: true },
|
|
388
|
+
];
|
|
389
|
+
let passed = 0;
|
|
390
|
+
let failed = 0;
|
|
391
|
+
cases.forEach(testCase => {
|
|
392
|
+
const result = { name: testCase.name, status: 'failed' };
|
|
393
|
+
const parsed = testCase.schema.safeParse(testCase.input);
|
|
394
|
+
if (testCase.shouldFail) {
|
|
395
|
+
if (parsed.success) {
|
|
396
|
+
result.error = 'Expected schema validation to fail';
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
result.status = 'passed';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (parsed.success) {
|
|
403
|
+
const expected = testCase.expected || {};
|
|
404
|
+
const matches = Object.entries(expected).every(([key, value]) => {
|
|
405
|
+
return parsed.data[key] === value;
|
|
406
|
+
});
|
|
407
|
+
if (matches) {
|
|
408
|
+
result.status = 'passed';
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
result.error = parsed.error?.message || 'Schema validation failed';
|
|
416
|
+
}
|
|
417
|
+
if (result.status === 'passed') {
|
|
418
|
+
passed++;
|
|
419
|
+
console.log(`✅ ${result.name}`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
failed++;
|
|
423
|
+
console.log(`❌ ${result.name}: ${result.error}`);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
427
|
+
return { passed, failed };
|
|
428
|
+
}
|
|
374
429
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
375
430
|
const getFileContentsResult = runGetFileContentsSchemaTests();
|
|
376
431
|
const fileContentResult = runGitLabFileContentSchemaTests();
|
|
377
432
|
const createPipelineResult = runCreatePipelineSchemaTests();
|
|
378
433
|
const createIssueNoteResult = runCreateIssueNoteSchemaTests();
|
|
379
|
-
const
|
|
380
|
-
const
|
|
434
|
+
const emojiReactionResult = runEmojiReactionSchemaTests();
|
|
435
|
+
const totalPassed = getFileContentsResult.passed + fileContentResult.passed + createPipelineResult.passed + createIssueNoteResult.passed + emojiReactionResult.passed;
|
|
436
|
+
const totalFailed = getFileContentsResult.failed + fileContentResult.failed + createPipelineResult.failed + createIssueNoteResult.failed + emojiReactionResult.failed;
|
|
381
437
|
console.log(`\nTotal Results: ${totalPassed} passed, ${totalFailed} failed`);
|
|
382
438
|
if (totalFailed > 0) {
|
|
383
439
|
process.exit(1);
|