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/CHANGELOG.md +16 -26
- 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/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
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
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(
|
|
184
|
+
return fail(`Не удалось прочитать версию релиза из ${CHANGELOG_FILE}. Ожидается заголовок "## 🚀 [X.Y.Z]".`);
|
|
160
185
|
}
|
|
161
|
-
if (!
|
|
162
|
-
return fail(`Версия "${version}"
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
776
|
+
getChangelogReleaseVersions,
|
|
777
|
+
getChangelogReleaseVersion,
|
|
778
|
+
requireChangelogReleaseVersion,
|
|
779
|
+
requireChartChangelogVersion,
|
|
780
|
+
requireNoPendingRelease,
|
|
781
|
+
requireLatestStableVersion,
|
|
782
|
+
requireStableTagAtHead,
|
|
783
|
+
requireYouTrackTask,
|
|
555
784
|
requireTagMissing,
|
|
556
|
-
|
|
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 {
|
|
5
|
-
const
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
if (!
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
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 (
|
|
54
|
+
if (bumpTypes.has('minor')) {
|
|
92
55
|
return 'minor';
|
|
93
56
|
}
|
|
94
57
|
return 'patch';
|
|
95
58
|
}
|
|
96
59
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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: '
|
|
177
|
-
{ name: '
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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: '
|
|
192
|
-
{ name: '
|
|
193
|
-
{ name: '
|
|
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-
|
|
183
|
+
{ name: 'push-dev-and-main', run: releasePush }
|
|
200
184
|
]
|
|
201
185
|
});
|
|
202
186
|
}
|
|
203
187
|
|
|
204
188
|
module.exports = {
|
|
205
|
-
|
|
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
|
};
|