spectrum-cli 3.1.1 → 4.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +81 -29
  3. package/index.js +6 -158
  4. package/package.json +4 -4
  5. package/src/changelog/commands.js +44 -0
  6. package/src/changelog/format.js +51 -0
  7. package/src/{changelog.js → changelog/fragments.js} +3 -136
  8. package/src/{hotfix-changelog.js → changelog/hotfix.js} +13 -18
  9. package/src/changelog/index.js +7 -0
  10. package/src/{open-release.js → changelog/open-release.js} +6 -6
  11. package/src/changelog/release-block.js +80 -0
  12. package/src/chart/deploy.js +216 -0
  13. package/src/chart/helmrelease.js +90 -0
  14. package/src/chart/index.js +30 -0
  15. package/src/chart/metadata.js +114 -0
  16. package/src/chart/registry.js +139 -0
  17. package/src/chart/routes.js +244 -0
  18. package/src/chart/start.js +98 -0
  19. package/src/chart/verify.js +166 -0
  20. package/src/cli/command-executor.js +181 -0
  21. package/src/cli/commands.js +136 -0
  22. package/src/{development.js → cli/development.js} +1 -1
  23. package/src/cli/help.js +55 -0
  24. package/src/cli/run-action.js +28 -0
  25. package/src/cli/update-check.js +51 -0
  26. package/src/{git.js → git/index.js} +4 -3
  27. package/src/hotfix/checks.js +144 -0
  28. package/src/hotfix/close.js +46 -0
  29. package/src/hotfix/deploy.js +37 -0
  30. package/src/hotfix/git.js +35 -0
  31. package/src/hotfix/index.js +5 -0
  32. package/src/hotfix/start.js +57 -0
  33. package/src/integrations/gitlab-url.js +16 -0
  34. package/src/preflight/changelog.js +130 -0
  35. package/src/preflight/chart.js +57 -0
  36. package/src/preflight/git.js +133 -0
  37. package/src/preflight/helmrelease.js +201 -0
  38. package/src/preflight/index.js +10 -0
  39. package/src/preflight/result.js +18 -0
  40. package/src/preflight/source.js +95 -0
  41. package/src/preflight/tags.js +139 -0
  42. package/src/release/close.js +39 -0
  43. package/src/release/index.js +7 -0
  44. package/src/{stable-tags.js → release/stable-tags.js} +1 -1
  45. package/src/release/start.js +51 -0
  46. package/src/release/steps.js +83 -0
  47. package/src/release/version.js +20 -0
  48. package/src/token/config.js +163 -0
  49. package/src/token/gitlab-api.js +59 -0
  50. package/src/token/index.js +6 -0
  51. package/src/token/rotate.js +157 -0
  52. package/src/chart.js +0 -1071
  53. package/src/command-executor.js +0 -70
  54. package/src/hotfix.js +0 -223
  55. package/src/preflight.js +0 -796
  56. package/src/release.js +0 -207
  57. package/src/token.js +0 -399
  58. /package/src/{changelog-config.js → changelog/config.js} +0 -0
  59. /package/src/{utils.js → common/utils.js} +0 -0
  60. /package/src/{version.js → common/version.js} +0 -0
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env node
2
- const { logError, logSuccess } = require('./utils');
3
-
4
- function normalizeCheckResult(result, fallbackReason) {
5
- if (typeof result === 'boolean') {
6
- return { ok: result, reason: result ? null : fallbackReason };
7
- }
8
-
9
- if (typeof result === 'string') {
10
- return { ok: false, reason: result };
11
- }
12
-
13
- if (result && typeof result === 'object') {
14
- return {
15
- ok: Boolean(result.ok),
16
- reason: result.reason || fallbackReason,
17
- data: result.data
18
- };
19
- }
20
-
21
- return { ok: false, reason: fallbackReason };
22
- }
23
-
24
- async function runCommand(spec) {
25
- const {
26
- name,
27
- checks = [],
28
- steps = [],
29
- context = {}
30
- } = spec || {};
31
-
32
- if (!name) {
33
- logError('❌', 'Требуется имя команды.');
34
- return false;
35
- }
36
-
37
- for (const check of checks) {
38
- const checkName = check.name || 'unknown-check';
39
- const checkResult = normalizeCheckResult(
40
- await check.run(context),
41
- `Проверка не пройдена: ${checkName}`
42
- );
43
-
44
- if (!checkResult.ok) {
45
- /* istanbul ignore next */
46
- logError('❌', '[%s] Предпроверка не пройдена (%s): %s', name, checkName, checkResult.reason || 'неизвестная причина');
47
- return false;
48
- }
49
-
50
- if (checkResult.data && typeof checkResult.data === 'object') {
51
- Object.assign(context, checkResult.data);
52
- }
53
- }
54
-
55
- for (const step of steps) {
56
- const stepName = step.name || 'unknown-step';
57
- const ok = await step.run(context);
58
- if (!ok) {
59
- logError('❌', '[%s] Шаг не выполнен: %s', name, stepName);
60
- return false;
61
- }
62
- }
63
-
64
- logSuccess('✅', '[%s] Выполнено.', name);
65
- return true;
66
- }
67
-
68
- module.exports = {
69
- runCommand
70
- };
package/src/hotfix.js DELETED
@@ -1,223 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { execFileSync } = require('child_process');
4
- const preflight = require('./preflight');
5
- const { execCommand, logSuccess, logError } = require('./utils');
6
- const { upVersion, compareVersions } = require('./version');
7
- const { STABLE_TAG_PATTERNS, parseStableTag, stableTagNames } = require('./stable-tags');
8
- const { CHANGELOG_FILE, CHANGELOG_DIR, FRAGMENT_TYPES, getFragmentType } = require('./changelog-config');
9
- const { parseChangelog, pendingDocument, prepareChangelog, normalizeLineEndings } = require('./hotfix-changelog');
10
-
11
- function git(...args) {
12
- try {
13
- return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
14
- } catch {
15
- throw new Error(`Git не выполнил «${args[0]}». Проверьте доступ к origin, состояние веток и конфликты.`);
16
- }
17
- }
18
-
19
- function exists(ref) {
20
- try {
21
- execFileSync('git', ['show-ref', '--verify', '--quiet', ref], { stdio: 'ignore' });
22
- return true;
23
- } catch (error) {
24
- if (error.status === 1) return false;
25
- throw new Error(`Не удалось проверить ref ${ref}.`);
26
- }
27
- }
28
-
29
- function ancestor(base, head) {
30
- try {
31
- execFileSync('git', ['merge-base', '--is-ancestor', base, head], { stdio: 'ignore' });
32
- return true;
33
- } catch (error) {
34
- if (error.status === 1) return false;
35
- throw new Error('Не удалось проверить историю Git.');
36
- }
37
- }
38
-
39
- function readAt(ref, file = CHANGELOG_FILE) {
40
- return git('show', `${ref}:${file}`);
41
- }
42
-
43
- function context(mode) {
44
- const root = git('rev-parse', '--show-toplevel').trim();
45
- if (path.relative(fs.realpathSync(root), fs.realpathSync(process.cwd())) !== '') {
46
- throw new Error('Запустите hotfix из корня репозитория.');
47
- }
48
- const branch = git('branch', '--show-current').trim();
49
- if (!['main', 'master'].includes(branch) && !/^hotfix\/.+/.test(branch)) {
50
- throw new Error('Hotfix-команды разрешены только на hotfix/*, main или master (не detached HEAD).');
51
- }
52
- if (mode !== 'start' && !['main', 'master'].includes(branch)) {
53
- throw new Error('hotfix deploy/close выполняются на main/master после merge хотфикса.');
54
- }
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.');
57
- }
58
- if (mode !== 'start' && git('status', '--porcelain').trim()) {
59
- throw new Error('Для deploy/close требуется чистое рабочее дерево.');
60
- }
61
- if (git('ls-files', '--unmerged').trim()) throw new Error('Сначала разрешите конфликты Git.');
62
- git('fetch', 'origin', '--prune', '--tags');
63
- const mainBranch = ['master', 'main'].find((name) => exists(`refs/remotes/origin/${name}`));
64
- if (!mainBranch) throw new Error('Не найдена production-ветка origin/main или origin/master.');
65
- const mainRef = `refs/remotes/origin/${mainBranch}`;
66
- const mainSha = git('rev-parse', mainRef).trim();
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
- }
71
- if (mode === 'start') {
72
- if (!ancestor(mainSha, head)) throw new Error('Сначала обновите hotfix из актуальной production-ветки.');
73
- } else if (branch !== mainBranch || head !== mainSha) {
74
- throw new Error(`Нужна ветка ${mainBranch}, точно совпадающая с origin/${mainBranch}.`);
75
- }
76
- // Query origin explicitly: a local-only tag must never change the baseline.
77
- const remoteTags = new Map(git('ls-remote', '--refs', '--tags', 'origin', ...STABLE_TAG_PATTERNS.map((tag) => `refs/tags/${tag}`))
78
- .trim().split('\n').filter(Boolean).map((line) => {
79
- const [sha, ref] = line.split(/\s+/);
80
- return [ref.slice('refs/tags/'.length), sha];
81
- }));
82
- const versions = git('tag', '--merged', mainSha, '--list', ...STABLE_TAG_PATTERNS).trim().split('\n')
83
- .filter((tag) => parseStableTag(tag) && remoteTags.has(tag))
84
- .map((tag) => ({ tag, ...parseStableTag(tag) })).sort((a, b) => compareVersions(a.version, b.version));
85
- const latest = versions.at(-1);
86
- const stable = latest?.version;
87
- if (!stable) throw new Error('Для хотфикса необходим опубликованный stable-тег production.');
88
- const stableTag = latest.tag;
89
- const stableSha = git('rev-parse', `refs/tags/${stableTag}^{commit}`).trim();
90
- for (const tag of stableTagNames(stable).filter((name) => remoteTags.has(name))) {
91
- if (git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== stableSha) {
92
- throw new Error(`Конфликт stable-тегов версии ${stable}: разные commits.`);
93
- }
94
- }
95
- const stableText = readAt(`refs/tags/${stableTag}`);
96
- const productionText = readAt(mainSha);
97
- const target = upVersion(stable, 'patch');
98
- return { mainBranch, mainSha, head, remoteTags, stable, stableTag, stableText, productionText, target };
99
- }
100
-
101
- function assertTagAvailable(ctx, allowMatchingLocal = false) {
102
- for (const tag of stableTagNames(ctx.target)) {
103
- if (ctx.remoteTags.has(tag)) throw new Error(`Тег ${tag} уже опубликован; эту версию нельзя выпустить повторно.`);
104
- if (exists(`refs/tags/${tag}`) &&
105
- (!allowMatchingLocal || tag !== `hotfix/${ctx.target}` || git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== ctx.head)) {
106
- throw new Error(`Локальный тег ${tag} уже существует. Проверьте его перед продолжением.`);
107
- }
108
- }
109
- }
110
-
111
- function assertOriginUnchanged(ctx) {
112
- const line = git('ls-remote', '--heads', 'origin', `refs/heads/${ctx.mainBranch}`).trim();
113
- if (line.split(/\s+/)[0] !== ctx.mainSha) {
114
- throw new Error('Production изменилась во время проверки. Обновите ветку и повторите команду.');
115
- }
116
- }
117
-
118
- function newFragments(baseRef) {
119
- const inherited = git('ls-tree', '-r', '-z', '--name-only', baseRef, '--', `${CHANGELOG_DIR}/`)
120
- .split('\0').filter(Boolean);
121
- for (const file of inherited) {
122
- if (!fs.existsSync(file) || normalizeLineEndings(fs.readFileSync(file, 'utf8')) !== normalizeLineEndings(readAt(baseRef, file))) {
123
- throw new Error(`Нельзя изменять унаследованный fragment ${file} в хотфиксе.`);
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
- });
140
- }
141
-
142
- function formatChangelog() {
143
- const runner = preflight.getPrettierRunner();
144
- if (!runner || !execCommand(`${runner} --write CHANGELOG.md`) || !execCommand(`${runner} --check CHANGELOG.md`)) {
145
- throw new Error('Не удалось отформатировать CHANGELOG.md; hotfix fragments сохранены.');
146
- }
147
- }
148
-
149
- function run(action) {
150
- try {
151
- action();
152
- return true;
153
- } catch (error) {
154
- logError('❌', error.message);
155
- return false;
156
- }
157
- }
158
-
159
- function hotfixStart() {
160
- return run(() => {
161
- const ctx = context('start');
162
- assertTagAvailable(ctx);
163
- const fragments = newFragments(ctx.mainSha);
164
- const original = fs.readFileSync(CHANGELOG_FILE, 'utf8');
165
- const updated = prepareChangelog({ text: original, ...ctx, fragments });
166
- assertOriginUnchanged(ctx);
167
- if (updated !== original || fragments.length) {
168
- try {
169
- fs.writeFileSync(CHANGELOG_FILE, updated);
170
- formatChangelog();
171
- // Formatting must not rewrite published history or lose merged notes.
172
- prepareChangelog({ text: fs.readFileSync(CHANGELOG_FILE, 'utf8'), ...ctx, fragments: [] });
173
- for (const fragment of fragments) fs.unlinkSync(fragment.filePath);
174
- } catch (error) {
175
- fs.writeFileSync(CHANGELOG_FILE, original);
176
- for (const fragment of fragments) fs.writeFileSync(fragment.filePath, fragment.content);
177
- throw error;
178
- }
179
- }
180
- logSuccess('✅', `Хотфикс ${ctx.target} подготовлен локально. Проверьте diff и включите CHANGELOG.md и удаления fragments в MR в ${ctx.mainBranch}.`);
181
- });
182
- }
183
-
184
- function hotfixDeploy() {
185
- return run(() => {
186
- const ctx = context('deploy');
187
- const prepared = pendingDocument(ctx.productionText, ctx.stableText, ctx.target);
188
- if (!prepared.pending) throw new Error(`В CHANGELOG.md нужен верхний подготовленный раздел ${ctx.target}.`);
189
- if (newFragments(`refs/tags/${ctx.stableTag}`).length) throw new Error('В production остались несобранные hotfix fragments.');
190
- assertTagAvailable(ctx, true);
191
- assertOriginUnchanged(ctx);
192
- const tag = `hotfix/${ctx.target}`;
193
- if (!exists(`refs/tags/${tag}`)) git('tag', tag, ctx.head);
194
- git('push', 'origin', `refs/tags/${tag}:refs/tags/${tag}`);
195
- logSuccess('✅', `Опубликован ${tag} на ${ctx.head}. Дождитесь успешного stable pipeline перед hotfix close.`);
196
- });
197
- }
198
-
199
- function hotfixClose() {
200
- return run(() => {
201
- const ctx = context('close');
202
- const top = parseChangelog(ctx.productionText).releases[0]?.version;
203
- const tag = `hotfix/${ctx.stable}`;
204
- if (top !== ctx.stable || !ctx.remoteTags.has(tag) || git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== ctx.head) {
205
- throw new Error('Сначала опубликуйте hotfix/X.Y.Z на текущем production commit.');
206
- }
207
- const dev = ['develop', 'dev'].find((name) => exists(`refs/remotes/origin/${name}`));
208
- if (!dev) throw new Error('Не найдена integration-ветка origin/dev или origin/develop.');
209
- const devRef = `refs/remotes/origin/${dev}`;
210
- if (exists(`refs/heads/${dev}`) && !ancestor(`refs/heads/${dev}`, devRef)) {
211
- throw new Error(`В ${dev} есть локальные непубликуемые commits. Сначала согласуйте их с origin/${dev}.`);
212
- }
213
- assertOriginUnchanged(ctx);
214
- if (exists(`refs/heads/${dev}`)) git('switch', dev);
215
- else git('switch', '--track', '-c', dev, devRef);
216
- git('merge', '--ff-only', devRef);
217
- git('merge', '--no-edit', ctx.head);
218
- git('push', 'origin', `refs/heads/${dev}:refs/heads/${dev}`);
219
- logSuccess('✅', `Хотфикс ${ctx.stable} сведён в ${dev}; production и файлы версий не изменены.`);
220
- });
221
- }
222
-
223
- module.exports = { hotfixStart, hotfixDeploy, hotfixClose };