specweave 0.28.19 ā 0.28.20
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/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +186 -19
- 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/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/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 +252 -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/hooks/.specweave/logs/dora-tracking.log +9 -0
|
@@ -3,23 +3,25 @@
|
|
|
3
3
|
* GitHub Increment Sync CLI
|
|
4
4
|
*
|
|
5
5
|
* For brownfield projects without living docs structure.
|
|
6
|
-
* Creates GitHub issues directly from increment spec.md with
|
|
6
|
+
* Creates GitHub issues directly from increment spec.md with CORRECT format.
|
|
7
7
|
*
|
|
8
|
-
* CORRECT FORMAT:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
8
|
+
* CORRECT FORMAT (SpecWeave Universal Hierarchy):
|
|
9
|
+
* - Feature (FS-XXX) ā GitHub Milestone
|
|
10
|
+
* - User Story (US-XXX) ā GitHub Issue with [FS-XXX][US-YYY] Title
|
|
11
|
+
* - Tasks (T-XXX) ā Checkboxes in User Story issue
|
|
12
|
+
* - ACs ā Checkboxes in User Story issue
|
|
11
13
|
*
|
|
12
14
|
* Usage:
|
|
13
15
|
* node github-increment-sync-cli.js <increment-id>
|
|
14
|
-
* node github-increment-sync-cli.js
|
|
16
|
+
* node github-increment-sync-cli.js 0002-thumbnail-optimizer-mvp
|
|
15
17
|
*
|
|
16
|
-
* @see
|
|
18
|
+
* @see CLAUDE.md (GitHub Issue Format rules)
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import { existsSync, readFileSync } from 'fs';
|
|
20
22
|
import * as fs from 'fs/promises';
|
|
21
23
|
import * as path from 'path';
|
|
22
|
-
import { IncrementIssueBuilder } from './increment-issue-builder.js';
|
|
24
|
+
import { IncrementIssueBuilder, UserStory, Task, IncrementData } from './increment-issue-builder.js';
|
|
23
25
|
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
24
26
|
|
|
25
27
|
interface GitHubConfig {
|
|
@@ -28,6 +30,17 @@ interface GitHubConfig {
|
|
|
28
30
|
token: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
interface SyncResult {
|
|
34
|
+
milestoneNumber: number;
|
|
35
|
+
milestoneUrl: string;
|
|
36
|
+
issues: Array<{
|
|
37
|
+
userStoryId: string;
|
|
38
|
+
issueNumber: number;
|
|
39
|
+
issueUrl: string;
|
|
40
|
+
title: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
async function loadGitHubConfig(): Promise<GitHubConfig | null> {
|
|
32
45
|
const projectRoot = process.cwd();
|
|
33
46
|
const configPath = path.join(projectRoot, '.specweave/config.json');
|
|
@@ -131,164 +144,272 @@ async function findIncrementFolder(incrementId: string): Promise<string | null>
|
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
/**
|
|
134
|
-
*
|
|
147
|
+
* Load existing GitHub links from metadata.json
|
|
135
148
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Search for issues with this feature ID in title
|
|
142
|
-
const result = await execFileNoThrow('gh', [
|
|
143
|
-
'search', 'issues',
|
|
144
|
-
`repo:${owner}/${repo}`,
|
|
145
|
-
`"[${featureId}]" in:title`,
|
|
146
|
-
'is:open',
|
|
147
|
-
'--json', 'number,title',
|
|
148
|
-
'--limit', '5'
|
|
149
|
-
]);
|
|
149
|
+
function loadExistingGitHubLinks(incrementPath: string): {
|
|
150
|
+
milestone?: number;
|
|
151
|
+
userStoryIssues: Record<string, number>;
|
|
152
|
+
} {
|
|
153
|
+
const metadataPath = path.join(incrementPath, 'metadata.json');
|
|
150
154
|
|
|
151
|
-
if (
|
|
152
|
-
return
|
|
155
|
+
if (!existsSync(metadataPath)) {
|
|
156
|
+
return { userStoryIssues: {} };
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
milestone: metadata.github?.milestone,
|
|
164
|
+
userStoryIssues: metadata.github?.userStoryIssues || {}
|
|
165
|
+
};
|
|
160
166
|
} catch {
|
|
161
|
-
|
|
167
|
+
return { userStoryIssues: {} };
|
|
162
168
|
}
|
|
169
|
+
}
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Update increment metadata with GitHub links
|
|
173
|
+
*/
|
|
174
|
+
async function updateIncrementMetadata(
|
|
175
|
+
incrementPath: string,
|
|
176
|
+
milestoneNumber: number,
|
|
177
|
+
userStoryIssues: Record<string, number>
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const metadataPath = path.join(incrementPath, 'metadata.json');
|
|
180
|
+
|
|
181
|
+
let metadata: Record<string, unknown> = {};
|
|
182
|
+
|
|
183
|
+
if (existsSync(metadataPath)) {
|
|
184
|
+
try {
|
|
185
|
+
metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
|
186
|
+
} catch {
|
|
187
|
+
// Start fresh
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update github section
|
|
192
|
+
metadata.github = {
|
|
193
|
+
milestone: milestoneNumber,
|
|
194
|
+
userStoryIssues,
|
|
195
|
+
lastSync: new Date().toISOString()
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
/**
|
|
168
|
-
* Create GitHub
|
|
202
|
+
* Create or get GitHub milestone for the feature
|
|
169
203
|
*/
|
|
170
|
-
async function
|
|
204
|
+
async function createOrGetMilestone(
|
|
171
205
|
owner: string,
|
|
172
206
|
repo: string,
|
|
207
|
+
featureId: string,
|
|
173
208
|
title: string,
|
|
174
|
-
|
|
175
|
-
labels: string[]
|
|
209
|
+
existingMilestone?: number
|
|
176
210
|
): Promise<{ number: number; url: string }> {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
'
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
// If we have an existing milestone, verify it exists
|
|
212
|
+
if (existingMilestone) {
|
|
213
|
+
const verifyResult = await execFileNoThrow('gh', [
|
|
214
|
+
'api', `repos/${owner}/${repo}/milestones/${existingMilestone}`,
|
|
215
|
+
'--jq', '.number'
|
|
216
|
+
]);
|
|
217
|
+
if (verifyResult.exitCode === 0) {
|
|
218
|
+
return {
|
|
219
|
+
number: existingMilestone,
|
|
220
|
+
url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
187
223
|
}
|
|
188
224
|
|
|
189
|
-
|
|
225
|
+
// Search for existing milestone by title
|
|
226
|
+
const searchResult = await execFileNoThrow('gh', [
|
|
227
|
+
'api', `repos/${owner}/${repo}/milestones`,
|
|
228
|
+
'--jq', `.[] | select(.title | contains("${featureId}")) | .number`
|
|
229
|
+
]);
|
|
190
230
|
|
|
191
|
-
if (
|
|
192
|
-
|
|
231
|
+
if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
|
|
232
|
+
const milestoneNumber = parseInt(searchResult.stdout.trim().split('\n')[0], 10);
|
|
233
|
+
return {
|
|
234
|
+
number: milestoneNumber,
|
|
235
|
+
url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
|
|
236
|
+
};
|
|
193
237
|
}
|
|
194
238
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
239
|
+
// Create new milestone
|
|
240
|
+
const milestoneTitle = `[${featureId}] ${title}`;
|
|
241
|
+
const createResult = await execFileNoThrow('gh', [
|
|
242
|
+
'api', `repos/${owner}/${repo}/milestones`,
|
|
243
|
+
'-X', 'POST',
|
|
244
|
+
'-f', `title=${milestoneTitle}`,
|
|
245
|
+
'-f', `description=Feature milestone for ${featureId}`,
|
|
246
|
+
'--jq', '.number'
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
if (createResult.exitCode !== 0) {
|
|
250
|
+
throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
|
|
199
251
|
}
|
|
200
252
|
|
|
253
|
+
const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
|
|
201
254
|
return {
|
|
202
|
-
number:
|
|
203
|
-
url:
|
|
255
|
+
number: milestoneNumber,
|
|
256
|
+
url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
|
|
204
257
|
};
|
|
205
258
|
}
|
|
206
259
|
|
|
207
260
|
/**
|
|
208
|
-
*
|
|
261
|
+
* Build issue body for a single user story
|
|
209
262
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
):
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
263
|
+
function buildUserStoryIssueBody(
|
|
264
|
+
story: UserStory,
|
|
265
|
+
tasks: Task[],
|
|
266
|
+
incrementData: IncrementData,
|
|
267
|
+
githubRepo: string
|
|
268
|
+
): string {
|
|
269
|
+
const incrementId = incrementData.frontmatter.increment;
|
|
270
|
+
let body = '';
|
|
271
|
+
|
|
272
|
+
// Header with metadata
|
|
273
|
+
body += `**Feature**: ${incrementData.frontmatter.feature_id || 'N/A'}\n`;
|
|
274
|
+
body += `**Status**: ${story.acceptanceCriteria.every(ac => ac.completed) ? 'complete' : 'in-progress'}\n`;
|
|
275
|
+
body += `**Priority**: ${story.priority || incrementData.frontmatter.priority || 'P2'}\n`;
|
|
276
|
+
|
|
277
|
+
body += `\n---\n\n`;
|
|
278
|
+
|
|
279
|
+
// User Story description
|
|
280
|
+
body += `## User Story\n\n`;
|
|
281
|
+
if (story.asA && story.iWant && story.soThat) {
|
|
282
|
+
body += `**As a** ${story.asA}\n`;
|
|
283
|
+
body += `**I want** ${story.iWant}\n`;
|
|
284
|
+
body += `**So that** ${story.soThat}\n\n`;
|
|
285
|
+
} else {
|
|
286
|
+
body += `${story.title}\n\n`;
|
|
225
287
|
}
|
|
226
|
-
}
|
|
227
288
|
|
|
228
|
-
|
|
229
|
-
* Load existing GitHub issue link from metadata.json
|
|
230
|
-
* This is the PRIMARY source for existing issue detection
|
|
231
|
-
*/
|
|
232
|
-
function loadExistingGitHubLink(incrementPath: string): { issue: number; url?: string } | null {
|
|
233
|
-
const metadataPath = path.join(incrementPath, 'metadata.json');
|
|
289
|
+
body += `---\n\n`;
|
|
234
290
|
|
|
235
|
-
|
|
236
|
-
|
|
291
|
+
// Acceptance Criteria
|
|
292
|
+
body += `## Acceptance Criteria\n\n`;
|
|
293
|
+
if (story.acceptanceCriteria.length > 0) {
|
|
294
|
+
const completed = story.acceptanceCriteria.filter(ac => ac.completed).length;
|
|
295
|
+
const total = story.acceptanceCriteria.length;
|
|
296
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
297
|
+
body += `Progress: ${completed}/${total} criteria met (${percentage}%)\n\n`;
|
|
298
|
+
|
|
299
|
+
for (const ac of story.acceptanceCriteria) {
|
|
300
|
+
const checkbox = ac.completed ? '[x]' : '[ ]';
|
|
301
|
+
body += `- ${checkbox} **${ac.id}**: ${ac.description}\n`;
|
|
302
|
+
}
|
|
303
|
+
body += '\n';
|
|
304
|
+
} else {
|
|
305
|
+
body += `*No acceptance criteria defined*\n\n`;
|
|
237
306
|
}
|
|
238
307
|
|
|
239
|
-
|
|
240
|
-
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
|
308
|
+
body += `---\n\n`;
|
|
241
309
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
};
|
|
248
|
-
}
|
|
310
|
+
// Tasks for this user story
|
|
311
|
+
const storyTasks = tasks.filter(t =>
|
|
312
|
+
t.userStories.includes(story.id) ||
|
|
313
|
+
t.userStories.some(us => us.includes(story.id.replace('US-', '')))
|
|
314
|
+
);
|
|
249
315
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
316
|
+
if (storyTasks.length > 0) {
|
|
317
|
+
body += `## Implementation Tasks\n\n`;
|
|
318
|
+
const completedTasks = storyTasks.filter(t => t.completed).length;
|
|
319
|
+
const totalTasks = storyTasks.length;
|
|
320
|
+
const taskPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
321
|
+
body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)\n\n`;
|
|
322
|
+
|
|
323
|
+
for (const task of storyTasks) {
|
|
324
|
+
const checkbox = task.completed ? '[x]' : '[ ]';
|
|
325
|
+
body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
|
|
256
326
|
}
|
|
327
|
+
body += '\n';
|
|
257
328
|
|
|
258
|
-
|
|
259
|
-
} catch {
|
|
260
|
-
return null;
|
|
329
|
+
body += `---\n\n`;
|
|
261
330
|
}
|
|
331
|
+
|
|
332
|
+
// Link to increment
|
|
333
|
+
body += `## SpecWeave Increment\n\n`;
|
|
334
|
+
body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})\n\n`;
|
|
335
|
+
|
|
336
|
+
body += `---\n\n`;
|
|
337
|
+
body += `š¤ Auto-synced by SpecWeave`;
|
|
338
|
+
|
|
339
|
+
return body;
|
|
262
340
|
}
|
|
263
341
|
|
|
264
342
|
/**
|
|
265
|
-
*
|
|
343
|
+
* Create or update GitHub issue for a user story
|
|
266
344
|
*/
|
|
267
|
-
async function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
345
|
+
async function syncUserStoryIssue(
|
|
346
|
+
owner: string,
|
|
347
|
+
repo: string,
|
|
348
|
+
featureId: string,
|
|
349
|
+
story: UserStory,
|
|
350
|
+
tasks: Task[],
|
|
351
|
+
incrementData: IncrementData,
|
|
352
|
+
milestoneNumber: number,
|
|
353
|
+
existingIssueNumber?: number
|
|
354
|
+
): Promise<{ number: number; url: string }> {
|
|
355
|
+
// CORRECT FORMAT: [FS-XXX][US-YYY] User Story Title
|
|
356
|
+
const title = `[${featureId}][${story.id}] ${story.title}`;
|
|
357
|
+
const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
|
|
358
|
+
|
|
359
|
+
// Labels
|
|
360
|
+
const labels = ['specweave', 'user-story'];
|
|
361
|
+
const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || 'p2';
|
|
362
|
+
labels.push(priority);
|
|
363
|
+
|
|
364
|
+
if (existingIssueNumber) {
|
|
365
|
+
// Update existing issue
|
|
366
|
+
const updateResult = await execFileNoThrow('gh', [
|
|
367
|
+
'issue', 'edit',
|
|
368
|
+
String(existingIssueNumber),
|
|
369
|
+
'--repo', `${owner}/${repo}`,
|
|
370
|
+
'--title', title,
|
|
371
|
+
'--body', body
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
if (updateResult.exitCode !== 0) {
|
|
375
|
+
throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
|
|
376
|
+
}
|
|
273
377
|
|
|
274
|
-
|
|
378
|
+
return {
|
|
379
|
+
number: existingIssueNumber,
|
|
380
|
+
url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
275
383
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
384
|
+
// Create new issue
|
|
385
|
+
const createArgs = [
|
|
386
|
+
'issue', 'create',
|
|
387
|
+
'--repo', `${owner}/${repo}`,
|
|
388
|
+
'--title', title,
|
|
389
|
+
'--body', body,
|
|
390
|
+
'--milestone', String(milestoneNumber)
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
if (labels.length > 0) {
|
|
394
|
+
createArgs.push('--label', labels.join(','));
|
|
282
395
|
}
|
|
283
396
|
|
|
284
|
-
|
|
285
|
-
metadata.github = {
|
|
286
|
-
issue: issueNumber,
|
|
287
|
-
url: issueUrl,
|
|
288
|
-
lastSync: new Date().toISOString()
|
|
289
|
-
};
|
|
397
|
+
const createResult = await execFileNoThrow('gh', createArgs);
|
|
290
398
|
|
|
291
|
-
|
|
399
|
+
if (createResult.exitCode !== 0) {
|
|
400
|
+
throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Parse issue URL from output
|
|
404
|
+
const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
|
|
405
|
+
if (!urlMatch) {
|
|
406
|
+
throw new Error('Could not parse issue URL from gh output');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
number: parseInt(urlMatch[1], 10),
|
|
411
|
+
url: urlMatch[0]
|
|
412
|
+
};
|
|
292
413
|
}
|
|
293
414
|
|
|
294
415
|
async function main() {
|
|
@@ -297,26 +418,28 @@ async function main() {
|
|
|
297
418
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
298
419
|
console.log('Usage: node github-increment-sync-cli.js <increment-id> [options]');
|
|
299
420
|
console.log('');
|
|
421
|
+
console.log('Creates GitHub issues with CORRECT format:');
|
|
422
|
+
console.log(' - Milestone: [FS-XXX] Feature Title');
|
|
423
|
+
console.log(' - Issues: [FS-XXX][US-YYY] User Story Title (one per US)');
|
|
424
|
+
console.log('');
|
|
300
425
|
console.log('Arguments:');
|
|
301
|
-
console.log(' increment-id Increment ID (e.g.,
|
|
426
|
+
console.log(' increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)');
|
|
302
427
|
console.log('');
|
|
303
428
|
console.log('Options:');
|
|
304
|
-
console.log(' --
|
|
305
|
-
console.log(' --dry-run Preview issue without creating');
|
|
429
|
+
console.log(' --dry-run Preview issues without creating');
|
|
306
430
|
console.log('');
|
|
307
431
|
console.log('Environment:');
|
|
308
432
|
console.log(' GITHUB_TOKEN Required - GitHub personal access token');
|
|
309
433
|
console.log('');
|
|
310
434
|
console.log('Example:');
|
|
311
|
-
console.log(' GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js
|
|
435
|
+
console.log(' GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002');
|
|
312
436
|
process.exit(args.length === 0 ? 1 : 0);
|
|
313
437
|
}
|
|
314
438
|
|
|
315
439
|
const incrementId = args[0];
|
|
316
|
-
const force = args.includes('--force');
|
|
317
440
|
const dryRun = args.includes('--dry-run');
|
|
318
441
|
|
|
319
|
-
console.log(`\nš GitHub Increment Sync CLI`);
|
|
442
|
+
console.log(`\nš GitHub Increment Sync CLI (Per-User-Story Mode)`);
|
|
320
443
|
console.log(` Increment: ${incrementId}`);
|
|
321
444
|
|
|
322
445
|
// Find increment folder
|
|
@@ -339,7 +462,6 @@ async function main() {
|
|
|
339
462
|
}
|
|
340
463
|
console.log(` Repository: ${config.owner}/${config.repo}`);
|
|
341
464
|
} else {
|
|
342
|
-
// Try to detect repo for dry-run preview (non-fatal)
|
|
343
465
|
config = await loadGitHubConfig().catch((): null => null);
|
|
344
466
|
if (config) {
|
|
345
467
|
console.log(` Repository: ${config.owner}/${config.repo}`);
|
|
@@ -348,7 +470,7 @@ async function main() {
|
|
|
348
470
|
}
|
|
349
471
|
}
|
|
350
472
|
|
|
351
|
-
// Parse increment
|
|
473
|
+
// Parse increment
|
|
352
474
|
const projectRoot = process.cwd();
|
|
353
475
|
const builder = new IncrementIssueBuilder(incrementPath, projectRoot);
|
|
354
476
|
|
|
@@ -356,6 +478,10 @@ async function main() {
|
|
|
356
478
|
console.log(`\nš Parsing increment spec.md...`);
|
|
357
479
|
const incrementData = await builder.parse();
|
|
358
480
|
|
|
481
|
+
const featureId = incrementData.frontmatter.feature_id ||
|
|
482
|
+
`FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, '0') || 'UNKNOWN'}`;
|
|
483
|
+
|
|
484
|
+
console.log(` š¦ Feature: ${featureId}`);
|
|
359
485
|
console.log(` š¦ Title: ${incrementData.title}`);
|
|
360
486
|
console.log(` š User Stories: ${incrementData.userStories.length}`);
|
|
361
487
|
|
|
@@ -363,19 +489,36 @@ async function main() {
|
|
|
363
489
|
(sum, us) => sum + us.acceptanceCriteria.length, 0
|
|
364
490
|
);
|
|
365
491
|
console.log(` ā Acceptance Criteria: ${totalACs}`);
|
|
492
|
+
console.log(` š§ Tasks: ${incrementData.tasks.length}`);
|
|
366
493
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
494
|
+
if (incrementData.userStories.length === 0) {
|
|
495
|
+
console.error(`\nā No user stories found in spec.md`);
|
|
496
|
+
console.error(' Ensure spec.md has ### US-XXX: Title sections');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
370
499
|
|
|
371
|
-
|
|
372
|
-
console.log(
|
|
373
|
-
console.log(`
|
|
500
|
+
// Preview issues
|
|
501
|
+
console.log(`\nš Issues to Create/Update:`);
|
|
502
|
+
console.log(` šÆ Milestone: [${featureId}] ${incrementData.title}`);
|
|
503
|
+
for (const story of incrementData.userStories) {
|
|
504
|
+
const storyTasks = incrementData.tasks.filter(t =>
|
|
505
|
+
t.userStories.includes(story.id) ||
|
|
506
|
+
t.userStories.some(us => us.includes(story.id.replace('US-', '')))
|
|
507
|
+
);
|
|
508
|
+
console.log(` š [${featureId}][${story.id}] ${story.title}`);
|
|
509
|
+
console.log(` āā ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
|
|
510
|
+
}
|
|
374
511
|
|
|
375
512
|
if (dryRun) {
|
|
376
|
-
console.log(`\nš Issue Body
|
|
377
|
-
|
|
378
|
-
|
|
513
|
+
console.log(`\nš Sample Issue Body (${incrementData.userStories[0].id}):\n`);
|
|
514
|
+
const sampleBody = buildUserStoryIssueBody(
|
|
515
|
+
incrementData.userStories[0],
|
|
516
|
+
incrementData.tasks,
|
|
517
|
+
incrementData,
|
|
518
|
+
config ? `${config.owner}/${config.repo}` : 'owner/repo'
|
|
519
|
+
);
|
|
520
|
+
console.log(sampleBody);
|
|
521
|
+
console.log(`\nā
Dry run complete (no issues created)`);
|
|
379
522
|
process.exit(0);
|
|
380
523
|
}
|
|
381
524
|
|
|
@@ -385,90 +528,51 @@ async function main() {
|
|
|
385
528
|
process.exit(1);
|
|
386
529
|
}
|
|
387
530
|
|
|
388
|
-
//
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
if (metadataLink) {
|
|
392
|
-
console.log(`\nš Found existing issue link in metadata: #${metadataLink.issue}`);
|
|
393
|
-
|
|
394
|
-
// Update existing issue with new format
|
|
395
|
-
console.log(`š Updating issue #${metadataLink.issue} with new format...`);
|
|
396
|
-
|
|
397
|
-
// Update body
|
|
398
|
-
await updateGitHubIssue(config.owner, config.repo, metadataLink.issue, issue.body);
|
|
399
|
-
console.log(` ā
Body updated with User Stories and ACs`);
|
|
400
|
-
|
|
401
|
-
// Update title to new format (fix the [0002] ā [FS-XXX] issue)
|
|
402
|
-
const updateTitleResult = await execFileNoThrow('gh', [
|
|
403
|
-
'issue', 'edit',
|
|
404
|
-
String(metadataLink.issue),
|
|
405
|
-
'--repo', `${config.owner}/${config.repo}`,
|
|
406
|
-
'--title', issue.title
|
|
407
|
-
]);
|
|
408
|
-
|
|
409
|
-
if (updateTitleResult.exitCode === 0) {
|
|
410
|
-
console.log(` ā
Title updated to: ${issue.title}`);
|
|
411
|
-
} else {
|
|
412
|
-
console.log(` ā ļø Could not update title (may need permissions)`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Update metadata with lastSync
|
|
416
|
-
await updateIncrementMetadata(
|
|
417
|
-
incrementPath,
|
|
418
|
-
metadataLink.issue,
|
|
419
|
-
`https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
console.log(`\nā
Sync complete!`);
|
|
423
|
-
console.log(` š https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`);
|
|
424
|
-
process.exit(0);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// STEP 2: Search GitHub by feature ID (fallback)
|
|
428
|
-
const featureId = incrementData.frontmatter.feature_id ||
|
|
429
|
-
`FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, '0') || 'UNKNOWN'}`;
|
|
430
|
-
|
|
431
|
-
console.log(`\nš Searching GitHub for existing issue [${featureId}]...`);
|
|
432
|
-
const existingIssue = await findExistingIssue(config.owner, config.repo, featureId);
|
|
433
|
-
|
|
434
|
-
if (existingIssue) {
|
|
435
|
-
console.log(` Found existing issue: #${existingIssue}`);
|
|
531
|
+
// Load existing links
|
|
532
|
+
const existingLinks = loadExistingGitHubLinks(incrementPath);
|
|
436
533
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(` ā
Issue #${existingIssue} updated`);
|
|
441
|
-
|
|
442
|
-
await updateIncrementMetadata(
|
|
443
|
-
incrementPath,
|
|
444
|
-
existingIssue,
|
|
445
|
-
`https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
console.log(`\nā
Sync complete!`);
|
|
449
|
-
console.log(` š https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`);
|
|
450
|
-
process.exit(0);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// STEP 3: Create new issue (no existing found)
|
|
454
|
-
console.log(` No existing issue found`);
|
|
455
|
-
console.log(`\nš Creating GitHub issue...`);
|
|
456
|
-
const created = await createGitHubIssue(
|
|
534
|
+
// Create or get milestone
|
|
535
|
+
console.log(`\nšÆ Creating/updating milestone...`);
|
|
536
|
+
const milestone = await createOrGetMilestone(
|
|
457
537
|
config.owner,
|
|
458
538
|
config.repo,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
539
|
+
featureId,
|
|
540
|
+
incrementData.title,
|
|
541
|
+
existingLinks.milestone
|
|
462
542
|
);
|
|
543
|
+
console.log(` ā
Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
|
|
544
|
+
|
|
545
|
+
// Create/update issues for each user story
|
|
546
|
+
console.log(`\nš Creating/updating user story issues...`);
|
|
547
|
+
const userStoryIssues: Record<string, number> = {};
|
|
548
|
+
|
|
549
|
+
for (const story of incrementData.userStories) {
|
|
550
|
+
const existingIssue = existingLinks.userStoryIssues[story.id];
|
|
551
|
+
|
|
552
|
+
const issue = await syncUserStoryIssue(
|
|
553
|
+
config.owner,
|
|
554
|
+
config.repo,
|
|
555
|
+
featureId,
|
|
556
|
+
story,
|
|
557
|
+
incrementData.tasks,
|
|
558
|
+
incrementData,
|
|
559
|
+
milestone.number,
|
|
560
|
+
existingIssue
|
|
561
|
+
);
|
|
463
562
|
|
|
464
|
-
|
|
563
|
+
userStoryIssues[story.id] = issue.number;
|
|
564
|
+
|
|
565
|
+
const action = existingIssue ? 'ā»ļø Updated' : 'ā
Created';
|
|
566
|
+
console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
|
|
567
|
+
}
|
|
465
568
|
|
|
466
569
|
// Update metadata
|
|
467
|
-
await updateIncrementMetadata(incrementPath,
|
|
468
|
-
console.log(
|
|
570
|
+
await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
|
|
571
|
+
console.log(`\nš Metadata updated`);
|
|
469
572
|
|
|
470
573
|
console.log(`\nā
Sync complete!`);
|
|
471
|
-
console.log(`
|
|
574
|
+
console.log(` šÆ Milestone: ${milestone.url}`);
|
|
575
|
+
console.log(` š Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
|
|
472
576
|
|
|
473
577
|
process.exit(0);
|
|
474
578
|
} catch (error) {
|