@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,567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
dotenv.config({ path: ['.env.pipeline.local', 'config/.env.pipeline.local'] });
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fetch from 'node-fetch';
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
export const JIRA_BASE_URL =
|
|
13
|
+
process.env.JIRA_BASE_URL || 'https://docupacellc.atlassian.net';
|
|
14
|
+
|
|
15
|
+
const JIRA_API_KEY = process.env.JIRA_API_KEY;
|
|
16
|
+
const JIRA_API_EMAIL = process.env.JIRA_API_EMAIL;
|
|
17
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
18
|
+
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4.1';
|
|
19
|
+
|
|
20
|
+
const CHANGESET_DIR = path.join(process.cwd(), '.release-notes');
|
|
21
|
+
|
|
22
|
+
// Define the Zod schema for OpenAI response
|
|
23
|
+
const openAiResponseSchema = z.object({
|
|
24
|
+
title: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe('A concise title for the release note (3-8 words)'),
|
|
27
|
+
summary: z.string().describe('A brief user-facing summary of changes'),
|
|
28
|
+
technicalDescription: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Developer-friendly description of technical changes'),
|
|
32
|
+
userFacingDescription: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Plain-language description of changes from a user perspective'),
|
|
36
|
+
technicalOnly: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.describe(
|
|
39
|
+
'Whether this change is technical-only with no user-visible impact',
|
|
40
|
+
),
|
|
41
|
+
changeSize: z
|
|
42
|
+
.enum(['Small', 'Medium', 'Large'])
|
|
43
|
+
.describe('Size/scope of the change'),
|
|
44
|
+
changeType: z.enum(['New', 'Improved', 'Fixed']).describe('Type of change'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Define the Zod schema for changelog summary
|
|
48
|
+
const changeLogSummarySchema = z.object({
|
|
49
|
+
summary: z
|
|
50
|
+
.string()
|
|
51
|
+
.describe(
|
|
52
|
+
'A concise summary of what is being implemented in this ticket or subtask',
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Log token usage information
|
|
58
|
+
* @param {string} ticketId - The JIRA ticket ID
|
|
59
|
+
* @param {Object} tokenUsage - Token usage data
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Strip sensitive or unnecessary fields from a JIRA ticket object
|
|
63
|
+
* @param {Object} ticketData - The JIRA ticket data to clean
|
|
64
|
+
* @returns {Object} - The cleaned JIRA ticket data
|
|
65
|
+
*/
|
|
66
|
+
function stripJiraTicketFields(ticketData) {
|
|
67
|
+
if (!ticketData || !ticketData.fields) return ticketData;
|
|
68
|
+
|
|
69
|
+
const fieldsToStrip = [
|
|
70
|
+
'customfield_10020',
|
|
71
|
+
'worklog',
|
|
72
|
+
'creator',
|
|
73
|
+
'reporter',
|
|
74
|
+
'assignee',
|
|
75
|
+
'customfield_10095',
|
|
76
|
+
'customfield_10000',
|
|
77
|
+
'votes',
|
|
78
|
+
'watches',
|
|
79
|
+
'customfield_10083',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const cleanedData = JSON.parse(JSON.stringify(ticketData)); // Create a deep copy
|
|
83
|
+
|
|
84
|
+
// Strip specified fields
|
|
85
|
+
fieldsToStrip.forEach((field) => {
|
|
86
|
+
if (cleanedData.fields[field]) {
|
|
87
|
+
delete cleanedData.fields[field];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Remove all null fields
|
|
92
|
+
Object.keys(cleanedData.fields).forEach((field) => {
|
|
93
|
+
if (cleanedData.fields[field] === null) {
|
|
94
|
+
delete cleanedData.fields[field];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Clean up comments if they exist
|
|
99
|
+
if (cleanedData.fields.comment && cleanedData.fields.comment.comments) {
|
|
100
|
+
cleanedData.fields.comment.comments =
|
|
101
|
+
cleanedData.fields.comment.comments.map((comment) => {
|
|
102
|
+
const cleanComment = { ...comment };
|
|
103
|
+
if (cleanComment.author) delete cleanComment.author;
|
|
104
|
+
if (cleanComment.updateAuthor) delete cleanComment.updateAuthor;
|
|
105
|
+
|
|
106
|
+
// Remove null fields from comments
|
|
107
|
+
Object.keys(cleanComment).forEach((key) => {
|
|
108
|
+
if (cleanComment[key] === null) {
|
|
109
|
+
delete cleanComment[key];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return cleanComment;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return cleanedData;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fetch JIRA ticket information
|
|
122
|
+
* @param {string} ticketId - The JIRA ticket ID
|
|
123
|
+
* @returns {Promise<Object>} - The JIRA ticket data
|
|
124
|
+
*/
|
|
125
|
+
async function fetchJiraTicket(ticketId) {
|
|
126
|
+
if (!JIRA_API_KEY || !JIRA_API_EMAIL) {
|
|
127
|
+
console.warn('JIRA credentials not provided. Skipping JIRA API call.');
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const auth = Buffer.from(`${JIRA_API_EMAIL}:${JIRA_API_KEY}`).toString(
|
|
132
|
+
'base64',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(
|
|
137
|
+
`${JIRA_BASE_URL}/rest/api/3/issue/${ticketId}`,
|
|
138
|
+
{
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Basic ${auth}`,
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
console.error(
|
|
148
|
+
`Error fetching JIRA ticket ${ticketId}: ${response.statusText}`,
|
|
149
|
+
);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
const cleanedData = stripJiraTicketFields(data);
|
|
155
|
+
|
|
156
|
+
// Check if the ticket is a subtask
|
|
157
|
+
const isSubtask = cleanedData.fields.issuetype?.subtask || false;
|
|
158
|
+
let mainTicketId, subTaskTicketId, mainTicketData, subTaskTicketData;
|
|
159
|
+
|
|
160
|
+
// If it's a subtask, fetch the parent ticket as well
|
|
161
|
+
if (isSubtask && cleanedData.fields.parent) {
|
|
162
|
+
const parentTicketId = cleanedData.fields.parent.key;
|
|
163
|
+
mainTicketId = parentTicketId;
|
|
164
|
+
subTaskTicketId = ticketId;
|
|
165
|
+
delete cleanedData.fields.parent;
|
|
166
|
+
|
|
167
|
+
console.log(
|
|
168
|
+
`Fetching parent ticket ${parentTicketId} for subtask ${ticketId}`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const parentResponse = await fetch(
|
|
172
|
+
`${JIRA_BASE_URL}/rest/api/3/issue/${parentTicketId}`,
|
|
173
|
+
{
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Basic ${auth}`,
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (parentResponse.ok) {
|
|
182
|
+
const parentData = await parentResponse.json();
|
|
183
|
+
const cleanedParentData = stripJiraTicketFields(parentData);
|
|
184
|
+
|
|
185
|
+
mainTicketData = cleanedParentData;
|
|
186
|
+
subTaskTicketData = cleanedData;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
mainTicketId = ticketId;
|
|
190
|
+
mainTicketData = cleanedData;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
mainTicketData,
|
|
195
|
+
subTaskTicketData,
|
|
196
|
+
mainTicketId,
|
|
197
|
+
subTaskTicketId,
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Error fetching JIRA ticket ${ticketId}:`, error.message);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate a changelog summary using OpenAI
|
|
207
|
+
* @param {Object} params - Parameters object
|
|
208
|
+
* @param {Object} params.taskTicketData - The task/subtask ticket data
|
|
209
|
+
* @param {Object} params.mainTicketData - The main ticket data
|
|
210
|
+
* @returns {Promise<Object>} - Changelog summary
|
|
211
|
+
*/
|
|
212
|
+
export async function generateChangeLog({ taskTicketData, mainTicketData }) {
|
|
213
|
+
if (!OPENAI_API_KEY) {
|
|
214
|
+
console.warn('OpenAI API key not provided. Skipping changelog generation.');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!taskTicketData || !mainTicketData) return null;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Determine if this is a subtask by comparing ticket keys
|
|
222
|
+
const isSubtask = taskTicketData.key !== mainTicketData.key;
|
|
223
|
+
|
|
224
|
+
let systemPrompt;
|
|
225
|
+
let userPrompt;
|
|
226
|
+
|
|
227
|
+
if (isSubtask) {
|
|
228
|
+
// For subtasks, provide context from the main ticket
|
|
229
|
+
systemPrompt = `You are analyzing a Jira subtask and its parent ticket. Generate a concise summary of what is being implemented in this subtask.
|
|
230
|
+
|
|
231
|
+
The summary should:
|
|
232
|
+
- Focus on what the subtask is implementing specifically
|
|
233
|
+
- Include context from the parent ticket to provide understanding
|
|
234
|
+
- Be 1-2 sentences maximum
|
|
235
|
+
- Be clear and user-facing (avoid overly technical jargon)
|
|
236
|
+
|
|
237
|
+
Return your response in this JSON format:
|
|
238
|
+
{
|
|
239
|
+
"summary": "A concise summary of what is being implemented in the subtask"
|
|
240
|
+
}`;
|
|
241
|
+
|
|
242
|
+
userPrompt = JSON.stringify(
|
|
243
|
+
{
|
|
244
|
+
mainTicket: mainTicketData,
|
|
245
|
+
subtask: taskTicketData,
|
|
246
|
+
},
|
|
247
|
+
null,
|
|
248
|
+
2,
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
// For regular tickets, just summarize the main ticket
|
|
252
|
+
systemPrompt = `You are analyzing a Jira ticket. Generate a concise summary of what is being implemented.
|
|
253
|
+
|
|
254
|
+
The summary should:
|
|
255
|
+
- Be 1-2 sentences maximum
|
|
256
|
+
- Be clear and user-facing (avoid overly technical jargon)
|
|
257
|
+
- Capture the main purpose of the ticket
|
|
258
|
+
|
|
259
|
+
Return your response in this JSON format:
|
|
260
|
+
{
|
|
261
|
+
"summary": "A concise summary of what is being implemented"
|
|
262
|
+
}`;
|
|
263
|
+
|
|
264
|
+
userPrompt = JSON.stringify(mainTicketData, null, 2);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({
|
|
274
|
+
model: OPENAI_MODEL,
|
|
275
|
+
messages: [
|
|
276
|
+
{ role: 'system', content: systemPrompt },
|
|
277
|
+
{ role: 'user', content: userPrompt },
|
|
278
|
+
],
|
|
279
|
+
response_format: {
|
|
280
|
+
type: 'json_schema',
|
|
281
|
+
json_schema: {
|
|
282
|
+
name: 'changelog_summary',
|
|
283
|
+
schema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
summary: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: 'A concise summary of what is being implemented',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: ['summary'],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
temperature: 0,
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
console.error(`Error from OpenAI API: ${response.statusText}`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const data = await response.json();
|
|
306
|
+
const content = JSON.parse(data.choices[0].message.content);
|
|
307
|
+
|
|
308
|
+
// Validate response using Zod schema
|
|
309
|
+
const validatedResponse = changeLogSummarySchema.parse(content);
|
|
310
|
+
|
|
311
|
+
return validatedResponse;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Error generating changelog summary:', error.message);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Generate enhanced release notes using OpenAI
|
|
320
|
+
* @param {Object} ticketData - The JIRA ticket data
|
|
321
|
+
* @returns {Promise<Object>} - Enhanced release notes
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
export async function generateEnhancedReleaseNotes(ticketData) {
|
|
325
|
+
if (!OPENAI_API_KEY) {
|
|
326
|
+
console.warn(
|
|
327
|
+
'OpenAI API key not provided. Skipping release notes enhancement.',
|
|
328
|
+
);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!ticketData) return null;
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const systemPrompt = `Process the provided JSON from a Jira ticket to generate structured release notes in JSON format.
|
|
336
|
+
Follow this strict schema to generate your response:
|
|
337
|
+
|
|
338
|
+
{
|
|
339
|
+
"title": "A concise title for the release note (3-8 words)",
|
|
340
|
+
"summary": "A brief user-facing summary of changes (max 2 sentences)",
|
|
341
|
+
"technicalDescription": "Developer-friendly description of technical changes in markdown format (max 5 sentences)",
|
|
342
|
+
"userFacingDescription": "Provide only if changes made to user interface. Plain-language description of changes from a user perspective in markdown format (max 5 sentences)",
|
|
343
|
+
"technicalOnly": boolean (true/false whether this change is only technical with no user-visible impact),
|
|
344
|
+
"changeType": "New" | "Improved" | "Fixed",
|
|
345
|
+
"changeSize": "Small" | "Medium" | "Large"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
When creating your response:
|
|
349
|
+
- Title: Create a concise title summarizing the change (3–8 words). Avoid repeating the Jira ticket title.
|
|
350
|
+
- Summary: Write a maximum two-sentence summary in user-facing language.
|
|
351
|
+
- Technical Description: Use developer-friendly language to explain what changed at the system or code level. Include only if specified: API impacts, architectural changes, or deployment considerations. Format using markdown. Use backticks for code (\`like_this\`), bold for important terms, and line breaks for readability. Avoid headings like Technical Changes
|
|
352
|
+
- User-Facing Description: Provide only if changes are made to the user interface. Use plain language to explain what users will see or benefit from. Include affected UI elements, workflows, or systems. Prefer bullet points if listing multiple effects or changes. Use markdown formatting for clarity.
|
|
353
|
+
- Technical Only: Set to true if the change has no visible effect to end users. Examples for technical only are API changes, refactoring of code or architectual changes that do not have changes to the user interface
|
|
354
|
+
- Change Type: Classify as “New”, “Improved”, or “Fixed”.
|
|
355
|
+
- Change Size: Estimate the impact as “Small”, “Medium”, or “Large”.
|
|
356
|
+
|
|
357
|
+
For Descriptions:
|
|
358
|
+
Avoid large blocks of text. Break content into short paragraphs or lists. Use markdown consistently: code, bold, italic, and single level bullet lists (- Item). Make your response easy to scan, especially the descriptions. Focus on what changed, how it works, and where it applies.
|
|
359
|
+
Avoid restating the ticket title. Focus on what changed, how it works, and where it applies.
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
const userPrompt = JSON.stringify(ticketData, null, 2);
|
|
363
|
+
|
|
364
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: {
|
|
367
|
+
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
|
368
|
+
'Content-Type': 'application/json',
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
model: OPENAI_MODEL,
|
|
372
|
+
messages: [
|
|
373
|
+
{ role: 'system', content: systemPrompt },
|
|
374
|
+
{ role: 'user', content: userPrompt },
|
|
375
|
+
],
|
|
376
|
+
response_format: {
|
|
377
|
+
type: 'json_schema',
|
|
378
|
+
json_schema: {
|
|
379
|
+
name: 'release_notes',
|
|
380
|
+
schema: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
title: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description:
|
|
386
|
+
'A concise title for the release note (3-8 words)',
|
|
387
|
+
},
|
|
388
|
+
summary: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description:
|
|
391
|
+
'A brief user-facing summary of changes (max 2 sentences)',
|
|
392
|
+
},
|
|
393
|
+
technicalDescription: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
description:
|
|
396
|
+
'Developer-friendly description of technical changes (max 5 sentences)',
|
|
397
|
+
},
|
|
398
|
+
userFacingDescription: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description:
|
|
401
|
+
'Plain-language description of changes from a user perspective (max 5 sentences)',
|
|
402
|
+
},
|
|
403
|
+
technicalOnly: {
|
|
404
|
+
type: 'boolean',
|
|
405
|
+
description:
|
|
406
|
+
'Whether this change is technical-only with no user-visible impact',
|
|
407
|
+
},
|
|
408
|
+
changeSize: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
enum: ['Small', 'Medium', 'Large'],
|
|
411
|
+
description: 'Size/scope of the change',
|
|
412
|
+
},
|
|
413
|
+
changeType: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
enum: ['New', 'Improved', 'Fixed'],
|
|
416
|
+
description: 'Type of change',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
required: [
|
|
420
|
+
'title',
|
|
421
|
+
'summary',
|
|
422
|
+
'technicalOnly',
|
|
423
|
+
'changeSize',
|
|
424
|
+
'changeType',
|
|
425
|
+
],
|
|
426
|
+
additionalProperties: false,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
temperature: 0, // Lower temperature for more structured/predictable responses
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
console.error(`Error from OpenAI API: ${response.statusText}`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const data = await response.json();
|
|
440
|
+
|
|
441
|
+
const content = JSON.parse(data.choices[0].message.content);
|
|
442
|
+
|
|
443
|
+
// Validate response using Zod schema
|
|
444
|
+
const validatedResponse = openAiResponseSchema.parse(content);
|
|
445
|
+
|
|
446
|
+
return validatedResponse;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error('Error generating enhanced release notes:', error.message);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function writeYamlFile(mainTicketId, enhancedNotes) {
|
|
454
|
+
// Create changeset directory if it doesn't exist
|
|
455
|
+
if (!fs.existsSync(path.join(process.cwd(), '.release-notes'))) {
|
|
456
|
+
fs.mkdirSync(path.join(process.cwd(), '.release-notes'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const yamlFileName = `${mainTicketId}.yaml`;
|
|
460
|
+
const yamlFilePath = path.join(CHANGESET_DIR, yamlFileName);
|
|
461
|
+
|
|
462
|
+
const yamlContent = yaml.dump(
|
|
463
|
+
{
|
|
464
|
+
ticketId: mainTicketId,
|
|
465
|
+
title: enhancedNotes.title,
|
|
466
|
+
summary: enhancedNotes.summary,
|
|
467
|
+
technicalDescription: {
|
|
468
|
+
__line: enhancedNotes.technicalDescription,
|
|
469
|
+
},
|
|
470
|
+
userFacingDescription: {
|
|
471
|
+
__line: enhancedNotes.userFacingDescription,
|
|
472
|
+
},
|
|
473
|
+
technicalOnly: enhancedNotes.technicalOnly,
|
|
474
|
+
changeSize: enhancedNotes.changeSize,
|
|
475
|
+
changeType: enhancedNotes.changeType,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
lineWidth: -1, // Don't wrap lines
|
|
479
|
+
noRefs: true,
|
|
480
|
+
schema: yaml.DEFAULT_SCHEMA.extend({
|
|
481
|
+
implicit: [
|
|
482
|
+
new yaml.Type('tag:yaml.org,2002:str', {
|
|
483
|
+
kind: 'scalar',
|
|
484
|
+
instanceOf: Object,
|
|
485
|
+
resolve: function (object) {
|
|
486
|
+
return object !== null && object['__line'];
|
|
487
|
+
},
|
|
488
|
+
construct: function (object) {
|
|
489
|
+
return object.__line;
|
|
490
|
+
},
|
|
491
|
+
represent: function (object) {
|
|
492
|
+
return { __line: object.__line };
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
495
|
+
],
|
|
496
|
+
}),
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Replace the problematic YAML output with properly formatted literal block scalars
|
|
501
|
+
const fixedYaml = yamlContent
|
|
502
|
+
.replace(/technicalDescription: '?(.*?)'?$/m, () => {
|
|
503
|
+
return `technicalDescription: |\n ${enhancedNotes.technicalDescription.replace(/\n/g, '\n ')}`;
|
|
504
|
+
})
|
|
505
|
+
.replace(/userFacingDescription: '?(.*?)'?$/m, () => {
|
|
506
|
+
return `userFacingDescription: |\n ${enhancedNotes.userFacingDescription.replace(/\n/g, '\n ')}`;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
fs.writeFileSync(yamlFilePath, fixedYaml, 'utf8');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function getReleaseNotes(ticketId) {
|
|
513
|
+
const ticketData = await fetchJiraTicket(ticketId);
|
|
514
|
+
|
|
515
|
+
if (ticketData) {
|
|
516
|
+
// Generate enhanced release notes
|
|
517
|
+
const { mainTicketData, subTaskTicketData, mainTicketId } = ticketData;
|
|
518
|
+
|
|
519
|
+
const yamlFileName = `${mainTicketId}.yaml`;
|
|
520
|
+
const yamlFilePath = path.join(CHANGESET_DIR, yamlFileName);
|
|
521
|
+
|
|
522
|
+
if (fs.existsSync(yamlFilePath)) {
|
|
523
|
+
console.log(
|
|
524
|
+
`Notes for ticket ${mainTicketId} already exist, loading from file`,
|
|
525
|
+
);
|
|
526
|
+
try {
|
|
527
|
+
const fileContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
528
|
+
const enhancedNotes = yaml.load(fileContent);
|
|
529
|
+
const changeLog = await generateChangeLog({
|
|
530
|
+
taskTicketData: subTaskTicketData ?? mainTicketData,
|
|
531
|
+
mainTicketData,
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
yamlFilePath,
|
|
535
|
+
enhancedNotes,
|
|
536
|
+
mainTicketId: enhancedNotes.ticketId,
|
|
537
|
+
summary: changeLog.summary,
|
|
538
|
+
};
|
|
539
|
+
} catch (error) {
|
|
540
|
+
console.error(
|
|
541
|
+
`Error loading YAML file for ${mainTicketId}:`,
|
|
542
|
+
error.message,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
const [changeLog, enhancedNotes] = await Promise.all([
|
|
547
|
+
generateChangeLog({
|
|
548
|
+
taskTicketData: subTaskTicketData ?? mainTicketData,
|
|
549
|
+
mainTicketData,
|
|
550
|
+
}),
|
|
551
|
+
generateEnhancedReleaseNotes(mainTicketData),
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
if (enhancedNotes) {
|
|
555
|
+
await writeYamlFile(mainTicketId, enhancedNotes);
|
|
556
|
+
return {
|
|
557
|
+
yamlFilePath,
|
|
558
|
+
enhancedNotes,
|
|
559
|
+
mainTicketId,
|
|
560
|
+
summary: changeLog.summary,
|
|
561
|
+
};
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error(`Cannot generate release notes for ${mainTicketId}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -2,20 +2,9 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { $ } from 'zx';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { setGitInfo } from '../next/util.js';
|
|
6
6
|
import { versionStatus } from './version.js';
|
|
7
7
|
|
|
8
|
-
const CHANGESET_DIR = '.changeset';
|
|
9
|
-
|
|
10
|
-
function areArraysEqualIgnoringOrder(arr1, arr2) {
|
|
11
|
-
if (arr1.length !== arr2.length) return false;
|
|
12
|
-
|
|
13
|
-
const sortedA = [...arr1].sort();
|
|
14
|
-
const sortedB = [...arr2].sort();
|
|
15
|
-
|
|
16
|
-
return sortedA.every((element, index) => element === sortedB[index]);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
export async function validateChangesets() {
|
|
20
9
|
await setGitInfo();
|
|
21
10
|
const destBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
package/src/changeset/version.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { setGitInfo } from '../next/util.js';
|
|
2
2
|
import { $ } from 'zx';
|
|
3
|
-
import fs from 'fs';
|
|
3
|
+
import fs, { readFileSync } from 'fs';
|
|
4
4
|
import { updateComponentVersion } from '../next/jira.js';
|
|
5
5
|
import { findJiraTicketId } from './util.js';
|
|
6
6
|
import { revertBaseBranch, updateBaseBranch } from './changeset-config.js';
|
|
7
7
|
import semver from 'semver';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const getPackageVersion = () => {
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const packageJsonPath = join(__dirname, '../../package.json');
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
15
|
+
return packageJson.version;
|
|
16
|
+
};
|
|
8
17
|
|
|
9
18
|
const changeSetPackage = `{
|
|
10
19
|
"name": "changeset-package",
|
|
@@ -18,11 +27,23 @@ const changeSetPackage = `{
|
|
|
18
27
|
"license": "ISC",
|
|
19
28
|
"dependencies": {},
|
|
20
29
|
"devDependencies": {
|
|
21
|
-
"@changesets/cli": "^2.27.10"
|
|
30
|
+
"@changesets/cli": "^2.27.10",
|
|
31
|
+
"@docupace/pipeline": "${getPackageVersion()}"
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
`;
|
|
25
35
|
|
|
36
|
+
export async function loadChangesetOutput() {
|
|
37
|
+
const outputPath = './changeset-output.json';
|
|
38
|
+
const jsonOutput = JSON.parse(await fs.promises.readFile(outputPath, 'utf8'));
|
|
39
|
+
return jsonOutput;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function removeChangesetOutput() {
|
|
43
|
+
const outputPath = './changeset-output.json';
|
|
44
|
+
await fs.promises.rm(outputPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
export function toShortVersion(version) {
|
|
27
48
|
const parsed = semver.parse(version);
|
|
28
49
|
if (!parsed) return null;
|
|
@@ -41,7 +62,6 @@ export async function versionStatus(sinceBranch) {
|
|
|
41
62
|
// Read the file and parse JSON
|
|
42
63
|
const jsonOutput = JSON.parse(await fs.promises.readFile(outputPath, 'utf8'));
|
|
43
64
|
|
|
44
|
-
await fs.promises.rm(outputPath);
|
|
45
65
|
return jsonOutput.releases;
|
|
46
66
|
}
|
|
47
67
|
|
|
@@ -68,6 +88,8 @@ export async function versionCommand(closeVersion = false) {
|
|
|
68
88
|
revertBaseBranch(originalBaseBranch);
|
|
69
89
|
await $`pnpm changeset version`;
|
|
70
90
|
|
|
91
|
+
await removeChangesetOutput();
|
|
92
|
+
|
|
71
93
|
return releases;
|
|
72
94
|
} catch {
|
|
73
95
|
revertBaseBranch(originalBaseBranch);
|
package/src/index.mjs
ADDED
package/src/lib/merge-driver.js
CHANGED
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
5
|
|
|
8
6
|
// Get __dirname equivalent in ESM
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
9
|
* Default merge logic function - can be overridden
|
|
@@ -16,7 +12,7 @@ const __dirname = path.dirname(__filename);
|
|
|
16
12
|
* @returns {string} - The merged content
|
|
17
13
|
* @throws {Error} - If the conflict cannot be automatically merged
|
|
18
14
|
*/
|
|
19
|
-
export function defaultMergeLogic(
|
|
15
|
+
export function defaultMergeLogic() {
|
|
20
16
|
// Default implementation - throws error
|
|
21
17
|
// Users should provide their own implementation
|
|
22
18
|
throw new Error(
|
|
@@ -78,7 +78,7 @@ function readMergeConfig(verbose = false) {
|
|
|
78
78
|
* Extract repository name from git URL
|
|
79
79
|
*/
|
|
80
80
|
function getRepoName(gitUrl) {
|
|
81
|
-
const match = gitUrl.match(/\/([
|
|
81
|
+
const match = gitUrl.match(/\/([^/]+)\.git$/);
|
|
82
82
|
return match ? match[1] : gitUrl.split('/').pop().replace('.git', '');
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -459,7 +459,6 @@ async function mergeAll(verbose = false) {
|
|
|
459
459
|
console.log(`═══════════════════════════════════════`);
|
|
460
460
|
|
|
461
461
|
allResults.forEach((result, index) => {
|
|
462
|
-
const { from, to } = result.mergeBranch;
|
|
463
462
|
const status = result.success ? '✅' : '⚠️';
|
|
464
463
|
console.log(`\n${status} Merge branch set ${index + 1}:`);
|
|
465
464
|
|