spectrum-cli 2.1.2 → 3.0.0
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/CHANGELOG.md +11 -27
- package/README.md +89 -55
- package/index.js +15 -32
- package/package.json +1 -1
- package/src/changelog-config.js +38 -0
- package/src/changelog.js +222 -251
- package/src/chart.js +256 -6
- package/src/git.js +2 -2
- package/src/preflight.js +255 -25
- package/src/release.js +95 -108
- package/src/utils.js +0 -10
- package/src/version.js +31 -40
- package/src/UNRELEASED.md +0 -33
package/src/changelog.js
CHANGED
|
@@ -2,114 +2,20 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
-
const { logSuccess, logError,
|
|
5
|
+
const { logSuccess, logError, execSilent, getCurrentBranch, colors } = require('./utils');
|
|
6
6
|
const { runCommand } = require('./command-executor');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!runner) {
|
|
20
|
-
logError('❌', 'Prettier недоступен. Установите его или используйте npx.');
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
return runner;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function checkChangelogWithPrettier() {
|
|
27
|
-
const runner = ensurePrettierAvailable();
|
|
28
|
-
if (!runner) return false;
|
|
29
|
-
const ok = execCommand(`${runner} --check CHANGELOG.md`);
|
|
30
|
-
if (!ok) {
|
|
31
|
-
logError('❌', 'Файл CHANGELOG.md не прошел проверку Prettier. Исправьте форматирование перед продолжением.');
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function changelogChangeHeader() {
|
|
38
|
-
try {
|
|
39
|
-
if (!checkChangelogWithPrettier()) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
const currentVersion = getVersion();
|
|
43
|
-
if (!currentVersion) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
|
|
48
|
-
const updatedChangelog = changelog.replace(
|
|
49
|
-
/## \[Unreleased\]/g,
|
|
50
|
-
`## 🚀 [${currentVersion}]`
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
fs.writeFileSync('CHANGELOG.md', updatedChangelog);
|
|
54
|
-
logSuccess('🔄', 'Заголовок обновлен до 🚀 [%s].', currentVersion);
|
|
55
|
-
return true;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function changelogRemoveEmptyChapters() {
|
|
62
|
-
try {
|
|
63
|
-
if (!checkChangelogWithPrettier()) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
const data = fs.readFileSync('CHANGELOG.md', 'utf8');
|
|
67
|
-
const updatedData = data.replaceAll(/###.*\n\n_.*_\n\n/gm, '');
|
|
68
|
-
fs.writeFileSync('CHANGELOG.md', updatedData);
|
|
69
|
-
|
|
70
|
-
logSuccess('🧹', 'Пустые разделы удалены.');
|
|
71
|
-
return true;
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function changelogAddUnreleasedBlock() {
|
|
78
|
-
try {
|
|
79
|
-
if (!checkChangelogWithPrettier()) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
const currentVersion = getVersion();
|
|
83
|
-
if (!currentVersion) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const unreleased = fs.readFileSync(path.join(__dirname, 'UNRELEASED.md'), 'utf8') + '\n';
|
|
88
|
-
|
|
89
|
-
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
|
|
90
|
-
const updatedChangelog = changelog.replace(
|
|
91
|
-
new RegExp(`## 🚀 \\[${currentVersion}\\]`),
|
|
92
|
-
`${unreleased}## 🚀 [${currentVersion}]`
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
fs.writeFileSync('CHANGELOG.md', updatedChangelog);
|
|
96
|
-
logSuccess('📋', 'Блок unreleased добавлен.');
|
|
97
|
-
return true;
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function changelogCommit() {
|
|
104
|
-
const addSuccess = execCommand('git add CHANGELOG.md');
|
|
105
|
-
const commitSuccess = execCommand('git commit --message "📝 Обновить changelog." --no-verify');
|
|
106
|
-
|
|
107
|
-
if (addSuccess && commitSuccess) {
|
|
108
|
-
logSuccess('📝', 'Коммит с обновленным changelog создан.');
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
7
|
+
const {
|
|
8
|
+
requireGitRepo,
|
|
9
|
+
requireFileExists,
|
|
10
|
+
requireChangelogFormatted,
|
|
11
|
+
requireChangelogFragments
|
|
12
|
+
} = require('./preflight');
|
|
13
|
+
const {
|
|
14
|
+
CHANGELOG_FILE,
|
|
15
|
+
CHANGELOG_DIR,
|
|
16
|
+
FRAGMENT_TYPES,
|
|
17
|
+
SECTION_TO_TYPE
|
|
18
|
+
} = require('./changelog-config');
|
|
113
19
|
|
|
114
20
|
function createReadlineInterface() {
|
|
115
21
|
return readline.createInterface({
|
|
@@ -134,32 +40,31 @@ async function extractTaskFromBranch() {
|
|
|
134
40
|
logError('❌', 'Не удалось получить имя текущей ветки.');
|
|
135
41
|
return null;
|
|
136
42
|
}
|
|
137
|
-
|
|
138
|
-
const taskMatch = currentBranch.match(
|
|
43
|
+
|
|
44
|
+
const taskMatch = currentBranch.match(/^[a-z][a-z0-9-]*\/([A-Z]+-[0-9]+)(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/);
|
|
139
45
|
if (!taskMatch) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return task;
|
|
46
|
+
logError(
|
|
47
|
+
'❌',
|
|
48
|
+
'Имя ветки "%s" должно соответствовать <type>/<YOUTRACK-ID> или <type>/<YOUTRACK-ID>-<slug>.',
|
|
49
|
+
currentBranch
|
|
50
|
+
);
|
|
51
|
+
return null;
|
|
147
52
|
}
|
|
148
|
-
|
|
53
|
+
|
|
149
54
|
return taskMatch[1];
|
|
150
55
|
}
|
|
151
56
|
|
|
152
57
|
async function getGitUser() {
|
|
153
58
|
let name = execSilent('git config user.name');
|
|
154
59
|
let email = execSilent('git config user.email');
|
|
155
|
-
|
|
60
|
+
|
|
156
61
|
if (!name || !email) {
|
|
157
62
|
console.log(`⚠️ ${colors.yellow}Имя и email пользователя git не настроены.${colors.reset}`);
|
|
158
|
-
console.log(
|
|
159
|
-
console.log(
|
|
160
|
-
console.log(
|
|
63
|
+
console.log('💡 Их можно настроить так:');
|
|
64
|
+
console.log(' git config user.name "Ваше Имя"');
|
|
65
|
+
console.log(' git config user.email "your.email@domain.com"');
|
|
161
66
|
console.log('');
|
|
162
|
-
|
|
67
|
+
|
|
163
68
|
if (!name) {
|
|
164
69
|
name = await askQuestion('👤 Введите ваше имя: ');
|
|
165
70
|
if (!name) {
|
|
@@ -167,7 +72,7 @@ async function getGitUser() {
|
|
|
167
72
|
return null;
|
|
168
73
|
}
|
|
169
74
|
}
|
|
170
|
-
|
|
75
|
+
|
|
171
76
|
if (!email) {
|
|
172
77
|
email = await askQuestion('📧 Введите ваш email: ');
|
|
173
78
|
if (!email || !email.includes('@')) {
|
|
@@ -176,210 +81,276 @@ async function getGitUser() {
|
|
|
176
81
|
}
|
|
177
82
|
}
|
|
178
83
|
}
|
|
179
|
-
|
|
84
|
+
|
|
180
85
|
return `[${name}](${email})`;
|
|
181
86
|
}
|
|
182
87
|
|
|
183
88
|
function formatMessage(message) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
return message;
|
|
89
|
+
const trimmedMessage = String(message || '').trim();
|
|
90
|
+
if (!trimmedMessage) return '';
|
|
91
|
+
return /[.!?]$/.test(trimmedMessage) ? trimmedMessage : `${trimmedMessage}.`;
|
|
188
92
|
}
|
|
189
93
|
|
|
190
94
|
function detectSectionFromBranch() {
|
|
191
95
|
const currentBranch = getCurrentBranch();
|
|
192
96
|
if (!currentBranch) return [];
|
|
193
|
-
|
|
194
|
-
const
|
|
97
|
+
|
|
98
|
+
const branchType = currentBranch.split('/', 1)[0].toLowerCase();
|
|
195
99
|
const sections = {
|
|
196
|
-
support: [
|
|
197
|
-
bugfix: [
|
|
198
|
-
feature: [
|
|
100
|
+
support: [FRAGMENT_TYPES.support.section, FRAGMENT_TYPES.security.section],
|
|
101
|
+
bugfix: [FRAGMENT_TYPES.fixed.section],
|
|
102
|
+
feature: [
|
|
103
|
+
FRAGMENT_TYPES.breaking.section,
|
|
104
|
+
FRAGMENT_TYPES.added.section,
|
|
105
|
+
FRAGMENT_TYPES.changed.section,
|
|
106
|
+
FRAGMENT_TYPES.deprecated.section,
|
|
107
|
+
FRAGMENT_TYPES.removed.section
|
|
108
|
+
]
|
|
199
109
|
};
|
|
200
|
-
|
|
201
|
-
if (
|
|
110
|
+
|
|
111
|
+
if (branchType === 'support') {
|
|
202
112
|
return sections.support;
|
|
203
|
-
}
|
|
113
|
+
}
|
|
114
|
+
if (branchType === 'bugfix' || branchType === 'fix' || branchType === 'hotfix') {
|
|
204
115
|
return sections.bugfix;
|
|
205
|
-
}
|
|
116
|
+
}
|
|
117
|
+
if (branchType === 'feature' || branchType === 'feat') {
|
|
206
118
|
return sections.feature;
|
|
207
119
|
}
|
|
208
|
-
|
|
120
|
+
|
|
209
121
|
return [];
|
|
210
122
|
}
|
|
211
123
|
|
|
212
124
|
async function selectSection(availableSections) {
|
|
213
|
-
|
|
214
|
-
availableSections
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'### 🗑 Removed',
|
|
220
|
-
'### 🪲 Fixed',
|
|
221
|
-
'### 🔐 Security',
|
|
222
|
-
'### 📦 Support'
|
|
223
|
-
];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (availableSections.length === 1) {
|
|
227
|
-
return availableSections[0];
|
|
125
|
+
const sections = availableSections.length > 0
|
|
126
|
+
? availableSections
|
|
127
|
+
: Object.values(FRAGMENT_TYPES).map((config) => config.section);
|
|
128
|
+
|
|
129
|
+
if (sections.length === 1) {
|
|
130
|
+
return sections[0];
|
|
228
131
|
}
|
|
229
|
-
|
|
230
|
-
console.log(
|
|
231
|
-
|
|
132
|
+
|
|
133
|
+
console.log('\n📋 Выберите раздел:');
|
|
134
|
+
sections.forEach((section, index) => {
|
|
232
135
|
console.log(` ${index + 1}. ${section}`);
|
|
233
136
|
});
|
|
234
|
-
|
|
137
|
+
|
|
235
138
|
const choice = await askQuestion('\n🔢 Введите номер раздела: ');
|
|
236
|
-
const choiceNum = parseInt(choice);
|
|
237
|
-
|
|
238
|
-
if (isNaN(choiceNum) || choiceNum < 1 || choiceNum >
|
|
139
|
+
const choiceNum = parseInt(choice, 10);
|
|
140
|
+
|
|
141
|
+
if (Number.isNaN(choiceNum) || choiceNum < 1 || choiceNum > sections.length) {
|
|
239
142
|
logError('❌', 'Неверный выбор.');
|
|
240
143
|
return null;
|
|
241
144
|
}
|
|
242
|
-
|
|
243
|
-
return
|
|
145
|
+
|
|
146
|
+
return sections[choiceNum - 1];
|
|
244
147
|
}
|
|
245
148
|
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (insertIndex < lines.length - 1) {
|
|
256
|
-
console.log(` ${colors.reset}${lines[insertIndex]}${colors.reset}`);
|
|
257
|
-
}
|
|
258
|
-
console.log('');
|
|
149
|
+
function sanitizeFragmentSlug(branchName, task) {
|
|
150
|
+
const branchWithoutTask = String(branchName || '').replace(new RegExp(task, 'i'), '-');
|
|
151
|
+
const slug = branchWithoutTask
|
|
152
|
+
.toLowerCase()
|
|
153
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
154
|
+
.replace(/^-+|-+$/g, '')
|
|
155
|
+
.slice(0, 80);
|
|
156
|
+
return slug || 'change';
|
|
259
157
|
}
|
|
260
158
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
run: async () => prepareChangelogEntry(message)
|
|
272
|
-
}
|
|
273
|
-
],
|
|
274
|
-
steps: [
|
|
275
|
-
{
|
|
276
|
-
name: 'append-entry',
|
|
277
|
-
run: (ctx) => appendPreparedChangelogEntry(ctx)
|
|
278
|
-
}
|
|
279
|
-
]
|
|
280
|
-
});
|
|
159
|
+
function createFragmentPath(task, type, branchName = getCurrentBranch()) {
|
|
160
|
+
const slug = sanitizeFragmentSlug(branchName, task);
|
|
161
|
+
return path.join(CHANGELOG_DIR, `${task}-${slug}.${type}.md`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function displayFragment(fragmentPath, entry) {
|
|
165
|
+
console.log('\n📝 Changelog fragment создан:');
|
|
166
|
+
console.log(` ${colors.green}${fragmentPath}${colors.reset}`);
|
|
167
|
+
console.log(` ${colors.green}${entry}${colors.reset}`);
|
|
168
|
+
console.log('');
|
|
281
169
|
}
|
|
282
170
|
|
|
283
171
|
async function prepareChangelogEntry(message) {
|
|
284
172
|
try {
|
|
173
|
+
const formattedMessage = formatMessage(message);
|
|
174
|
+
if (!formattedMessage) {
|
|
175
|
+
return { ok: false, reason: 'Сообщение changelog не может быть пустым.' };
|
|
176
|
+
}
|
|
177
|
+
|
|
285
178
|
const task = await extractTaskFromBranch();
|
|
286
179
|
if (!task) return { ok: false, reason: 'Не удалось определить ID задачи.' };
|
|
287
180
|
|
|
288
181
|
const user = await getGitUser();
|
|
289
182
|
if (!user) return { ok: false, reason: 'Не удалось определить пользователя git.' };
|
|
290
183
|
|
|
291
|
-
const
|
|
292
|
-
const entry = `- ${task} ${formattedMessage} ${user}`;
|
|
293
|
-
const availableSections = detectSectionFromBranch();
|
|
294
|
-
const selectedSection = await selectSection(availableSections);
|
|
184
|
+
const selectedSection = await selectSection(detectSectionFromBranch());
|
|
295
185
|
if (!selectedSection) return { ok: false, reason: 'Не удалось выбрать раздел.' };
|
|
296
186
|
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let sectionIndex = -1;
|
|
301
|
-
for (let i = 0; i < lines.length; i++) {
|
|
302
|
-
if (lines[i] === selectedSection) {
|
|
303
|
-
sectionIndex = i;
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (sectionIndex === -1) {
|
|
309
|
-
return { ok: false, reason: `Не удалось найти раздел "${selectedSection}" в CHANGELOG.md.` };
|
|
310
|
-
}
|
|
187
|
+
const type = SECTION_TO_TYPE[selectedSection];
|
|
188
|
+
if (!type) return { ok: false, reason: `Неизвестный раздел changelog: "${selectedSection}".` };
|
|
311
189
|
|
|
312
190
|
return {
|
|
313
191
|
ok: true,
|
|
314
192
|
data: {
|
|
315
|
-
|
|
316
|
-
entry
|
|
193
|
+
fragmentState: {
|
|
194
|
+
entry: `- ${task} ${formattedMessage} ${user}`,
|
|
195
|
+
fragmentPath: createFragmentPath(task, type),
|
|
317
196
|
selectedSection,
|
|
318
|
-
|
|
319
|
-
sectionIndex
|
|
197
|
+
type
|
|
320
198
|
}
|
|
321
199
|
}
|
|
322
200
|
};
|
|
323
201
|
} catch (error) {
|
|
324
|
-
return { ok: false, reason: `Ошибка при подготовке
|
|
202
|
+
return { ok: false, reason: `Ошибка при подготовке changelog fragment: ${error.message}` };
|
|
325
203
|
}
|
|
326
204
|
}
|
|
327
205
|
|
|
328
206
|
function appendPreparedChangelogEntry(context) {
|
|
329
207
|
try {
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
332
|
-
|
|
208
|
+
const fragmentState = context.fragmentState;
|
|
209
|
+
if (!fragmentState) return false;
|
|
210
|
+
|
|
211
|
+
fs.mkdirSync(CHANGELOG_DIR, { recursive: true });
|
|
212
|
+
let entries = [];
|
|
213
|
+
if (fs.existsSync(fragmentState.fragmentPath)) {
|
|
214
|
+
entries = fs.readFileSync(fragmentState.fragmentPath, 'utf8')
|
|
215
|
+
.split('\n')
|
|
216
|
+
.map((line) => line.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
if (entries.some((line) => !line.startsWith('- '))) {
|
|
219
|
+
logError('❌', 'Существующий fragment имеет неверный формат: %s', fragmentState.fragmentPath);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
333
222
|
}
|
|
334
223
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const entry = appendState.entry;
|
|
338
|
-
let insertIndex = appendState.sectionIndex + 2;
|
|
339
|
-
let defaultTextLines = 0;
|
|
340
|
-
|
|
341
|
-
while (insertIndex + defaultTextLines < lines.length && lines[insertIndex + defaultTextLines].startsWith('_')) {
|
|
342
|
-
defaultTextLines++;
|
|
224
|
+
if (!entries.includes(fragmentState.entry)) {
|
|
225
|
+
entries.push(fragmentState.entry);
|
|
343
226
|
}
|
|
227
|
+
fs.writeFileSync(fragmentState.fragmentPath, `${entries.join('\n')}\n`);
|
|
228
|
+
displayFragment(fragmentState.fragmentPath, fragmentState.entry);
|
|
229
|
+
logSuccess('✅', 'Запись добавлена в fragment раздела %s', fragmentState.selectedSection);
|
|
230
|
+
return true;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logError('❌', 'Ошибка при записи changelog fragment: %s', error.message);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
344
236
|
|
|
345
|
-
|
|
237
|
+
function changelogAppend(message) {
|
|
238
|
+
return runCommand({
|
|
239
|
+
name: 'changelog append',
|
|
240
|
+
checks: [
|
|
241
|
+
{ name: 'git-repo', run: requireGitRepo },
|
|
242
|
+
{ name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
|
|
243
|
+
{ name: 'prepare-fragment', run: () => prepareChangelogEntry(message) }
|
|
244
|
+
],
|
|
245
|
+
steps: [
|
|
246
|
+
{ name: 'write-fragment', run: appendPreparedChangelogEntry }
|
|
247
|
+
]
|
|
248
|
+
});
|
|
249
|
+
}
|
|
346
250
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
251
|
+
function changelogCheck() {
|
|
252
|
+
return runCommand({
|
|
253
|
+
name: 'changelog check',
|
|
254
|
+
checks: [
|
|
255
|
+
{ name: 'git-repo', run: requireGitRepo },
|
|
256
|
+
{ name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
|
|
257
|
+
{ name: 'changelog-prettier-check', run: requireChangelogFormatted },
|
|
258
|
+
{ name: 'changelog-fragments', run: requireChangelogFragments }
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
355
262
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
263
|
+
function stripLegacyUnreleasedBlock(changelog) {
|
|
264
|
+
const unreleasedMatch = /^## \[Unreleased\]\s*$/m.exec(changelog);
|
|
265
|
+
if (!unreleasedMatch) return changelog;
|
|
266
|
+
|
|
267
|
+
const afterUnreleased = unreleasedMatch.index + unreleasedMatch[0].length;
|
|
268
|
+
const nextHeadingMatch = /^## /m.exec(changelog.slice(afterUnreleased));
|
|
269
|
+
const end = nextHeadingMatch ? afterUnreleased + nextHeadingMatch.index : changelog.length;
|
|
270
|
+
return `${changelog.slice(0, unreleasedMatch.index).trimEnd()}\n\n${changelog.slice(end).trimStart()}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderReleaseBlock(version, fragments, date = new Date().toISOString().slice(0, 10)) {
|
|
274
|
+
const fragmentsByType = new Map();
|
|
275
|
+
for (const fragment of fragments) {
|
|
276
|
+
const entries = fragmentsByType.get(fragment.type) || [];
|
|
277
|
+
entries.push(...fragment.entries);
|
|
278
|
+
fragmentsByType.set(fragment.type, entries);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const lines = [`## 🚀 [${version}] - ${date}`];
|
|
282
|
+
for (const [type, config] of Object.entries(FRAGMENT_TYPES)) {
|
|
283
|
+
const entries = fragmentsByType.get(type);
|
|
284
|
+
if (!entries || entries.length === 0) continue;
|
|
285
|
+
lines.push('', config.section, '', ...entries);
|
|
286
|
+
}
|
|
287
|
+
return `${lines.join('\n')}\n`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function insertReleaseBlock(changelog, releaseBlock, version) {
|
|
291
|
+
const normalized = stripLegacyUnreleasedBlock(changelog).trimEnd();
|
|
292
|
+
const escapedVersion = String(version).replace(/\./g, '\\.');
|
|
293
|
+
const versionPattern = new RegExp(`^## 🚀 \\[${escapedVersion}\\](?:\\s|$)`, 'm');
|
|
294
|
+
if (versionPattern.test(normalized)) {
|
|
295
|
+
throw new Error(`Версия ${version} уже присутствует в ${CHANGELOG_FILE}.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const firstReleaseMatch = /^## /m.exec(normalized);
|
|
299
|
+
if (!firstReleaseMatch) {
|
|
300
|
+
return `${normalized}\n\n${releaseBlock}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `${normalized.slice(0, firstReleaseMatch.index).trimEnd()}\n\n${releaseBlock}\n${normalized.slice(firstReleaseMatch.index).trimStart()}\n`;
|
|
304
|
+
}
|
|
360
305
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
306
|
+
function changelogBuildRelease(context, date) {
|
|
307
|
+
try {
|
|
308
|
+
const version = context.newVersion;
|
|
309
|
+
const fragments = context.changelogFragments;
|
|
310
|
+
if (!version || !Array.isArray(fragments) || fragments.length === 0) return false;
|
|
311
|
+
|
|
312
|
+
const changelog = fs.readFileSync(CHANGELOG_FILE, 'utf8');
|
|
313
|
+
const releaseBlock = renderReleaseBlock(version, fragments, date);
|
|
314
|
+
const updatedChangelog = insertReleaseBlock(changelog, releaseBlock, version);
|
|
315
|
+
fs.writeFileSync(CHANGELOG_FILE, updatedChangelog);
|
|
316
|
+
logSuccess('📋', '%s собран из %s changelog fragments.', `CHANGELOG ${version}`, fragments.length);
|
|
364
317
|
return true;
|
|
365
318
|
} catch (error) {
|
|
366
|
-
logError('❌', '
|
|
319
|
+
logError('❌', 'Не удалось собрать релизный changelog: %s', error.message);
|
|
367
320
|
return false;
|
|
368
321
|
}
|
|
369
322
|
}
|
|
370
323
|
|
|
324
|
+
function changelogRemoveFragments(context) {
|
|
325
|
+
try {
|
|
326
|
+
const fragments = context.changelogFragments;
|
|
327
|
+
if (!Array.isArray(fragments) || fragments.length === 0) return false;
|
|
328
|
+
for (const fragment of fragments) {
|
|
329
|
+
fs.unlinkSync(fragment.filePath);
|
|
330
|
+
}
|
|
331
|
+
logSuccess('🧹', 'Использованные changelog fragments удалены: %s.', fragments.length);
|
|
332
|
+
return true;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
logError('❌', 'Не удалось удалить changelog fragments: %s', error.message);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
371
338
|
|
|
372
339
|
module.exports = {
|
|
373
|
-
changelogChangeHeader,
|
|
374
|
-
changelogRemoveEmptyChapters,
|
|
375
|
-
changelogAddUnreleasedBlock,
|
|
376
|
-
changelogCommit,
|
|
377
340
|
changelogAppend,
|
|
341
|
+
changelogCheck,
|
|
378
342
|
extractTaskFromBranch,
|
|
379
343
|
getGitUser,
|
|
380
344
|
formatMessage,
|
|
381
345
|
detectSectionFromBranch,
|
|
382
346
|
selectSection,
|
|
347
|
+
sanitizeFragmentSlug,
|
|
348
|
+
createFragmentPath,
|
|
383
349
|
prepareChangelogEntry,
|
|
384
|
-
appendPreparedChangelogEntry
|
|
350
|
+
appendPreparedChangelogEntry,
|
|
351
|
+
stripLegacyUnreleasedBlock,
|
|
352
|
+
renderReleaseBlock,
|
|
353
|
+
insertReleaseBlock,
|
|
354
|
+
changelogBuildRelease,
|
|
355
|
+
changelogRemoveFragments
|
|
385
356
|
};
|