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
package/src/preflight.js
DELETED
|
@@ -1,796 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const { parseStableTag, stableTagNames } = require('./stable-tags');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execSilent, execCommand, getCurrentBranch, getMainBranch, getDevelopBranch } = require('./utils');
|
|
6
|
-
const {
|
|
7
|
-
CHANGELOG_FILE,
|
|
8
|
-
CHANGELOG_DIR,
|
|
9
|
-
FRAGMENT_TYPES,
|
|
10
|
-
getFragmentType
|
|
11
|
-
} = require('./changelog-config');
|
|
12
|
-
|
|
13
|
-
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
14
|
-
const STABLE_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
15
|
-
const YOUTRACK_TASK_PATTERN = /^[A-Z]+-[0-9]+$/;
|
|
16
|
-
|
|
17
|
-
function ok(data) {
|
|
18
|
-
return { ok: true, data };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fail(reason) {
|
|
22
|
-
return { ok: false, reason };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function toPosixPath(filePath) {
|
|
26
|
-
return String(filePath || '').replace(/\\/g, '/');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getPrettierRunner() {
|
|
30
|
-
const npxVersion = execSilent('npx --yes prettier --version');
|
|
31
|
-
if (npxVersion) return 'npx --yes prettier';
|
|
32
|
-
const directVersion = execSilent('prettier --version');
|
|
33
|
-
if (directVersion) return 'prettier';
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function requireGitRepo() {
|
|
38
|
-
if (!execCommand('git rev-parse --is-inside-work-tree')) {
|
|
39
|
-
return fail('Текущая директория не является git-репозиторием.');
|
|
40
|
-
}
|
|
41
|
-
return ok();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function requireCleanWorkingTree() {
|
|
45
|
-
const status = execSilent('git status --porcelain');
|
|
46
|
-
if (status === null) {
|
|
47
|
-
return fail('Не удалось получить состояние рабочего дерева git.');
|
|
48
|
-
}
|
|
49
|
-
if (status.trim() !== '') {
|
|
50
|
-
return fail('Рабочее дерево не чистое.');
|
|
51
|
-
}
|
|
52
|
-
return ok();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function requireMainAndDevBranches() {
|
|
56
|
-
const branches = execSilent('git branch -r');
|
|
57
|
-
if (!branches) {
|
|
58
|
-
return fail('Не удалось получить список удаленных веток.');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const mainBranch = getMainBranch();
|
|
62
|
-
const devBranch = getDevelopBranch();
|
|
63
|
-
const hasMain = branches.includes(`origin/${mainBranch}`);
|
|
64
|
-
const hasDev = branches.includes(`origin/${devBranch}`);
|
|
65
|
-
|
|
66
|
-
if (!hasMain || !hasDev) {
|
|
67
|
-
return fail(`Отсутствуют обязательные удаленные ветки (origin/${mainBranch}, origin/${devBranch}).`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return ok({ mainBranch, devBranch });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function requireCurrentBranch(expectedBranch) {
|
|
74
|
-
const currentBranch = getCurrentBranch();
|
|
75
|
-
if (!currentBranch) {
|
|
76
|
-
return fail('Не удалось определить текущую ветку.');
|
|
77
|
-
}
|
|
78
|
-
if (currentBranch !== expectedBranch) {
|
|
79
|
-
return fail(`Текущая ветка "${currentBranch}", ожидалась "${expectedBranch}".`);
|
|
80
|
-
}
|
|
81
|
-
return ok({ currentBranch });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function requireOnDevBranch() {
|
|
85
|
-
const devBranch = getDevelopBranch();
|
|
86
|
-
const currentBranch = getCurrentBranch();
|
|
87
|
-
if (!currentBranch) {
|
|
88
|
-
return fail('Не удалось определить текущую ветку.');
|
|
89
|
-
}
|
|
90
|
-
if (currentBranch !== devBranch) {
|
|
91
|
-
return fail(`Текущая ветка "${currentBranch}", ожидалась "${devBranch}".`);
|
|
92
|
-
}
|
|
93
|
-
return ok({ devBranch, currentBranch });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
function requireOnMainBranch() {
|
|
98
|
-
const mainBranch = getMainBranch();
|
|
99
|
-
const currentBranch = getCurrentBranch();
|
|
100
|
-
if (!currentBranch) {
|
|
101
|
-
return fail('Не удалось определить текущую ветку.');
|
|
102
|
-
}
|
|
103
|
-
if (currentBranch !== mainBranch) {
|
|
104
|
-
return fail(`Текущая ветка "${currentBranch}", ожидалась "${mainBranch}".`);
|
|
105
|
-
}
|
|
106
|
-
return ok({ mainBranch, currentBranch });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function requireRemoteOrigin() {
|
|
110
|
-
const remoteUrl = execSilent('git remote get-url origin');
|
|
111
|
-
if (!remoteUrl) {
|
|
112
|
-
return fail('Удаленный репозиторий "origin" не настроен.');
|
|
113
|
-
}
|
|
114
|
-
return ok({ remoteOrigin: remoteUrl });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function requireRemoteReachable() {
|
|
118
|
-
const remoteHeads = execSilent('git ls-remote --heads origin');
|
|
119
|
-
if (remoteHeads === null) {
|
|
120
|
-
return fail('Не удалось получить доступ к удаленному репозиторию "origin".');
|
|
121
|
-
}
|
|
122
|
-
return ok();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function requireCurrentBranchUpToDateWithRemote() {
|
|
126
|
-
const currentBranch = getCurrentBranch();
|
|
127
|
-
if (!currentBranch) {
|
|
128
|
-
return fail('Не удалось определить текущую ветку.');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!execCommand('git fetch origin --prune --tags')) {
|
|
132
|
-
return fail('Не удалось получить изменения с удаленного репозитория.');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const upstreamBranch = execSilent('git rev-parse --abbrev-ref --symbolic-full-name "@{u}"');
|
|
136
|
-
if (!upstreamBranch || !upstreamBranch.trim()) {
|
|
137
|
-
return fail(`Для текущей ветки "${currentBranch}" не настроена upstream-ветка.`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const aheadBehind = execSilent(`git rev-list --left-right --count HEAD...${upstreamBranch.trim()}`);
|
|
141
|
-
if (!aheadBehind) {
|
|
142
|
-
return fail(`Не удалось сравнить текущую ветку "${currentBranch}" с "${upstreamBranch.trim()}".`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const parts = aheadBehind.trim().split(/\s+/);
|
|
146
|
-
const behindBy = Number(parts[1] || 0);
|
|
147
|
-
if (!Number.isFinite(behindBy)) {
|
|
148
|
-
return fail(`Не удалось разобрать информацию ahead/behind для "${upstreamBranch.trim()}".`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (behindBy > 0) {
|
|
152
|
-
return fail(`В удаленной ветке есть новые коммиты (${behindBy}). Выполните "git pull" и повторите попытку.`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return ok({ currentBranch, upstreamBranch: upstreamBranch.trim() });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function requireDevContainsRemoteMain() {
|
|
159
|
-
const mainBranch = getMainBranch();
|
|
160
|
-
const remoteMain = `origin/${mainBranch}`;
|
|
161
|
-
if (!execCommand(`git merge-base --is-ancestor ${remoteMain} HEAD`)) {
|
|
162
|
-
return fail(`Текущая ветка не содержит ${remoteMain}. Смержите ${remoteMain} в dev и повторите попытку.`);
|
|
163
|
-
}
|
|
164
|
-
return ok({ remoteMain });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function requireFileExists(filePath) {
|
|
168
|
-
if (!fs.existsSync(filePath)) {
|
|
169
|
-
return fail(`Обязательный файл "${filePath}" не существует.`);
|
|
170
|
-
}
|
|
171
|
-
return ok();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const CHANGELOG_HEADING_PATTERN = /^##\s+(?:\S+\s+)?\[(\d+\.\d+\.\d+)\]/gm;
|
|
175
|
-
|
|
176
|
-
function getChangelogReleaseVersions(changelogPath = CHANGELOG_FILE) {
|
|
177
|
-
let changelog;
|
|
178
|
-
try {
|
|
179
|
-
changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
180
|
-
} catch (error) {
|
|
181
|
-
return [];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return [...changelog.matchAll(CHANGELOG_HEADING_PATTERN)].map((match) => match[1]);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getChangelogReleaseVersion(changelogPath = CHANGELOG_FILE) {
|
|
188
|
-
return getChangelogReleaseVersions(changelogPath)[0] || null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function requireChangelogReleaseVersion() {
|
|
192
|
-
const version = getChangelogReleaseVersion();
|
|
193
|
-
if (!version) {
|
|
194
|
-
return fail(`Не удалось прочитать версию релиза из ${CHANGELOG_FILE}. Ожидается заголовок "## 🚀 [X.Y.Z]".`);
|
|
195
|
-
}
|
|
196
|
-
if (!STABLE_SEMVER_PATTERN.test(version)) {
|
|
197
|
-
return fail(`Версия "${version}" из ${CHANGELOG_FILE} должна быть стабильным SemVer X.Y.Z.`);
|
|
198
|
-
}
|
|
199
|
-
return ok({ version });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function requireChartChangelogVersion(chartDir, version) {
|
|
203
|
-
const changelogPath = toPosixPath(path.join(String(chartDir || ''), 'CHANGELOG.md'));
|
|
204
|
-
if (!fs.existsSync(changelogPath)) {
|
|
205
|
-
return fail(`Файл "${changelogPath}" не существует. Заполните changelog чарта перед созданием тега.`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let changelog;
|
|
209
|
-
try {
|
|
210
|
-
changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
211
|
-
} catch (error) {
|
|
212
|
-
return fail(`Не удалось прочитать "${changelogPath}".`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const escapedVersion = String(version || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
216
|
-
const headingPattern = new RegExp(`^##\\s+(?:\\S+\\s+)?\\[${escapedVersion}\\]`, 'm');
|
|
217
|
-
if (!headingPattern.test(changelog)) {
|
|
218
|
-
return fail(`В "${changelogPath}" нет заголовка "## [${version}]". Добавьте запись о версии перед созданием тега.`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return ok({ chartChangelogPath: changelogPath });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function compareStableVersions(left, right) {
|
|
225
|
-
const leftParts = String(left).split('.').map(Number);
|
|
226
|
-
const rightParts = String(right).split('.').map(Number);
|
|
227
|
-
for (let index = 0; index < 3; index += 1) {
|
|
228
|
-
if (leftParts[index] !== rightParts[index]) {
|
|
229
|
-
return leftParts[index] - rightParts[index];
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return 0;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function requireNoPendingRelease(stableVersion) {
|
|
236
|
-
if (!STABLE_SEMVER_PATTERN.test(String(stableVersion || ''))) {
|
|
237
|
-
return fail('Для проверки незакрытых релизов требуется последний стабильный SemVer X.Y.Z.');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const changelogReleaseVersions = getChangelogReleaseVersions();
|
|
241
|
-
const pendingVersions = [...new Set(changelogReleaseVersions)]
|
|
242
|
-
.filter((version) => compareStableVersions(version, stableVersion) > 0)
|
|
243
|
-
.sort((left, right) => compareStableVersions(right, left));
|
|
244
|
-
|
|
245
|
-
if (pendingVersions.length > 0) {
|
|
246
|
-
return fail(
|
|
247
|
-
`В ${CHANGELOG_FILE} найдены незакрытые релизы новее stable-версии ${stableVersion}: ${pendingVersions.join(', ')}. ` +
|
|
248
|
-
'Сначала выполните "spectrum release deploy", дождитесь успешного stable pipeline и выполните "spectrum release close".'
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return ok({ changelogReleaseVersions });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function requireLatestStableVersion() {
|
|
256
|
-
const mainBranch = getMainBranch();
|
|
257
|
-
const tagNames = execSilent(`git tag --merged origin/${mainBranch} --list "release/*" "hotfix/*" "v*"`);
|
|
258
|
-
const remoteTagRefs = execSilent('git ls-remote --refs --tags origin "refs/tags/release/*" "refs/tags/hotfix/*" "refs/tags/v*"');
|
|
259
|
-
if (tagNames === null || remoteTagRefs === null) {
|
|
260
|
-
return fail(`Не удалось проверить стабильные теги, достижимые из origin/${mainBranch}.`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const remoteTagNames = new Set();
|
|
264
|
-
for (const line of remoteTagRefs.split('\n')) {
|
|
265
|
-
const match = line.trim().match(/^[0-9a-f]+\s+refs\/tags\/(.+)$/i);
|
|
266
|
-
if (match && parseStableTag(match[1])) remoteTagNames.add(match[1]);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const versions = new Set();
|
|
270
|
-
for (const line of tagNames.split('\n')) {
|
|
271
|
-
const tagName = line.trim();
|
|
272
|
-
const parsed = parseStableTag(tagName);
|
|
273
|
-
if (parsed && remoteTagNames.has(tagName)) {
|
|
274
|
-
versions.add(parsed.version);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (versions.size === 0) {
|
|
279
|
-
return fail(`В origin/${mainBranch} не найден достижимый stable-тег release/X.Y.Z, hotfix/X.Y.Z или vX.Y.Z.`);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const stableVersion = [...versions].sort(compareStableVersions).at(-1);
|
|
283
|
-
return ok({ stableVersion });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function requireStableTagAtHead(version) {
|
|
287
|
-
if (!STABLE_SEMVER_PATTERN.test(String(version || ''))) return fail('Требуется stable-версия X.Y.Z.');
|
|
288
|
-
const head = execSilent('git rev-parse HEAD');
|
|
289
|
-
const names = stableTagNames(version);
|
|
290
|
-
const patterns = names.flatMap((tag) => [`"refs/tags/${tag}"`, `"refs/tags/${tag}^{}"`]).join(' ');
|
|
291
|
-
const tagRefs = execSilent(`git ls-remote --tags origin ${patterns}`);
|
|
292
|
-
if (!head || tagRefs === null) return fail(`Не удалось проверить stable-теги версии ${version}.`);
|
|
293
|
-
const refs = new Map(tagRefs.trim().split('\n').filter(Boolean).map((line) => {
|
|
294
|
-
const [sha, ref] = line.trim().split(/\s+/);
|
|
295
|
-
return [ref, sha];
|
|
296
|
-
}));
|
|
297
|
-
const commits = names.map((tag) => refs.get(`refs/tags/${tag}^{}`) || refs.get(`refs/tags/${tag}`)).filter(Boolean);
|
|
298
|
-
if (!commits.length) return fail(`Stable-тег версии ${version} отсутствует в origin.`);
|
|
299
|
-
if (commits.some((commit) => commit !== head)) return fail(`Stable-тег ${version} не совпадает с HEAD или конфликтует с alias.`);
|
|
300
|
-
return ok({ stableVersion: version });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function requireYouTrackTask(task) {
|
|
304
|
-
if (!YOUTRACK_TASK_PATTERN.test(String(task || ''))) {
|
|
305
|
-
return fail('Номер задачи должен соответствовать формату YOUTRACK-ID, например AR-123.');
|
|
306
|
-
}
|
|
307
|
-
return ok({ task });
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function requireTagMissing(tagName) {
|
|
311
|
-
const parsed = parseStableTag(tagName);
|
|
312
|
-
const names = parsed ? stableTagNames(parsed.version) : [tagName];
|
|
313
|
-
const localTag = execSilent(`git tag -l ${names.map((tag) => `"${tag}"`).join(' ')}`);
|
|
314
|
-
if (localTag === null) return fail('Не удалось проверить локальные теги.');
|
|
315
|
-
if (localTag.trim()) return fail(`Версия тега ${tagName} уже существует локально: ${localTag}.`);
|
|
316
|
-
const remoteTag = execSilent(`git ls-remote --tags origin ${names.map((tag) => `"refs/tags/${tag}"`).join(' ')}`);
|
|
317
|
-
if (remoteTag === null) return fail('Не удалось проверить теги origin.');
|
|
318
|
-
if (remoteTag.trim()) return fail(`Версия тега ${tagName} уже существует на origin.`);
|
|
319
|
-
return ok();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function requireReleaseVersionAvailable(version) {
|
|
323
|
-
if (!STABLE_SEMVER_PATTERN.test(String(version || ''))) {
|
|
324
|
-
return fail('Целевая release-версия должна соответствовать X.Y.Z.');
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const localBranches = execSilent(`git branch --list "hotfix/*-${version}"`);
|
|
328
|
-
if (localBranches && localBranches.trim()) {
|
|
329
|
-
return fail(`Версия "${version}" уже используется локальной hotfix-веткой.`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const remoteBranches = execSilent(`git ls-remote --heads origin "refs/heads/hotfix/*-${version}"`);
|
|
333
|
-
if (remoteBranches === null) {
|
|
334
|
-
return fail('Не удалось проверить hotfix-ветки в origin.');
|
|
335
|
-
}
|
|
336
|
-
if (remoteBranches && remoteBranches.trim()) {
|
|
337
|
-
return fail(`Версия "${version}" уже используется hotfix-веткой в origin.`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const names = stableTagNames(version);
|
|
341
|
-
const localTag = execSilent(`git tag --list ${names.map((tag) => `"${tag}"`).join(' ')}`);
|
|
342
|
-
if (localTag === null) return fail('Не удалось проверить локальные stable-теги.');
|
|
343
|
-
if (localTag && localTag.trim()) {
|
|
344
|
-
return fail(`Релизный тег версии ${version} уже существует локально.`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const remoteTag = execSilent(`git ls-remote --tags origin ${names.flatMap((tag) => [`"refs/tags/${tag}"`, `"refs/tags/${tag}^{}"`]).join(' ')}`);
|
|
348
|
-
if (remoteTag === null) {
|
|
349
|
-
return fail(`Не удалось проверить тег версии ${version} в origin.`);
|
|
350
|
-
}
|
|
351
|
-
if (remoteTag && remoteTag.trim()) {
|
|
352
|
-
return fail(`Релизный тег версии ${version} уже существует в origin.`);
|
|
353
|
-
}
|
|
354
|
-
return ok();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function requirePrettierAvailable() {
|
|
358
|
-
const runner = getPrettierRunner();
|
|
359
|
-
if (!runner) {
|
|
360
|
-
return fail('Prettier недоступен.');
|
|
361
|
-
}
|
|
362
|
-
return ok({ prettierRunner: runner });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function requireChangelogFormatted() {
|
|
366
|
-
const runner = getPrettierRunner();
|
|
367
|
-
if (!runner) {
|
|
368
|
-
return fail('Prettier недоступен.');
|
|
369
|
-
}
|
|
370
|
-
if (!execCommand(`${runner} --check CHANGELOG.md`)) {
|
|
371
|
-
return fail('Файл CHANGELOG.md не прошел проверку Prettier.');
|
|
372
|
-
}
|
|
373
|
-
return ok({ prettierRunner: runner });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function findChangelogFragmentFiles(baseDir = CHANGELOG_DIR) {
|
|
377
|
-
if (!fs.existsSync(baseDir)) {
|
|
378
|
-
return [];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
return fs.readdirSync(baseDir, { withFileTypes: true })
|
|
383
|
-
.filter((entry) => entry.isFile() && !entry.name.startsWith('.'))
|
|
384
|
-
.map((entry) => toPosixPath(path.join(baseDir, entry.name)))
|
|
385
|
-
.sort();
|
|
386
|
-
} catch (error) {
|
|
387
|
-
return [];
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function requireChangelogFragments() {
|
|
392
|
-
if (!fs.existsSync(CHANGELOG_DIR)) {
|
|
393
|
-
return fail(`Директория changelog fragments "${CHANGELOG_DIR}" не существует.`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const fragmentFiles = findChangelogFragmentFiles();
|
|
397
|
-
if (fragmentFiles.length === 0) {
|
|
398
|
-
return fail(`В директории "${CHANGELOG_DIR}" нет changelog fragments.`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const changelogFragments = [];
|
|
402
|
-
for (const filePath of fragmentFiles) {
|
|
403
|
-
const type = getFragmentType(filePath);
|
|
404
|
-
if (!type) {
|
|
405
|
-
return fail(`Неверное имя changelog fragment "${filePath}". Ожидается "<name>.<type>.md".`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let content;
|
|
409
|
-
try {
|
|
410
|
-
content = fs.readFileSync(filePath, 'utf8');
|
|
411
|
-
} catch (error) {
|
|
412
|
-
return fail(`Не удалось прочитать changelog fragment "${filePath}".`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const entries = content
|
|
416
|
-
.split('\n')
|
|
417
|
-
.map((line) => line.trim())
|
|
418
|
-
.filter(Boolean);
|
|
419
|
-
if (entries.length === 0) {
|
|
420
|
-
return fail(`Changelog fragment "${filePath}" пуст.`);
|
|
421
|
-
}
|
|
422
|
-
if (entries.some((line) => !line.startsWith('- '))) {
|
|
423
|
-
return fail(`Каждая непустая строка "${filePath}" должна начинаться с "- ".`);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
changelogFragments.push({
|
|
427
|
-
filePath,
|
|
428
|
-
type,
|
|
429
|
-
section: FRAGMENT_TYPES[type].section,
|
|
430
|
-
bump: FRAGMENT_TYPES[type].bump,
|
|
431
|
-
entries
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return ok({ changelogFragments });
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function requireSingleChart() {
|
|
439
|
-
const chartsDir = 'charts';
|
|
440
|
-
if (!fs.existsSync(chartsDir)) {
|
|
441
|
-
return fail('Не удалось найти директорию charts.');
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const entries = fs.readdirSync(chartsDir, { withFileTypes: true });
|
|
445
|
-
const chartFiles = entries
|
|
446
|
-
.filter((entry) => entry.isDirectory())
|
|
447
|
-
.map((entry) => `${chartsDir}/${entry.name}/Chart.yaml`)
|
|
448
|
-
.filter((chartFilePath) => fs.existsSync(chartFilePath));
|
|
449
|
-
|
|
450
|
-
if (chartFiles.length === 0) {
|
|
451
|
-
return fail('Не удалось найти Chart.yaml по пути charts/<chart-name>/Chart.yaml.');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (chartFiles.length > 1) {
|
|
455
|
-
return fail(`Найдено несколько chart-файлов: ${chartFiles.join(', ')}.`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const chartFilePath = chartFiles[0];
|
|
459
|
-
const chartYaml = fs.readFileSync(chartFilePath, 'utf8');
|
|
460
|
-
const nameMatch = chartYaml.match(/^\s*name:\s*([^\s#]+)\s*$/m);
|
|
461
|
-
const chartName = nameMatch ? nameMatch[1] : null;
|
|
462
|
-
if (!chartName) {
|
|
463
|
-
return fail(`Не удалось прочитать имя чарта из ${chartFilePath}.`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return ok({ chartFilePath, chartName });
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function findValuesYamlFiles(baseDir = 'charts') {
|
|
470
|
-
if (!fs.existsSync(baseDir)) {
|
|
471
|
-
return [];
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const stack = [baseDir];
|
|
475
|
-
const files = [];
|
|
476
|
-
|
|
477
|
-
while (stack.length > 0) {
|
|
478
|
-
const current = stack.pop();
|
|
479
|
-
let entries = [];
|
|
480
|
-
try {
|
|
481
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
482
|
-
} catch (error) {
|
|
483
|
-
return [];
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
for (const entry of entries) {
|
|
487
|
-
const fullPath = path.join(current, entry.name);
|
|
488
|
-
if (entry.isDirectory()) {
|
|
489
|
-
stack.push(fullPath);
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (entry.isFile() && entry.name === 'values.yaml') {
|
|
494
|
-
files.push(toPosixPath(fullPath));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return files.sort();
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function findHelmReleaseFiles(baseDir = '.') {
|
|
503
|
-
if (!fs.existsSync(baseDir)) {
|
|
504
|
-
return [];
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const skipDirs = new Set(['.git', 'node_modules']);
|
|
508
|
-
const stack = [baseDir];
|
|
509
|
-
const files = [];
|
|
510
|
-
|
|
511
|
-
while (stack.length > 0) {
|
|
512
|
-
const current = stack.pop();
|
|
513
|
-
let entries = [];
|
|
514
|
-
try {
|
|
515
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
516
|
-
} catch (error) {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
for (const entry of entries) {
|
|
521
|
-
const fullPath = path.join(current, entry.name);
|
|
522
|
-
if (entry.isDirectory()) {
|
|
523
|
-
if (!skipDirs.has(entry.name)) {
|
|
524
|
-
stack.push(fullPath);
|
|
525
|
-
}
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (entry.isFile() && entry.name.toLowerCase() === 'helmrelease.yaml') {
|
|
530
|
-
files.push(toPosixPath(fullPath));
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return files.sort();
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function requireHelmReleaseFiles() {
|
|
539
|
-
const helmReleaseFiles = findHelmReleaseFiles('.');
|
|
540
|
-
if (helmReleaseFiles.length === 0) {
|
|
541
|
-
return fail('Не удалось найти файлы helmrelease.yaml в репозитории.');
|
|
542
|
-
}
|
|
543
|
-
return ok({ helmReleaseFiles });
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function requireSingleValuesYaml() {
|
|
547
|
-
const valuesFiles = findValuesYamlFiles('charts');
|
|
548
|
-
if (valuesFiles.length === 0) {
|
|
549
|
-
return fail('Не удалось найти values.yaml по маске charts/**/values.yaml.');
|
|
550
|
-
}
|
|
551
|
-
if (valuesFiles.length > 1) {
|
|
552
|
-
return fail(`Найдено несколько файлов values.yaml: ${valuesFiles.join(', ')}.`);
|
|
553
|
-
}
|
|
554
|
-
return ok({ valuesYamlPath: valuesFiles[0] });
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function extractYamlList(content, sectionName) {
|
|
558
|
-
const lines = String(content || '').split('\n');
|
|
559
|
-
let inIngress = false;
|
|
560
|
-
let inPaths = false;
|
|
561
|
-
let inSection = false;
|
|
562
|
-
let ingressIndent = 0;
|
|
563
|
-
let pathsIndent = 0;
|
|
564
|
-
let sectionIndent = 0;
|
|
565
|
-
const values = [];
|
|
566
|
-
|
|
567
|
-
for (const rawLine of lines) {
|
|
568
|
-
const line = rawLine.replace(/\r$/, '');
|
|
569
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
570
|
-
const trimmed = line.trim();
|
|
571
|
-
|
|
572
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (/^ingress:\s*(#.*)?$/.test(trimmed)) {
|
|
577
|
-
inIngress = true;
|
|
578
|
-
inPaths = false;
|
|
579
|
-
inSection = false;
|
|
580
|
-
ingressIndent = indent;
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (inIngress && indent <= ingressIndent && !/^ingress:\s*(#.*)?$/.test(trimmed)) {
|
|
585
|
-
inIngress = false;
|
|
586
|
-
inPaths = false;
|
|
587
|
-
inSection = false;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (!inIngress) {
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (/^paths:\s*(#.*)?$/.test(trimmed)) {
|
|
595
|
-
inPaths = true;
|
|
596
|
-
inSection = false;
|
|
597
|
-
pathsIndent = indent;
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (inPaths && indent <= pathsIndent && !/^paths:\s*(#.*)?$/.test(trimmed)) {
|
|
602
|
-
inPaths = false;
|
|
603
|
-
inSection = false;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (!inPaths) {
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const sectionRegex = new RegExp(`^${sectionName}:\\s*(#.*)?$`);
|
|
611
|
-
if (sectionRegex.test(trimmed)) {
|
|
612
|
-
inSection = true;
|
|
613
|
-
sectionIndent = indent;
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (inSection && indent <= sectionIndent && !sectionRegex.test(trimmed)) {
|
|
618
|
-
inSection = false;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (!inSection) {
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const itemMatch = line.match(/^\s*-\s+(.+?)(?:\s+#.*)?$/);
|
|
626
|
-
if (itemMatch) {
|
|
627
|
-
values.push(itemMatch[1].trim());
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return values;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function requireIngressPathSections(valuesYamlPath) {
|
|
635
|
-
if (!valuesYamlPath || !fs.existsSync(valuesYamlPath)) {
|
|
636
|
-
return fail(`Обязательный файл "${valuesYamlPath}" не существует.`);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
let content = '';
|
|
640
|
-
try {
|
|
641
|
-
content = fs.readFileSync(valuesYamlPath, 'utf8');
|
|
642
|
-
} catch (error) {
|
|
643
|
-
return fail(`Не удалось прочитать "${valuesYamlPath}".`);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const api = extractYamlList(content, 'api');
|
|
647
|
-
const pages = extractYamlList(content, 'pages');
|
|
648
|
-
const assets = extractYamlList(content, 'assets');
|
|
649
|
-
|
|
650
|
-
if (api.length === 0) {
|
|
651
|
-
return fail(`Отсутствует или пуст ingress.paths.api в "${valuesYamlPath}".`);
|
|
652
|
-
}
|
|
653
|
-
if (pages.length === 0) {
|
|
654
|
-
return fail(`Отсутствует или пуст ingress.paths.pages в "${valuesYamlPath}".`);
|
|
655
|
-
}
|
|
656
|
-
if (assets.length === 0) {
|
|
657
|
-
return fail(`Отсутствует или пуст ingress.paths.assets в "${valuesYamlPath}".`);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return ok({
|
|
661
|
-
valuesIngressPaths: { api, pages, assets }
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function requireSourcePathDirectory(sourcePath) {
|
|
666
|
-
if (!sourcePath || typeof sourcePath !== 'string') {
|
|
667
|
-
return fail('Требуется путь к исходникам.');
|
|
668
|
-
}
|
|
669
|
-
const resolved = path.resolve(sourcePath);
|
|
670
|
-
if (!fs.existsSync(resolved)) {
|
|
671
|
-
return fail(`Путь к исходникам не существует: "${sourcePath}".`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
let stat;
|
|
675
|
-
try {
|
|
676
|
-
stat = fs.statSync(resolved);
|
|
677
|
-
} catch (error) {
|
|
678
|
-
return fail(`Не удалось получить доступ к пути исходников: "${sourcePath}".`);
|
|
679
|
-
}
|
|
680
|
-
if (!stat.isDirectory()) {
|
|
681
|
-
return fail(`Путь к исходникам не является директорией: "${sourcePath}".`);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return ok({ sourcePath: resolved });
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function hasNextConfig(sourcePath) {
|
|
688
|
-
const files = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'];
|
|
689
|
-
return files.some((fileName) => fs.existsSync(path.join(sourcePath, fileName)));
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function requireNextProject(sourcePath) {
|
|
693
|
-
if (!sourcePath) {
|
|
694
|
-
return fail('Требуется путь к исходникам.');
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const pkgPath = path.join(sourcePath, 'package.json');
|
|
698
|
-
if (!fs.existsSync(pkgPath)) {
|
|
699
|
-
return fail(`Не удалось найти package.json в "${sourcePath}".`);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
let pkg;
|
|
703
|
-
try {
|
|
704
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
705
|
-
} catch (error) {
|
|
706
|
-
return fail(`Не удалось разобрать package.json в "${sourcePath}".`);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const dependencies = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
710
|
-
const hasNextDependency = typeof dependencies.next === 'string';
|
|
711
|
-
const nextConfigExists = hasNextConfig(sourcePath);
|
|
712
|
-
|
|
713
|
-
if (!hasNextDependency && !nextConfigExists) {
|
|
714
|
-
return fail(`"${sourcePath}" не похож на проект Next.js.`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return ok({
|
|
718
|
-
sourcePackageJsonPath: pkgPath,
|
|
719
|
-
sourcePackageManager: fs.existsSync(path.join(sourcePath, 'yarn.lock'))
|
|
720
|
-
? 'yarn'
|
|
721
|
-
: fs.existsSync(path.join(sourcePath, 'bun.lockb'))
|
|
722
|
-
? 'bun'
|
|
723
|
-
: 'npm',
|
|
724
|
-
sourceHasLockfile: Boolean(
|
|
725
|
-
fs.existsSync(path.join(sourcePath, 'package-lock.json')) ||
|
|
726
|
-
fs.existsSync(path.join(sourcePath, 'yarn.lock')) ||
|
|
727
|
-
fs.existsSync(path.join(sourcePath, 'bun.lockb'))
|
|
728
|
-
)
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function requireBuildCommandSupport(sourcePath) {
|
|
733
|
-
if (!sourcePath) {
|
|
734
|
-
return fail('Требуется путь к исходникам.');
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const pkgPath = path.join(sourcePath, 'package.json');
|
|
738
|
-
if (!fs.existsSync(pkgPath)) {
|
|
739
|
-
return fail(`Не удалось найти package.json в "${sourcePath}".`);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
let pkg;
|
|
743
|
-
try {
|
|
744
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
745
|
-
} catch (error) {
|
|
746
|
-
return fail(`Не удалось разобрать package.json в "${sourcePath}".`);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (!pkg.scripts || typeof pkg.scripts.build !== 'string') {
|
|
750
|
-
return fail(`В "${pkgPath}" отсутствует скрипт build.`);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return ok({ sourceBuildScript: pkg.scripts.build });
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
module.exports = {
|
|
757
|
-
SEMVER_PATTERN,
|
|
758
|
-
STABLE_SEMVER_PATTERN,
|
|
759
|
-
YOUTRACK_TASK_PATTERN,
|
|
760
|
-
getPrettierRunner,
|
|
761
|
-
requireGitRepo,
|
|
762
|
-
requireCleanWorkingTree,
|
|
763
|
-
requireMainAndDevBranches,
|
|
764
|
-
requireCurrentBranch,
|
|
765
|
-
requireOnDevBranch,
|
|
766
|
-
requireOnMainBranch,
|
|
767
|
-
requireRemoteOrigin,
|
|
768
|
-
requireRemoteReachable,
|
|
769
|
-
requireCurrentBranchUpToDateWithRemote,
|
|
770
|
-
requireDevContainsRemoteMain,
|
|
771
|
-
requireFileExists,
|
|
772
|
-
getChangelogReleaseVersions,
|
|
773
|
-
getChangelogReleaseVersion,
|
|
774
|
-
requireChangelogReleaseVersion,
|
|
775
|
-
requireChartChangelogVersion,
|
|
776
|
-
requireNoPendingRelease,
|
|
777
|
-
requireLatestStableVersion,
|
|
778
|
-
requireStableTagAtHead,
|
|
779
|
-
requireYouTrackTask,
|
|
780
|
-
requireTagMissing,
|
|
781
|
-
requireReleaseVersionAvailable,
|
|
782
|
-
requirePrettierAvailable,
|
|
783
|
-
requireChangelogFormatted,
|
|
784
|
-
findChangelogFragmentFiles,
|
|
785
|
-
requireChangelogFragments,
|
|
786
|
-
requireSingleChart,
|
|
787
|
-
findValuesYamlFiles,
|
|
788
|
-
findHelmReleaseFiles,
|
|
789
|
-
extractYamlList,
|
|
790
|
-
requireSingleValuesYaml,
|
|
791
|
-
requireHelmReleaseFiles,
|
|
792
|
-
requireIngressPathSections,
|
|
793
|
-
requireSourcePathDirectory,
|
|
794
|
-
requireNextProject,
|
|
795
|
-
requireBuildCommandSupport
|
|
796
|
-
};
|