@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.
@@ -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
+ }