@docupace/pipeline 1.0.85 → 1.0.87
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/designs/convert-to-canny.md +20 -0
- package/package.json +10 -4
- package/src/auth0/configure.js +1 -3
- package/src/changeset/canny.js +119 -0
- package/src/changeset/changelog-module.cjs +37 -0
- package/src/changeset/changelog.js +210 -0
- package/src/changeset/cli.js +27 -42
- package/src/changeset/config.js +20 -0
- package/src/changeset/convert.js +461 -0
- package/src/changeset/create-canny.js +86 -0
- package/src/changeset/create-simple.js +131 -0
- package/src/changeset/create.js +41 -123
- package/src/changeset/openai.js +567 -0
- package/src/changeset/validate.js +1 -12
- package/src/changeset/version.js +25 -3
- package/src/index.mjs +2 -0
- package/src/lib/merge-driver.js +1 -5
- package/src/merge/cli-merge-all.js +1 -2
- package/src/merge/cli-merge.js +4 -4
- package/src/merge/merge-changelog.js +1 -1
- package/src/merge/pr.js +3 -3
- package/src/next/connect.js +0 -9
- package/src/next/deploy.js +58 -26
- package/src/next/jira.js +51 -0
- package/src/next/util.js +1 -1
- package/testCanny.sh +4 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { getReleaseNotes, JIRA_BASE_URL } from './openai.js';
|
|
6
|
+
import { getJiraReleaseNotes, updateJiraReleaseNotes } from '../next/jira.js';
|
|
7
|
+
import { uploadToCanny } from './canny.js';
|
|
8
|
+
import { labelIDMap } from './config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Update multiple details links in text
|
|
12
|
+
* Similar to updateDetailsLinkInText but supports multiple [[details]](...) links
|
|
13
|
+
* @param {string} text - Text containing [[details]](link) patterns
|
|
14
|
+
* @param {Function} processDetailsLink - Function to process each link
|
|
15
|
+
* @param {Object} params - Parameters to pass to processDetailsLink
|
|
16
|
+
* @returns {Promise<string>} - Updated text with processed links
|
|
17
|
+
*/
|
|
18
|
+
async function updateMultipleDetailsLinks(text, processDetailsLink, params) {
|
|
19
|
+
const detailsLinkRegex = /\[\[details\]\]\((.*?)\)/g;
|
|
20
|
+
|
|
21
|
+
let result = text;
|
|
22
|
+
const matches = [...text.matchAll(detailsLinkRegex)];
|
|
23
|
+
|
|
24
|
+
for (const match of matches) {
|
|
25
|
+
const originalLink = match[1];
|
|
26
|
+
const newLink = await processDetailsLink(originalLink, params);
|
|
27
|
+
|
|
28
|
+
if (newLink) {
|
|
29
|
+
// Replace this specific occurrence
|
|
30
|
+
result = result.replace(
|
|
31
|
+
`[[details]](${originalLink})`,
|
|
32
|
+
`[[details]](${newLink})`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract Jira ticket IDs from a CHANGELOG.md file starting from a specific version
|
|
42
|
+
* Scans the entire file to handle unordered versions
|
|
43
|
+
* @param {string} changelogPath - Path to the CHANGELOG.md file
|
|
44
|
+
* @param {string} startVersion - Version to start from (e.g., "1.14.0")
|
|
45
|
+
* @returns {Array<string>} - Array of Jira ticket IDs
|
|
46
|
+
*/
|
|
47
|
+
function extractJiraTicketsFromChangelog(changelogPath, startVersion) {
|
|
48
|
+
const content = fs.readFileSync(changelogPath, 'utf8');
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
|
|
51
|
+
const tickets = [];
|
|
52
|
+
let shouldCapture = false;
|
|
53
|
+
|
|
54
|
+
// Parse major.minor from startVersion (ignore patch)
|
|
55
|
+
const [startMajor, startMinor] = startVersion.split('.').map(Number);
|
|
56
|
+
|
|
57
|
+
// Scan entire file to handle unordered versions
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
// Check for version headers like ## 1.14.52
|
|
60
|
+
const versionMatch = line.match(/^##\s+(\d+)\.(\d+)\.(\d+)/);
|
|
61
|
+
if (versionMatch) {
|
|
62
|
+
const [, major, minor] = versionMatch.map(Number);
|
|
63
|
+
|
|
64
|
+
// Check if this version meets the criteria
|
|
65
|
+
if (major > startMajor || (major === startMajor && minor >= startMinor)) {
|
|
66
|
+
shouldCapture = true;
|
|
67
|
+
} else {
|
|
68
|
+
shouldCapture = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract Jira ticket IDs from lines like: - ea7bce9: Some text [DC-91190]
|
|
73
|
+
// Skip lines that contain [[details]] as they have already been processed
|
|
74
|
+
if (shouldCapture && !line.includes('[[details]]')) {
|
|
75
|
+
const ticketMatch = line.match(/\[([A-Z]+-\d+)\]/g);
|
|
76
|
+
if (ticketMatch) {
|
|
77
|
+
ticketMatch.forEach((match) => {
|
|
78
|
+
const ticketId = match.replace(/[[\]]/g, '');
|
|
79
|
+
if (!tickets.includes(ticketId)) {
|
|
80
|
+
tickets.push(ticketId);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tickets;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get labels for Canny based on version
|
|
92
|
+
* @param {string} version - Version string (e.g., "1.14.52")
|
|
93
|
+
* @param {Object} releaseNote - Release note object from YAML
|
|
94
|
+
* @returns {Array<string>} - Array of label names
|
|
95
|
+
*/
|
|
96
|
+
function getLabelsForConversion(version) {
|
|
97
|
+
const parsed = semver.parse(version);
|
|
98
|
+
if (!parsed) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const majorMinor = `${parsed.major}.${parsed.minor}`;
|
|
102
|
+
const componentName = 'Enterprise';
|
|
103
|
+
const labels = [`${componentName} - ${majorMinor}`];
|
|
104
|
+
return labels;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get label IDs for Canny
|
|
109
|
+
* @param {Array<string>} labels - Array of label names
|
|
110
|
+
* @returns {Promise<Array<string>>} - Array of label IDs
|
|
111
|
+
*/
|
|
112
|
+
async function getLabelIDsForConversion(labels) {
|
|
113
|
+
let cannyConfig;
|
|
114
|
+
try {
|
|
115
|
+
const cannyConfigPath = path.join(
|
|
116
|
+
process.cwd(),
|
|
117
|
+
'.changeset',
|
|
118
|
+
'canny.json',
|
|
119
|
+
);
|
|
120
|
+
cannyConfig = JSON.parse(fs.readFileSync(cannyConfigPath, 'utf-8'));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('Canny read canny.json file', e);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const mergedLabelMap = { ...labelIDMap, ...cannyConfig?.labels };
|
|
126
|
+
return labels.map((label) => mergedLabelMap[label]).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stripAfterTicketLink(logMessage) {
|
|
130
|
+
return logMessage.replace(/\s*\[\[[A-Z]+-\d+\]\]\([^)]+\).*/s, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract summary description from changelog entry text
|
|
135
|
+
* @param {string} text - Changelog entry text
|
|
136
|
+
* @returns {string} - Extracted summary
|
|
137
|
+
*/
|
|
138
|
+
function extractSummaryFromChangelog(text) {
|
|
139
|
+
const lines = text.split(/\r?\n/);
|
|
140
|
+
const summaryLines = [];
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
if (lines[i].startsWith('[[')) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
summaryLines.push(lines[i]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return stripAfterTicketLink(summaryLines.join('\n').trim());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fetch new link for conversion workflow
|
|
154
|
+
* This is specifically designed for convert-complete and doesn't modify fetchNewLink
|
|
155
|
+
* @param {string} oldLink - Original link (yaml file path)
|
|
156
|
+
* @param {Object} params - Parameters containing version and taskTicketId
|
|
157
|
+
* @param {string} params.version - Version string from changelog (e.g., "1.14.52")
|
|
158
|
+
* @param {string} params.taskTicketId - Task ticket ID from changelog entry
|
|
159
|
+
* @param {string} params.changelogEntry - Full changelog entry text for summary extraction
|
|
160
|
+
* @param {string} params.changelogPath - Path to the CHANGELOG.md file
|
|
161
|
+
* @returns {Promise<string>} - New Canny URL or existing URL
|
|
162
|
+
*/
|
|
163
|
+
async function fetchNewLinkForConversion(oldLink, params) {
|
|
164
|
+
if (!oldLink.includes('.yaml')) {
|
|
165
|
+
return oldLink; // Not a yaml link, return as-is
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { version, taskTicketId, changelogEntry, changelogPath } = params;
|
|
169
|
+
|
|
170
|
+
// Normalize and resolve the yaml file path
|
|
171
|
+
// The oldLink is a relative path from the changelog (e.g., .release-notes/DC-91190.yaml)
|
|
172
|
+
// We need to resolve it relative to the CHANGELOG.md directory
|
|
173
|
+
const normalizedOldLink = oldLink.replaceAll('\\', '/');
|
|
174
|
+
const changelogDir = path.dirname(changelogPath);
|
|
175
|
+
const yamlPath = path.resolve(changelogDir, normalizedOldLink);
|
|
176
|
+
|
|
177
|
+
if (!fs.existsSync(yamlPath)) {
|
|
178
|
+
console.error(`❌ YAML file not found: ${yamlPath}`);
|
|
179
|
+
return oldLink;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Read the yaml file to get release note data
|
|
183
|
+
const releaseNote = yaml.load(fs.readFileSync(yamlPath, 'utf-8'));
|
|
184
|
+
const mainTicketId = releaseNote.ticketId;
|
|
185
|
+
|
|
186
|
+
// Extract summary from the changelog entry
|
|
187
|
+
const summary = changelogEntry
|
|
188
|
+
? extractSummaryFromChangelog(changelogEntry)
|
|
189
|
+
: '';
|
|
190
|
+
|
|
191
|
+
console.log(`📝 Processing ${taskTicketId} (main: ${mainTicketId})...`);
|
|
192
|
+
|
|
193
|
+
// Calculate labels based on the version
|
|
194
|
+
const labels = getLabelsForConversion(version, releaseNote);
|
|
195
|
+
const labelIDs = await getLabelIDsForConversion(labels);
|
|
196
|
+
|
|
197
|
+
// Helper function to upload to Canny
|
|
198
|
+
const uploadToCannyInternal = async () => {
|
|
199
|
+
console.log(`📤 Uploading ${mainTicketId} to Canny...`);
|
|
200
|
+
const data = await uploadToCanny(releaseNote, labelIDs);
|
|
201
|
+
return data;
|
|
202
|
+
//return { url: 'test url for ' + releaseNote.ticketId };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Helper function to update Jira
|
|
206
|
+
const uploadToJiraInternal = async (ticketId, url) => {
|
|
207
|
+
console.log(
|
|
208
|
+
`📝 Updating Jira ${ticketId} with Canny URL ${url} and Summary : ${summary}`,
|
|
209
|
+
);
|
|
210
|
+
await updateJiraReleaseNotes(ticketId, summary, url, labels);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Check if the main ticket has release notes in Jira
|
|
214
|
+
const jiraReleaseInfo = await getJiraReleaseNotes(mainTicketId);
|
|
215
|
+
|
|
216
|
+
// If task ticket differs from main ticket, handle both
|
|
217
|
+
if (mainTicketId !== taskTicketId) {
|
|
218
|
+
const subtaskJiraReleaseInfo = await getJiraReleaseNotes(taskTicketId);
|
|
219
|
+
|
|
220
|
+
// If subtask already has a URL, return it
|
|
221
|
+
if (subtaskJiraReleaseInfo?.url) {
|
|
222
|
+
console.log(`✅ ${taskTicketId} already has Canny URL`);
|
|
223
|
+
return subtaskJiraReleaseInfo.url;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If main ticket has a URL, use it for subtask
|
|
227
|
+
if (jiraReleaseInfo?.url) {
|
|
228
|
+
await uploadToJiraInternal(taskTicketId, jiraReleaseInfo.url);
|
|
229
|
+
return jiraReleaseInfo.url;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Otherwise, upload to Canny and update both tickets
|
|
233
|
+
const data = await uploadToCannyInternal();
|
|
234
|
+
await uploadToJiraInternal(taskTicketId, data.url);
|
|
235
|
+
await uploadToJiraInternal(mainTicketId, data.url);
|
|
236
|
+
return data.url;
|
|
237
|
+
} else {
|
|
238
|
+
// Task ticket is the same as main ticket
|
|
239
|
+
if (jiraReleaseInfo?.url) {
|
|
240
|
+
console.log(`✅ ${mainTicketId} already has Canny URL`);
|
|
241
|
+
return jiraReleaseInfo.url;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Upload to Canny and update Jira
|
|
245
|
+
const data = await uploadToCannyInternal();
|
|
246
|
+
await uploadToJiraInternal(mainTicketId, data.url);
|
|
247
|
+
return data.url;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Replace Jira ticket references in CHANGELOG.md with enhanced links
|
|
253
|
+
* @param {string} changelogPath - Path to the CHANGELOG.md file
|
|
254
|
+
* @param {string} ticketId - Jira ticket ID (e.g., "DC-91190")
|
|
255
|
+
* @param {string} yamlFilePath - Path to the generated YAML file
|
|
256
|
+
*/
|
|
257
|
+
function replaceTicketInChangelog(changelogPath, ticketId, yamlFilePath) {
|
|
258
|
+
let content = fs.readFileSync(changelogPath, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Calculate relative path from the CHANGELOG.md location to the yaml file
|
|
261
|
+
const changelogDir = path.dirname(changelogPath);
|
|
262
|
+
const relYamlFilePath = path
|
|
263
|
+
.relative(changelogDir, yamlFilePath)
|
|
264
|
+
.replaceAll('\\', '/');
|
|
265
|
+
|
|
266
|
+
// Replace [DC-XXXX] with [[DC-XXXX]](url) [[details]](yaml-path)
|
|
267
|
+
// Use a regex that matches [TICKET-ID] but not when it's already been replaced
|
|
268
|
+
const regex = new RegExp(`\\[${ticketId}\\](?!\\()`, 'g');
|
|
269
|
+
const replacement = `[[${ticketId}]](${JIRA_BASE_URL}/browse/${ticketId}) [[details]](${relYamlFilePath})`;
|
|
270
|
+
|
|
271
|
+
content = content.replace(regex, replacement);
|
|
272
|
+
|
|
273
|
+
fs.writeFileSync(changelogPath, content, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Convert changelog entries to Canny format - Part 1 (Prepare)
|
|
278
|
+
* This function processes the changelog to generate YAML files and update ticket references
|
|
279
|
+
* @param {string} versionId - Version ID (e.g., "1.14") to start processing from
|
|
280
|
+
* @param {string} changelogPath - Optional path to CHANGELOG.md (defaults to ./CHANGELOG.md)
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
export async function convertPrepare(
|
|
284
|
+
versionId,
|
|
285
|
+
changelogPath = './CHANGELOG.md',
|
|
286
|
+
) {
|
|
287
|
+
console.log(`🔄 Starting convert-prepare for version ${versionId}...`);
|
|
288
|
+
|
|
289
|
+
const resolvedChangelogPath = path.resolve(process.cwd(), changelogPath);
|
|
290
|
+
|
|
291
|
+
if (!fs.existsSync(resolvedChangelogPath)) {
|
|
292
|
+
throw new Error(`CHANGELOG.md not found at ${resolvedChangelogPath}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Extract Jira tickets from the changelog
|
|
296
|
+
console.log('📋 Extracting Jira tickets from changelog...');
|
|
297
|
+
const tickets = extractJiraTicketsFromChangelog(
|
|
298
|
+
resolvedChangelogPath,
|
|
299
|
+
versionId,
|
|
300
|
+
);
|
|
301
|
+
console.log(`Found ${tickets.length} tickets to process`);
|
|
302
|
+
|
|
303
|
+
// Process tickets in parallel batches of 5
|
|
304
|
+
const BATCH_SIZE = 5;
|
|
305
|
+
let processedCount = 0;
|
|
306
|
+
let errorCount = 0;
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < tickets.length; i += BATCH_SIZE) {
|
|
309
|
+
const batch = tickets.slice(i, i + BATCH_SIZE);
|
|
310
|
+
console.log(
|
|
311
|
+
`\n🔄 Processing batch ${Math.floor(i / BATCH_SIZE) + 1} (${batch.length} tickets)...`,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Process all tickets in the batch in parallel
|
|
315
|
+
const batchPromises = batch.map(async (ticketId) => {
|
|
316
|
+
try {
|
|
317
|
+
console.log(`📝 Processing ${ticketId}...`);
|
|
318
|
+
|
|
319
|
+
// Generate release notes and YAML file
|
|
320
|
+
const { yamlFilePath } = await getReleaseNotes(ticketId);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
ticketId,
|
|
325
|
+
yamlFilePath,
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(`❌ Error processing ${ticketId}:`, error.message);
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
ticketId,
|
|
332
|
+
error: error.message,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Wait for all tickets in the batch to complete
|
|
338
|
+
const results = await Promise.all(batchPromises);
|
|
339
|
+
|
|
340
|
+
// Update the changelog with all successful results from this batch
|
|
341
|
+
for (const result of results) {
|
|
342
|
+
if (result.success) {
|
|
343
|
+
replaceTicketInChangelog(
|
|
344
|
+
resolvedChangelogPath,
|
|
345
|
+
result.ticketId,
|
|
346
|
+
result.yamlFilePath,
|
|
347
|
+
);
|
|
348
|
+
console.log(`✅ Successfully processed ${result.ticketId}`);
|
|
349
|
+
processedCount++;
|
|
350
|
+
} else {
|
|
351
|
+
errorCount++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(
|
|
356
|
+
`✨ Batch ${Math.floor(i / BATCH_SIZE) + 1} completed. Updated changelog.`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(`\n✨ Convert-prepare completed!`);
|
|
361
|
+
console.log(` Processed: ${processedCount}`);
|
|
362
|
+
console.log(` Errors: ${errorCount}`);
|
|
363
|
+
console.log(`\n📄 Updated changelog at: ${resolvedChangelogPath}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Parse changelog to extract context for each details link
|
|
368
|
+
* @param {string} content - Changelog content
|
|
369
|
+
* @param {string} changelogPath - Path to the CHANGELOG.md file
|
|
370
|
+
* @returns {Map<string, Object>} - Map of yaml file paths to their context
|
|
371
|
+
*/
|
|
372
|
+
function parseChangelogContext(content, changelogPath) {
|
|
373
|
+
const lines = content.split('\n');
|
|
374
|
+
const contextMap = new Map();
|
|
375
|
+
let currentVersion = null;
|
|
376
|
+
|
|
377
|
+
for (let i = 0; i < lines.length; i++) {
|
|
378
|
+
const line = lines[i];
|
|
379
|
+
|
|
380
|
+
// Check for version headers like ## 1.14.52
|
|
381
|
+
const versionMatch = line.match(/^##\s+(\d+\.\d+\.\d+)/);
|
|
382
|
+
if (versionMatch) {
|
|
383
|
+
currentVersion = versionMatch[1];
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Extract entries with details links
|
|
388
|
+
// Format: - commit: text [TICKET-ID] [[TICKET-ID]](url) [[details]](yaml-path)
|
|
389
|
+
const detailsMatch = line.match(/\[\[details\]\]\((.*?\.yaml)\)/);
|
|
390
|
+
if (detailsMatch && currentVersion) {
|
|
391
|
+
const yamlPath = detailsMatch[1];
|
|
392
|
+
|
|
393
|
+
// Extract ticket ID from the line
|
|
394
|
+
const ticketMatch = line.match(/\[\[([A-Z]+-\d+)\]\]/);
|
|
395
|
+
const taskTicketId = ticketMatch ? ticketMatch[1] : null;
|
|
396
|
+
|
|
397
|
+
// Extract the full entry text (everything after "- commit:")
|
|
398
|
+
const entryMatch = line.match(/^-\s+[a-f0-9]+:\s+(.*)/);
|
|
399
|
+
const changelogEntry = entryMatch ? entryMatch[1] : line;
|
|
400
|
+
|
|
401
|
+
if (taskTicketId) {
|
|
402
|
+
contextMap.set(yamlPath, {
|
|
403
|
+
version: currentVersion,
|
|
404
|
+
taskTicketId,
|
|
405
|
+
changelogEntry,
|
|
406
|
+
changelogPath,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return contextMap;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Convert changelog entries to Canny format - Part 2 (Complete)
|
|
417
|
+
* This function updates the details links to point to Canny URLs
|
|
418
|
+
* @param {string} changelogPath - Optional path to CHANGELOG.md (defaults to ./CHANGELOG.md)
|
|
419
|
+
*/
|
|
420
|
+
export async function convertComplete(changelogPath = './CHANGELOG.md') {
|
|
421
|
+
console.log('🔄 Starting convert-complete...');
|
|
422
|
+
|
|
423
|
+
const resolvedChangelogPath = path.resolve(process.cwd(), changelogPath);
|
|
424
|
+
|
|
425
|
+
if (!fs.existsSync(resolvedChangelogPath)) {
|
|
426
|
+
throw new Error(`CHANGELOG.md not found at ${resolvedChangelogPath}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Read the changelog content
|
|
430
|
+
let content = fs.readFileSync(resolvedChangelogPath, 'utf8');
|
|
431
|
+
|
|
432
|
+
console.log('🔗 Parsing changelog context...');
|
|
433
|
+
const contextMap = parseChangelogContext(content, resolvedChangelogPath);
|
|
434
|
+
|
|
435
|
+
console.log(`📝 Found ${contextMap.size} entries to process`);
|
|
436
|
+
console.log('🔗 Updating details links to Canny URLs...');
|
|
437
|
+
|
|
438
|
+
// Create a wrapper function that looks up context for each link
|
|
439
|
+
const processLinkWithContext = async (oldLink) => {
|
|
440
|
+
const context = contextMap.get(oldLink);
|
|
441
|
+
if (!context) {
|
|
442
|
+
console.warn(`⚠️ No context found for ${oldLink}, skipping...`);
|
|
443
|
+
return oldLink;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return await fetchNewLinkForConversion(oldLink, context);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Update all [[details]] links in the changelog to point to Canny URLs
|
|
450
|
+
const updatedContent = await updateMultipleDetailsLinks(
|
|
451
|
+
content,
|
|
452
|
+
processLinkWithContext,
|
|
453
|
+
null,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// Write the updated content back to the changelog
|
|
457
|
+
fs.writeFileSync(resolvedChangelogPath, updatedContent, 'utf8');
|
|
458
|
+
|
|
459
|
+
console.log(`\n✨ Convert-complete completed!`);
|
|
460
|
+
console.log(`📄 Updated changelog at: ${resolvedChangelogPath}`);
|
|
461
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createChangesetDirectory,
|
|
3
|
+
extractJiraTicketId,
|
|
4
|
+
getCurrentBranch,
|
|
5
|
+
} from './create.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { getReleaseNotes, JIRA_BASE_URL } from './openai.js';
|
|
9
|
+
|
|
10
|
+
export async function createChangesetForPackageCanny(
|
|
11
|
+
packages,
|
|
12
|
+
selectedJiraIssueKey,
|
|
13
|
+
) {
|
|
14
|
+
try {
|
|
15
|
+
let jiraTicketId;
|
|
16
|
+
|
|
17
|
+
if (selectedJiraIssueKey) {
|
|
18
|
+
jiraTicketId = selectedJiraIssueKey;
|
|
19
|
+
} else {
|
|
20
|
+
const branchName = await getCurrentBranch();
|
|
21
|
+
|
|
22
|
+
jiraTicketId = extractJiraTicketId(branchName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Generating release notes for ${jiraTicketId}`);
|
|
26
|
+
|
|
27
|
+
const { enhancedNotes, yamlFilePath, mainTicketId, summary } =
|
|
28
|
+
await getReleaseNotes(jiraTicketId);
|
|
29
|
+
|
|
30
|
+
const { changeType } = enhancedNotes;
|
|
31
|
+
const change_type = changeType == 'Fixed' ? 'patch' : 'minor';
|
|
32
|
+
|
|
33
|
+
// Step 4: Generate release notes using OpenAI
|
|
34
|
+
console.log(`Change type: ${change_type}`);
|
|
35
|
+
console.log(`Release note: ${summary}`);
|
|
36
|
+
|
|
37
|
+
// Step 5: Create a changeset .md file for all packages
|
|
38
|
+
await writeChangesetFile(
|
|
39
|
+
packages,
|
|
40
|
+
jiraTicketId,
|
|
41
|
+
mainTicketId,
|
|
42
|
+
change_type,
|
|
43
|
+
summary,
|
|
44
|
+
yamlFilePath,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
console.log('Changeset created successfully!');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Error: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Writes the changeset file
|
|
56
|
+
*/
|
|
57
|
+
async function writeChangesetFile(
|
|
58
|
+
packages,
|
|
59
|
+
taskTicketId,
|
|
60
|
+
mainTicketId,
|
|
61
|
+
changeType,
|
|
62
|
+
summary,
|
|
63
|
+
yamlFilePath,
|
|
64
|
+
) {
|
|
65
|
+
// Generate the YAML frontmatter for multiple packages
|
|
66
|
+
const packageEntries = packages
|
|
67
|
+
.map((pkg) => `'${pkg.name}': ${changeType}`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
|
|
70
|
+
const relYamlFilePath = path
|
|
71
|
+
.relative(path.resolve(process.cwd(), '.changeset'), yamlFilePath)
|
|
72
|
+
.replaceAll('\\', '/');
|
|
73
|
+
|
|
74
|
+
const changesetContent = `---
|
|
75
|
+
${packageEntries}
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
${summary}
|
|
79
|
+
[[${taskTicketId}]](${JIRA_BASE_URL}/browse/${taskTicketId}) [[details]](${relYamlFilePath})`;
|
|
80
|
+
|
|
81
|
+
const changesetFile = `./.changeset/${taskTicketId}.md`;
|
|
82
|
+
|
|
83
|
+
await createChangesetDirectory();
|
|
84
|
+
await fs.promises.writeFile(changesetFile, changesetContent);
|
|
85
|
+
console.log(`Created changeset file at ${changesetFile}`);
|
|
86
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createChangesetDirectory,
|
|
3
|
+
extractJiraTicketId,
|
|
4
|
+
getCurrentBranch,
|
|
5
|
+
} from './create.js';
|
|
6
|
+
import OpenAI from 'openai';
|
|
7
|
+
import z from 'zod';
|
|
8
|
+
import { zodResponseFormat } from 'openai/helpers/zod';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
|
|
11
|
+
export async function createChangesetForPackageSimple(packages) {
|
|
12
|
+
try {
|
|
13
|
+
// Step 1: Get the current branch name
|
|
14
|
+
const branchName = await getCurrentBranch();
|
|
15
|
+
|
|
16
|
+
// Step 2: Extract Jira ticket number
|
|
17
|
+
const jiraTicketId = extractJiraTicketId(branchName);
|
|
18
|
+
|
|
19
|
+
// Step 3: Fetch Jira ticket data
|
|
20
|
+
const jiraTicketData = await fetchJiraTicketData(jiraTicketId);
|
|
21
|
+
const jiraSummary = jiraTicketData.fields.summary;
|
|
22
|
+
console.log(
|
|
23
|
+
`Generating release notes for ${jiraTicketId} - ${jiraSummary}`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Step 4: Generate release notes using OpenAI
|
|
27
|
+
const { change_type, summary } = await generateReleaseNotes(jiraTicketData);
|
|
28
|
+
console.log(`Change type: ${change_type}`);
|
|
29
|
+
console.log(`Release note: ${summary}`);
|
|
30
|
+
|
|
31
|
+
// Step 5: Create a changeset .md file for all packages
|
|
32
|
+
await writeJiraChangesetFile(packages, jiraTicketId, change_type, summary);
|
|
33
|
+
|
|
34
|
+
console.log('Changeset created successfully!');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`Error: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetches Jira ticket data from Jira API
|
|
43
|
+
*/
|
|
44
|
+
async function fetchJiraTicketData(jiraTicketId) {
|
|
45
|
+
const jiraBaseUrl = 'https://docupacellc.atlassian.net';
|
|
46
|
+
const jiraEmail = process.env.JIRA_API_EMAIL;
|
|
47
|
+
const jiraApiKey = process.env.JIRA_API_KEY;
|
|
48
|
+
const jiraFullApiKey = `${jiraEmail}:${jiraApiKey}`;
|
|
49
|
+
const jiraUrl = `${jiraBaseUrl}/rest/api/2/issue/${jiraTicketId}`;
|
|
50
|
+
|
|
51
|
+
const jiraResponse = await fetch(jiraUrl, {
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Basic ${Buffer.from(jiraFullApiKey).toString('base64')}`,
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!jiraResponse.ok) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Failed to fetch Jira ticket ${jiraTicketId}. Status: ${jiraResponse.status}, Ticket URL: ${jiraUrl}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return await jiraResponse.json();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generates release notes using OpenAI
|
|
69
|
+
*/
|
|
70
|
+
async function generateReleaseNotes(jiraTicketData) {
|
|
71
|
+
const openaiClient = new OpenAI({
|
|
72
|
+
apiKey: process.env['OPENAI_API_KEY'],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const ChangeRequestSchema = z.object({
|
|
76
|
+
change_type: z.enum(['patch', 'minor']),
|
|
77
|
+
summary: z.string(),
|
|
78
|
+
jira_ticket_id: z.string(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const openAiResponse = await openaiClient.chat.completions.create({
|
|
82
|
+
model: 'gpt-4o-mini',
|
|
83
|
+
messages: [
|
|
84
|
+
{
|
|
85
|
+
role: 'system',
|
|
86
|
+
content: `Process JSON from a Jira ticket to generate simple maximum 2 sentence release note for the ticket and determine the change type for bug it should be a patch for new feature it should be a minor change`,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
role: 'user',
|
|
90
|
+
content: JSON.stringify(jiraTicketData),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
response_format: zodResponseFormat(ChangeRequestSchema, 'changeset_schema'),
|
|
94
|
+
temperature: 0,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const assistantResponse = openAiResponse.choices[0].message.content;
|
|
98
|
+
return JSON.parse(assistantResponse);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Writes the changeset file
|
|
103
|
+
*/
|
|
104
|
+
async function writeJiraChangesetFile(
|
|
105
|
+
packages,
|
|
106
|
+
jiraTicketId,
|
|
107
|
+
changeType,
|
|
108
|
+
summary,
|
|
109
|
+
) {
|
|
110
|
+
const text = `${summary} [${jiraTicketId}]`;
|
|
111
|
+
await writeChangesetFile(packages, jiraTicketId, changeType, text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function writeChangesetFile(packages, filename, changeType, text) {
|
|
115
|
+
const packageEntries = packages
|
|
116
|
+
.map((pkg) => `'${pkg.name}': ${changeType}`)
|
|
117
|
+
.join('\n');
|
|
118
|
+
|
|
119
|
+
const changesetContent = `---
|
|
120
|
+
${packageEntries}
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
${text}
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const changesetFile = `./.changeset/${filename}.md`;
|
|
127
|
+
|
|
128
|
+
await createChangesetDirectory();
|
|
129
|
+
await fs.promises.writeFile(changesetFile, changesetContent);
|
|
130
|
+
console.log(`Created changeset file at ${changesetFile}`);
|
|
131
|
+
}
|