spectrum-cli 3.2.0 → 5.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 (61) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +61 -30
  3. package/index.js +6 -174
  4. package/package.json +4 -4
  5. package/src/changelog/commands.js +44 -0
  6. package/src/{changelog-config.js → changelog/config.js} +1 -1
  7. package/src/changelog/format.js +51 -0
  8. package/src/changelog/fragments.js +223 -0
  9. package/src/{hotfix-changelog.js → changelog/hotfix.js} +2 -2
  10. package/src/changelog/index.js +7 -0
  11. package/src/{open-release.js → changelog/open-release.js} +6 -6
  12. package/src/changelog/release-block.js +80 -0
  13. package/src/chart/deploy.js +216 -0
  14. package/src/chart/helmrelease.js +90 -0
  15. package/src/chart/index.js +30 -0
  16. package/src/chart/metadata.js +114 -0
  17. package/src/chart/registry.js +139 -0
  18. package/src/chart/routes.js +244 -0
  19. package/src/chart/start.js +98 -0
  20. package/src/chart/verify.js +166 -0
  21. package/src/cli/command-executor.js +181 -0
  22. package/src/cli/commands.js +136 -0
  23. package/src/{development.js → cli/development.js} +1 -1
  24. package/src/cli/help.js +55 -0
  25. package/src/cli/run-action.js +28 -0
  26. package/src/{update-check.js → cli/update-check.js} +8 -2
  27. package/src/{git.js → git/index.js} +4 -3
  28. package/src/hotfix/checks.js +144 -0
  29. package/src/hotfix/close.js +46 -0
  30. package/src/hotfix/deploy.js +37 -0
  31. package/src/hotfix/git.js +35 -0
  32. package/src/hotfix/index.js +5 -0
  33. package/src/hotfix/start.js +57 -0
  34. package/src/integrations/gitlab-url.js +16 -0
  35. package/src/preflight/changelog.js +130 -0
  36. package/src/preflight/chart.js +57 -0
  37. package/src/preflight/git.js +133 -0
  38. package/src/preflight/helmrelease.js +201 -0
  39. package/src/preflight/index.js +10 -0
  40. package/src/preflight/result.js +18 -0
  41. package/src/preflight/source.js +95 -0
  42. package/src/preflight/tags.js +139 -0
  43. package/src/release/close.js +39 -0
  44. package/src/release/index.js +7 -0
  45. package/src/{stable-tags.js → release/stable-tags.js} +1 -1
  46. package/src/release/start.js +51 -0
  47. package/src/release/steps.js +83 -0
  48. package/src/release/version.js +20 -0
  49. package/src/token/config.js +163 -0
  50. package/src/token/gitlab-api.js +59 -0
  51. package/src/token/index.js +6 -0
  52. package/src/token/rotate.js +157 -0
  53. package/src/changelog.js +0 -418
  54. package/src/chart.js +0 -1071
  55. package/src/command-executor.js +0 -70
  56. package/src/hotfix.js +0 -208
  57. package/src/preflight.js +0 -796
  58. package/src/release.js +0 -207
  59. package/src/token.js +0 -399
  60. /package/src/{utils.js → common/utils.js} +0 -0
  61. /package/src/{version.js → common/version.js} +0 -0
