spectrum-cli 2.1.2 → 3.0.1

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/src/preflight.js CHANGED
@@ -1,9 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const { execSilent, execCommand, getCurrentBranch, getMainBranch, getDevelopBranch, getVersion } = require('./utils');
4
+ const { execSilent, execCommand, getCurrentBranch, getMainBranch, getDevelopBranch } = require('./utils');
5
+ const {
6
+ CHANGELOG_FILE,
7
+ CHANGELOG_DIR,
8
+ FRAGMENT_TYPES,
9
+ getFragmentType
10
+ } = require('./changelog-config');
5
11
 
6
12
  const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
13
+ const STABLE_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
14
+ const YOUTRACK_TASK_PATTERN = /^[A-Z]+-[0-9]+$/;
7
15
 
8
16
  function ok(data) {
9
17
  return { ok: true, data };
@@ -119,7 +127,7 @@ function requireCurrentBranchUpToDateWithRemote() {
119
127
  return fail('Не удалось определить текущую ветку.');
120
128
  }
121
129
 
122
- if (!execCommand('git fetch --all --prune --jobs=10')) {
130
+ if (!execCommand('git fetch origin --prune --tags')) {
123
131
  return fail('Не удалось получить изменения с удаленного репозитория.');
124
132
  }
125
133
 
@@ -153,17 +161,157 @@ function requireFileExists(filePath) {
153
161
  return ok();
154
162
  }
155
163
 
156
- function requirePackageVersion() {
157
- const version = getVersion();
164
+ const CHANGELOG_HEADING_PATTERN = /^##\s+(?:\S+\s+)?\[(\d+\.\d+\.\d+)\]/gm;
165
+
166
+ function getChangelogReleaseVersions(changelogPath = CHANGELOG_FILE) {
167
+ let changelog;
168
+ try {
169
+ changelog = fs.readFileSync(changelogPath, 'utf8');
170
+ } catch (error) {
171
+ return [];
172
+ }
173
+
174
+ return [...changelog.matchAll(CHANGELOG_HEADING_PATTERN)].map((match) => match[1]);
175
+ }
176
+
177
+ function getChangelogReleaseVersion(changelogPath = CHANGELOG_FILE) {
178
+ return getChangelogReleaseVersions(changelogPath)[0] || null;
179
+ }
180
+
181
+ function requireChangelogReleaseVersion() {
182
+ const version = getChangelogReleaseVersion();
158
183
  if (!version) {
159
- return fail('Не удалось прочитать версию из package.json.');
184
+ return fail(`Не удалось прочитать версию релиза из ${CHANGELOG_FILE}. Ожидается заголовок "## 🚀 [X.Y.Z]".`);
160
185
  }
161
- if (!SEMVER_PATTERN.test(version)) {
162
- return fail(`Версия "${version}" не соответствует semver.`);
186
+ if (!STABLE_SEMVER_PATTERN.test(version)) {
187
+ return fail(`Версия "${version}" из ${CHANGELOG_FILE} должна быть стабильным SemVer X.Y.Z.`);
163
188
  }
164
189
  return ok({ version });
165
190
  }
166
191
 
192
+ function requireChartChangelogVersion(chartDir, version) {
193
+ const changelogPath = toPosixPath(path.join(String(chartDir || ''), 'CHANGELOG.md'));
194
+ if (!fs.existsSync(changelogPath)) {
195
+ return fail(`Файл "${changelogPath}" не существует. Заполните changelog чарта перед созданием тега.`);
196
+ }
197
+
198
+ let changelog;
199
+ try {
200
+ changelog = fs.readFileSync(changelogPath, 'utf8');
201
+ } catch (error) {
202
+ return fail(`Не удалось прочитать "${changelogPath}".`);
203
+ }
204
+
205
+ const escapedVersion = String(version || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
206
+ const headingPattern = new RegExp(`^##\\s+(?:\\S+\\s+)?\\[${escapedVersion}\\]`, 'm');
207
+ if (!headingPattern.test(changelog)) {
208
+ return fail(`В "${changelogPath}" нет заголовка "## [${version}]". Добавьте запись о версии перед созданием тега.`);
209
+ }
210
+
211
+ return ok({ chartChangelogPath: changelogPath });
212
+ }
213
+
214
+ function compareStableVersions(left, right) {
215
+ const leftParts = String(left).split('.').map(Number);
216
+ const rightParts = String(right).split('.').map(Number);
217
+ for (let index = 0; index < 3; index += 1) {
218
+ if (leftParts[index] !== rightParts[index]) {
219
+ return leftParts[index] - rightParts[index];
220
+ }
221
+ }
222
+ return 0;
223
+ }
224
+
225
+ function requireNoPendingRelease(stableVersion) {
226
+ if (!STABLE_SEMVER_PATTERN.test(String(stableVersion || ''))) {
227
+ return fail('Для проверки незакрытых релизов требуется последний стабильный SemVer X.Y.Z.');
228
+ }
229
+
230
+ const changelogReleaseVersions = getChangelogReleaseVersions();
231
+ const pendingVersions = [...new Set(changelogReleaseVersions)]
232
+ .filter((version) => compareStableVersions(version, stableVersion) > 0)
233
+ .sort((left, right) => compareStableVersions(right, left));
234
+
235
+ if (pendingVersions.length > 0) {
236
+ return fail(
237
+ `В ${CHANGELOG_FILE} найдены незакрытые релизы новее стабильного тега v${stableVersion}: ${pendingVersions.join(', ')}. ` +
238
+ 'Сначала выполните "spectrum release deploy", дождитесь успешного stable pipeline и выполните "spectrum release close".'
239
+ );
240
+ }
241
+
242
+ return ok({ changelogReleaseVersions });
243
+ }
244
+
245
+ function requireLatestStableVersion() {
246
+ const mainBranch = getMainBranch();
247
+ const tagNames = execSilent(`git tag --merged origin/${mainBranch} --list "v*"`);
248
+ const remoteTagRefs = execSilent('git ls-remote --refs --tags origin "refs/tags/v*"');
249
+ if (tagNames === null || remoteTagRefs === null) {
250
+ return fail(`Не удалось проверить стабильные теги, достижимые из origin/${mainBranch}.`);
251
+ }
252
+
253
+ const remoteTagNames = new Set();
254
+ for (const line of remoteTagRefs.split('\n')) {
255
+ const match = line.trim().match(/^[0-9a-f]+\s+refs\/tags\/(v\d+\.\d+\.\d+)$/i);
256
+ if (match) remoteTagNames.add(match[1]);
257
+ }
258
+
259
+ const versions = new Set();
260
+ for (const line of tagNames.split('\n')) {
261
+ const tagName = line.trim();
262
+ const match = tagName.match(/^v(\d+\.\d+\.\d+)$/);
263
+ if (match && remoteTagNames.has(tagName) && STABLE_SEMVER_PATTERN.test(match[1])) {
264
+ versions.add(match[1]);
265
+ }
266
+ }
267
+
268
+ if (versions.size === 0) {
269
+ return fail(`В origin/${mainBranch} не найден достижимый стабильный тег vX.Y.Z.`);
270
+ }
271
+
272
+ const stableVersion = [...versions].sort(compareStableVersions).at(-1);
273
+ return ok({ stableVersion });
274
+ }
275
+
276
+ function requireStableTagAtHead(version) {
277
+ if (!STABLE_SEMVER_PATTERN.test(String(version || ''))) {
278
+ return fail('Для закрытия релиза требуется стабильная версия X.Y.Z.');
279
+ }
280
+
281
+ const head = execSilent('git rev-parse HEAD');
282
+ const tagRefs = execSilent(`git ls-remote --tags origin "refs/tags/v${version}" "refs/tags/v${version}^{}"`);
283
+ if (!head || tagRefs === null) {
284
+ return fail(`Не удалось проверить стабильный тег "v${version}".`);
285
+ }
286
+
287
+ let tagCommit = null;
288
+ for (const line of tagRefs.split('\n')) {
289
+ const [sha, ref] = line.trim().split(/\s+/);
290
+ if (ref === `refs/tags/v${version}^{}`) {
291
+ tagCommit = sha;
292
+ break;
293
+ }
294
+ if (ref === `refs/tags/v${version}`) {
295
+ tagCommit = sha;
296
+ }
297
+ }
298
+
299
+ if (!tagCommit) {
300
+ return fail(`Стабильный тег "v${version}" отсутствует в origin.`);
301
+ }
302
+ if (tagCommit !== head) {
303
+ return fail(`Стабильный тег "v${version}" указывает не на текущий commit main/master.`);
304
+ }
305
+ return ok({ stableVersion: version });
306
+ }
307
+
308
+ function requireYouTrackTask(task) {
309
+ if (!YOUTRACK_TASK_PATTERN.test(String(task || ''))) {
310
+ return fail('Номер задачи должен соответствовать формату YOUTRACK-ID, например AR-123.');
311
+ }
312
+ return ok({ task });
313
+ }
314
+
167
315
  function requireTagMissing(tagName) {
168
316
  const localTag = execSilent(`git tag -l "${tagName}"`);
169
317
  if (localTag && localTag.trim()) {
@@ -178,25 +326,35 @@ function requireTagMissing(tagName) {
178
326
  return ok();
179
327
  }
180
328
 
181
- function requireOnReleaseBranch() {
182
- const currentBranch = getCurrentBranch();
183
- if (!currentBranch) {
184
- return fail('Не удалось определить текущую ветку.');
329
+ function requireReleaseVersionAvailable(version) {
330
+ if (!STABLE_SEMVER_PATTERN.test(String(version || ''))) {
331
+ return fail('Целевая release-версия должна соответствовать X.Y.Z.');
185
332
  }
186
- if (!currentBranch.startsWith('release/')) {
187
- return fail(`Текущая ветка "${currentBranch}", ожидалась "release/*".`);
333
+
334
+ const localBranches = execSilent(`git branch --list "hotfix/*-${version}"`);
335
+ if (localBranches && localBranches.trim()) {
336
+ return fail(`Версия "${version}" уже используется локальной hotfix-веткой.`);
337
+ }
338
+
339
+ const remoteBranches = execSilent(`git ls-remote --heads origin "refs/heads/hotfix/*-${version}"`);
340
+ if (remoteBranches === null) {
341
+ return fail('Не удалось проверить hotfix-ветки в origin.');
342
+ }
343
+ if (remoteBranches && remoteBranches.trim()) {
344
+ return fail(`Версия "${version}" уже используется hotfix-веткой в origin.`);
345
+ }
346
+
347
+ const localTag = execSilent(`git tag --list "v${version}"`);
348
+ if (localTag && localTag.trim()) {
349
+ return fail(`Релизный тег "v${version}" уже существует локально.`);
188
350
  }
189
- return ok({ currentBranch });
190
- }
191
351
 
192
- function requireReleaseBranchMissing(branchName) {
193
- const localBranch = execSilent(`git branch --list "${branchName}"`);
194
- if (localBranch && localBranch.trim()) {
195
- return fail(`Ветка "${branchName}" уже существует локально.`);
352
+ const remoteTag = execSilent(`git ls-remote --tags origin "refs/tags/v${version}" "refs/tags/v${version}^{}"`);
353
+ if (remoteTag === null) {
354
+ return fail(`Не удалось проверить тег "v${version}" в origin.`);
196
355
  }
197
- const remoteBranch = execSilent(`git ls-remote --heads origin "refs/heads/${branchName}"`);
198
- if (remoteBranch && remoteBranch.trim()) {
199
- return fail(`Ветка "${branchName}" уже существует на origin.`);
356
+ if (remoteTag && remoteTag.trim()) {
357
+ return fail(`Релизный тег "v${version}" уже существует в origin.`);
200
358
  }
201
359
  return ok();
202
360
  }
@@ -220,6 +378,68 @@ function requireChangelogFormatted() {
220
378
  return ok({ prettierRunner: runner });
221
379
  }
222
380
 
381
+ function findChangelogFragmentFiles(baseDir = CHANGELOG_DIR) {
382
+ if (!fs.existsSync(baseDir)) {
383
+ return [];
384
+ }
385
+
386
+ try {
387
+ return fs.readdirSync(baseDir, { withFileTypes: true })
388
+ .filter((entry) => entry.isFile() && !entry.name.startsWith('.'))
389
+ .map((entry) => toPosixPath(path.join(baseDir, entry.name)))
390
+ .sort();
391
+ } catch (error) {
392
+ return [];
393
+ }
394
+ }
395
+
396
+ function requireChangelogFragments() {
397
+ if (!fs.existsSync(CHANGELOG_DIR)) {
398
+ return fail(`Директория changelog fragments "${CHANGELOG_DIR}" не существует.`);
399
+ }
400
+
401
+ const fragmentFiles = findChangelogFragmentFiles();
402
+ if (fragmentFiles.length === 0) {
403
+ return fail(`В директории "${CHANGELOG_DIR}" нет changelog fragments.`);
404
+ }
405
+
406
+ const changelogFragments = [];
407
+ for (const filePath of fragmentFiles) {
408
+ const type = getFragmentType(filePath);
409
+ if (!type) {
410
+ return fail(`Неверное имя changelog fragment "${filePath}". Ожидается "<name>.<type>.md".`);
411
+ }
412
+
413
+ let content;
414
+ try {
415
+ content = fs.readFileSync(filePath, 'utf8');
416
+ } catch (error) {
417
+ return fail(`Не удалось прочитать changelog fragment "${filePath}".`);
418
+ }
419
+
420
+ const entries = content
421
+ .split('\n')
422
+ .map((line) => line.trim())
423
+ .filter(Boolean);
424
+ if (entries.length === 0) {
425
+ return fail(`Changelog fragment "${filePath}" пуст.`);
426
+ }
427
+ if (entries.some((line) => !line.startsWith('- '))) {
428
+ return fail(`Каждая непустая строка "${filePath}" должна начинаться с "- ".`);
429
+ }
430
+
431
+ changelogFragments.push({
432
+ filePath,
433
+ type,
434
+ section: FRAGMENT_TYPES[type].section,
435
+ bump: FRAGMENT_TYPES[type].bump,
436
+ entries
437
+ });
438
+ }
439
+
440
+ return ok({ changelogFragments });
441
+ }
442
+
223
443
  function requireSingleChart() {
224
444
  const chartsDir = 'charts';
225
445
  if (!fs.existsSync(chartsDir)) {
@@ -540,6 +760,8 @@ function requireBuildCommandSupport(sourcePath) {
540
760
 
541
761
  module.exports = {
542
762
  SEMVER_PATTERN,
763
+ STABLE_SEMVER_PATTERN,
764
+ YOUTRACK_TASK_PATTERN,
543
765
  getPrettierRunner,
544
766
  requireGitRepo,
545
767
  requireCleanWorkingTree,
@@ -551,12 +773,20 @@ module.exports = {
551
773
  requireRemoteReachable,
552
774
  requireCurrentBranchUpToDateWithRemote,
553
775
  requireFileExists,
554
- requirePackageVersion,
776
+ getChangelogReleaseVersions,
777
+ getChangelogReleaseVersion,
778
+ requireChangelogReleaseVersion,
779
+ requireChartChangelogVersion,
780
+ requireNoPendingRelease,
781
+ requireLatestStableVersion,
782
+ requireStableTagAtHead,
783
+ requireYouTrackTask,
555
784
  requireTagMissing,
556
- requireOnReleaseBranch,
557
- requireReleaseBranchMissing,
785
+ requireReleaseVersionAvailable,
558
786
  requirePrettierAvailable,
559
787
  requireChangelogFormatted,
788
+ findChangelogFragmentFiles,
789
+ requireChangelogFragments,
560
790
  requireSingleChart,
561
791
  findValuesYamlFiles,
562
792
  findHelmReleaseFiles,
package/src/release.js CHANGED
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
2
  const { goToDevBranch, goToMainBranch, updateCurrentBranch } = require('./git');
4
- const { changelogChangeHeader, changelogRemoveEmptyChapters, changelogAddUnreleasedBlock } = require('./changelog');
5
- const path = require('path');
6
- const { getVersion, logSuccess, logError, execCommand, getCurrentBranch, getMainBranch, getMergeRequestUrl, getPackageManager } = require('./utils');
3
+ const { changelogBuildRelease, changelogRemoveFragments } = require('./changelog');
4
+ const { logSuccess, logError, execCommand, getCurrentBranch, getMainBranch } = require('./utils');
7
5
  const { runCommand } = require('./command-executor');
8
- const { upVersion, updateVersionFile } = require('./version');
6
+ const { upVersion } = require('./version');
9
7
  const {
10
8
  requireGitRepo,
11
9
  requireCleanWorkingTree,
@@ -13,44 +11,32 @@ const {
13
11
  requireMainAndDevBranches,
14
12
  requireOnDevBranch,
15
13
  requireOnMainBranch,
16
- requireReleaseBranchMissing,
17
- requirePackageVersion,
14
+ requireReleaseVersionAvailable,
15
+ requireChangelogReleaseVersion,
16
+ requireNoPendingRelease,
17
+ requireLatestStableVersion,
18
+ requireStableTagAtHead,
18
19
  requireFileExists,
19
- requireChangelogFormatted
20
+ requireChangelogFormatted,
21
+ requireChangelogFragments,
22
+ getPrettierRunner
20
23
  } = require('./preflight');
24
+ const { CHANGELOG_FILE, CHANGELOG_DIR } = require('./changelog-config');
21
25
 
22
- function releaseCreate() {
23
- if (!goToDevBranch()) return false;
24
- if (!updateCurrentBranch()) return false;
25
-
26
- const currentVersion = getVersion();
27
- if (!currentVersion) {
28
- logError('❌', 'Не удалось получить версию из package.json');
26
+ function releaseFormatChangelog() {
27
+ const runner = getPrettierRunner();
28
+ if (!runner) {
29
+ logError('❌', 'Prettier недоступен.');
29
30
  return false;
30
31
  }
31
-
32
- if (!execCommand(`git switch -c release/${currentVersion}`)) {
32
+ if (!execCommand(`${runner} --write ${CHANGELOG_FILE}`)) {
33
+ logError('❌', 'Не удалось отформатировать %s с помощью Prettier.', CHANGELOG_FILE);
33
34
  return false;
34
35
  }
35
-
36
- logSuccess('🌱', 'Создана новая release-ветка %s.', getCurrentBranch());
36
+ logSuccess('🎨', 'Файл %s отформатирован.', CHANGELOG_FILE);
37
37
  return true;
38
38
  }
39
39
 
40
- function releasePush() {
41
- const currentBranch = getCurrentBranch();
42
- if (!execCommand(`git push origin ${currentBranch}`)) {
43
- return false;
44
- }
45
-
46
- logSuccess('📤', 'Release-ветка %s отправлена.', currentBranch);
47
- const mrUrl = getMergeRequestUrl(currentBranch, getMainBranch());
48
- if (mrUrl) {
49
- logSuccess('🌐', 'Создать Merge Request: %s', mrUrl);
50
- }
51
- return goToMainBranch();
52
- }
53
-
54
40
  function releaseCheckChangelogLint() {
55
41
  const result = requireChangelogFormatted();
56
42
  if (!result.ok) {
@@ -60,66 +46,71 @@ function releaseCheckChangelogLint() {
60
46
  return true;
61
47
  }
62
48
 
63
- function detectBumpType() {
64
- const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
65
- const lines = changelog.split('\n');
66
-
67
- let inUnreleased = false;
68
- let currentSection = null;
69
- const sectionsWithEntries = new Set();
70
-
71
- for (const line of lines) {
72
- if (line.startsWith('## [Unreleased]')) {
73
- inUnreleased = true;
74
- continue;
75
- }
76
- if (inUnreleased && line.startsWith('## ')) {
77
- break;
78
- }
79
- if (inUnreleased && line.startsWith('### ')) {
80
- currentSection = line.trim();
81
- continue;
82
- }
83
- if (inUnreleased && currentSection && line.startsWith('- ')) {
84
- sectionsWithEntries.add(currentSection);
85
- }
86
- }
87
-
88
- if (sectionsWithEntries.has('### 💥 Breaking change')) {
49
+ function detectBumpType(fragments) {
50
+ const bumpTypes = new Set((fragments || []).map((fragment) => fragment.bump));
51
+ if (bumpTypes.has('major')) {
89
52
  return 'major';
90
53
  }
91
- if (sectionsWithEntries.has('### 🆕 Added')) {
54
+ if (bumpTypes.has('minor')) {
92
55
  return 'minor';
93
56
  }
94
57
  return 'patch';
95
58
  }
96
59
 
97
- function runInstall() {
98
- const pm = getPackageManager() || 'npm';
99
- if (!execCommand(`${pm} install`)) {
100
- logError('❌', 'Не удалось выполнить %s install.', pm);
101
- return false;
102
- }
103
- logSuccess('📦', 'Lock-файл обновлен (%s install).', pm);
60
+ function resolveReleaseVersion(stableVersion, fragments) {
61
+ const bumpType = detectBumpType(fragments);
62
+ const newVersion = upVersion(stableVersion, bumpType);
63
+ if (!newVersion) return null;
64
+ return { bumpType, newVersion };
65
+ }
66
+
67
+ function releaseCommit(context) {
68
+ if (!execCommand(`git add --all -- ${CHANGELOG_FILE} ${CHANGELOG_DIR}`)) return false;
69
+ if (!execCommand(`git commit --message "📝 Подготовить релиз ${context.newVersion}." --no-verify`)) return false;
70
+
71
+ logSuccess('📝', 'Коммит со схлопнутыми changelog fragments создан.');
104
72
  return true;
105
73
  }
106
74
 
107
- function releaseCommit() {
108
- const lockFiles = { npm: 'package-lock.json', yarn: 'yarn.lock', bun: 'bun.lockb' };
109
- const pm = getPackageManager();
110
- const filesToAdd = ['CHANGELOG.md', 'package.json'];
111
- if (pm && lockFiles[pm]) {
112
- filesToAdd.push(lockFiles[pm]);
75
+ function getReleasePushCommand(context) {
76
+ const devBranch = context && context.devBranch;
77
+ const targetBranch = (context && context.mainBranch) || getMainBranch();
78
+ if (!devBranch || !targetBranch) return null;
79
+
80
+ return `git push --atomic origin ${devBranch}:${devBranch} ${devBranch}:${targetBranch}`;
81
+ }
82
+
83
+ function releasePush(context) {
84
+ const command = getReleasePushCommand(context);
85
+ if (!command) {
86
+ logError('❌', 'Не удалось сформировать команду публикации релиза.');
87
+ return false;
113
88
  }
114
89
 
115
- const addSuccess = execCommand(`git add ${filesToAdd.join(' ')}`);
116
- const commitSuccess = execCommand('git commit --message "📝 Обновить changelog и версию." --no-verify');
90
+ if (!execCommand(command)) {
91
+ logError(
92
+ '❌',
93
+ 'Релиз не отправлен в dev и main/master. Убедитесь, что dev содержит актуальный main/master и прямой push разрешен. Повторите: %s',
94
+ command
95
+ );
96
+ return false;
97
+ }
117
98
 
118
- if (addSuccess && commitSuccess) {
119
- logSuccess('📝', 'Коммит с обновленным changelog и версией создан.');
120
- return true;
99
+ const devBranch = context.devBranch;
100
+ const targetBranch = context.mainBranch || getMainBranch();
101
+ logSuccess('📤', 'Релиз атомарно отправлен в origin/%s и origin/%s.', devBranch, targetBranch);
102
+ return true;
103
+ }
104
+
105
+ function pushDev(context) {
106
+ const devBranch = context.devBranch || getCurrentBranch();
107
+ const command = `git push origin ${devBranch}`;
108
+ if (!execCommand(command)) {
109
+ logError('❌', 'Dev-ветка не отправлена. Повторите: %s', command);
110
+ return false;
121
111
  }
122
- return false;
112
+ logSuccess('📤', 'Ветка %s отправлена.', devBranch);
113
+ return true;
123
114
  }
124
115
 
125
116
  function releaseClose() {
@@ -130,7 +121,9 @@ function releaseClose() {
130
121
  { name: 'clean-working-tree', run: requireCleanWorkingTree },
131
122
  { name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
132
123
  { name: 'on-main-branch', run: requireOnMainBranch },
133
- { name: 'main-and-dev-branches', run: requireMainAndDevBranches }
124
+ { name: 'main-and-dev-branches', run: requireMainAndDevBranches },
125
+ { name: 'changelog-release-version', run: requireChangelogReleaseVersion },
126
+ { name: 'stable-tag-at-head', run: (ctx) => requireStableTagAtHead(ctx.version) }
134
127
  ],
135
128
  steps: [
136
129
  { name: 'switch-main', run: () => goToMainBranch() },
@@ -148,17 +141,7 @@ function releaseClose() {
148
141
  return true;
149
142
  }
150
143
  },
151
- {
152
- name: 'push-dev',
153
- run: () => {
154
- const currentBranch = getCurrentBranch();
155
- if (!execCommand(`git push origin ${currentBranch} -o ci.skip`)) {
156
- return false;
157
- }
158
- logSuccess('📤', 'Ветка %s отправлена.', currentBranch);
159
- return true;
160
- }
161
- }
144
+ { name: 'push-dev', run: pushDev }
162
145
  ]
163
146
  });
164
147
  }
@@ -171,40 +154,44 @@ function releaseStart() {
171
154
  { name: 'clean-working-tree', run: requireCleanWorkingTree },
172
155
  { name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
173
156
  { name: 'main-and-dev-branches', run: requireMainAndDevBranches },
174
- { name: 'package-version', run: requirePackageVersion },
175
157
  { name: 'on-dev-branch', run: requireOnDevBranch },
176
- { name: 'changelog-exists', run: () => requireFileExists('CHANGELOG.md') },
177
- { name: 'unreleased-template-exists', run: () => requireFileExists(path.join(__dirname, 'UNRELEASED.md')) },
158
+ { name: 'stable-version', run: requireLatestStableVersion },
159
+ { name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
160
+ { name: 'no-pending-release', run: (ctx) => requireNoPendingRelease(ctx.stableVersion) },
178
161
  { name: 'changelog-prettier-check', run: requireChangelogFormatted },
162
+ { name: 'changelog-fragments', run: requireChangelogFragments },
179
163
  {
180
164
  name: 'detect-bump-type',
181
165
  run: (ctx) => {
182
- const bumpType = detectBumpType();
183
- const newVersion = upVersion(ctx.version, bumpType);
184
- const branchCheck = requireReleaseBranchMissing(`release/${newVersion}`);
185
- if (!branchCheck.ok) return branchCheck;
186
- return { ok: true, data: { bumpType, newVersion } };
166
+ const resolved = resolveReleaseVersion(ctx.stableVersion, ctx.changelogFragments);
167
+ if (!resolved) {
168
+ return { ok: false, reason: 'Не удалось вычислить release-версию от последнего стабильного тега.' };
169
+ }
170
+
171
+ const versionCheck = requireReleaseVersionAvailable(resolved.newVersion);
172
+ if (!versionCheck.ok) return versionCheck;
173
+ return { ok: true, data: resolved };
187
174
  }
188
175
  }
189
176
  ],
190
177
  steps: [
191
- { name: 'bump-version', run: (ctx) => updateVersionFile(ctx.bumpType) },
192
- { name: 'run-install', run: runInstall },
193
- { name: 'change-header', run: changelogChangeHeader },
194
- { name: 'remove-empty-chapters', run: changelogRemoveEmptyChapters },
195
- { name: 'add-unreleased-block', run: changelogAddUnreleasedBlock },
178
+ { name: 'build-release-changelog', run: changelogBuildRelease },
179
+ { name: 'remove-changelog-fragments', run: changelogRemoveFragments },
180
+ { name: 'format-changelog', run: releaseFormatChangelog },
196
181
  { name: 'lint-changelog', run: releaseCheckChangelogLint },
197
- { name: 'create-release-branch', run: releaseCreate },
198
182
  { name: 'commit-release', run: releaseCommit },
199
- { name: 'push-release', run: releasePush }
183
+ { name: 'push-dev-and-main', run: releasePush }
200
184
  ]
201
185
  });
202
186
  }
203
187
 
204
188
  module.exports = {
205
- releaseCreate,
189
+ getReleasePushCommand,
190
+ releaseCommit,
206
191
  releasePush,
207
192
  releaseClose,
208
193
  releaseStart,
209
- detectBumpType
194
+ detectBumpType,
195
+ resolveReleaseVersion,
196
+ pushDev
210
197
  };
package/src/utils.js CHANGED
@@ -83,15 +83,6 @@ function getPackageManager() {
83
83
  return null;
84
84
  }
85
85
 
86
- function getVersion() {
87
- try {
88
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
89
- return packageJson.version;
90
- } catch (error) {
91
- return null;
92
- }
93
- }
94
-
95
86
  function getRemoteUrl() {
96
87
  const raw = execSilent('git remote get-url origin');
97
88
  if (!raw) return null;
@@ -125,7 +116,6 @@ module.exports = {
125
116
  getMainBranch,
126
117
  getDevelopBranch,
127
118
  getPackageManager,
128
- getVersion,
129
119
  getRemoteUrl,
130
120
  getMergeRequestUrl
131
121
  };