zephyr-scale-mcp-server 0.4.3 → 0.4.5
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/build/tool-handlers.js +120 -20
- package/build/tool-schemas.js +12 -10
- package/package.json +1 -1
- package/src/tool-handlers.ts +122 -23
- package/src/tool-schemas.ts +12 -10
- package/src/types.ts +2 -1
package/build/tool-handlers.js
CHANGED
|
@@ -108,13 +108,19 @@ export class ZephyrToolHandlers {
|
|
|
108
108
|
if (!test_script)
|
|
109
109
|
return;
|
|
110
110
|
if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
|
|
111
|
-
const items = test_script.steps.map((step) =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
const items = test_script.steps.map((step) => {
|
|
112
|
+
// If step is a call-to-test (testCaseKey), use the testCase variant
|
|
113
|
+
if (step.testCaseKey) {
|
|
114
|
+
return { testCase: { testCaseKey: step.testCaseKey } };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
inline: {
|
|
118
|
+
description: step.description || '',
|
|
119
|
+
testData: step.testData || null,
|
|
120
|
+
expectedResult: step.expectedResult || null,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
118
124
|
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`, { mode: 'OVERWRITE', items });
|
|
119
125
|
}
|
|
120
126
|
else if (test_script.type === 'BDD' && test_script.text) {
|
|
@@ -206,22 +212,82 @@ export class ZephyrToolHandlers {
|
|
|
206
212
|
const { test_case_key, bdd_content, name } = args;
|
|
207
213
|
const converted = convertToGherkin(bdd_content);
|
|
208
214
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
215
|
+
// Fetch the test case first to get the numeric ID.
|
|
216
|
+
// After a DC→Cloud migration, the project key prefix in the test case key (e.g. "CNIDS")
|
|
217
|
+
// may no longer match an active Cloud project, causing key-based write endpoints to return 404.
|
|
218
|
+
// Falling back to the numeric ID bypasses that project-key validation.
|
|
219
|
+
let tc = null;
|
|
220
|
+
try {
|
|
221
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
222
|
+
tc = getResponse.data;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// GET failed — proceed with key only; write will surface the real error
|
|
226
|
+
}
|
|
209
227
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
// Primary path: POST to dedicated testscript endpoint using the key
|
|
229
|
+
let scriptUpdateError = null;
|
|
230
|
+
try {
|
|
231
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`, { type: 'bdd', text: finalText });
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
scriptUpdateError = err;
|
|
235
|
+
}
|
|
236
|
+
// Fallback: if testscript POST failed (e.g. migrated project key), try PUT on the full
|
|
237
|
+
// test case record with the testScript field embedded — some Cloud instances accept this
|
|
238
|
+
// for migrated test cases where the project is deactivated.
|
|
239
|
+
if (scriptUpdateError) {
|
|
240
|
+
if (!tc) {
|
|
241
|
+
throw scriptUpdateError; // no test case data to build PUT payload, surface original error
|
|
242
|
+
}
|
|
243
|
+
const putPayload = {
|
|
244
|
+
id: tc.id,
|
|
245
|
+
key: test_case_key,
|
|
246
|
+
name: (typeof name === 'string' && name.trim().length > 0) ? name : tc.name,
|
|
247
|
+
status: tc.status,
|
|
248
|
+
priority: tc.priority,
|
|
249
|
+
project: tc.project,
|
|
250
|
+
testScript: { type: 'bdd', text: finalText },
|
|
251
|
+
};
|
|
252
|
+
for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
|
|
253
|
+
if (tc[field] !== undefined && tc[field] !== null)
|
|
254
|
+
putPayload[field] = tc[field];
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(tc.labels) && tc.labels.length > 0)
|
|
257
|
+
putPayload.labels = tc.labels;
|
|
258
|
+
if (tc.customFields && Object.keys(tc.customFields).length > 0)
|
|
259
|
+
putPayload.customFields = tc.customFields;
|
|
260
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text',
|
|
264
|
+
text: `✅ Updated ${test_case_key} with BDD content successfully (Cloud v2, via PUT fallback for migrated project)`,
|
|
265
|
+
}],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Primary path succeeded — optionally rename
|
|
212
269
|
if (typeof name === 'string' && name.trim().length > 0) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
if (!tc) {
|
|
271
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
272
|
+
tc = getResponse.data;
|
|
273
|
+
}
|
|
274
|
+
const putPayload = {
|
|
218
275
|
id: tc.id,
|
|
219
276
|
key: test_case_key,
|
|
220
277
|
name,
|
|
221
278
|
status: tc.status,
|
|
222
279
|
priority: tc.priority,
|
|
223
280
|
project: tc.project,
|
|
224
|
-
}
|
|
281
|
+
};
|
|
282
|
+
for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
|
|
283
|
+
if (tc[field] !== undefined && tc[field] !== null)
|
|
284
|
+
putPayload[field] = tc[field];
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(tc.labels) && tc.labels.length > 0)
|
|
287
|
+
putPayload.labels = tc.labels;
|
|
288
|
+
if (tc.customFields && Object.keys(tc.customFields).length > 0)
|
|
289
|
+
putPayload.customFields = tc.customFields;
|
|
290
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
225
291
|
}
|
|
226
292
|
return {
|
|
227
293
|
content: [{
|
|
@@ -454,9 +520,10 @@ export class ZephyrToolHandlers {
|
|
|
454
520
|
return this.createTestRunDC(args);
|
|
455
521
|
}
|
|
456
522
|
async createTestRunCloud(args) {
|
|
457
|
-
const { project_key, name, test_case_keys, folder, planned_start_date, planned_end_date, description, owner, environment, custom_fields, } = args;
|
|
523
|
+
const { project_key, name, test_case_keys, folder, planned_start_date, planned_end_date, description, owner, environment, custom_fields, issue_links, issue_key, jira_project_version, } = args;
|
|
458
524
|
// Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
|
|
459
|
-
// plannedEndDate, statusName, folderId, ownerId, customFields
|
|
525
|
+
// plannedEndDate, statusName, folderId, ownerId, jiraProjectVersion, customFields
|
|
526
|
+
// Note: environment is NOT a TestCycleInput field on Cloud — it belongs on TestExecutionInput
|
|
460
527
|
const payload = { projectKey: project_key, name };
|
|
461
528
|
if (description)
|
|
462
529
|
payload.description = description;
|
|
@@ -469,6 +536,9 @@ export class ZephyrToolHandlers {
|
|
|
469
536
|
// Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
|
|
470
537
|
if (owner)
|
|
471
538
|
payload.ownerId = owner;
|
|
539
|
+
// Link to a Jira project version/release (integer ID)
|
|
540
|
+
if (jira_project_version)
|
|
541
|
+
payload.jiraProjectVersion = jira_project_version;
|
|
472
542
|
if (folder) {
|
|
473
543
|
const folderId = await resolveFolderIdByPath(this.axiosInstance, project_key, folder, 'TEST_CYCLE');
|
|
474
544
|
if (folderId !== null)
|
|
@@ -483,14 +553,43 @@ export class ZephyrToolHandlers {
|
|
|
483
553
|
// Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
|
|
484
554
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
485
555
|
for (const testCaseKey of test_case_keys) {
|
|
486
|
-
|
|
556
|
+
const execPayload = {
|
|
487
557
|
projectKey: project_key,
|
|
488
558
|
testCaseKey,
|
|
489
559
|
testCycleKey: cycleKey,
|
|
490
560
|
statusName: 'Not Executed',
|
|
491
|
-
}
|
|
561
|
+
};
|
|
562
|
+
// environment is set at execution level on Cloud, not cycle level
|
|
563
|
+
if (environment)
|
|
564
|
+
execPayload.environmentName = environment;
|
|
565
|
+
await this.axiosInstance.post('/testexecutions', execPayload);
|
|
492
566
|
}
|
|
493
567
|
}
|
|
568
|
+
// Step 3: link Jira issues via POST /testcycles/{key}/links/issues
|
|
569
|
+
// Merge issue_key (single) and issue_links (array) into one list
|
|
570
|
+
const allIssueLinks = [
|
|
571
|
+
...(issue_key ? [issue_key] : []),
|
|
572
|
+
...(issue_links ?? []),
|
|
573
|
+
];
|
|
574
|
+
const linkWarnings = [];
|
|
575
|
+
if (allIssueLinks.length > 0) {
|
|
576
|
+
for (const ik of allIssueLinks) {
|
|
577
|
+
try {
|
|
578
|
+
const issueId = await this.resolveJiraIssueId(ik);
|
|
579
|
+
await this.axiosInstance.post(`${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/links/issues`, { issueId });
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
linkWarnings.push(`${ik}: ${this.formatError(e)}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
587
|
+
const credHint = missingCreds && linkWarnings.length > 0
|
|
588
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
|
|
589
|
+
: '';
|
|
590
|
+
const warningText = linkWarnings.length > 0
|
|
591
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
592
|
+
: '';
|
|
494
593
|
return {
|
|
495
594
|
content: [{
|
|
496
595
|
type: 'text',
|
|
@@ -498,7 +597,8 @@ export class ZephyrToolHandlers {
|
|
|
498
597
|
key: cycleKey,
|
|
499
598
|
name,
|
|
500
599
|
testCaseCount: test_case_keys?.length || 0,
|
|
501
|
-
|
|
600
|
+
linkedIssues: allIssueLinks.length - linkWarnings.length,
|
|
601
|
+
}, null, 2)}${warningText}`,
|
|
502
602
|
}],
|
|
503
603
|
};
|
|
504
604
|
}
|
package/build/tool-schemas.js
CHANGED
|
@@ -74,14 +74,12 @@ export const toolSchemas = [
|
|
|
74
74
|
},
|
|
75
75
|
status: {
|
|
76
76
|
type: 'string',
|
|
77
|
-
description: 'Test case status (optional)',
|
|
78
|
-
enum: ['Draft', 'Approved', 'Deprecated'],
|
|
77
|
+
description: 'Test case status (optional, default: "Draft"). Value must match a status name configured in your Zephyr project (e.g. "Draft", "Approved", "Deprecated"). Note: always overridden to "Draft" on creation.',
|
|
79
78
|
default: 'Draft',
|
|
80
79
|
},
|
|
81
80
|
priority: {
|
|
82
81
|
type: 'string',
|
|
83
|
-
description: 'Test case priority (optional)',
|
|
84
|
-
enum: ['High', 'Normal', 'Low'],
|
|
82
|
+
description: 'Test case priority (optional). Value must match a priority name configured in your Zephyr project (e.g. "High", "Normal", "Low", "Critical"). Use zephyr://testcase/EXISTING-KEY to check your project\'s valid values.',
|
|
85
83
|
},
|
|
86
84
|
precondition: {
|
|
87
85
|
type: 'string',
|
|
@@ -206,7 +204,7 @@ export const toolSchemas = [
|
|
|
206
204
|
properties: {
|
|
207
205
|
test_run_key: {
|
|
208
206
|
type: 'string',
|
|
209
|
-
description: 'Test run key (e.g., PROJ-
|
|
207
|
+
description: 'Test run key (e.g., PROJ-R123)',
|
|
210
208
|
},
|
|
211
209
|
},
|
|
212
210
|
required: ['test_run_key'],
|
|
@@ -271,17 +269,21 @@ export const toolSchemas = [
|
|
|
271
269
|
},
|
|
272
270
|
environment: {
|
|
273
271
|
type: 'string',
|
|
274
|
-
description: 'Test environment (optional)',
|
|
272
|
+
description: 'Test environment name (optional). On Cloud, applied to each test execution (environmentName). On Data Center, set at cycle level.',
|
|
275
273
|
},
|
|
276
274
|
issue_key: {
|
|
277
275
|
type: 'string',
|
|
278
|
-
description: 'Single issue key to link to the test
|
|
276
|
+
description: 'Single Jira issue key to link to the test cycle (e.g. "PROJ-123"). On Cloud, resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars.',
|
|
279
277
|
},
|
|
280
278
|
issue_links: {
|
|
281
279
|
type: 'array',
|
|
282
|
-
description: 'Array of issue
|
|
280
|
+
description: 'Array of Jira issue keys to link to the test cycle (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars. Failures are reported as warnings and do not fail the tool call.',
|
|
283
281
|
items: { type: 'string' },
|
|
284
282
|
},
|
|
283
|
+
jira_project_version: {
|
|
284
|
+
type: 'integer',
|
|
285
|
+
description: 'Jira project version/release ID to link this test cycle to (optional, Cloud only — use the numeric version ID).',
|
|
286
|
+
},
|
|
285
287
|
custom_fields: {
|
|
286
288
|
type: 'object',
|
|
287
289
|
description: 'Custom fields object (optional)',
|
|
@@ -316,7 +318,7 @@ export const toolSchemas = [
|
|
|
316
318
|
},
|
|
317
319
|
test_run_keys: {
|
|
318
320
|
type: 'array',
|
|
319
|
-
description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-
|
|
321
|
+
description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-R152", "PROJ-R161"])',
|
|
320
322
|
items: { type: 'string' },
|
|
321
323
|
minItems: 1
|
|
322
324
|
},
|
|
@@ -395,7 +397,7 @@ export const toolSchemas = [
|
|
|
395
397
|
properties: {
|
|
396
398
|
test_run_key: {
|
|
397
399
|
type: 'string',
|
|
398
|
-
description: 'Test run key (e.g., PROJ-
|
|
400
|
+
description: 'Test run key (e.g., PROJ-R161)',
|
|
399
401
|
},
|
|
400
402
|
test_case_keys: {
|
|
401
403
|
type: 'array',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephyr-scale-mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Zephyr Scale test case management with comprehensive STEP_BY_STEP, PLAIN_TEXT, and BDD support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
package/src/tool-handlers.ts
CHANGED
|
@@ -124,13 +124,19 @@ export class ZephyrToolHandlers {
|
|
|
124
124
|
if (!test_script) return;
|
|
125
125
|
|
|
126
126
|
if (test_script.type === 'STEP_BY_STEP' && test_script.steps && test_script.steps.length > 0) {
|
|
127
|
-
const items = test_script.steps.map((step: any) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
const items = test_script.steps.map((step: any) => {
|
|
128
|
+
// If step is a call-to-test (testCaseKey), use the testCase variant
|
|
129
|
+
if (step.testCaseKey) {
|
|
130
|
+
return { testCase: { testCaseKey: step.testCaseKey } };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
inline: {
|
|
134
|
+
description: step.description || '',
|
|
135
|
+
testData: step.testData || null,
|
|
136
|
+
expectedResult: step.expectedResult || null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
});
|
|
134
140
|
await this.axiosInstance.post(
|
|
135
141
|
`${this.jiraConfig.apiEndpoints.testcase}/${testKey}/teststeps`,
|
|
136
142
|
{ mode: 'OVERWRITE', items }
|
|
@@ -223,26 +229,82 @@ export class ZephyrToolHandlers {
|
|
|
223
229
|
const converted = convertToGherkin(bdd_content);
|
|
224
230
|
const finalText = converted && converted.trim().length > 0 ? converted : bdd_content;
|
|
225
231
|
|
|
232
|
+
// Fetch the test case first to get the numeric ID.
|
|
233
|
+
// After a DC→Cloud migration, the project key prefix in the test case key (e.g. "CNIDS")
|
|
234
|
+
// may no longer match an active Cloud project, causing key-based write endpoints to return 404.
|
|
235
|
+
// Falling back to the numeric ID bypasses that project-key validation.
|
|
236
|
+
let tc: any = null;
|
|
226
237
|
try {
|
|
227
|
-
await this.axiosInstance.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
239
|
+
tc = getResponse.data;
|
|
240
|
+
} catch {
|
|
241
|
+
// GET failed — proceed with key only; write will surface the real error
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Primary path: POST to dedicated testscript endpoint using the key
|
|
246
|
+
let scriptUpdateError: any = null;
|
|
247
|
+
try {
|
|
248
|
+
await this.axiosInstance.post(
|
|
249
|
+
`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}/testscript`,
|
|
250
|
+
{ type: 'bdd', text: finalText }
|
|
251
|
+
);
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
scriptUpdateError = err;
|
|
254
|
+
}
|
|
231
255
|
|
|
232
|
-
//
|
|
256
|
+
// Fallback: if testscript POST failed (e.g. migrated project key), try PUT on the full
|
|
257
|
+
// test case record with the testScript field embedded — some Cloud instances accept this
|
|
258
|
+
// for migrated test cases where the project is deactivated.
|
|
259
|
+
if (scriptUpdateError) {
|
|
260
|
+
if (!tc) {
|
|
261
|
+
throw scriptUpdateError; // no test case data to build PUT payload, surface original error
|
|
262
|
+
}
|
|
263
|
+
const putPayload: any = {
|
|
264
|
+
id: tc.id,
|
|
265
|
+
key: test_case_key,
|
|
266
|
+
name: (typeof name === 'string' && name.trim().length > 0) ? name : tc.name,
|
|
267
|
+
status: tc.status,
|
|
268
|
+
priority: tc.priority,
|
|
269
|
+
project: tc.project,
|
|
270
|
+
testScript: { type: 'bdd', text: finalText },
|
|
271
|
+
};
|
|
272
|
+
for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
|
|
273
|
+
if (tc[field] !== undefined && tc[field] !== null) putPayload[field] = tc[field];
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(tc.labels) && tc.labels.length > 0) putPayload.labels = tc.labels;
|
|
276
|
+
if (tc.customFields && Object.keys(tc.customFields).length > 0) putPayload.customFields = tc.customFields;
|
|
277
|
+
|
|
278
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
content: [{
|
|
282
|
+
type: 'text',
|
|
283
|
+
text: `✅ Updated ${test_case_key} with BDD content successfully (Cloud v2, via PUT fallback for migrated project)`,
|
|
284
|
+
}],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Primary path succeeded — optionally rename
|
|
233
289
|
if (typeof name === 'string' && name.trim().length > 0) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
290
|
+
if (!tc) {
|
|
291
|
+
const getResponse = await this.axiosInstance.get(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`);
|
|
292
|
+
tc = getResponse.data;
|
|
293
|
+
}
|
|
294
|
+
const putPayload: any = {
|
|
239
295
|
id: tc.id,
|
|
240
296
|
key: test_case_key,
|
|
241
297
|
name,
|
|
242
298
|
status: tc.status,
|
|
243
299
|
priority: tc.priority,
|
|
244
300
|
project: tc.project,
|
|
245
|
-
}
|
|
301
|
+
};
|
|
302
|
+
for (const field of ['objective', 'precondition', 'estimatedTime', 'component', 'owner', 'folder']) {
|
|
303
|
+
if (tc[field] !== undefined && tc[field] !== null) putPayload[field] = tc[field];
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(tc.labels) && tc.labels.length > 0) putPayload.labels = tc.labels;
|
|
306
|
+
if (tc.customFields && Object.keys(tc.customFields).length > 0) putPayload.customFields = tc.customFields;
|
|
307
|
+
await this.axiosInstance.put(`${this.jiraConfig.apiEndpoints.testcase}/${test_case_key}`, putPayload);
|
|
246
308
|
}
|
|
247
309
|
|
|
248
310
|
return {
|
|
@@ -504,11 +566,13 @@ export class ZephyrToolHandlers {
|
|
|
504
566
|
const {
|
|
505
567
|
project_key, name, test_case_keys, folder,
|
|
506
568
|
planned_start_date, planned_end_date, description,
|
|
507
|
-
owner, environment, custom_fields,
|
|
569
|
+
owner, environment, custom_fields, issue_links, issue_key,
|
|
570
|
+
jira_project_version,
|
|
508
571
|
} = args;
|
|
509
572
|
|
|
510
573
|
// Cloud v2 TestCycleInput: projectKey, name, description, plannedStartDate,
|
|
511
|
-
// plannedEndDate, statusName, folderId, ownerId, customFields
|
|
574
|
+
// plannedEndDate, statusName, folderId, ownerId, jiraProjectVersion, customFields
|
|
575
|
+
// Note: environment is NOT a TestCycleInput field on Cloud — it belongs on TestExecutionInput
|
|
512
576
|
const payload: any = { projectKey: project_key, name };
|
|
513
577
|
|
|
514
578
|
if (description) payload.description = description;
|
|
@@ -517,6 +581,8 @@ export class ZephyrToolHandlers {
|
|
|
517
581
|
if (custom_fields) payload.customFields = custom_fields;
|
|
518
582
|
// Cloud v2 TestCycleInput supports ownerId (Jira Account ID)
|
|
519
583
|
if (owner) payload.ownerId = owner;
|
|
584
|
+
// Link to a Jira project version/release (integer ID)
|
|
585
|
+
if (jira_project_version) payload.jiraProjectVersion = jira_project_version;
|
|
520
586
|
if (folder) {
|
|
521
587
|
const folderId = await resolveFolderIdByPath(
|
|
522
588
|
this.axiosInstance, project_key, folder, 'TEST_CYCLE'
|
|
@@ -535,15 +601,47 @@ export class ZephyrToolHandlers {
|
|
|
535
601
|
// Step 2: add test cases via test executions (Cloud v2 has no /testcycles/{key}/testcases)
|
|
536
602
|
if (test_case_keys && test_case_keys.length > 0) {
|
|
537
603
|
for (const testCaseKey of test_case_keys) {
|
|
538
|
-
|
|
604
|
+
const execPayload: any = {
|
|
539
605
|
projectKey: project_key,
|
|
540
606
|
testCaseKey,
|
|
541
607
|
testCycleKey: cycleKey,
|
|
542
608
|
statusName: 'Not Executed',
|
|
543
|
-
}
|
|
609
|
+
};
|
|
610
|
+
// environment is set at execution level on Cloud, not cycle level
|
|
611
|
+
if (environment) execPayload.environmentName = environment;
|
|
612
|
+
await this.axiosInstance.post('/testexecutions', execPayload);
|
|
544
613
|
}
|
|
545
614
|
}
|
|
546
615
|
|
|
616
|
+
// Step 3: link Jira issues via POST /testcycles/{key}/links/issues
|
|
617
|
+
// Merge issue_key (single) and issue_links (array) into one list
|
|
618
|
+
const allIssueLinks = [
|
|
619
|
+
...(issue_key ? [issue_key] : []),
|
|
620
|
+
...(issue_links ?? []),
|
|
621
|
+
];
|
|
622
|
+
const linkWarnings: string[] = [];
|
|
623
|
+
if (allIssueLinks.length > 0) {
|
|
624
|
+
for (const ik of allIssueLinks) {
|
|
625
|
+
try {
|
|
626
|
+
const issueId = await this.resolveJiraIssueId(ik);
|
|
627
|
+
await this.axiosInstance.post(
|
|
628
|
+
`${this.jiraConfig.apiEndpoints.testrun}/${cycleKey}/links/issues`,
|
|
629
|
+
{ issueId }
|
|
630
|
+
);
|
|
631
|
+
} catch (e) {
|
|
632
|
+
linkWarnings.push(`${ik}: ${this.formatError(e)}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const missingCreds = !process.env.JIRA_USERNAME || !process.env.JIRA_API_TOKEN;
|
|
638
|
+
const credHint = missingCreds && linkWarnings.length > 0
|
|
639
|
+
? '\n💡 Tip: Set JIRA_USERNAME and JIRA_API_TOKEN env vars to enable issue linking on Cloud.'
|
|
640
|
+
: '';
|
|
641
|
+
const warningText = linkWarnings.length > 0
|
|
642
|
+
? `\n⚠️ Some issue links failed:\n${linkWarnings.map(w => ` - ${w}`).join('\n')}${credHint}`
|
|
643
|
+
: '';
|
|
644
|
+
|
|
547
645
|
return {
|
|
548
646
|
content: [{
|
|
549
647
|
type: 'text',
|
|
@@ -551,7 +649,8 @@ export class ZephyrToolHandlers {
|
|
|
551
649
|
key: cycleKey,
|
|
552
650
|
name,
|
|
553
651
|
testCaseCount: test_case_keys?.length || 0,
|
|
554
|
-
|
|
652
|
+
linkedIssues: allIssueLinks.length - linkWarnings.length,
|
|
653
|
+
}, null, 2)}${warningText}`,
|
|
555
654
|
}],
|
|
556
655
|
};
|
|
557
656
|
} catch (error) {
|
package/src/tool-schemas.ts
CHANGED
|
@@ -74,14 +74,12 @@ export const toolSchemas = [
|
|
|
74
74
|
},
|
|
75
75
|
status: {
|
|
76
76
|
type: 'string',
|
|
77
|
-
description: 'Test case status (optional)',
|
|
78
|
-
enum: ['Draft', 'Approved', 'Deprecated'],
|
|
77
|
+
description: 'Test case status (optional, default: "Draft"). Value must match a status name configured in your Zephyr project (e.g. "Draft", "Approved", "Deprecated"). Note: always overridden to "Draft" on creation.',
|
|
79
78
|
default: 'Draft',
|
|
80
79
|
},
|
|
81
80
|
priority: {
|
|
82
81
|
type: 'string',
|
|
83
|
-
description: 'Test case priority (optional)',
|
|
84
|
-
enum: ['High', 'Normal', 'Low'],
|
|
82
|
+
description: 'Test case priority (optional). Value must match a priority name configured in your Zephyr project (e.g. "High", "Normal", "Low", "Critical"). Use zephyr://testcase/EXISTING-KEY to check your project\'s valid values.',
|
|
85
83
|
},
|
|
86
84
|
precondition: {
|
|
87
85
|
type: 'string',
|
|
@@ -206,7 +204,7 @@ export const toolSchemas = [
|
|
|
206
204
|
properties: {
|
|
207
205
|
test_run_key: {
|
|
208
206
|
type: 'string',
|
|
209
|
-
description: 'Test run key (e.g., PROJ-
|
|
207
|
+
description: 'Test run key (e.g., PROJ-R123)',
|
|
210
208
|
},
|
|
211
209
|
},
|
|
212
210
|
required: ['test_run_key'],
|
|
@@ -271,17 +269,21 @@ export const toolSchemas = [
|
|
|
271
269
|
},
|
|
272
270
|
environment: {
|
|
273
271
|
type: 'string',
|
|
274
|
-
description: 'Test environment (optional)',
|
|
272
|
+
description: 'Test environment name (optional). On Cloud, applied to each test execution (environmentName). On Data Center, set at cycle level.',
|
|
275
273
|
},
|
|
276
274
|
issue_key: {
|
|
277
275
|
type: 'string',
|
|
278
|
-
description: 'Single issue key to link to the test
|
|
276
|
+
description: 'Single Jira issue key to link to the test cycle (e.g. "PROJ-123"). On Cloud, resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars.',
|
|
279
277
|
},
|
|
280
278
|
issue_links: {
|
|
281
279
|
type: 'array',
|
|
282
|
-
description: 'Array of issue
|
|
280
|
+
description: 'Array of Jira issue keys to link to the test cycle (e.g. ["PROJ-123", "PROJ-456"]). On Cloud, each key is resolved to a numeric ID via Jira REST API — requires JIRA_USERNAME + JIRA_API_TOKEN env vars. Failures are reported as warnings and do not fail the tool call.',
|
|
283
281
|
items: { type: 'string' },
|
|
284
282
|
},
|
|
283
|
+
jira_project_version: {
|
|
284
|
+
type: 'integer',
|
|
285
|
+
description: 'Jira project version/release ID to link this test cycle to (optional, Cloud only — use the numeric version ID).',
|
|
286
|
+
},
|
|
285
287
|
custom_fields: {
|
|
286
288
|
type: 'object',
|
|
287
289
|
description: 'Custom fields object (optional)',
|
|
@@ -316,7 +318,7 @@ export const toolSchemas = [
|
|
|
316
318
|
},
|
|
317
319
|
test_run_keys: {
|
|
318
320
|
type: 'array',
|
|
319
|
-
description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-
|
|
321
|
+
description: 'Array of test run keys to search in (required for Data Center, optional for Cloud — e.g., ["PROJ-R152", "PROJ-R161"])',
|
|
320
322
|
items: { type: 'string' },
|
|
321
323
|
minItems: 1
|
|
322
324
|
},
|
|
@@ -395,7 +397,7 @@ export const toolSchemas = [
|
|
|
395
397
|
properties: {
|
|
396
398
|
test_run_key: {
|
|
397
399
|
type: 'string',
|
|
398
|
-
description: 'Test run key (e.g., PROJ-
|
|
400
|
+
description: 'Test run key (e.g., PROJ-R161)',
|
|
399
401
|
},
|
|
400
402
|
test_case_keys: {
|
|
401
403
|
type: 'array',
|
package/src/types.ts
CHANGED
|
@@ -64,7 +64,8 @@ export interface TestRunArgs {
|
|
|
64
64
|
planned_end_date?: string;
|
|
65
65
|
description?: string;
|
|
66
66
|
owner?: string;
|
|
67
|
-
environment?: string;
|
|
67
|
+
environment?: string; // Cloud: mapped to environmentName on each TestExecutionInput; DC: cycle-level field
|
|
68
|
+
jira_project_version?: number; // Cloud only: Jira project version/release ID (integer)
|
|
68
69
|
issue_key?: string;
|
|
69
70
|
issue_links?: string[];
|
|
70
71
|
custom_fields?: Record<string, any>;
|