spectrum-cli 3.1.1 → 3.2.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 +12 -0
- package/README.md +25 -3
- package/index.js +16 -0
- package/package.json +1 -1
- package/src/changelog.js +63 -1
- package/src/hotfix-changelog.js +11 -16
- package/src/hotfix.js +15 -30
- package/src/update-check.js +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.2.0] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### 🆕 Added
|
|
11
|
+
|
|
12
|
+
- Добавлена команда `spectrum changelog write`: выравнивает заголовки релизов и хотфиксов и форматирует `CHANGELOG.md` через Prettier.
|
|
13
|
+
- Добавлена команда `spectrum upgrade` для глобального обновления CLI через npm.
|
|
14
|
+
- Перед выполнением команд CLI проверяет наличие новой версии в npm с таймаутом 200 мс и выводит версии и команду обновления; ошибка проверки не прерывает команду.
|
|
15
|
+
|
|
16
|
+
### 🪲 Fixed
|
|
17
|
+
|
|
18
|
+
- `spectrum hotfix start` собирает все текущие фрагменты из `.changelog/` независимо от истории Git.
|
|
19
|
+
|
|
8
20
|
## 🚀 [3.1.1] - 2026-09-24
|
|
9
21
|
|
|
10
22
|
### 🪲 Fixed
|
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ npm install -g spectrum-cli
|
|
|
14
14
|
spectrum --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Перед выполнением каждой команды CLI проверяет опубликованную версию `spectrum-cli`
|
|
18
|
+
в npm Registry. Если доступна новая версия, показывает текущую и новую версии и
|
|
19
|
+
команду `spectrum upgrade`. Она запускает `npm install -g spectrum-cli`.
|
|
20
|
+
Проверка ограничена 200 мс; недоступность Registry не влияет на выполнение
|
|
21
|
+
команды и не выводит ошибку.
|
|
22
|
+
|
|
17
23
|
### 🔧 Из исходников
|
|
18
24
|
|
|
19
25
|
```bash
|
|
@@ -57,6 +63,7 @@ spectrum chart verify ~/repo # Сравнить ingress paths AS IS vs TO BE д
|
|
|
57
63
|
# 📝 Управление changelog
|
|
58
64
|
spectrum changelog append "Сообщение" # Создать fragment в .changelog/
|
|
59
65
|
spectrum changelog check # Проверить CHANGELOG.md и все fragments
|
|
66
|
+
spectrum changelog write # Выравнять заголовки CHANGELOG.md и запустить Prettier
|
|
60
67
|
|
|
61
68
|
# 🔑 GitLab токены
|
|
62
69
|
spectrum token rotate # Пролить GITLAB_PRIVATE_TOKEN в CI variables
|
|
@@ -197,6 +204,12 @@ spectrum-cli/
|
|
|
197
204
|
|
|
198
205
|
1. **`spectrum hotfix start`** обновляет сведения об origin и выбирает опубликованный
|
|
199
206
|
stable-тег, достижимый из production. Цель — этот stable плюс один patch.
|
|
207
|
+
Как и `release start`, команда читает все файлы текущей `.changelog/` независимо
|
|
208
|
+
от того, когда они попали в Git. Проверяет их общими правилами changelog и
|
|
209
|
+
допускает только patch-типы; файлы `added` и `breaking` требуют обычного релиза.
|
|
210
|
+
Формат CHANGELOG.md проверяется только в текущей ветке. История может отличаться
|
|
211
|
+
от stable-тега: её исправления не блокируют хотфикс. Снимок production используется
|
|
212
|
+
для сохранения записей уже подготовленного хотфикса, без строгой проверки старых заголовков.
|
|
200
213
|
Команда формирует верхний раздел `## 🩹 [X.Y.Z] - YYYY-MM-DD` и удаляет только
|
|
201
214
|
собранные fragments. Она не делает commit, staging, push, merge или tag,
|
|
202
215
|
не меняет файлы версий и сохраняет посторонние локальные изменения.
|
|
@@ -206,7 +219,7 @@ spectrum-cli/
|
|
|
206
219
|
читаются без переписывания опубликованной истории. Без новых fragments подготовленный раздел
|
|
207
220
|
остаётся без изменений. Например, от `v6.35.0` первый и повторные запуски
|
|
208
221
|
готовят `6.35.1`; только после публикации `hotfix/6.35.1` следующий хотфикс получит
|
|
209
|
-
`6.35.2`.
|
|
222
|
+
`6.35.2`.
|
|
210
223
|
3. Проверьте diff, закоммитьте и отправьте исправление по правилам проекта.
|
|
211
224
|
**В MR из hotfix в production уже должны быть верхний patch-раздел и удаления
|
|
212
225
|
использованных fragments.** Merge выполняется отдельно после ревью. Если
|
|
@@ -215,7 +228,7 @@ spectrum-cli/
|
|
|
215
228
|
4. После доставки подготовленного исправления в production через MR или
|
|
216
229
|
разрешённый проектом push и проверки RC выполните **`spectrum hotfix deploy`** на чистом
|
|
217
230
|
`main/master`, точно совпадающем с origin. Команда проверяет следующий patch,
|
|
218
|
-
|
|
231
|
+
отсутствие несобранных hotfix fragments,
|
|
219
232
|
создаёт `hotfix/X.Y.Z` на проверенном SHA и отправляет **только этот тег**.
|
|
220
233
|
Повтор после ошибки push допускает существующий локальный тег лишь на том же
|
|
221
234
|
SHA; опубликованный тег не перемещается. Ветки не сливаются.
|
|
@@ -367,9 +380,18 @@ Fragment должен содержать только непустые Markdown-
|
|
|
367
380
|
> Совет: Приведите файл к нужному формату командой:
|
|
368
381
|
>
|
|
369
382
|
> ```bash
|
|
370
|
-
>
|
|
383
|
+
> spectrum changelog write
|
|
371
384
|
> ```
|
|
372
385
|
|
|
386
|
+
### `spectrum changelog write`
|
|
387
|
+
|
|
388
|
+
Команда приводит все релизные заголовки `CHANGELOG.md` к виду `## 🚀 [X.Y.Z]`,
|
|
389
|
+
`## 🩹 [X.Y.Z]` или `## [X.Y.Z]` с необязательной датой, затем запускает Prettier.
|
|
390
|
+
Старые даты через точки и текст записей сохраняются. Неверный заголовок, включая
|
|
391
|
+
`Unreleased`, останавливает команду без удаления записей. При ошибке Prettier
|
|
392
|
+
исходный `CHANGELOG.md` восстанавливается. Файлы `.changelog/` команда не меняет;
|
|
393
|
+
commit и push не выполняет.
|
|
394
|
+
|
|
373
395
|
## 🎯 Философия
|
|
374
396
|
|
|
375
397
|
- **Простота** - минимум команд, максимум функциональности
|
package/index.js
CHANGED
|
@@ -8,9 +8,15 @@ const hotfix = require("./src/hotfix");
|
|
|
8
8
|
const changelog = require("./src/changelog");
|
|
9
9
|
const chart = require("./src/chart");
|
|
10
10
|
const token = require("./src/token");
|
|
11
|
+
const { checkForUpdates, upgrade } = require("./src/update-check");
|
|
11
12
|
const { version: pkgVersion } = require("./package.json");
|
|
12
13
|
|
|
13
14
|
async function runAction(action, ...args) {
|
|
15
|
+
try {
|
|
16
|
+
await checkForUpdates();
|
|
17
|
+
} catch {
|
|
18
|
+
// A failed update check must not affect the command.
|
|
19
|
+
}
|
|
14
20
|
try {
|
|
15
21
|
const result = await action(...args);
|
|
16
22
|
if (!result) {
|
|
@@ -27,6 +33,11 @@ program
|
|
|
27
33
|
.description("🚀 Spectrum CLI для процесса разработки")
|
|
28
34
|
.version(pkgVersion, "-v, --version");
|
|
29
35
|
|
|
36
|
+
program
|
|
37
|
+
.command("upgrade")
|
|
38
|
+
.description("Обновить Spectrum CLI через npm install -g spectrum-cli")
|
|
39
|
+
.action(() => runAction(upgrade));
|
|
40
|
+
|
|
30
41
|
// Команды релиза
|
|
31
42
|
const releaseCmd = program
|
|
32
43
|
.command("release")
|
|
@@ -74,6 +85,11 @@ changelogCmd
|
|
|
74
85
|
.description("Проверить CHANGELOG.md и changelog fragments")
|
|
75
86
|
.action(() => runAction(changelog.changelogCheck));
|
|
76
87
|
|
|
88
|
+
changelogCmd
|
|
89
|
+
.command("write")
|
|
90
|
+
.description("Привести заголовки CHANGELOG.md к формату релиза и запустить Prettier")
|
|
91
|
+
.action(() => runAction(changelog.changelogWrite));
|
|
92
|
+
|
|
77
93
|
// Команды chart
|
|
78
94
|
const chartCmd = program
|
|
79
95
|
.command("chart")
|
package/package.json
CHANGED
package/src/changelog.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
-
const { logSuccess, logError, execSilent, getCurrentBranch, colors } = require('./utils');
|
|
5
|
+
const { logSuccess, logError, execSilent, execCommand, getCurrentBranch, colors } = require('./utils');
|
|
6
6
|
const { runCommand } = require('./command-executor');
|
|
7
7
|
const {
|
|
8
8
|
requireGitRepo,
|
|
9
9
|
requireFileExists,
|
|
10
|
+
requirePrettierAvailable,
|
|
10
11
|
requireChangelogFormatted,
|
|
11
12
|
requireChangelogFragments
|
|
12
13
|
} = require('./preflight');
|
|
@@ -260,6 +261,65 @@ function changelogCheck() {
|
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
function normalizeChangelogHeadings(text) {
|
|
265
|
+
const version = '(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)';
|
|
266
|
+
const date = '(\\d{4}-\\d{2}-\\d{2}|\\d{4}\\.\\d{2}\\.\\d{2}|\\d{2}\\.\\d{2}\\.\\d{4})';
|
|
267
|
+
const releaseHeading = new RegExp(`^##[ \\t]*(?:(🚀|🩹)[ \\t]*)?\\[${version}\\](?:[ \\t]+-[ \\t]+${date})?[ \\t]*\\r?$`);
|
|
268
|
+
return text.replace(/^##(?!#)[^\n]*$/gm, (heading) => {
|
|
269
|
+
const match = releaseHeading.exec(heading);
|
|
270
|
+
if (!match) throw new Error(`Некорректный заголовок ${CHANGELOG_FILE}: ${heading.trimEnd()}`);
|
|
271
|
+
const marker = match[1] ? `${match[1]} ` : '';
|
|
272
|
+
const releaseVersion = `${match[2]}.${match[3]}.${match[4]}`;
|
|
273
|
+
const releaseDate = match[5] ? ` - ${match[5]}` : '';
|
|
274
|
+
return `## ${marker}[${releaseVersion}]${releaseDate}${heading.endsWith('\r') ? '\r' : ''}`;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeCurrentChangelog(context) {
|
|
279
|
+
let original;
|
|
280
|
+
let mayHaveChanged = false;
|
|
281
|
+
try {
|
|
282
|
+
original = fs.readFileSync(CHANGELOG_FILE, 'utf8');
|
|
283
|
+
const normalized = normalizeChangelogHeadings(original);
|
|
284
|
+
if (normalized !== original) {
|
|
285
|
+
mayHaveChanged = true;
|
|
286
|
+
fs.writeFileSync(CHANGELOG_FILE, normalized);
|
|
287
|
+
}
|
|
288
|
+
mayHaveChanged = true;
|
|
289
|
+
if (!execCommand(`${context.prettierRunner} --write ${CHANGELOG_FILE}`) ||
|
|
290
|
+
!execCommand(`${context.prettierRunner} --check ${CHANGELOG_FILE}`)) {
|
|
291
|
+
throw new Error('Prettier не смог отформатировать CHANGELOG.md.');
|
|
292
|
+
}
|
|
293
|
+
const formatted = fs.readFileSync(CHANGELOG_FILE, 'utf8');
|
|
294
|
+
if (normalizeChangelogHeadings(formatted) !== formatted) {
|
|
295
|
+
throw new Error('После Prettier заголовки CHANGELOG.md не соответствуют формату релизов.');
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (mayHaveChanged && original !== undefined) {
|
|
300
|
+
try {
|
|
301
|
+
fs.writeFileSync(CHANGELOG_FILE, original);
|
|
302
|
+
} catch (restoreError) {
|
|
303
|
+
logError('❌', 'Не удалось восстановить %s: %s', CHANGELOG_FILE, restoreError.message);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
logError('❌', 'Не удалось отформатировать %s: %s', CHANGELOG_FILE, error.message);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function changelogWrite() {
|
|
312
|
+
return runCommand({
|
|
313
|
+
name: 'changelog write',
|
|
314
|
+
checks: [
|
|
315
|
+
{ name: 'git-repo', run: requireGitRepo },
|
|
316
|
+
{ name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
|
|
317
|
+
{ name: 'prettier-available', run: requirePrettierAvailable }
|
|
318
|
+
],
|
|
319
|
+
steps: [{ name: 'write-changelog', run: writeCurrentChangelog }]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
263
323
|
function stripLegacyUnreleasedBlock(changelog) {
|
|
264
324
|
const unreleasedMatch = /^## \[Unreleased\]\s*$/m.exec(changelog);
|
|
265
325
|
if (!unreleasedMatch) return changelog;
|
|
@@ -339,6 +399,8 @@ function changelogRemoveFragments(context) {
|
|
|
339
399
|
module.exports = {
|
|
340
400
|
changelogAppend,
|
|
341
401
|
changelogCheck,
|
|
402
|
+
changelogWrite,
|
|
403
|
+
normalizeChangelogHeadings,
|
|
342
404
|
extractTaskFromBranch,
|
|
343
405
|
getGitUser,
|
|
344
406
|
formatMessage,
|
package/src/hotfix-changelog.js
CHANGED
|
@@ -2,20 +2,23 @@ const { FRAGMENT_TYPES, SECTION_TO_TYPE } = require('./changelog-config');
|
|
|
2
2
|
const { renderReleaseBlock } = require('./changelog');
|
|
3
3
|
|
|
4
4
|
// Parse release boundaries without rewriting any published text.
|
|
5
|
-
function parseChangelog(text) {
|
|
5
|
+
function parseChangelog(text, { validate = true } = {}) {
|
|
6
6
|
const headings = [...text.matchAll(/^## .+$/gm)];
|
|
7
7
|
const releases = headings.map((heading, index) => {
|
|
8
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
9
|
if (!match) {
|
|
10
|
+
// Production snapshots supply pending notes, not files to validate. Skip
|
|
11
|
+
// historical non-release sections (including their bodies).
|
|
12
|
+
if (!validate) return null;
|
|
10
13
|
throw new Error(`Некорректный заголовок CHANGELOG.md: ${heading[0]}. Ожидается ## [X.Y.Z], дата необязательна.`);
|
|
11
14
|
}
|
|
12
|
-
// Historical duplicates are preserved; callers validate the pending version
|
|
15
|
+
// Historical duplicates are preserved; callers validate the pending version.
|
|
13
16
|
return {
|
|
14
17
|
version: match[2], date: match[3] || null, marker: match[1] || '',
|
|
15
18
|
text: text.slice(heading.index, headings[index + 1]?.index ?? text.length),
|
|
16
19
|
body: text.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? text.length),
|
|
17
20
|
};
|
|
18
|
-
});
|
|
21
|
+
}).filter(Boolean);
|
|
19
22
|
return { intro: text.slice(0, headings[0]?.index ?? text.length), releases };
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -47,26 +50,18 @@ function normalizeLineEndings(text) {
|
|
|
47
50
|
return text.replace(/\r\n/g, '\n');
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function pendingDocument(text, stableText, target) {
|
|
55
|
-
const document = parseChangelog(text);
|
|
56
|
-
const stable = parseChangelog(stableText);
|
|
53
|
+
function pendingDocument(text, target, { validate = true } = {}) {
|
|
54
|
+
const document = parseChangelog(text, { validate });
|
|
57
55
|
const pending = document.releases[0]?.version === target ? document.releases.shift() : null;
|
|
58
56
|
if (document.releases.some((release) => release.version === target)) {
|
|
59
57
|
throw new Error('Новый patch-раздел должен быть единственным и находиться в начале CHANGELOG.md.');
|
|
60
58
|
}
|
|
61
|
-
if (history(document) !== history(stable) || normalizeLineEndings(document.intro).trim() !== normalizeLineEndings(stable.intro).trim()) {
|
|
62
|
-
throw new Error('История CHANGELOG.md отличается от stable: допустим только верхний раздел следующего patch.');
|
|
63
|
-
}
|
|
64
59
|
return { document, pending, entries: pending ? releaseEntries(pending, true) : [] };
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
function prepareChangelog({ text,
|
|
68
|
-
const current = pendingDocument(text,
|
|
69
|
-
const production = pendingDocument(productionText,
|
|
62
|
+
function prepareChangelog({ text, productionText, target, fragments, date }) {
|
|
63
|
+
const current = pendingDocument(text, target);
|
|
64
|
+
const production = pendingDocument(productionText, target, { validate: false });
|
|
70
65
|
const entryKey = (entry) => `${entry.type}:${entry.entries[0]}`;
|
|
71
66
|
const currentKeys = new Set(current.entries.map(entryKey));
|
|
72
67
|
if (production.entries.some((entry) => !currentKeys.has(entryKey(entry)))) {
|
package/src/hotfix.js
CHANGED
|
@@ -5,8 +5,8 @@ const preflight = require('./preflight');
|
|
|
5
5
|
const { execCommand, logSuccess, logError } = require('./utils');
|
|
6
6
|
const { upVersion, compareVersions } = require('./version');
|
|
7
7
|
const { STABLE_TAG_PATTERNS, parseStableTag, stableTagNames } = require('./stable-tags');
|
|
8
|
-
const { CHANGELOG_FILE
|
|
9
|
-
const { parseChangelog, pendingDocument, prepareChangelog
|
|
8
|
+
const { CHANGELOG_FILE } = require('./changelog-config');
|
|
9
|
+
const { parseChangelog, pendingDocument, prepareChangelog } = require('./hotfix-changelog');
|
|
10
10
|
|
|
11
11
|
function git(...args) {
|
|
12
12
|
try {
|
|
@@ -92,10 +92,9 @@ function context(mode) {
|
|
|
92
92
|
throw new Error(`Конфликт stable-тегов версии ${stable}: разные commits.`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
const stableText = readAt(`refs/tags/${stableTag}`);
|
|
96
95
|
const productionText = readAt(mainSha);
|
|
97
96
|
const target = upVersion(stable, 'patch');
|
|
98
|
-
return { mainBranch, mainSha, head, remoteTags, stable, stableTag,
|
|
97
|
+
return { mainBranch, mainSha, head, remoteTags, stable, stableTag, productionText, target };
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
function assertTagAvailable(ctx, allowMatchingLocal = false) {
|
|
@@ -115,28 +114,14 @@ function assertOriginUnchanged(ctx) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (!fs.existsSync(CHANGELOG_DIR)) return [];
|
|
127
|
-
return fs.readdirSync(CHANGELOG_DIR).sort().filter((name) => !name.startsWith('.'))
|
|
128
|
-
.map((name) => `${CHANGELOG_DIR}/${name}`).filter((file) => !inherited.includes(file))
|
|
129
|
-
.map((filePath) => {
|
|
130
|
-
const type = getFragmentType(filePath);
|
|
131
|
-
if (!type || !fs.lstatSync(filePath).isFile()) throw new Error(`Некорректный fragment ${filePath}.`);
|
|
132
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
133
|
-
const entries = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
134
|
-
if (!entries.length || entries.some((line) => !line.startsWith('- ') || line.length > 120)) {
|
|
135
|
-
throw new Error(`Пустой или неверно оформленный fragment ${filePath}: записи «- …», до 120 символов.`);
|
|
136
|
-
}
|
|
137
|
-
if (FRAGMENT_TYPES[type].bump !== 'patch') throw new Error(`Хотфикс не допускает minor/major: ${filePath}.`);
|
|
138
|
-
return { filePath, content, type, ...FRAGMENT_TYPES[type], entries };
|
|
139
|
-
});
|
|
117
|
+
function currentFragments() {
|
|
118
|
+
if (!preflight.findChangelogFragmentFiles().length) return [];
|
|
119
|
+
const result = preflight.requireChangelogFragments();
|
|
120
|
+
if (!result.ok) throw new Error(result.reason);
|
|
121
|
+
return result.data.changelogFragments.map((fragment) => {
|
|
122
|
+
if (fragment.bump !== 'patch') throw new Error(`Хотфикс не допускает minor/major: ${fragment.filePath}.`);
|
|
123
|
+
return { ...fragment, content: fs.readFileSync(fragment.filePath, 'utf8') };
|
|
124
|
+
});
|
|
140
125
|
}
|
|
141
126
|
|
|
142
127
|
function formatChangelog() {
|
|
@@ -160,7 +145,7 @@ function hotfixStart() {
|
|
|
160
145
|
return run(() => {
|
|
161
146
|
const ctx = context('start');
|
|
162
147
|
assertTagAvailable(ctx);
|
|
163
|
-
const fragments =
|
|
148
|
+
const fragments = currentFragments();
|
|
164
149
|
const original = fs.readFileSync(CHANGELOG_FILE, 'utf8');
|
|
165
150
|
const updated = prepareChangelog({ text: original, ...ctx, fragments });
|
|
166
151
|
assertOriginUnchanged(ctx);
|
|
@@ -168,7 +153,7 @@ function hotfixStart() {
|
|
|
168
153
|
try {
|
|
169
154
|
fs.writeFileSync(CHANGELOG_FILE, updated);
|
|
170
155
|
formatChangelog();
|
|
171
|
-
//
|
|
156
|
+
// Validate the formatted current file and keep all merged hotfix notes.
|
|
172
157
|
prepareChangelog({ text: fs.readFileSync(CHANGELOG_FILE, 'utf8'), ...ctx, fragments: [] });
|
|
173
158
|
for (const fragment of fragments) fs.unlinkSync(fragment.filePath);
|
|
174
159
|
} catch (error) {
|
|
@@ -184,9 +169,9 @@ function hotfixStart() {
|
|
|
184
169
|
function hotfixDeploy() {
|
|
185
170
|
return run(() => {
|
|
186
171
|
const ctx = context('deploy');
|
|
187
|
-
const prepared = pendingDocument(ctx.productionText, ctx.
|
|
172
|
+
const prepared = pendingDocument(ctx.productionText, ctx.target);
|
|
188
173
|
if (!prepared.pending) throw new Error(`В CHANGELOG.md нужен верхний подготовленный раздел ${ctx.target}.`);
|
|
189
|
-
if (
|
|
174
|
+
if (currentFragments().length) throw new Error('В production остались несобранные hotfix fragments.');
|
|
190
175
|
assertTagAvailable(ctx, true);
|
|
191
176
|
assertOriginUnchanged(ctx);
|
|
192
177
|
const tag = `hotfix/${ctx.target}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const childProcess = require('child_process');
|
|
2
|
+
const { name, version: currentVersion } = require('../package.json');
|
|
3
|
+
const { compareVersions } = require('./version');
|
|
4
|
+
|
|
5
|
+
const UPDATE_CHECK_TIMEOUT_MS = 200;
|
|
6
|
+
|
|
7
|
+
async function checkForUpdates() {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
let timer;
|
|
10
|
+
try {
|
|
11
|
+
const timeout = new Promise((resolve) => {
|
|
12
|
+
timer = setTimeout(() => {
|
|
13
|
+
controller.abort();
|
|
14
|
+
resolve(null);
|
|
15
|
+
}, UPDATE_CHECK_TIMEOUT_MS);
|
|
16
|
+
});
|
|
17
|
+
const request = (async () => {
|
|
18
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
headers: { accept: 'application/json' }
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) return null;
|
|
23
|
+
return (await response.json()).version;
|
|
24
|
+
})();
|
|
25
|
+
const latestVersion = await Promise.race([request, timeout]);
|
|
26
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
27
|
+
console.log(`⚠️ Доступна новая версия Spectrum CLI: сейчас ${currentVersion}, доступна ${latestVersion}.\nОбновить: spectrum upgrade`);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Network and registry errors must never block a command.
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function upgrade() {
|
|
37
|
+
const result = childProcess.spawnSync('npm', ['install', '-g', name], {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
shell: process.platform === 'win32'
|
|
40
|
+
});
|
|
41
|
+
if (result.error) throw result.error;
|
|
42
|
+
return result.status === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { checkForUpdates, upgrade, UPDATE_CHECK_TIMEOUT_MS };
|