spectrum-cli 3.1.0 → 3.1.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 +8 -0
- package/README.md +12 -7
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/changelog.js +2 -2
- package/src/hotfix-changelog.js +16 -10
- package/src/hotfix.js +9 -5
- package/src/open-release.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 🚀 [3.1.1] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### 🪲 Fixed
|
|
11
|
+
|
|
12
|
+
- Хотфиксы создают раздел с 🩹 и поддерживают запуск подготовки из main/master.
|
|
13
|
+
- Дата раздела changelog необязательна; опубликованная история в прежнем формате сохраняется.
|
|
14
|
+
- Исправлены проверка корня репозитория и сравнение CRLF/LF в release/hotfix на Windows.
|
|
15
|
+
|
|
8
16
|
## 🚀 [3.1.0] - 2026-09-24
|
|
9
17
|
|
|
10
18
|
### 🆕 Added
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ spectrum release deploy # Создать стабильный те
|
|
|
42
42
|
spectrum release close # Свести stable main/master в dev
|
|
43
43
|
|
|
44
44
|
# Срочный patch без переноса незавершённого dev в production
|
|
45
|
-
spectrum hotfix start #
|
|
45
|
+
spectrum hotfix start # Локальная подготовка 🩹 CHANGELOG на hotfix/* или main/master
|
|
46
46
|
spectrum hotfix deploy # После merge MR: stable-тег на актуальном main/master
|
|
47
47
|
spectrum hotfix close # После stable pipeline: main/master → dev
|
|
48
48
|
|
|
@@ -182,24 +182,28 @@ spectrum-cli/
|
|
|
182
182
|
|
|
183
183
|
Все hotfix-команды сначала проверяют ветку: разрешены только `hotfix/*`, `main`
|
|
184
184
|
или `master`. `dev`, `develop`, `feature/*` и detached HEAD отклоняются до fetch.
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
Для `start` подходят `hotfix/<TASK>[-slug]` и production-ветка `main/master`,
|
|
186
|
+
содержащая актуальный origin. Локальные исправления и commits допустимы.
|
|
187
|
+
`deploy` и `close` требуют чистый production commit, совпадающий с origin.
|
|
187
188
|
|
|
188
189
|
### Изолированный цикл хотфикса
|
|
189
190
|
|
|
190
191
|
Создайте `hotfix/<TASK>` или `hotfix/<TASK>-<slug>` от актуального `origin/main`
|
|
191
|
-
или `origin/master
|
|
192
|
+
или `origin/master`, либо работайте прямо в актуальной production-ветке
|
|
193
|
+
`main/master`. Включайте только срочное исправление и его patch fragments
|
|
192
194
|
(`fixed`, `security`, `support` либо другой совместимый patch-тип).
|
|
193
195
|
`added` и `breaking` требуют обычного релиза. Все команды запускаются из корня
|
|
194
196
|
репозитория; для `start` нужен Prettier, как для обычной сборки changelog.
|
|
195
197
|
|
|
196
198
|
1. **`spectrum hotfix start`** обновляет сведения об origin и выбирает опубликованный
|
|
197
199
|
stable-тег, достижимый из production. Цель — этот stable плюс один patch.
|
|
198
|
-
Команда формирует верхний
|
|
200
|
+
Команда формирует верхний раздел `## 🩹 [X.Y.Z] - YYYY-MM-DD` и удаляет только
|
|
199
201
|
собранные fragments. Она не делает commit, staging, push, merge или tag,
|
|
200
202
|
не меняет файлы версий и сохраняет посторонние локальные изменения.
|
|
201
203
|
2. Повторный `start` дополняет тот же неопубликованный раздел, сохраняя дату и
|
|
202
|
-
существующие записи без дублей.
|
|
204
|
+
существующие записи без дублей. Дата необязательна: раздел без даты останется
|
|
205
|
+
без даты. Старые заголовки с `🚀`, `🩹`, без значка и с датами через точки
|
|
206
|
+
читаются без переписывания опубликованной истории. Без новых fragments подготовленный раздел
|
|
203
207
|
остаётся без изменений. Например, от `v6.35.0` первый и повторные запуски
|
|
204
208
|
готовят `6.35.1`; только после публикации `hotfix/6.35.1` следующий хотфикс получит
|
|
205
209
|
`6.35.2`. Унаследованные от production fragments не собираются и не меняются.
|
|
@@ -208,7 +212,8 @@ spectrum-cli/
|
|
|
208
212
|
использованных fragments.** Merge выполняется отдельно после ревью. Если
|
|
209
213
|
другой хотфикс уже в production, сначала перенесите его изменения в свою
|
|
210
214
|
ветку и повторите подготовку: пока stable-тега нет, раздел и версия общие.
|
|
211
|
-
4. После
|
|
215
|
+
4. После доставки подготовленного исправления в production через MR или
|
|
216
|
+
разрешённый проектом push и проверки RC выполните **`spectrum hotfix deploy`** на чистом
|
|
212
217
|
`main/master`, точно совпадающем с origin. Команда проверяет следующий patch,
|
|
213
218
|
неизменность опубликованной истории и отсутствие несобранных hotfix fragments,
|
|
214
219
|
создаёт `hotfix/X.Y.Z` на проверенном SHA и отправляет **только этот тег**.
|
package/index.js
CHANGED
|
@@ -50,7 +50,7 @@ releaseCmd
|
|
|
50
50
|
const hotfixCmd = program.command("hotfix").description("Изолированный цикл срочного patch-релиза");
|
|
51
51
|
|
|
52
52
|
hotfixCmd.command("start")
|
|
53
|
-
.description("Подготовить CHANGELOG на hotfix/*
|
|
53
|
+
.description("Подготовить 🩹 CHANGELOG на hotfix/* или main/master, без commit, push и merge")
|
|
54
54
|
.action(() => runAction(hotfix.hotfixStart));
|
|
55
55
|
hotfixCmd.command("deploy")
|
|
56
56
|
.description("После merge хотфикса отправить stable-тег с main/master")
|
package/package.json
CHANGED
package/src/changelog.js
CHANGED
|
@@ -270,7 +270,7 @@ function stripLegacyUnreleasedBlock(changelog) {
|
|
|
270
270
|
return `${changelog.slice(0, unreleasedMatch.index).trimEnd()}\n\n${changelog.slice(end).trimStart()}`;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
function renderReleaseBlock(version, fragments, date = new Date().toISOString().slice(0, 10)) {
|
|
273
|
+
function renderReleaseBlock(version, fragments, date = new Date().toISOString().slice(0, 10), marker = '🚀') {
|
|
274
274
|
const fragmentsByType = new Map();
|
|
275
275
|
for (const fragment of fragments) {
|
|
276
276
|
const entries = fragmentsByType.get(fragment.type) || [];
|
|
@@ -278,7 +278,7 @@ function renderReleaseBlock(version, fragments, date = new Date().toISOString().
|
|
|
278
278
|
fragmentsByType.set(fragment.type, entries);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
const lines = [`##
|
|
281
|
+
const lines = [`## ${marker ? `${marker} ` : ''}[${version}]${date ? ` - ${date}` : ''}`];
|
|
282
282
|
for (const [type, config] of Object.entries(FRAGMENT_TYPES)) {
|
|
283
283
|
const entries = fragmentsByType.get(type);
|
|
284
284
|
if (!entries || entries.length === 0) continue;
|
package/src/hotfix-changelog.js
CHANGED
|
@@ -4,15 +4,14 @@ const { renderReleaseBlock } = require('./changelog');
|
|
|
4
4
|
// Parse release boundaries without rewriting any published text.
|
|
5
5
|
function parseChangelog(text) {
|
|
6
6
|
const headings = [...text.matchAll(/^## .+$/gm)];
|
|
7
|
-
const seen = new Set();
|
|
8
7
|
const releases = headings.map((heading, index) => {
|
|
9
|
-
const match = heading[0].match(/^## (
|
|
10
|
-
if (!match
|
|
11
|
-
throw new Error(
|
|
8
|
+
const match = heading[0].match(/^##[ \t]+(?:(🚀|🩹)[ \t]*)?\[(\d+\.\d+\.\d+)\](?:[ \t]+-[ \t]+(\d{4}-\d{2}-\d{2}|\d{4}\.\d{2}\.\d{2}|\d{2}\.\d{2}\.\d{4}))?[ \t]*\r?$/);
|
|
9
|
+
if (!match) {
|
|
10
|
+
throw new Error(`Некорректный заголовок CHANGELOG.md: ${heading[0]}. Ожидается ## [X.Y.Z], дата необязательна.`);
|
|
12
11
|
}
|
|
13
|
-
|
|
12
|
+
// Historical duplicates are preserved; callers validate the pending version and immutable history.
|
|
14
13
|
return {
|
|
15
|
-
version: match[
|
|
14
|
+
version: match[2], date: match[3] || null, marker: match[1] || '',
|
|
16
15
|
text: text.slice(heading.index, headings[index + 1]?.index ?? text.length),
|
|
17
16
|
body: text.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? text.length),
|
|
18
17
|
};
|
|
@@ -44,15 +43,22 @@ function releaseEntries(release, patchOnly = false) {
|
|
|
44
43
|
return entries;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function normalizeLineEndings(text) {
|
|
47
|
+
return text.replace(/\r\n/g, '\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
47
50
|
function history(document) {
|
|
48
|
-
return document.releases.map((release) => release.text.trim()).join('\n\n');
|
|
51
|
+
return document.releases.map((release) => normalizeLineEndings(release.text).trim()).join('\n\n');
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function pendingDocument(text, stableText, target) {
|
|
52
55
|
const document = parseChangelog(text);
|
|
53
56
|
const stable = parseChangelog(stableText);
|
|
54
57
|
const pending = document.releases[0]?.version === target ? document.releases.shift() : null;
|
|
55
|
-
if (
|
|
58
|
+
if (document.releases.some((release) => release.version === target)) {
|
|
59
|
+
throw new Error('Новый patch-раздел должен быть единственным и находиться в начале CHANGELOG.md.');
|
|
60
|
+
}
|
|
61
|
+
if (history(document) !== history(stable) || normalizeLineEndings(document.intro).trim() !== normalizeLineEndings(stable.intro).trim()) {
|
|
56
62
|
throw new Error('История CHANGELOG.md отличается от stable: допустим только верхний раздел следующего patch.');
|
|
57
63
|
}
|
|
58
64
|
return { document, pending, entries: pending ? releaseEntries(pending, true) : [] };
|
|
@@ -79,10 +85,10 @@ function prepareChangelog({ text, stableText, productionText, target, fragments,
|
|
|
79
85
|
currentKeys.add(entryKey(entry));
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
|
-
const block = renderReleaseBlock(target, merged, current.pending
|
|
88
|
+
const block = renderReleaseBlock(target, merged, current.pending ? current.pending.date : date, '🩹');
|
|
83
89
|
// Preserve the published tail byte-for-byte, including its spacing.
|
|
84
90
|
const tail = current.document.releases.map((release) => release.text).join('');
|
|
85
91
|
return `${current.document.intro.trimEnd()}\n\n${block.trim()}\n${tail ? `\n${tail}` : ''}`;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
module.exports = { parseChangelog, releaseEntries, pendingDocument, prepareChangelog };
|
|
94
|
+
module.exports = { parseChangelog, releaseEntries, pendingDocument, prepareChangelog, normalizeLineEndings };
|
package/src/hotfix.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const { execFileSync } = require('child_process');
|
|
3
4
|
const preflight = require('./preflight');
|
|
4
5
|
const { execCommand, logSuccess, logError } = require('./utils');
|
|
5
6
|
const { upVersion, compareVersions } = require('./version');
|
|
6
7
|
const { STABLE_TAG_PATTERNS, parseStableTag, stableTagNames } = require('./stable-tags');
|
|
7
8
|
const { CHANGELOG_FILE, CHANGELOG_DIR, FRAGMENT_TYPES, getFragmentType } = require('./changelog-config');
|
|
8
|
-
const { parseChangelog, pendingDocument, prepareChangelog } = require('./hotfix-changelog');
|
|
9
|
+
const { parseChangelog, pendingDocument, prepareChangelog, normalizeLineEndings } = require('./hotfix-changelog');
|
|
9
10
|
|
|
10
11
|
function git(...args) {
|
|
11
12
|
try {
|
|
@@ -41,7 +42,7 @@ function readAt(ref, file = CHANGELOG_FILE) {
|
|
|
41
42
|
|
|
42
43
|
function context(mode) {
|
|
43
44
|
const root = git('rev-parse', '--show-toplevel').trim();
|
|
44
|
-
if (fs.realpathSync(root)
|
|
45
|
+
if (path.relative(fs.realpathSync(root), fs.realpathSync(process.cwd())) !== '') {
|
|
45
46
|
throw new Error('Запустите hotfix из корня репозитория.');
|
|
46
47
|
}
|
|
47
48
|
const branch = git('branch', '--show-current').trim();
|
|
@@ -51,8 +52,8 @@ function context(mode) {
|
|
|
51
52
|
if (mode !== 'start' && !['main', 'master'].includes(branch)) {
|
|
52
53
|
throw new Error('hotfix deploy/close выполняются на main/master после merge хотфикса.');
|
|
53
54
|
}
|
|
54
|
-
if (mode === 'start' && !/^hotfix\/[A-Z]+-[0-9]+(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/.test(branch)) {
|
|
55
|
-
throw new Error('hotfix start выполняется
|
|
55
|
+
if (mode === 'start' && !['main', 'master'].includes(branch) && !/^hotfix\/[A-Z]+-[0-9]+(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/.test(branch)) {
|
|
56
|
+
throw new Error('hotfix start выполняется на hotfix/<TASK>[-slug], main или master.');
|
|
56
57
|
}
|
|
57
58
|
if (mode !== 'start' && git('status', '--porcelain').trim()) {
|
|
58
59
|
throw new Error('Для deploy/close требуется чистое рабочее дерево.');
|
|
@@ -64,6 +65,9 @@ function context(mode) {
|
|
|
64
65
|
const mainRef = `refs/remotes/origin/${mainBranch}`;
|
|
65
66
|
const mainSha = git('rev-parse', mainRef).trim();
|
|
66
67
|
const head = git('rev-parse', 'HEAD').trim();
|
|
68
|
+
if (['main', 'master'].includes(branch) && branch !== mainBranch) {
|
|
69
|
+
throw new Error(`Production-ветка origin — ${mainBranch}. Переключитесь на неё или на hotfix/<TASK>[-slug].`);
|
|
70
|
+
}
|
|
67
71
|
if (mode === 'start') {
|
|
68
72
|
if (!ancestor(mainSha, head)) throw new Error('Сначала обновите hotfix из актуальной production-ветки.');
|
|
69
73
|
} else if (branch !== mainBranch || head !== mainSha) {
|
|
@@ -115,7 +119,7 @@ function newFragments(baseRef) {
|
|
|
115
119
|
const inherited = git('ls-tree', '-r', '-z', '--name-only', baseRef, '--', `${CHANGELOG_DIR}/`)
|
|
116
120
|
.split('\0').filter(Boolean);
|
|
117
121
|
for (const file of inherited) {
|
|
118
|
-
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== readAt(baseRef, file)) {
|
|
122
|
+
if (!fs.existsSync(file) || normalizeLineEndings(fs.readFileSync(file, 'utf8')) !== normalizeLineEndings(readAt(baseRef, file))) {
|
|
119
123
|
throw new Error(`Нельзя изменять унаследованный fragment ${file} в хотфиксе.`);
|
|
120
124
|
}
|
|
121
125
|
}
|
package/src/open-release.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const { execSilent, logError, logSuccess } = require('./utils');
|
|
3
3
|
const { compareVersions } = require('./version');
|
|
4
4
|
const { CHANGELOG_FILE, CHANGELOG_DIR } = require('./changelog-config');
|
|
5
|
-
const { parseChangelog, releaseEntries } = require('./hotfix-changelog');
|
|
5
|
+
const { parseChangelog, releaseEntries, normalizeLineEndings } = require('./hotfix-changelog');
|
|
6
6
|
const { renderReleaseBlock, stripLegacyUnreleasedBlock } = require('./changelog');
|
|
7
7
|
const { requireChangelogFragments } = require('./preflight');
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ function resolveReleaseState(localText, productionText, stableVersion) {
|
|
|
23
23
|
if (local.open && production.open && local.open.version !== production.open.version) {
|
|
24
24
|
throw new Error('Открытые версии в dev и production различаются. Сначала согласуйте CHANGELOG.md.');
|
|
25
25
|
}
|
|
26
|
-
const history = ({ document }) => document.releases.map((release) => release.text.trim()).join('\n\n');
|
|
26
|
+
const history = ({ document }) => document.releases.map((release) => normalizeLineEndings(release.text).trim()).join('\n\n');
|
|
27
27
|
if (history(local) !== history(production)) {
|
|
28
28
|
throw new Error('Опубликованная история CHANGELOG.md изменена относительно production.');
|
|
29
29
|
}
|
|
@@ -65,7 +65,7 @@ function appendOpenRelease(context) {
|
|
|
65
65
|
keys.add(key);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
const block = renderReleaseBlock(open.version, merged, open.date);
|
|
68
|
+
const block = renderReleaseBlock(open.version, merged, open.date, open.marker);
|
|
69
69
|
const tail = local.document.releases.map((release) => release.text).join('');
|
|
70
70
|
const updated = `${local.document.intro.trimEnd()}\n\n${block.trim()}\n${tail ? `\n${tail}` : ''}`;
|
|
71
71
|
fs.writeFileSync(CHANGELOG_FILE, updated);
|