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.
- package/CHANGELOG.md +34 -0
- package/README.md +81 -29
- package/index.js +6 -158
- package/package.json +4 -4
- package/src/changelog/commands.js +44 -0
- package/src/changelog/format.js +51 -0
- package/src/{changelog.js → changelog/fragments.js} +3 -136
- package/src/{hotfix-changelog.js → changelog/hotfix.js} +13 -18
- package/src/changelog/index.js +7 -0
- package/src/{open-release.js → changelog/open-release.js} +6 -6
- package/src/changelog/release-block.js +80 -0
- package/src/chart/deploy.js +216 -0
- package/src/chart/helmrelease.js +90 -0
- package/src/chart/index.js +30 -0
- package/src/chart/metadata.js +114 -0
- package/src/chart/registry.js +139 -0
- package/src/chart/routes.js +244 -0
- package/src/chart/start.js +98 -0
- package/src/chart/verify.js +166 -0
- package/src/cli/command-executor.js +181 -0
- package/src/cli/commands.js +136 -0
- package/src/{development.js → cli/development.js} +1 -1
- package/src/cli/help.js +55 -0
- package/src/cli/run-action.js +28 -0
- package/src/cli/update-check.js +51 -0
- package/src/{git.js → git/index.js} +4 -3
- package/src/hotfix/checks.js +144 -0
- package/src/hotfix/close.js +46 -0
- package/src/hotfix/deploy.js +37 -0
- package/src/hotfix/git.js +35 -0
- package/src/hotfix/index.js +5 -0
- package/src/hotfix/start.js +57 -0
- package/src/integrations/gitlab-url.js +16 -0
- package/src/preflight/changelog.js +130 -0
- package/src/preflight/chart.js +57 -0
- package/src/preflight/git.js +133 -0
- package/src/preflight/helmrelease.js +201 -0
- package/src/preflight/index.js +10 -0
- package/src/preflight/result.js +18 -0
- package/src/preflight/source.js +95 -0
- package/src/preflight/tags.js +139 -0
- package/src/release/close.js +39 -0
- package/src/release/index.js +7 -0
- package/src/{stable-tags.js → release/stable-tags.js} +1 -1
- package/src/release/start.js +51 -0
- package/src/release/steps.js +83 -0
- package/src/release/version.js +20 -0
- package/src/token/config.js +163 -0
- package/src/token/gitlab-api.js +59 -0
- package/src/token/index.js +6 -0
- package/src/token/rotate.js +157 -0
- package/src/chart.js +0 -1071
- package/src/command-executor.js +0 -70
- package/src/hotfix.js +0 -223
- package/src/preflight.js +0 -796
- package/src/release.js +0 -207
- package/src/token.js +0 -399
- /package/src/{changelog-config.js → changelog/config.js} +0 -0
- /package/src/{utils.js → common/utils.js} +0 -0
- /package/src/{version.js → common/version.js} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const preflight = require('../preflight');
|
|
4
|
+
const { logError } = require('../common/utils');
|
|
5
|
+
const { reportPreflight } = require('../cli/command-executor');
|
|
6
|
+
const { upVersion, compareVersions } = require('../common/version');
|
|
7
|
+
const { STABLE_TAG_PATTERNS, parseStableTag, stableTagNames } = require('../release/stable-tags');
|
|
8
|
+
const { git, exists, ancestor, readAt } = require('./git');
|
|
9
|
+
function preflightReport(commandName) {
|
|
10
|
+
let failures = 0;
|
|
11
|
+
return {
|
|
12
|
+
check(name, action, description, unavailableReason) {
|
|
13
|
+
if (unavailableReason) {
|
|
14
|
+
logError('❌', 'Предпроверка не выполнена (%s): %s', name, unavailableReason);
|
|
15
|
+
failures += 1;
|
|
16
|
+
return { ok: false };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const value = action();
|
|
20
|
+
reportPreflight(commandName, name, typeof description === 'function' ? description(value) : description);
|
|
21
|
+
return { ok: true, value };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logError('❌', 'Предпроверка не пройдена (%s): %s', name, error?.message || String(error));
|
|
24
|
+
failures += 1;
|
|
25
|
+
return { ok: false };
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
get ok() { return failures === 0; }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function context(mode, checks) {
|
|
33
|
+
const root = checks.check('repository-root', () => {
|
|
34
|
+
const root = git('rev-parse', '--show-toplevel').trim();
|
|
35
|
+
if (path.relative(fs.realpathSync(root), fs.realpathSync(process.cwd())) !== '') {
|
|
36
|
+
throw new Error('Запустите hotfix из корня репозитория.');
|
|
37
|
+
}
|
|
38
|
+
}, 'команда запущена из корня Git-репозитория');
|
|
39
|
+
const branch = checks.check('branch', () => {
|
|
40
|
+
const branch = git('branch', '--show-current').trim();
|
|
41
|
+
if (!['main', 'master'].includes(branch) && !/^hotfix\/.+/.test(branch)) {
|
|
42
|
+
throw new Error('Hotfix-команды разрешены только на hotfix/*, main или master (не detached HEAD).');
|
|
43
|
+
}
|
|
44
|
+
if (mode !== 'start' && !['main', 'master'].includes(branch)) {
|
|
45
|
+
throw new Error('hotfix deploy/close выполняются на main/master после merge хотфикса.');
|
|
46
|
+
}
|
|
47
|
+
if (mode === 'start' && !['main', 'master'].includes(branch) && !/^hotfix\/[A-Z]+-[0-9]+(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/.test(branch)) {
|
|
48
|
+
throw new Error('hotfix start выполняется на hotfix/<TASK>[-slug], main или master.');
|
|
49
|
+
}
|
|
50
|
+
return branch;
|
|
51
|
+
}, (value) => `ветка ${value} разрешена для этой команды`,
|
|
52
|
+
root.ok ? null : 'не пройдена проверка корня репозитория');
|
|
53
|
+
if (mode !== 'start') checks.check('clean-working-tree', () => {
|
|
54
|
+
if (git('status', '--porcelain').trim()) throw new Error('Для deploy/close требуется чистое рабочее дерево.');
|
|
55
|
+
}, 'рабочее дерево Git чистое', root.ok ? null : 'не пройдена проверка корня репозитория');
|
|
56
|
+
checks.check('merge-conflicts', () => {
|
|
57
|
+
if (git('ls-files', '--unmerged').trim()) throw new Error('Сначала разрешите конфликты Git.');
|
|
58
|
+
}, 'неразрешённых конфликтов Git нет', root.ok ? null : 'не пройдена проверка корня репозитория');
|
|
59
|
+
let productionRef;
|
|
60
|
+
const history = checks.check('production-history', () => {
|
|
61
|
+
git('fetch', 'origin', '--prune', '--tags');
|
|
62
|
+
const mainBranch = ['master', 'main'].find((name) => exists(`refs/remotes/origin/${name}`));
|
|
63
|
+
if (!mainBranch) throw new Error('Не найдена production-ветка origin/main или origin/master.');
|
|
64
|
+
const mainSha = git('rev-parse', `refs/remotes/origin/${mainBranch}`).trim();
|
|
65
|
+
const head = git('rev-parse', 'HEAD').trim();
|
|
66
|
+
productionRef = { mainBranch, mainSha, head };
|
|
67
|
+
if (['main', 'master'].includes(branch.value) && branch.value !== mainBranch) {
|
|
68
|
+
throw new Error(`Production-ветка origin — ${mainBranch}. Переключитесь на неё или на hotfix/<TASK>[-slug].`);
|
|
69
|
+
}
|
|
70
|
+
if (mode === 'start') {
|
|
71
|
+
if (!ancestor(mainSha, head)) throw new Error('Сначала обновите hotfix из актуальной production-ветки.');
|
|
72
|
+
} else if (branch.value !== mainBranch || head !== mainSha) {
|
|
73
|
+
throw new Error(`Нужна ветка ${mainBranch}, точно совпадающая с origin/${mainBranch}.`);
|
|
74
|
+
}
|
|
75
|
+
return { mainBranch, mainSha, head };
|
|
76
|
+
}, (value) => mode === 'start'
|
|
77
|
+
? `текущая ветка содержит origin/${value.mainBranch}`
|
|
78
|
+
: `текущий commit совпадает с origin/${value.mainBranch}`,
|
|
79
|
+
root.ok && branch.ok ? null : 'не пройдена проверка корня репозитория или ветки');
|
|
80
|
+
const stable = checks.check('stable-version', () => {
|
|
81
|
+
const { mainSha } = productionRef;
|
|
82
|
+
// Query origin explicitly: a local-only tag must never change the baseline.
|
|
83
|
+
const remoteTags = new Map(git('ls-remote', '--refs', '--tags', 'origin', ...STABLE_TAG_PATTERNS.map((tag) => `refs/tags/${tag}`))
|
|
84
|
+
.trim().split('\n').filter(Boolean).map((line) => {
|
|
85
|
+
const [sha, ref] = line.split(/\s+/);
|
|
86
|
+
return [ref.slice('refs/tags/'.length), sha];
|
|
87
|
+
}));
|
|
88
|
+
const versions = git('tag', '--merged', mainSha, '--list', ...STABLE_TAG_PATTERNS).trim().split('\n')
|
|
89
|
+
.filter((tag) => parseStableTag(tag) && remoteTags.has(tag))
|
|
90
|
+
.map((tag) => ({ tag, ...parseStableTag(tag) })).sort((a, b) => compareVersions(a.version, b.version));
|
|
91
|
+
const latest = versions.at(-1);
|
|
92
|
+
const stable = latest?.version;
|
|
93
|
+
if (!stable) throw new Error('Для хотфикса необходим опубликованный stable-тег production.');
|
|
94
|
+
const stableTag = latest.tag;
|
|
95
|
+
const stableSha = git('rev-parse', `refs/tags/${stableTag}^{commit}`).trim();
|
|
96
|
+
for (const tag of stableTagNames(stable).filter((name) => remoteTags.has(name))) {
|
|
97
|
+
if (git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== stableSha) {
|
|
98
|
+
throw new Error(`Конфликт stable-тегов версии ${stable}: разные commits.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const productionText = readAt(mainSha);
|
|
102
|
+
return { remoteTags, stable, stableTag, productionText, target: upVersion(stable, 'patch') };
|
|
103
|
+
}, (value) => `последний опубликованный stable-тег ${value.stableTag} достижим из production`,
|
|
104
|
+
productionRef ? null : 'не удалось определить production commit');
|
|
105
|
+
return { repositoryRootOk: root.ok, ...productionRef, ...history.value, ...stable.value };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assertTagAvailable(ctx, allowMatchingLocal = false) {
|
|
109
|
+
for (const tag of stableTagNames(ctx.target)) {
|
|
110
|
+
if (ctx.remoteTags.has(tag)) throw new Error(`Тег ${tag} уже опубликован; эту версию нельзя выпустить повторно.`);
|
|
111
|
+
if (exists(`refs/tags/${tag}`) &&
|
|
112
|
+
(!allowMatchingLocal || tag !== `hotfix/${ctx.target}` || git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== ctx.head)) {
|
|
113
|
+
throw new Error(`Локальный тег ${tag} уже существует. Проверьте его перед продолжением.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertOriginUnchanged(ctx) {
|
|
119
|
+
const line = git('ls-remote', '--heads', 'origin', `refs/heads/${ctx.mainBranch}`).trim();
|
|
120
|
+
if (line.split(/\s+/)[0] !== ctx.mainSha) {
|
|
121
|
+
throw new Error('Production изменилась во время проверки. Обновите ветку и повторите команду.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function currentFragments() {
|
|
126
|
+
if (!preflight.findChangelogFragmentFiles().length) return [];
|
|
127
|
+
const result = preflight.requireChangelogFragments();
|
|
128
|
+
if (!result.ok) throw new Error(result.reason);
|
|
129
|
+
return result.data.changelogFragments.map((fragment) => {
|
|
130
|
+
if (fragment.bump !== 'patch') throw new Error(`Хотфикс не допускает minor/major: ${fragment.filePath}.`);
|
|
131
|
+
return { ...fragment, content: fs.readFileSync(fragment.filePath, 'utf8') };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function run(action) {
|
|
136
|
+
try {
|
|
137
|
+
return action() !== false;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logError('❌', error.message);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { preflightReport, context, assertTagAvailable, assertOriginUnchanged, currentFragments, run };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { logSuccess } = require('../common/utils');
|
|
2
|
+
const { isDryRun, reportDryRun } = require('../cli/command-executor');
|
|
3
|
+
const { parseChangelog } = require('../changelog/hotfix');
|
|
4
|
+
const { git, exists, ancestor } = require('./git');
|
|
5
|
+
const { preflightReport, context, assertOriginUnchanged, run } = require('./checks');
|
|
6
|
+
function hotfixClose() {
|
|
7
|
+
return run(() => {
|
|
8
|
+
const checks = preflightReport('hotfix close');
|
|
9
|
+
const ctx = context('close', checks);
|
|
10
|
+
const tag = `hotfix/${ctx.stable}`;
|
|
11
|
+
checks.check('stable-tag-at-head', () => {
|
|
12
|
+
const top = parseChangelog(ctx.productionText).releases[0]?.version;
|
|
13
|
+
if (top !== ctx.stable || !ctx.remoteTags.has(tag) || git('rev-parse', `refs/tags/${tag}^{commit}`).trim() !== ctx.head) {
|
|
14
|
+
throw new Error('Сначала опубликуйте hotfix/X.Y.Z на текущем production commit.');
|
|
15
|
+
}
|
|
16
|
+
}, `опубликованный тег ${tag} указывает на текущий commit`,
|
|
17
|
+
ctx.stable && ctx.productionText && ctx.remoteTags && ctx.head ? null : 'не определены stable-тег или production commit');
|
|
18
|
+
const integration = checks.check('integration-history', () => {
|
|
19
|
+
const dev = ['develop', 'dev'].find((name) => exists(`refs/remotes/origin/${name}`));
|
|
20
|
+
if (!dev) throw new Error('Не найдена integration-ветка origin/dev или origin/develop.');
|
|
21
|
+
const devRef = `refs/remotes/origin/${dev}`;
|
|
22
|
+
if (exists(`refs/heads/${dev}`) && !ancestor(`refs/heads/${dev}`, devRef)) {
|
|
23
|
+
throw new Error(`В ${dev} есть локальные непубликуемые commits. Сначала согласуйте их с origin/${dev}.`);
|
|
24
|
+
}
|
|
25
|
+
return { dev, devRef };
|
|
26
|
+
}, (value) => `в локальной ветке ${value.dev} нет неопубликованных коммитов`,
|
|
27
|
+
ctx.mainBranch ? null : 'не проверена история production');
|
|
28
|
+
checks.check('origin-unchanged', () => assertOriginUnchanged(ctx),
|
|
29
|
+
'production-ветка на origin не изменилась во время проверки',
|
|
30
|
+
ctx.mainBranch && ctx.mainSha ? null : 'не проверена история production');
|
|
31
|
+
if (!checks.ok) return false;
|
|
32
|
+
if (isDryRun()) {
|
|
33
|
+
reportDryRun('hotfix close');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const { dev, devRef } = integration.value;
|
|
37
|
+
if (exists(`refs/heads/${dev}`)) git('switch', dev);
|
|
38
|
+
else git('switch', '--track', '-c', dev, devRef);
|
|
39
|
+
git('merge', '--ff-only', devRef);
|
|
40
|
+
git('merge', '--no-edit', ctx.head);
|
|
41
|
+
git('push', 'origin', `refs/heads/${dev}:refs/heads/${dev}`);
|
|
42
|
+
logSuccess('✅', `Хотфикс ${ctx.stable} сведён в ${dev}; production и файлы версий не изменены.`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { hotfixClose };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { logSuccess } = require('../common/utils');
|
|
2
|
+
const { isDryRun, reportDryRun } = require('../cli/command-executor');
|
|
3
|
+
const { pendingDocument } = require('../changelog/hotfix');
|
|
4
|
+
const { git, exists } = require('./git');
|
|
5
|
+
const { preflightReport, context, assertTagAvailable, assertOriginUnchanged, currentFragments, run } = require('./checks');
|
|
6
|
+
function hotfixDeploy() {
|
|
7
|
+
return run(() => {
|
|
8
|
+
const checks = preflightReport('hotfix deploy');
|
|
9
|
+
const ctx = context('deploy', checks);
|
|
10
|
+
checks.check('prepared-changelog', () => {
|
|
11
|
+
const prepared = pendingDocument(ctx.productionText, ctx.target);
|
|
12
|
+
if (!prepared.pending) throw new Error(`В CHANGELOG.md нужен верхний подготовленный раздел ${ctx.target}.`);
|
|
13
|
+
}, `верхний раздел CHANGELOG.md содержит хотфикс ${ctx.target}`,
|
|
14
|
+
ctx.productionText && ctx.target ? null : 'не определены production changelog или версия хотфикса');
|
|
15
|
+
checks.check('changelog-fragments', () => {
|
|
16
|
+
if (currentFragments().length) throw new Error('В production остались несобранные hotfix fragments.');
|
|
17
|
+
}, 'несобранных hotfix fragments нет',
|
|
18
|
+
ctx.repositoryRootOk ? null : 'не пройдена проверка корня репозитория');
|
|
19
|
+
checks.check('tag-missing', () => assertTagAvailable(ctx, true),
|
|
20
|
+
`тег hotfix/${ctx.target} доступен для публикации`,
|
|
21
|
+
ctx.target ? null : 'не определена целевая версия хотфикса');
|
|
22
|
+
checks.check('origin-unchanged', () => assertOriginUnchanged(ctx),
|
|
23
|
+
'production-ветка на origin не изменилась во время проверки',
|
|
24
|
+
ctx.mainBranch && ctx.mainSha ? null : 'не проверена история production');
|
|
25
|
+
if (!checks.ok) return false;
|
|
26
|
+
if (isDryRun()) {
|
|
27
|
+
reportDryRun('hotfix deploy');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const tag = `hotfix/${ctx.target}`;
|
|
31
|
+
if (!exists(`refs/tags/${tag}`)) git('tag', tag, ctx.head);
|
|
32
|
+
git('push', 'origin', `refs/tags/${tag}:refs/tags/${tag}`);
|
|
33
|
+
logSuccess('✅', `Опубликован ${tag} на ${ctx.head}. Дождитесь успешного stable pipeline перед hotfix close.`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { hotfixDeploy };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const { CHANGELOG_FILE } = require('../changelog/config');
|
|
3
|
+
function git(...args) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
6
|
+
} catch {
|
|
7
|
+
throw new Error(`Git не выполнил «${args[0]}». Проверьте доступ к origin, состояние веток и конфликты.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function exists(ref) {
|
|
12
|
+
try {
|
|
13
|
+
execFileSync('git', ['show-ref', '--verify', '--quiet', ref], { stdio: 'ignore' });
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.status === 1) return false;
|
|
17
|
+
throw new Error(`Не удалось проверить ref ${ref}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ancestor(base, head) {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync('git', ['merge-base', '--is-ancestor', base, head], { stdio: 'ignore' });
|
|
24
|
+
return true;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.status === 1) return false;
|
|
27
|
+
throw new Error('Не удалось проверить историю Git.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readAt(ref, file = CHANGELOG_FILE) {
|
|
32
|
+
return git('show', `${ref}:${file}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { git, exists, ancestor, readAt };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const preflight = require('../preflight');
|
|
3
|
+
const { execCommand, logSuccess } = require('../common/utils');
|
|
4
|
+
const { isDryRun, reportDryRun } = require('../cli/command-executor');
|
|
5
|
+
const { CHANGELOG_FILE } = require('../changelog/config');
|
|
6
|
+
const { prepareChangelog } = require('../changelog/hotfix');
|
|
7
|
+
const { preflightReport, context, assertTagAvailable, assertOriginUnchanged, currentFragments, run } = require('./checks');
|
|
8
|
+
function formatChangelog() {
|
|
9
|
+
const runner = preflight.getPrettierRunner();
|
|
10
|
+
if (!runner || !execCommand(`${runner} --write CHANGELOG.md`) || !execCommand(`${runner} --check CHANGELOG.md`)) {
|
|
11
|
+
throw new Error('Не удалось отформатировать CHANGELOG.md; hotfix fragments сохранены.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hotfixStart() {
|
|
16
|
+
return run(() => {
|
|
17
|
+
const checks = preflightReport('hotfix start');
|
|
18
|
+
const ctx = context('start', checks);
|
|
19
|
+
checks.check('tag-missing', () => assertTagAvailable(ctx),
|
|
20
|
+
`теги версии ${ctx.target} отсутствуют локально и на origin`,
|
|
21
|
+
ctx.target ? null : 'не определена целевая версия хотфикса');
|
|
22
|
+
const fragmentsResult = checks.check('changelog-fragments', currentFragments,
|
|
23
|
+
(value) => `проверены ${value.length} patch fragments`,
|
|
24
|
+
ctx.repositoryRootOk ? null : 'не пройдена проверка корня репозитория');
|
|
25
|
+
let original;
|
|
26
|
+
const changelog = checks.check('changelog-state', () => {
|
|
27
|
+
original = fs.readFileSync(CHANGELOG_FILE, 'utf8');
|
|
28
|
+
return prepareChangelog({ text: original, ...ctx, fragments: fragmentsResult.value });
|
|
29
|
+
}, `CHANGELOG.md допускает подготовку хотфикса ${ctx.target}`,
|
|
30
|
+
ctx.target && fragmentsResult.ok ? null : 'не определена версия хотфикса или не проверены fragments');
|
|
31
|
+
checks.check('origin-unchanged', () => assertOriginUnchanged(ctx),
|
|
32
|
+
'production-ветка на origin не изменилась во время проверки',
|
|
33
|
+
ctx.mainBranch && ctx.mainSha ? null : 'не проверена история production');
|
|
34
|
+
if (!checks.ok) return false;
|
|
35
|
+
const updated = changelog.value;
|
|
36
|
+
if (isDryRun()) {
|
|
37
|
+
reportDryRun('hotfix start');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (updated !== original || fragmentsResult.value.length) {
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(CHANGELOG_FILE, updated);
|
|
43
|
+
formatChangelog();
|
|
44
|
+
// Validate the formatted current file and keep all merged hotfix notes.
|
|
45
|
+
prepareChangelog({ text: fs.readFileSync(CHANGELOG_FILE, 'utf8'), ...ctx, fragments: [] });
|
|
46
|
+
for (const fragment of fragmentsResult.value) fs.unlinkSync(fragment.filePath);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fs.writeFileSync(CHANGELOG_FILE, original);
|
|
49
|
+
for (const fragment of fragmentsResult.value) fs.writeFileSync(fragment.filePath, fragment.content);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
logSuccess('✅', `Хотфикс ${ctx.target} подготовлен локально. Проверьте diff и включите CHANGELOG.md и удаления fragments в MR в ${ctx.mainBranch}.`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { hotfixStart };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
function parseGitlabUrl(raw) {
|
|
3
|
+
const url = new URL(String(raw || '').trim());
|
|
4
|
+
const pathname = url.pathname.replace(/^\//, '').replace(/\.git$/, '').replace(/\/$/, '');
|
|
5
|
+
if (!url.origin || !pathname) {
|
|
6
|
+
throw new Error('invalid gitlab url');
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
origin: url.origin,
|
|
10
|
+
path: pathname,
|
|
11
|
+
encodedPath: encodeURIComponent(pathname),
|
|
12
|
+
url: String(raw).trim()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { parseGitlabUrl };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSilent, execCommand } = require('../common/utils');
|
|
4
|
+
const { CHANGELOG_FILE, CHANGELOG_DIR, FRAGMENT_TYPES, getFragmentType } = require('../changelog/config');
|
|
5
|
+
const { ok, fail, STABLE_SEMVER_PATTERN, toPosixPath } = require('./result');
|
|
6
|
+
function getPrettierRunner() {
|
|
7
|
+
const npxVersion = execSilent('npx --yes prettier --version');
|
|
8
|
+
if (npxVersion) return 'npx --yes prettier';
|
|
9
|
+
const directVersion = execSilent('prettier --version');
|
|
10
|
+
if (directVersion) return 'prettier';
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requireFileExists(filePath) {
|
|
15
|
+
if (!fs.existsSync(filePath)) {
|
|
16
|
+
return fail(`Обязательный файл "${filePath}" не существует.`);
|
|
17
|
+
}
|
|
18
|
+
return ok();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CHANGELOG_HEADING_PATTERN = /^##\s+(?:\S+\s+)?\[(\d+\.\d+\.\d+)\]/gm;
|
|
22
|
+
|
|
23
|
+
function getChangelogReleaseVersions(changelogPath = CHANGELOG_FILE) {
|
|
24
|
+
let changelog;
|
|
25
|
+
try {
|
|
26
|
+
changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [...changelog.matchAll(CHANGELOG_HEADING_PATTERN)].map((match) => match[1]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getChangelogReleaseVersion(changelogPath = CHANGELOG_FILE) {
|
|
35
|
+
return getChangelogReleaseVersions(changelogPath)[0] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireChangelogReleaseVersion() {
|
|
39
|
+
const version = getChangelogReleaseVersion();
|
|
40
|
+
if (!version) {
|
|
41
|
+
return fail(`Не удалось прочитать версию релиза из ${CHANGELOG_FILE}. Ожидается заголовок "## 🚀 [X.Y.Z]".`);
|
|
42
|
+
}
|
|
43
|
+
if (!STABLE_SEMVER_PATTERN.test(version)) {
|
|
44
|
+
return fail(`Версия "${version}" из ${CHANGELOG_FILE} должна быть стабильным SemVer X.Y.Z.`);
|
|
45
|
+
}
|
|
46
|
+
return ok({ version });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requirePrettierAvailable() {
|
|
50
|
+
const runner = getPrettierRunner();
|
|
51
|
+
if (!runner) {
|
|
52
|
+
return fail('Prettier недоступен.');
|
|
53
|
+
}
|
|
54
|
+
return ok({ prettierRunner: runner });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requireChangelogFormatted() {
|
|
58
|
+
const runner = getPrettierRunner();
|
|
59
|
+
if (!runner) {
|
|
60
|
+
return fail('Prettier недоступен.');
|
|
61
|
+
}
|
|
62
|
+
if (!execCommand(`${runner} --check CHANGELOG.md`)) {
|
|
63
|
+
return fail('Файл CHANGELOG.md не прошел проверку Prettier.');
|
|
64
|
+
}
|
|
65
|
+
return ok({ prettierRunner: runner });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findChangelogFragmentFiles(baseDir = CHANGELOG_DIR) {
|
|
69
|
+
if (!fs.existsSync(baseDir)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return fs.readdirSync(baseDir, { withFileTypes: true })
|
|
75
|
+
.filter((entry) => entry.isFile() && !entry.name.startsWith('.'))
|
|
76
|
+
.map((entry) => toPosixPath(path.join(baseDir, entry.name)))
|
|
77
|
+
.sort();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requireChangelogFragments() {
|
|
84
|
+
if (!fs.existsSync(CHANGELOG_DIR)) {
|
|
85
|
+
return fail(`Директория changelog fragments "${CHANGELOG_DIR}" не существует.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fragmentFiles = findChangelogFragmentFiles();
|
|
89
|
+
if (fragmentFiles.length === 0) {
|
|
90
|
+
return fail(`В директории "${CHANGELOG_DIR}" нет changelog fragments.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const changelogFragments = [];
|
|
94
|
+
for (const filePath of fragmentFiles) {
|
|
95
|
+
const type = getFragmentType(filePath);
|
|
96
|
+
if (!type) {
|
|
97
|
+
return fail(`Неверное имя changelog fragment "${filePath}". Ожидается "<name>.<type>.md".`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let content;
|
|
101
|
+
try {
|
|
102
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return fail(`Не удалось прочитать changelog fragment "${filePath}".`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entries = content
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => line.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
if (entries.length === 0) {
|
|
112
|
+
return fail(`Changelog fragment "${filePath}" пуст.`);
|
|
113
|
+
}
|
|
114
|
+
if (entries.some((line) => !line.startsWith('- '))) {
|
|
115
|
+
return fail(`Каждая непустая строка "${filePath}" должна начинаться с "- ".`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
changelogFragments.push({
|
|
119
|
+
filePath,
|
|
120
|
+
type,
|
|
121
|
+
section: FRAGMENT_TYPES[type].section,
|
|
122
|
+
bump: FRAGMENT_TYPES[type].bump,
|
|
123
|
+
entries
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ok({ changelogFragments });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { getPrettierRunner, requireFileExists, getChangelogReleaseVersions, getChangelogReleaseVersion, requireChangelogReleaseVersion, requirePrettierAvailable, requireChangelogFormatted, findChangelogFragmentFiles, requireChangelogFragments };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ok, fail, SEMVER_PATTERN, toPosixPath } = require('./result');
|
|
4
|
+
function requireChartChangelogVersion(chartDir) {
|
|
5
|
+
const changelogPath = toPosixPath(path.join(String(chartDir || ''), 'CHANGELOG.md'));
|
|
6
|
+
if (!fs.existsSync(changelogPath)) {
|
|
7
|
+
return fail(`Файл "${changelogPath}" не существует. Заполните changelog чарта перед созданием тега.`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let changelog;
|
|
11
|
+
try {
|
|
12
|
+
changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return fail(`Не удалось прочитать "${changelogPath}".`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const firstHeading = changelog.match(/^##(?!#)[^\r\n]*$/m)?.[0];
|
|
18
|
+
const version = firstHeading?.match(/^##\s+(?:\S+\s+)?\[([^\]]+)\](?:\s|$)/)?.[1];
|
|
19
|
+
if (!version || !SEMVER_PATTERN.test(version)) {
|
|
20
|
+
return fail(`Верхний раздел "${changelogPath}" должен содержать версию SemVer в заголовке "## [X.Y.Z]".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ok({ chartChangelogPath: changelogPath, version });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireSingleChart() {
|
|
27
|
+
const chartsDir = 'charts';
|
|
28
|
+
if (!fs.existsSync(chartsDir)) {
|
|
29
|
+
return fail('Не удалось найти директорию charts.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entries = fs.readdirSync(chartsDir, { withFileTypes: true });
|
|
33
|
+
const chartFiles = entries
|
|
34
|
+
.filter((entry) => entry.isDirectory())
|
|
35
|
+
.map((entry) => `${chartsDir}/${entry.name}/Chart.yaml`)
|
|
36
|
+
.filter((chartFilePath) => fs.existsSync(chartFilePath));
|
|
37
|
+
|
|
38
|
+
if (chartFiles.length === 0) {
|
|
39
|
+
return fail('Не удалось найти Chart.yaml по пути charts/<chart-name>/Chart.yaml.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (chartFiles.length > 1) {
|
|
43
|
+
return fail(`Найдено несколько chart-файлов: ${chartFiles.join(', ')}.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chartFilePath = chartFiles[0];
|
|
47
|
+
const chartYaml = fs.readFileSync(chartFilePath, 'utf8');
|
|
48
|
+
const nameMatch = chartYaml.match(/^\s*name:\s*([^\s#]+)\s*$/m);
|
|
49
|
+
const chartName = nameMatch ? nameMatch[1] : null;
|
|
50
|
+
if (!chartName) {
|
|
51
|
+
return fail(`Не удалось прочитать имя чарта из ${chartFilePath}.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ok({ chartFilePath, chartName });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { requireChartChangelogVersion, requireSingleChart };
|