specweave 0.28.19 → 0.28.22
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/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +8 -6
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +230 -165
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +2 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js +25 -5
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +7 -3
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +193 -23
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
- package/dist/src/cli/helpers/init/language-selection.js +1 -1
- package/dist/src/cli/helpers/init/language-selection.js.map +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js +1 -1
- package/dist/src/cli/helpers/init/repository-setup.js.map +1 -1
- package/dist/src/config/types.d.ts +6 -6
- package/dist/src/core/background/index.d.ts +11 -0
- package/dist/src/core/background/index.d.ts.map +1 -0
- package/dist/src/core/background/index.js +11 -0
- package/dist/src/core/background/index.js.map +1 -0
- package/dist/src/core/background/job-manager.d.ts +65 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -0
- package/dist/src/core/background/job-manager.js +192 -0
- package/dist/src/core/background/job-manager.js.map +1 -0
- package/dist/src/core/background/types.d.ts +59 -0
- package/dist/src/core/background/types.d.ts.map +1 -0
- package/dist/src/core/background/types.js +8 -0
- package/dist/src/core/background/types.js.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
- package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
- package/dist/src/core/repo-structure/prompt-consolidator.js +6 -36
- package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
- package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
- package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
- package/dist/src/core/repo-structure/repo-initializer.js +260 -0
- package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/types/spec-metadata.d.ts +2 -0
- package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
- package/dist/src/importers/import-coordinator.d.ts +20 -0
- package/dist/src/importers/import-coordinator.d.ts.map +1 -1
- package/dist/src/importers/import-coordinator.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +2 -2
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-jobs.md +160 -0
- package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
- package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
- package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +6 -0
- package/plugins/specweave-github/lib/github-increment-sync-cli.js +268 -155
- package/plugins/specweave-github/lib/github-increment-sync-cli.ts +313 -209
- package/plugins/specweave-github/lib/github-status-sync.js +37 -1
- package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
- package/plugins/specweave-github/lib/increment-issue-builder.js +26 -5
- package/plugins/specweave-github/lib/increment-issue-builder.ts +36 -5
- package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
- package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
- package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
- package/plugins/specweave-release/commands/specweave-release-npm.md +187 -8
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +9 -0
|
@@ -26,6 +26,9 @@ class GitHubStatusSync {
|
|
|
26
26
|
/**
|
|
27
27
|
* Update GitHub issue status
|
|
28
28
|
*
|
|
29
|
+
* Preserves existing labels that are not status-related.
|
|
30
|
+
* Only replaces labels that start with "status:" prefix.
|
|
31
|
+
*
|
|
29
32
|
* @param issueNumber - GitHub issue number
|
|
30
33
|
* @param status - New status (state and labels)
|
|
31
34
|
*/
|
|
@@ -37,10 +40,43 @@ class GitHubStatusSync {
|
|
|
37
40
|
state: status.state
|
|
38
41
|
};
|
|
39
42
|
if (status.labels && status.labels.length > 0) {
|
|
40
|
-
|
|
43
|
+
const currentLabels = await this.getCurrentLabels(issueNumber);
|
|
44
|
+
const preservedLabels = currentLabels.filter(
|
|
45
|
+
(label) => !label.startsWith("status:")
|
|
46
|
+
);
|
|
47
|
+
const newStatusLabels = status.labels.filter(
|
|
48
|
+
(label) => label.startsWith("status:")
|
|
49
|
+
);
|
|
50
|
+
const newOtherLabels = status.labels.filter(
|
|
51
|
+
(label) => !label.startsWith("status:")
|
|
52
|
+
);
|
|
53
|
+
const mergedLabels = [.../* @__PURE__ */ new Set([
|
|
54
|
+
...preservedLabels,
|
|
55
|
+
...newStatusLabels,
|
|
56
|
+
...newOtherLabels
|
|
57
|
+
])];
|
|
58
|
+
updateData.labels = mergedLabels;
|
|
41
59
|
}
|
|
42
60
|
await this.octokit.rest.issues.update(updateData);
|
|
43
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Get current labels from GitHub issue
|
|
64
|
+
*
|
|
65
|
+
* @param issueNumber - GitHub issue number
|
|
66
|
+
* @returns Array of current label names
|
|
67
|
+
*/
|
|
68
|
+
async getCurrentLabels(issueNumber) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await this.octokit.rest.issues.get({
|
|
71
|
+
owner: this.owner,
|
|
72
|
+
repo: this.repo,
|
|
73
|
+
issue_number: issueNumber
|
|
74
|
+
});
|
|
75
|
+
return response.data.labels.map((label) => typeof label === "string" ? label : label.name).filter(Boolean);
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
44
80
|
/**
|
|
45
81
|
* Post status change comment to GitHub issue
|
|
46
82
|
*
|
|
@@ -53,6 +53,9 @@ export class GitHubStatusSync {
|
|
|
53
53
|
/**
|
|
54
54
|
* Update GitHub issue status
|
|
55
55
|
*
|
|
56
|
+
* Preserves existing labels that are not status-related.
|
|
57
|
+
* Only replaces labels that start with "status:" prefix.
|
|
58
|
+
*
|
|
56
59
|
* @param issueNumber - GitHub issue number
|
|
57
60
|
* @param status - New status (state and labels)
|
|
58
61
|
*/
|
|
@@ -60,21 +63,74 @@ export class GitHubStatusSync {
|
|
|
60
63
|
issueNumber: number,
|
|
61
64
|
status: ExternalStatus
|
|
62
65
|
): Promise<void> {
|
|
63
|
-
const updateData:
|
|
66
|
+
const updateData: {
|
|
67
|
+
owner: string;
|
|
68
|
+
repo: string;
|
|
69
|
+
issue_number: number;
|
|
70
|
+
state: 'open' | 'closed';
|
|
71
|
+
labels?: string[];
|
|
72
|
+
} = {
|
|
64
73
|
owner: this.owner,
|
|
65
74
|
repo: this.repo,
|
|
66
75
|
issue_number: issueNumber,
|
|
67
|
-
state: status.state
|
|
76
|
+
state: status.state as 'open' | 'closed'
|
|
68
77
|
};
|
|
69
78
|
|
|
70
|
-
//
|
|
79
|
+
// Merge labels - preserve non-status labels, replace status labels
|
|
71
80
|
if (status.labels && status.labels.length > 0) {
|
|
72
|
-
|
|
81
|
+
// Fetch current labels to preserve non-status ones
|
|
82
|
+
const currentLabels = await this.getCurrentLabels(issueNumber);
|
|
83
|
+
|
|
84
|
+
// Filter out status-related labels (start with "status:")
|
|
85
|
+
const preservedLabels = currentLabels.filter(
|
|
86
|
+
label => !label.startsWith('status:')
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Get new status labels from the provided status
|
|
90
|
+
const newStatusLabels = status.labels.filter(
|
|
91
|
+
label => label.startsWith('status:')
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Get non-status labels from the provided status (e.g., priority, type)
|
|
95
|
+
const newOtherLabels = status.labels.filter(
|
|
96
|
+
label => !label.startsWith('status:')
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Combine: preserved non-status + new status + new other labels
|
|
100
|
+
const mergedLabels = [...new Set([
|
|
101
|
+
...preservedLabels,
|
|
102
|
+
...newStatusLabels,
|
|
103
|
+
...newOtherLabels
|
|
104
|
+
])];
|
|
105
|
+
|
|
106
|
+
updateData.labels = mergedLabels;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
await this.octokit.rest.issues.update(updateData);
|
|
76
110
|
}
|
|
77
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Get current labels from GitHub issue
|
|
114
|
+
*
|
|
115
|
+
* @param issueNumber - GitHub issue number
|
|
116
|
+
* @returns Array of current label names
|
|
117
|
+
*/
|
|
118
|
+
private async getCurrentLabels(issueNumber: number): Promise<string[]> {
|
|
119
|
+
try {
|
|
120
|
+
const response = await this.octokit.rest.issues.get({
|
|
121
|
+
owner: this.owner,
|
|
122
|
+
repo: this.repo,
|
|
123
|
+
issue_number: issueNumber
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return response.data.labels
|
|
127
|
+
.map((label: any) => (typeof label === 'string' ? label : label.name))
|
|
128
|
+
.filter(Boolean) as string[];
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
78
134
|
/**
|
|
79
135
|
* Post status change comment to GitHub issue
|
|
80
136
|
*
|
|
@@ -207,6 +207,24 @@ class IncrementIssueBuilder {
|
|
|
207
207
|
body += `---
|
|
208
208
|
|
|
209
209
|
`;
|
|
210
|
+
const storyTasks = incrementData.tasks.filter(
|
|
211
|
+
(t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.toUpperCase() === story.id.toUpperCase())
|
|
212
|
+
);
|
|
213
|
+
if (storyTasks.length > 0) {
|
|
214
|
+
body += `## Tasks
|
|
215
|
+
|
|
216
|
+
`;
|
|
217
|
+
const completedTasks = storyTasks.filter((t) => t.completed).length;
|
|
218
|
+
body += `Progress: ${completedTasks}/${storyTasks.length} tasks
|
|
219
|
+
|
|
220
|
+
`;
|
|
221
|
+
for (const task of storyTasks) {
|
|
222
|
+
const checkbox = task.completed ? "[x]" : "[ ]";
|
|
223
|
+
body += `- ${checkbox} **${task.id}**: ${task.title}
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
body += "\n---\n\n";
|
|
227
|
+
}
|
|
210
228
|
body += `## Implementation
|
|
211
229
|
|
|
212
230
|
`;
|
|
@@ -225,8 +243,10 @@ class IncrementIssueBuilder {
|
|
|
225
243
|
body += `\u{1F916} Auto-synced by SpecWeave Increment Sync`;
|
|
226
244
|
const labels = ["specweave", "user-story"];
|
|
227
245
|
if (incrementData.frontmatter.type) {
|
|
228
|
-
labels.push(incrementData.frontmatter.type);
|
|
246
|
+
labels.push(incrementData.frontmatter.type.toLowerCase());
|
|
229
247
|
}
|
|
248
|
+
const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || "p2";
|
|
249
|
+
labels.push(priority);
|
|
230
250
|
return { title, body, labels };
|
|
231
251
|
}
|
|
232
252
|
/**
|
|
@@ -338,10 +358,11 @@ class IncrementIssueBuilder {
|
|
|
338
358
|
|
|
339
359
|
`;
|
|
340
360
|
body += `\u{1F916} Auto-synced by SpecWeave Increment Sync`;
|
|
341
|
-
const labels = ["specweave", "increment"
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
361
|
+
const labels = ["specweave", "increment"];
|
|
362
|
+
const typeLabel = incrementData.frontmatter.type?.toLowerCase() || "enhancement";
|
|
363
|
+
labels.push(typeLabel);
|
|
364
|
+
const priority = incrementData.frontmatter.priority?.toLowerCase() || "p2";
|
|
365
|
+
labels.push(priority);
|
|
345
366
|
return { title, body, labels };
|
|
346
367
|
}
|
|
347
368
|
/**
|
|
@@ -22,6 +22,7 @@ export interface IncrementFrontmatter {
|
|
|
22
22
|
type?: string;
|
|
23
23
|
status?: string;
|
|
24
24
|
created?: string;
|
|
25
|
+
priority?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface AcceptanceCriterion {
|
|
@@ -37,6 +38,7 @@ export interface UserStory {
|
|
|
37
38
|
iWant: string;
|
|
38
39
|
soThat: string;
|
|
39
40
|
acceptanceCriteria: AcceptanceCriterion[];
|
|
41
|
+
priority?: string;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface Task {
|
|
@@ -322,6 +324,24 @@ export class IncrementIssueBuilder {
|
|
|
322
324
|
|
|
323
325
|
body += `---\n\n`;
|
|
324
326
|
|
|
327
|
+
// Tasks for this user story
|
|
328
|
+
const storyTasks = incrementData.tasks.filter(t =>
|
|
329
|
+
t.userStories.includes(story.id) ||
|
|
330
|
+
t.userStories.some(us => us.toUpperCase() === story.id.toUpperCase())
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (storyTasks.length > 0) {
|
|
334
|
+
body += `## Tasks\n\n`;
|
|
335
|
+
const completedTasks = storyTasks.filter(t => t.completed).length;
|
|
336
|
+
body += `Progress: ${completedTasks}/${storyTasks.length} tasks\n\n`;
|
|
337
|
+
|
|
338
|
+
for (const task of storyTasks) {
|
|
339
|
+
const checkbox = task.completed ? '[x]' : '[ ]';
|
|
340
|
+
body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
|
|
341
|
+
}
|
|
342
|
+
body += '\n---\n\n';
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
// Link to increment
|
|
326
346
|
body += `## Implementation\n\n`;
|
|
327
347
|
if (githubRepo) {
|
|
@@ -335,10 +355,16 @@ export class IncrementIssueBuilder {
|
|
|
335
355
|
|
|
336
356
|
// Labels
|
|
337
357
|
const labels = ['specweave', 'user-story'];
|
|
358
|
+
|
|
359
|
+
// Add type label if available
|
|
338
360
|
if (incrementData.frontmatter.type) {
|
|
339
|
-
labels.push(incrementData.frontmatter.type);
|
|
361
|
+
labels.push(incrementData.frontmatter.type.toLowerCase());
|
|
340
362
|
}
|
|
341
363
|
|
|
364
|
+
// Add priority label (from story or default to p2)
|
|
365
|
+
const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || 'p2';
|
|
366
|
+
labels.push(priority);
|
|
367
|
+
|
|
342
368
|
return { title, body, labels };
|
|
343
369
|
}
|
|
344
370
|
|
|
@@ -438,10 +464,15 @@ export class IncrementIssueBuilder {
|
|
|
438
464
|
body += `🤖 Auto-synced by SpecWeave Increment Sync`;
|
|
439
465
|
|
|
440
466
|
// Labels
|
|
441
|
-
const labels = ['specweave', 'increment'
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
467
|
+
const labels = ['specweave', 'increment'];
|
|
468
|
+
|
|
469
|
+
// Add type label (default to 'enhancement' if not specified)
|
|
470
|
+
const typeLabel = incrementData.frontmatter.type?.toLowerCase() || 'enhancement';
|
|
471
|
+
labels.push(typeLabel);
|
|
472
|
+
|
|
473
|
+
// Add priority label (default to 'p2' if not specified)
|
|
474
|
+
const priority = incrementData.frontmatter.priority?.toLowerCase() || 'p2';
|
|
475
|
+
labels.push(priority);
|
|
445
476
|
|
|
446
477
|
return { title, body, labels };
|
|
447
478
|
}
|
|
@@ -132,6 +132,7 @@ class JiraSpecSync {
|
|
|
132
132
|
async createJiraEpic(spec) {
|
|
133
133
|
const epicSummary = `[${spec.metadata.id.toUpperCase()}] ${spec.metadata.title}`;
|
|
134
134
|
const epicDescription = this.generateEpicDescription(spec);
|
|
135
|
+
const issueType = this.mapTypeToJira(spec.metadata.type, "Epic");
|
|
135
136
|
const payload = {
|
|
136
137
|
fields: {
|
|
137
138
|
project: {
|
|
@@ -140,9 +141,13 @@ class JiraSpecSync {
|
|
|
140
141
|
summary: epicSummary,
|
|
141
142
|
description: epicDescription,
|
|
142
143
|
issuetype: {
|
|
143
|
-
name:
|
|
144
|
+
name: issueType
|
|
144
145
|
},
|
|
145
|
-
labels: [`spec:${spec.metadata.id}`, `priority:${spec.metadata.priority}`]
|
|
146
|
+
labels: [`spec:${spec.metadata.id}`, `priority:${spec.metadata.priority}`],
|
|
147
|
+
// Set native JIRA priority field (P0→Highest, P1→High, P2→Medium, P3→Low)
|
|
148
|
+
priority: {
|
|
149
|
+
name: this.mapPriorityToJira(spec.metadata.priority)
|
|
150
|
+
}
|
|
146
151
|
}
|
|
147
152
|
};
|
|
148
153
|
const response = await this.client.post("/issue", payload);
|
|
@@ -213,7 +218,8 @@ class JiraSpecSync {
|
|
|
213
218
|
summary: storySummary,
|
|
214
219
|
description: storyDescription,
|
|
215
220
|
epicLink: epicKey,
|
|
216
|
-
labels: [`user-story`, `spec:${spec.metadata.id}`, `priority:${us.priority}`]
|
|
221
|
+
labels: [`user-story`, `spec:${spec.metadata.id}`, `priority:${us.priority}`],
|
|
222
|
+
priority: us.priority
|
|
217
223
|
});
|
|
218
224
|
created.push(us.id);
|
|
219
225
|
console.log(` \u2705 Created ${us.id} \u2192 Story ${newStory.key}`);
|
|
@@ -347,6 +353,7 @@ ${acList}
|
|
|
347
353
|
* Create Jira Story
|
|
348
354
|
*/
|
|
349
355
|
async createStory(story) {
|
|
356
|
+
const issueType = this.mapTypeToJira(story.type, "Story");
|
|
350
357
|
const payload = {
|
|
351
358
|
fields: {
|
|
352
359
|
project: {
|
|
@@ -355,12 +362,16 @@ ${acList}
|
|
|
355
362
|
summary: story.summary,
|
|
356
363
|
description: story.description,
|
|
357
364
|
issuetype: {
|
|
358
|
-
name:
|
|
365
|
+
name: issueType
|
|
359
366
|
},
|
|
360
367
|
labels: story.labels,
|
|
361
368
|
// Link to epic (field name may vary by Jira configuration)
|
|
362
|
-
customfield_10014: story.epicLink
|
|
369
|
+
customfield_10014: story.epicLink,
|
|
363
370
|
// Epic Link field (adjust if needed)
|
|
371
|
+
// Set native JIRA priority field
|
|
372
|
+
priority: {
|
|
373
|
+
name: this.mapPriorityToJira(story.priority)
|
|
374
|
+
}
|
|
364
375
|
}
|
|
365
376
|
};
|
|
366
377
|
const response = await this.client.post("/issue", payload);
|
|
@@ -411,6 +422,43 @@ ${acList}
|
|
|
411
422
|
}
|
|
412
423
|
});
|
|
413
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Map SpecWeave priority to JIRA priority name
|
|
427
|
+
*
|
|
428
|
+
* JIRA standard priority names: Highest, High, Medium, Low, Lowest
|
|
429
|
+
*/
|
|
430
|
+
mapPriorityToJira(priority) {
|
|
431
|
+
if (!priority) return "Medium";
|
|
432
|
+
const map = {
|
|
433
|
+
P0: "Highest",
|
|
434
|
+
P1: "High",
|
|
435
|
+
P2: "Medium",
|
|
436
|
+
P3: "Low",
|
|
437
|
+
p0: "Highest",
|
|
438
|
+
p1: "High",
|
|
439
|
+
p2: "Medium",
|
|
440
|
+
p3: "Low"
|
|
441
|
+
};
|
|
442
|
+
return map[priority] || "Medium";
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Map SpecWeave type to JIRA issue type
|
|
446
|
+
*
|
|
447
|
+
* Supports: Epic, Story, Bug, Task
|
|
448
|
+
*/
|
|
449
|
+
mapTypeToJira(type, defaultType = "Story") {
|
|
450
|
+
if (!type) return defaultType;
|
|
451
|
+
const normalizedType = type.toLowerCase();
|
|
452
|
+
const map = {
|
|
453
|
+
bug: "Bug",
|
|
454
|
+
feature: "Epic",
|
|
455
|
+
epic: "Epic",
|
|
456
|
+
story: "Story",
|
|
457
|
+
task: "Task",
|
|
458
|
+
enhancement: "Story"
|
|
459
|
+
};
|
|
460
|
+
return map[normalizedType] || defaultType;
|
|
461
|
+
}
|
|
414
462
|
}
|
|
415
463
|
export {
|
|
416
464
|
JiraSpecSync
|
|
@@ -223,7 +223,19 @@ export class JiraSpecSync {
|
|
|
223
223
|
const epicSummary = `[${spec.metadata.id.toUpperCase()}] ${spec.metadata.title}`;
|
|
224
224
|
const epicDescription = this.generateEpicDescription(spec);
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
// Determine issue type based on spec type (supports Bug for bug-type specs)
|
|
227
|
+
const issueType = this.mapTypeToJira(spec.metadata.type, 'Epic');
|
|
228
|
+
|
|
229
|
+
const payload: {
|
|
230
|
+
fields: {
|
|
231
|
+
project: { key: string };
|
|
232
|
+
summary: string;
|
|
233
|
+
description: string;
|
|
234
|
+
issuetype: { name: string };
|
|
235
|
+
labels: string[];
|
|
236
|
+
priority?: { name: string };
|
|
237
|
+
};
|
|
238
|
+
} = {
|
|
227
239
|
fields: {
|
|
228
240
|
project: {
|
|
229
241
|
key: this.config.projectKey
|
|
@@ -231,9 +243,13 @@ export class JiraSpecSync {
|
|
|
231
243
|
summary: epicSummary,
|
|
232
244
|
description: epicDescription,
|
|
233
245
|
issuetype: {
|
|
234
|
-
name:
|
|
246
|
+
name: issueType
|
|
235
247
|
},
|
|
236
|
-
labels: [`spec:${spec.metadata.id}`, `priority:${spec.metadata.priority}`]
|
|
248
|
+
labels: [`spec:${spec.metadata.id}`, `priority:${spec.metadata.priority}`],
|
|
249
|
+
// Set native JIRA priority field (P0→Highest, P1→High, P2→Medium, P3→Low)
|
|
250
|
+
priority: {
|
|
251
|
+
name: this.mapPriorityToJira(spec.metadata.priority)
|
|
252
|
+
}
|
|
237
253
|
}
|
|
238
254
|
};
|
|
239
255
|
|
|
@@ -329,7 +345,8 @@ export class JiraSpecSync {
|
|
|
329
345
|
summary: storySummary,
|
|
330
346
|
description: storyDescription,
|
|
331
347
|
epicLink: epicKey,
|
|
332
|
-
labels: [`user-story`, `spec:${spec.metadata.id}`, `priority:${us.priority}`]
|
|
348
|
+
labels: [`user-story`, `spec:${spec.metadata.id}`, `priority:${us.priority}`],
|
|
349
|
+
priority: us.priority
|
|
333
350
|
});
|
|
334
351
|
|
|
335
352
|
created.push(us.id);
|
|
@@ -497,8 +514,23 @@ ${acList}
|
|
|
497
514
|
description: string;
|
|
498
515
|
epicLink: string;
|
|
499
516
|
labels: string[];
|
|
517
|
+
priority?: string;
|
|
518
|
+
type?: string;
|
|
500
519
|
}): Promise<JiraStory> {
|
|
501
|
-
|
|
520
|
+
// Determine issue type (supports Bug for bug-type stories)
|
|
521
|
+
const issueType = this.mapTypeToJira(story.type, 'Story');
|
|
522
|
+
|
|
523
|
+
const payload: {
|
|
524
|
+
fields: {
|
|
525
|
+
project: { key: string };
|
|
526
|
+
summary: string;
|
|
527
|
+
description: string;
|
|
528
|
+
issuetype: { name: string };
|
|
529
|
+
labels: string[];
|
|
530
|
+
customfield_10014: string;
|
|
531
|
+
priority?: { name: string };
|
|
532
|
+
};
|
|
533
|
+
} = {
|
|
502
534
|
fields: {
|
|
503
535
|
project: {
|
|
504
536
|
key: this.config.projectKey
|
|
@@ -506,11 +538,15 @@ ${acList}
|
|
|
506
538
|
summary: story.summary,
|
|
507
539
|
description: story.description,
|
|
508
540
|
issuetype: {
|
|
509
|
-
name:
|
|
541
|
+
name: issueType
|
|
510
542
|
},
|
|
511
543
|
labels: story.labels,
|
|
512
544
|
// Link to epic (field name may vary by Jira configuration)
|
|
513
|
-
customfield_10014: story.epicLink // Epic Link field (adjust if needed)
|
|
545
|
+
customfield_10014: story.epicLink, // Epic Link field (adjust if needed)
|
|
546
|
+
// Set native JIRA priority field
|
|
547
|
+
priority: {
|
|
548
|
+
name: this.mapPriorityToJira(story.priority)
|
|
549
|
+
}
|
|
514
550
|
}
|
|
515
551
|
};
|
|
516
552
|
|
|
@@ -579,4 +615,48 @@ ${acList}
|
|
|
579
615
|
}
|
|
580
616
|
});
|
|
581
617
|
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Map SpecWeave priority to JIRA priority name
|
|
621
|
+
*
|
|
622
|
+
* JIRA standard priority names: Highest, High, Medium, Low, Lowest
|
|
623
|
+
*/
|
|
624
|
+
private mapPriorityToJira(priority?: string): string {
|
|
625
|
+
if (!priority) return 'Medium';
|
|
626
|
+
|
|
627
|
+
const map: Record<string, string> = {
|
|
628
|
+
P0: 'Highest',
|
|
629
|
+
P1: 'High',
|
|
630
|
+
P2: 'Medium',
|
|
631
|
+
P3: 'Low',
|
|
632
|
+
p0: 'Highest',
|
|
633
|
+
p1: 'High',
|
|
634
|
+
p2: 'Medium',
|
|
635
|
+
p3: 'Low'
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
return map[priority] || 'Medium';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Map SpecWeave type to JIRA issue type
|
|
643
|
+
*
|
|
644
|
+
* Supports: Epic, Story, Bug, Task
|
|
645
|
+
*/
|
|
646
|
+
private mapTypeToJira(type?: string, defaultType: string = 'Story'): string {
|
|
647
|
+
if (!type) return defaultType;
|
|
648
|
+
|
|
649
|
+
const normalizedType = type.toLowerCase();
|
|
650
|
+
|
|
651
|
+
const map: Record<string, string> = {
|
|
652
|
+
bug: 'Bug',
|
|
653
|
+
feature: 'Epic',
|
|
654
|
+
epic: 'Epic',
|
|
655
|
+
story: 'Story',
|
|
656
|
+
task: 'Task',
|
|
657
|
+
enhancement: 'Story'
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
return map[normalizedType] || defaultType;
|
|
661
|
+
}
|
|
582
662
|
}
|
|
@@ -33,25 +33,31 @@ class JiraStatusSync {
|
|
|
33
33
|
* JIRA requires using transitions to change status.
|
|
34
34
|
* Cannot directly set status field.
|
|
35
35
|
*
|
|
36
|
+
* Handles missing transitions gracefully by logging a warning
|
|
37
|
+
* instead of throwing an error.
|
|
38
|
+
*
|
|
36
39
|
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
37
40
|
* @param status - Desired status
|
|
41
|
+
* @returns true if transition succeeded, false if not available
|
|
38
42
|
*/
|
|
39
43
|
async updateStatus(issueKey, status) {
|
|
40
44
|
const transitionsResponse = await this.client.get(`/issue/${issueKey}/transitions`);
|
|
41
45
|
const transitions = transitionsResponse.data.transitions;
|
|
42
46
|
const targetTransition = transitions.find(
|
|
43
|
-
(t) => t.to.name === status.state
|
|
47
|
+
(t) => t.to.name.toLowerCase() === status.state.toLowerCase()
|
|
44
48
|
);
|
|
45
49
|
if (!targetTransition) {
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
console.warn(
|
|
51
|
+
`\u26A0\uFE0F Cannot transition ${issueKey} to "${status.state}". Available transitions: ${transitions.map((t) => t.to.name).join(", ")}. This may be expected if your JIRA workflow doesn't support this status.`
|
|
48
52
|
);
|
|
53
|
+
return false;
|
|
49
54
|
}
|
|
50
55
|
await this.client.post(`/issue/${issueKey}/transitions`, {
|
|
51
56
|
transition: {
|
|
52
57
|
id: targetTransition.id
|
|
53
58
|
}
|
|
54
59
|
});
|
|
60
|
+
return true;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Post comment about status change to JIRA issue
|
|
@@ -85,24 +85,31 @@ export class JiraStatusSync {
|
|
|
85
85
|
* JIRA requires using transitions to change status.
|
|
86
86
|
* Cannot directly set status field.
|
|
87
87
|
*
|
|
88
|
+
* Handles missing transitions gracefully by logging a warning
|
|
89
|
+
* instead of throwing an error.
|
|
90
|
+
*
|
|
88
91
|
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
89
92
|
* @param status - Desired status
|
|
93
|
+
* @returns true if transition succeeded, false if not available
|
|
90
94
|
*/
|
|
91
|
-
async updateStatus(issueKey: string, status: ExternalStatus): Promise<
|
|
95
|
+
async updateStatus(issueKey: string, status: ExternalStatus): Promise<boolean> {
|
|
92
96
|
// 1. Get available transitions for this issue
|
|
93
97
|
const transitionsResponse = await this.client.get(`/issue/${issueKey}/transitions`);
|
|
94
98
|
const transitions: JiraTransition[] = transitionsResponse.data.transitions;
|
|
95
99
|
|
|
96
|
-
// 2. Find transition that leads to desired status
|
|
100
|
+
// 2. Find transition that leads to desired status (case-insensitive)
|
|
97
101
|
const targetTransition = transitions.find(
|
|
98
|
-
(t) => t.to.name === status.state
|
|
102
|
+
(t) => t.to.name.toLowerCase() === status.state.toLowerCase()
|
|
99
103
|
);
|
|
100
104
|
|
|
101
105
|
if (!targetTransition) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
// Log warning instead of throwing - workflow may not support this transition
|
|
107
|
+
console.warn(
|
|
108
|
+
`⚠️ Cannot transition ${issueKey} to "${status.state}". ` +
|
|
109
|
+
`Available transitions: ${transitions.map(t => t.to.name).join(', ')}. ` +
|
|
110
|
+
`This may be expected if your JIRA workflow doesn't support this status.`
|
|
105
111
|
);
|
|
112
|
+
return false;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
// 3. Execute transition
|
|
@@ -111,6 +118,8 @@ export class JiraStatusSync {
|
|
|
111
118
|
id: targetTransition.id
|
|
112
119
|
}
|
|
113
120
|
});
|
|
121
|
+
|
|
122
|
+
return true;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
/**
|