@@ -0,0 +1,114 @@
1
+ const fs = require('fs');
2
+ const { execSync } = require('child_process');
3
+ const { SEMVER_PATTERN } = require('../preflight');
4
+ const CHARTS_DIR = 'charts';
5
+
6
+ function getChartFiles() {
7
+ try {
8
+ if (!fs.existsSync(CHARTS_DIR)) {
9
+ return [];
10
+ }
11
+
12
+ const entries = fs.readdirSync(CHARTS_DIR, { withFileTypes: true });
13
+ const chartFiles = entries
14
+ .filter((entry) => entry.isDirectory())
15
+ .map((entry) => `${CHARTS_DIR}/${entry.name}/Chart.yaml`)
16
+ .filter((chartFilePath) => fs.existsSync(chartFilePath));
17
+
18
+ return chartFiles;
19
+ } catch (error) {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ function getChartName(chartFilePath) {
25
+ try {
26
+ if (!chartFilePath || !fs.existsSync(chartFilePath)) {
27
+ return null;
28
+ }
29
+
30
+ const chartYaml = fs.readFileSync(chartFilePath, 'utf8');
31
+ const nameMatch = chartYaml.match(/^\s*name:\s*([^\s#]+)\s*$/m);
32
+ return nameMatch ? nameMatch[1] : null;
33
+ } catch (error) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function isSemver(version) {
39
+ return SEMVER_PATTERN.test(version);
40
+ }
41
+
42
+ function compareSemver(a, b) {
43
+ const parse = (value) => {
44
+ const [rawMain, rawMeta] = String(value || '').split('+', 2);
45
+ const [rawVersion, rawPre] = rawMain.split('-', 2);
46
+ const versionParts = rawVersion.split('.').map((part) => Number(part));
47
+ const preParts = rawPre ? rawPre.split('.') : [];
48
+ return { versionParts, preParts, hasPre: Boolean(rawPre), meta: rawMeta || '' };
49
+ };
50
+ const pa = parse(a);
51
+ const pb = parse(b);
52
+
53
+ for (let i = 0; i < 3; i += 1) {
54
+ const diff = (pa.versionParts[i] || 0) - (pb.versionParts[i] || 0);
55
+ if (diff !== 0) return diff;
56
+ }
57
+
58
+ if (pa.hasPre && !pb.hasPre) return -1;
59
+ if (!pa.hasPre && pb.hasPre) return 1;
60
+ if (!pa.hasPre && !pb.hasPre) return 0;
61
+
62
+ const len = Math.max(pa.preParts.length, pb.preParts.length);
63
+ for (let i = 0; i < len; i += 1) {
64
+ const aa = pa.preParts[i];
65
+ const bb = pb.preParts[i];
66
+ if (aa === undefined) return -1;
67
+ if (bb === undefined) return 1;
68
+ const aNum = /^\d+$/.test(aa);
69
+ const bNum = /^\d+$/.test(bb);
70
+ if (aNum && bNum) {
71
+ const diff = Number(aa) - Number(bb);
72
+ if (diff !== 0) return diff;
73
+ continue;
74
+ }
75
+ if (aNum && !bNum) return -1;
76
+ if (!aNum && bNum) return 1;
77
+ if (aa < bb) return -1;
78
+ if (aa > bb) return 1;
79
+ }
80
+
81
+ return 0;
82
+ }
83
+
84
+ function getLatestRemoteChartVersion(chartName) {
85
+ const safeChartName = String(chartName || '').trim();
86
+ if (!safeChartName) return null;
87
+
88
+ const prefix = `chart-${safeChartName}-`;
89
+ let output = '';
90
+ try {
91
+ output = execSync(`git ls-remote --tags origin "refs/tags/${prefix}*"`, { stdio: 'pipe', encoding: 'utf8' });
92
+ } catch (error) {
93
+ return null;
94
+ }
95
+
96
+ const versions = String(output || '')
97
+ .split('\n')
98
+ .map((line) => line.trim())
99
+ .filter(Boolean)
100
+ .map((line) => line.split(/\s+/)[1] || '')
101
+ .map((ref) => ref.replace('refs/tags/', ''))
102
+ .filter((tag) => tag.startsWith(prefix))
103
+ .map((tag) => tag.slice(prefix.length))
104
+ .filter((version) => isSemver(version));
105
+
106
+ if (versions.length === 0) {
107
+ return null;
108
+ }
109
+
110
+ versions.sort(compareSemver);
111
+ return versions[versions.length - 1];
112
+ }
113
+
114
+ module.exports = { getChartFiles, getChartName, isSemver, compareSemver, getLatestRemoteChartVersion };
@@ -0,0 +1,139 @@
1
+ const { logSuccess, logError, getRemoteUrl } = require('../common/utils');
2
+ const { parseGitlabUrl } = require('../integrations/gitlab-url');
3
+ const HELM_REGISTRY_CHANNEL = 'stable';
4
+ const REGISTRY_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
5
+ const REGISTRY_WAIT_INTERVAL_MS = 15 * 1000;
6
+
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function getGitlabProject() {
12
+ const baseUrl = getRemoteUrl();
13
+ if (!baseUrl) {
14
+ return null;
15
+ }
16
+ try {
17
+ return parseGitlabUrl(baseUrl);
18
+ } catch (error) {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function helmIndexHasChartVersion(indexYamlContent, chartName, version) {
24
+ const escapedChartName = String(chartName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
+ const chartHeadingPattern = new RegExp(`^${escapedChartName}:\\s*(#.*)?$`);
26
+ const lines = String(indexYamlContent || '').split('\n');
27
+ let inEntries = false;
28
+ let inChart = false;
29
+ let entriesIndent = 0;
30
+ let chartIndent = 0;
31
+
32
+ for (const rawLine of lines) {
33
+ const line = rawLine.replace(/\r$/, '');
34
+ const trimmed = line.trim();
35
+ if (!trimmed || trimmed.startsWith('#')) {
36
+ continue;
37
+ }
38
+
39
+ const indent = line.match(/^\s*/)[0].length;
40
+
41
+ if (/^entries:\s*(#.*)?$/.test(trimmed) && indent === 0) {
42
+ inEntries = true;
43
+ inChart = false;
44
+ entriesIndent = indent;
45
+ continue;
46
+ }
47
+
48
+ if (inEntries && indent <= entriesIndent) {
49
+ inEntries = false;
50
+ inChart = false;
51
+ }
52
+
53
+ if (!inEntries) {
54
+ continue;
55
+ }
56
+
57
+ if (chartHeadingPattern.test(trimmed)) {
58
+ inChart = true;
59
+ chartIndent = indent;
60
+ continue;
61
+ }
62
+
63
+ if (inChart && indent <= chartIndent) {
64
+ inChart = false;
65
+ }
66
+
67
+ if (!inChart) {
68
+ continue;
69
+ }
70
+
71
+ const versionMatch = trimmed.match(/^-?\s*version:\s*["']?([^"'\s#]+)["']?\s*(#.*)?$/);
72
+ if (versionMatch && versionMatch[1] === String(version)) {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ async function fetchChartVersionFromRegistry(chartName, version) {
81
+ const project = getGitlabProject();
82
+ if (!project) {
83
+ return { ok: false, reason: 'Не удалось определить GitLab-проект из remote "origin".' };
84
+ }
85
+
86
+ const accessToken = process.env.GITLAB_PRIVATE_TOKEN;
87
+ if (!accessToken) {
88
+ return {
89
+ ok: false,
90
+ reason: 'Не задана переменная окружения GITLAB_PRIVATE_TOKEN. Она нужна для проверки публикации чарта в Helm-registry.'
91
+ };
92
+ }
93
+
94
+ const indexUrl = `${project.origin}/api/v4/projects/${project.encodedPath}/packages/helm/${HELM_REGISTRY_CHANNEL}/index.yaml`;
95
+ let response;
96
+ try {
97
+ response = await fetch(indexUrl, { headers: { 'PRIVATE-TOKEN': accessToken } });
98
+ } catch (error) {
99
+ return { ok: false, reason: `Не удалось запросить Helm-registry (${indexUrl}): ${error.message}.` };
100
+ }
101
+
102
+ if (!response.ok) {
103
+ return { ok: false, reason: `Helm-registry вернул HTTP ${response.status} (${indexUrl}).` };
104
+ }
105
+
106
+ const indexYaml = await response.text();
107
+ return {
108
+ ok: true,
109
+ found: helmIndexHasChartVersion(indexYaml, chartName, version),
110
+ indexUrl
111
+ };
112
+ }
113
+
114
+ async function waitForChartInRegistry(chartName, version, options = {}) {
115
+ const timeoutMs = options.timeoutMs === undefined ? REGISTRY_WAIT_TIMEOUT_MS : options.timeoutMs;
116
+ const intervalMs = options.intervalMs === undefined ? REGISTRY_WAIT_INTERVAL_MS : options.intervalMs;
117
+ const sleepFn = options.sleep || sleep;
118
+ const deadline = Date.now() + timeoutMs;
119
+
120
+ while (true) {
121
+ const result = await fetchChartVersionFromRegistry(chartName, version);
122
+ if (!result.ok) {
123
+ logError('❌', '%s', result.reason);
124
+ return false;
125
+ }
126
+ if (result.found) {
127
+ logSuccess('📦', 'Версия %s чарта %s опубликована в Helm-registry.', version, chartName);
128
+ return true;
129
+ }
130
+ if (Date.now() >= deadline) {
131
+ logError('❌', 'Версия %s чарта %s не появилась в Helm-registry за отведенное время.', version, chartName);
132
+ return false;
133
+ }
134
+ logSuccess('⏳', 'Версия %s чарта %s еще не опубликована, продолжаю ожидание...', version, chartName);
135
+ await sleepFn(intervalMs);
136
+ }
137
+ }
138
+
139
+ module.exports = { helmIndexHasChartVersion, fetchChartVersionFromRegistry, waitForChartInRegistry };
@@ -0,0 +1,244 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ function normalizeList(list) {
4
+ const result = [];
5
+ const seen = new Set();
6
+ for (const item of (Array.isArray(list) ? list : [])) {
7
+ const value = String(item || '').trim();
8
+ if (!value || seen.has(value)) {
9
+ continue;
10
+ }
11
+ seen.add(value);
12
+ result.push(value);
13
+ }
14
+ return result;
15
+ }
16
+
17
+ function ensureEndsWithDollar(value) {
18
+ const text = String(value || '').trim();
19
+ if (!text) {
20
+ return '';
21
+ }
22
+ return text.endsWith('$') ? text : `${text}$`;
23
+ }
24
+
25
+ function normalizeToBeList(list) {
26
+ return normalizeList(list.map(ensureEndsWithDollar));
27
+ }
28
+
29
+ function segmentToPattern(segment) {
30
+ if (segment === 'index') {
31
+ return '';
32
+ }
33
+ if (/^\[\[\.\.\..+\]\]$/.test(segment)) {
34
+ return '.*';
35
+ }
36
+ if (/^\[\.\.\..+\]$/.test(segment)) {
37
+ return '.+';
38
+ }
39
+ if (/^\[.+\]$/.test(segment)) {
40
+ return '[^/]+';
41
+ }
42
+ return segment;
43
+ }
44
+
45
+ function toRouteFromFileSegments(segments) {
46
+ const transformed = segments.map(segmentToPattern).filter(Boolean);
47
+ const route = `/${transformed.join('/')}`.replace(/\/+/g, '/');
48
+ return route === '' ? '/' : route;
49
+ }
50
+
51
+ function parseApiRouteFromAppFile(filePath) {
52
+ const rel = filePath.replace(/\\/g, '/').replace(/^app\/api\//, '').replace(/(^|\/)route\.[^.]+$/, '');
53
+ if (!rel) {
54
+ return '/api';
55
+ }
56
+ return `/api/${toRouteFromFileSegments(rel.split('/')).replace(/^\//, '')}`.replace(/\/+$/, '');
57
+ }
58
+
59
+ function parseApiRouteFromPagesFile(filePath) {
60
+ const rel = filePath.replace(/\\/g, '/').replace(/^pages\/api\//, '').replace(/\.[^.]+$/, '');
61
+ const route = toRouteFromFileSegments(rel.split('/'));
62
+ return `/api${route === '/' ? '' : route}`.replace(/\/+$/, '') || '/api';
63
+ }
64
+
65
+ function parsePageRouteFromPagesFile(filePath) {
66
+ const rel = filePath.replace(/\\/g, '/').replace(/^pages\//, '').replace(/\.[^.]+$/, '');
67
+ const chunks = rel.split('/').filter(Boolean);
68
+ const first = chunks[0] || '';
69
+ const excludedFiles = new Set(['_app', '_document', '_error', '404', '500']);
70
+ if (first === 'api' || excludedFiles.has(first)) {
71
+ return null;
72
+ }
73
+ return toRouteFromFileSegments(chunks);
74
+ }
75
+
76
+ function parsePageRouteFromAppFile(filePath) {
77
+ const rel = filePath.replace(/\\/g, '/').replace(/^app\//, '').replace(/(^|\/)page\.[^.]+$/, '');
78
+ if (rel.startsWith('api/')) {
79
+ return null;
80
+ }
81
+ if (!rel) {
82
+ return '/';
83
+ }
84
+ const chunks = rel
85
+ .split('/')
86
+ .filter(Boolean)
87
+ .filter((part) => !/^\(.+\)$/.test(part) && !part.startsWith('@'));
88
+ return toRouteFromFileSegments(chunks);
89
+ }
90
+
91
+ function collectFilesRecursively(dirPath, matcher) {
92
+ if (!fs.existsSync(dirPath)) {
93
+ return [];
94
+ }
95
+
96
+ const stack = [dirPath];
97
+ const found = [];
98
+ while (stack.length > 0) {
99
+ const current = stack.pop();
100
+ let entries = [];
101
+ try {
102
+ entries = fs.readdirSync(current, { withFileTypes: true });
103
+ } catch (error) {
104
+ return found;
105
+ }
106
+
107
+ for (const entry of entries) {
108
+ const fullPath = path.join(current, entry.name);
109
+ if (entry.isDirectory()) {
110
+ stack.push(fullPath);
111
+ continue;
112
+ }
113
+ if (entry.isFile() && matcher(fullPath)) {
114
+ found.push(fullPath);
115
+ }
116
+ }
117
+ }
118
+ return found;
119
+ }
120
+
121
+ function collectRoutesFromFilesystem(sourcePath) {
122
+ const api = [];
123
+ const pages = [];
124
+ const appDirCandidates = [path.join(sourcePath, 'app'), path.join(sourcePath, 'src', 'app')];
125
+ const pagesDirCandidates = [path.join(sourcePath, 'pages'), path.join(sourcePath, 'src', 'pages')];
126
+
127
+ for (const appDir of appDirCandidates) {
128
+ const appApiFiles = collectFilesRecursively(
129
+ path.join(appDir, 'api'),
130
+ (file) => /[\\/]route\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file)
131
+ );
132
+ for (const absFile of appApiFiles) {
133
+ const rel = path.relative(path.join(appDir, 'api'), absFile).replace(/\\/g, '/');
134
+ api.push(parseApiRouteFromAppFile(`app/api/${rel}`));
135
+ }
136
+
137
+ const appPageFiles = collectFilesRecursively(appDir, (file) => /[\\/]page\.(js|jsx|ts|tsx|mdx)$/.test(file));
138
+ for (const absFile of appPageFiles) {
139
+ const rel = path.relative(appDir, absFile).replace(/\\/g, '/');
140
+ const route = parsePageRouteFromAppFile(`app/${rel}`);
141
+ if (route) {
142
+ pages.push(route);
143
+ }
144
+ }
145
+ }
146
+
147
+ for (const pagesDir of pagesDirCandidates) {
148
+ const pagesApiFiles = collectFilesRecursively(path.join(pagesDir, 'api'), (file) => /\.(js|jsx|ts|tsx|mjs|cjs)$/.test(file));
149
+ for (const absFile of pagesApiFiles) {
150
+ const rel = path.relative(path.join(pagesDir, 'api'), absFile).replace(/\\/g, '/');
151
+ api.push(parseApiRouteFromPagesFile(`pages/api/${rel}`));
152
+ }
153
+
154
+ const pagesFiles = collectFilesRecursively(pagesDir, (file) => /\.(js|jsx|ts|tsx|mdx)$/.test(file));
155
+ for (const absFile of pagesFiles) {
156
+ const rel = path.relative(pagesDir, absFile).replace(/\\/g, '/');
157
+ const route = parsePageRouteFromPagesFile(`pages/${rel}`);
158
+ if (route) {
159
+ pages.push(route);
160
+ }
161
+ }
162
+ }
163
+
164
+ return {
165
+ api: normalizeList(api),
166
+ pages: normalizeList(pages)
167
+ };
168
+ }
169
+
170
+ function readJsonIfExists(filePath) {
171
+ if (!fs.existsSync(filePath)) {
172
+ return null;
173
+ }
174
+ try {
175
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
176
+ } catch (error) {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ function collectRoutesFromBuildArtifacts(sourcePath) {
182
+ const api = [];
183
+ const pages = [];
184
+
185
+ const routesManifest = readJsonIfExists(path.join(sourcePath, '.next', 'routes-manifest.json'));
186
+ if (routesManifest) {
187
+ const combined = []
188
+ .concat(routesManifest.staticRoutes || [])
189
+ .concat(routesManifest.dynamicRoutes || [])
190
+ .concat(routesManifest.dataRoutes || []);
191
+ for (const routeInfo of combined) {
192
+ const route = routeInfo.page || routeInfo.pathname || routeInfo.route;
193
+ if (!route || typeof route !== 'string') {
194
+ continue;
195
+ }
196
+ if (route.startsWith('/api')) {
197
+ api.push(route);
198
+ } else {
199
+ pages.push(route);
200
+ }
201
+ }
202
+ }
203
+
204
+ const appRoutesManifest = readJsonIfExists(path.join(sourcePath, '.next', 'server', 'app-path-routes-manifest.json'))
205
+ || readJsonIfExists(path.join(sourcePath, '.next', 'server', 'app-paths-manifest.json'))
206
+ || readJsonIfExists(path.join(sourcePath, '.next', 'app-path-routes-manifest.json'));
207
+ if (appRoutesManifest && typeof appRoutesManifest === 'object') {
208
+ Object.entries(appRoutesManifest).forEach(([key, routeValue]) => {
209
+ const route = typeof routeValue === 'string' ? routeValue : key;
210
+ if (typeof route !== 'string') {
211
+ return;
212
+ }
213
+ if (route.startsWith('/_')) {
214
+ return;
215
+ }
216
+ if (route.startsWith('/api')) {
217
+ api.push(route);
218
+ } else {
219
+ pages.push(route);
220
+ }
221
+ });
222
+ }
223
+
224
+ const pagesManifest = readJsonIfExists(path.join(sourcePath, '.next', 'server', 'pages-manifest.json'));
225
+ if (pagesManifest && typeof pagesManifest === 'object') {
226
+ Object.keys(pagesManifest).forEach((route) => {
227
+ if (route === '/_app' || route === '/_document' || route === '/_error' || route === '/404' || route === '/500') {
228
+ return;
229
+ }
230
+ if (route.startsWith('/api')) {
231
+ api.push(route);
232
+ } else {
233
+ pages.push(route);
234
+ }
235
+ });
236
+ }
237
+
238
+ return {
239
+ api: normalizeList(api),
240
+ pages: normalizeList(pages)
241
+ };
242
+ }
243
+
244
+ module.exports = { normalizeList, normalizeToBeList, collectRoutesFromFilesystem, collectRoutesFromBuildArtifacts };
@@ -0,0 +1,98 @@
1
+ const path = require('path');
2
+ const { logSuccess, logError, execCommand } = require('../common/utils');
3
+ const { runCommand } = require('../cli/command-executor');
4
+ const { requireGitRepo, requireCleanWorkingTree, requireOnMainBranch, requireCurrentBranchUpToDateWithRemote, requireSingleChart, requireTagMissing, requireChartChangelogVersion } = require('../preflight');
5
+ const { getLatestRemoteChartVersion, compareSemver } = require('./metadata');
6
+ const { fetchChartVersionFromRegistry, waitForChartInRegistry } = require('./registry');
7
+ function chartStart(options = {}) {
8
+ const force = Boolean(options.force);
9
+ const wait = options.wait !== false;
10
+
11
+ return runCommand({
12
+ name: 'chart start',
13
+ checks: [
14
+ { name: 'git-repo', run: requireGitRepo },
15
+ { name: 'clean-working-tree', run: requireCleanWorkingTree },
16
+ { name: 'on-main-branch', run: requireOnMainBranch },
17
+ { name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
18
+ { name: 'single-chart', run: requireSingleChart },
19
+ {
20
+ name: 'chart-changelog-version',
21
+ requires: ['chartFilePath'],
22
+ run: (ctx) => requireChartChangelogVersion(path.dirname(ctx.chartFilePath))
23
+ },
24
+ {
25
+ name: 'tag-missing',
26
+ requires: ['chartName', 'version'],
27
+ run: (ctx) => requireTagMissing(`chart-${ctx.chartName}-${ctx.version}`)
28
+ },
29
+ {
30
+ name: 'version-not-downgrade',
31
+ requires: ['chartName', 'version'],
32
+ run: (ctx) => {
33
+ const latestVersion = getLatestRemoteChartVersion(ctx.chartName);
34
+ if (!latestVersion) {
35
+ return { ok: true };
36
+ }
37
+ if (compareSemver(ctx.version, latestVersion) > 0) {
38
+ return { ok: true, data: { latestChartVersion: latestVersion } };
39
+ }
40
+ if (force) {
41
+ logError('⚠️', 'Версия %s не больше последней версии тега %s. Продолжаю из-за --force.', ctx.version, latestVersion);
42
+ return { ok: true, data: { latestChartVersion: latestVersion } };
43
+ }
44
+ return {
45
+ ok: false,
46
+ reason: `Версия "${ctx.version}" не больше последней версии тега "${latestVersion}" на origin. Для сознательного отката используйте --force.`
47
+ };
48
+ }
49
+ },
50
+ {
51
+ name: 'registry-version-missing',
52
+ requires: ['chartName', 'version'],
53
+ run: async (ctx) => {
54
+ const result = await fetchChartVersionFromRegistry(ctx.chartName, ctx.version);
55
+ if (!result.ok) return { ok: false, reason: result.reason };
56
+ if (result.found) {
57
+ return { ok: false, reason: `Версия ${ctx.version} чарта ${ctx.chartName} уже опубликована в Helm-registry.` };
58
+ }
59
+ return { ok: true };
60
+ }
61
+ }
62
+ ],
63
+ steps: [
64
+ {
65
+ name: 'create-tag',
66
+ run: (ctx) => {
67
+ const tagName = `chart-${ctx.chartName}-${ctx.version}`;
68
+ if (!execCommand(`git tag "${tagName}"`)) {
69
+ logError('❌', 'Не удалось создать тег %s.', tagName);
70
+ return false;
71
+ }
72
+ logSuccess('🔖', 'Создан тег %s.', tagName);
73
+ return true;
74
+ }
75
+ },
76
+ {
77
+ name: 'push-tag',
78
+ run: (ctx) => {
79
+ const tagName = `chart-${ctx.chartName}-${ctx.version}`;
80
+ if (!execCommand(`git push origin "${tagName}"`)) {
81
+ logError('❌', 'Не удалось отправить тег %s в origin.', tagName);
82
+ return false;
83
+ }
84
+ logSuccess('🚀', 'Тег %s отправлен в origin.', tagName);
85
+ return true;
86
+ }
87
+ },
88
+ ...(wait
89
+ ? [{
90
+ name: 'wait-for-registry',
91
+ run: (ctx) => waitForChartInRegistry(ctx.chartName, ctx.version)
92
+ }]
93
+ : [])
94
+ ]
95
+ });
96
+ }
97
+
98
+ module.exports = { chartStart };