spectrum-cli 2.1.1 → 2.1.2
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 +6 -0
- package/README.md +8 -5
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/token.js +27 -19
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,12 @@ _Список правок для обеспечения безопасност
|
|
|
39
39
|
|
|
40
40
|
_Список правок для обеспечения технической поддержки._
|
|
41
41
|
|
|
42
|
+
## 🚀 [2.1.2] - 2026-08-17
|
|
43
|
+
|
|
44
|
+
### 🛠 Changed
|
|
45
|
+
|
|
46
|
+
- Команда `spectrum token rotate` запрашивает два токена: owner PAT для работы с CI variables и `GITLAB_PRIVATE_TOKEN` для записи в переменные.
|
|
47
|
+
|
|
42
48
|
## 🚀 [2.1.1] - 2026-08-17
|
|
43
49
|
|
|
44
50
|
### 🛠 Changed
|
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ spectrum-cli/
|
|
|
139
139
|
- `spectrum chart create <version>`: `git-repo`, `clean-working-tree`, `on-main-branch`, `valid-semver` (переданный `<version>` — semver), `single-chart` (ровно один `charts/<chart-name>/Chart.yaml`), `tag-missing` (тега `chart-<name>-<version>` нет локально и на `origin`).
|
|
140
140
|
- `spectrum chart deploy`: `git-repo`, `clean-working-tree`, `on-main-branch` (текущая ветка `main`), `remote-origin` (настроен `origin`), `remote-reachable` (доступен `origin`), `single-chart`, `helmrelease-files` (найдены `helmrelease.yaml`).
|
|
141
141
|
- `spectrum chart verify <source_path>`: `git-repo`, `single-values-yaml` (ровно один `charts/**/values.yaml`), `values-ingress-sections` (есть `ingress.paths.api/pages/assets`), `source-path-directory`, `next-project`, `build-command-support`.
|
|
142
|
-
- `spectrum token rotate`: `load-config` (есть валидный `~/.config/spectrum-cli/config.yaml`), `ask-
|
|
142
|
+
- `spectrum token rotate`: `load-config` (есть валидный `~/.config/spectrum-cli/config.yaml`), `ask-tokens` (owner PAT и `GITLAB_PRIVATE_TOKEN` только в памяти), `check-access` (все группы и проекты доступны owner PAT).
|
|
143
143
|
|
|
144
144
|
## 🔄 Workflow релиза
|
|
145
145
|
|
|
@@ -210,14 +210,17 @@ projects: []
|
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
- `bot` и `token_ttl_months` читаются только из конфига.
|
|
213
|
-
-
|
|
213
|
+
- Скрытым вводом запрашиваются два токена. Оба живут только в памяти.
|
|
214
|
+
- Owner PAT: ходит в API, проверяет доступ, читает/удаляет/создает CI variables. Для групп нужен Owner, для проектов — Maintainer+.
|
|
215
|
+
- `GITLAB_PRIVATE_TOKEN`: только значение, которое пишется в CI variable.
|
|
214
216
|
- Для каждой группы и проекта CI variable `GITLAB_PRIVATE_TOKEN` удаляется (если есть) и создается заново как masked and hidden.
|
|
215
217
|
- Description переменной: `PAT от бота <bot>, владелец Колосов. Истекает YYYY-MM-DD.` Дата = сегодня + `token_ttl_months`.
|
|
216
218
|
|
|
217
219
|
1. Загружает или создает конфиг
|
|
218
|
-
2. Запрашивает
|
|
219
|
-
3.
|
|
220
|
-
4.
|
|
220
|
+
2. Запрашивает owner PAT
|
|
221
|
+
3. Запрашивает `GITLAB_PRIVATE_TOKEN`
|
|
222
|
+
4. Проверяет доступ ко всем группам и проектам owner PAT
|
|
223
|
+
5. Обновляет CI variables во всех целях
|
|
221
224
|
|
|
222
225
|
## 📝 Работа с Changelog
|
|
223
226
|
|
package/index.js
CHANGED
|
@@ -107,7 +107,7 @@ const tokenCmd = program
|
|
|
107
107
|
|
|
108
108
|
tokenCmd
|
|
109
109
|
.command("rotate")
|
|
110
|
-
.description("Пролить GITLAB_PRIVATE_TOKEN в CI variables
|
|
110
|
+
.description("Пролить GITLAB_PRIVATE_TOKEN в CI variables через owner PAT")
|
|
111
111
|
.action(() => runAction(token.tokenRotate));
|
|
112
112
|
|
|
113
113
|
// Переопределяем help, чтобы показать кастомный формат с алиасами
|
package/package.json
CHANGED
package/src/token.js
CHANGED
|
@@ -123,11 +123,11 @@ function parseTargets(urls, kind) {
|
|
|
123
123
|
return { ok: true, targets };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async function
|
|
126
|
+
async function promptHidden(message) {
|
|
127
127
|
return new Promise((resolve) => {
|
|
128
128
|
const stdin = process.stdin;
|
|
129
129
|
const stdout = process.stdout;
|
|
130
|
-
stdout.write(
|
|
130
|
+
stdout.write(message);
|
|
131
131
|
|
|
132
132
|
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
133
133
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
@@ -165,9 +165,9 @@ async function promptPrivateToken() {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
async function gitlabRequest(origin,
|
|
168
|
+
async function gitlabRequest(origin, accessToken, method, apiPath, body) {
|
|
169
169
|
const url = `${origin}/api/v4${apiPath}`;
|
|
170
|
-
const headers = { 'PRIVATE-TOKEN':
|
|
170
|
+
const headers = { 'PRIVATE-TOKEN': accessToken };
|
|
171
171
|
const options = { method, headers };
|
|
172
172
|
if (body !== undefined) {
|
|
173
173
|
headers['Content-Type'] = 'application/json';
|
|
@@ -193,13 +193,13 @@ async function gitlabRequest(origin, privateToken, method, apiPath, body) {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
async function gitlabGetAll(origin,
|
|
196
|
+
async function gitlabGetAll(origin, accessToken, apiPath) {
|
|
197
197
|
const items = [];
|
|
198
198
|
let page = 1;
|
|
199
199
|
|
|
200
200
|
while (true) {
|
|
201
201
|
const separator = apiPath.includes('?') ? '&' : '?';
|
|
202
|
-
const result = await gitlabRequest(origin,
|
|
202
|
+
const result = await gitlabRequest(origin, accessToken, 'GET', `${apiPath}${separator}per_page=100&page=${page}`);
|
|
203
203
|
if (!result.ok) {
|
|
204
204
|
return result;
|
|
205
205
|
}
|
|
@@ -272,10 +272,17 @@ function loadOrCreateConfig() {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
async function
|
|
276
|
-
logSuccess('🔑', 'Запрашиваю
|
|
277
|
-
const
|
|
278
|
-
if (!
|
|
275
|
+
async function askTokens(ctx) {
|
|
276
|
+
logSuccess('🔑', 'Запрашиваю owner PAT для работы с CI variables. Значение останется только в памяти.');
|
|
277
|
+
const ownerToken = await module.exports.promptHidden('🔑 Введите owner PAT: ');
|
|
278
|
+
if (!ownerToken) {
|
|
279
|
+
return { ok: false, reason: 'Owner PAT не указан.' };
|
|
280
|
+
}
|
|
281
|
+
logSuccess('🔑', 'Owner PAT принят в память.');
|
|
282
|
+
|
|
283
|
+
logSuccess('🔑', 'Запрашиваю %s для записи в CI variables. Значение останется только в памяти.', TOKEN_NAME);
|
|
284
|
+
const ciToken = await module.exports.promptHidden(`🔑 Введите ${TOKEN_NAME}: `);
|
|
285
|
+
if (!ciToken) {
|
|
279
286
|
return { ok: false, reason: `${TOKEN_NAME} не указан.` };
|
|
280
287
|
}
|
|
281
288
|
|
|
@@ -284,7 +291,8 @@ async function askPrivateToken(ctx) {
|
|
|
284
291
|
return {
|
|
285
292
|
ok: true,
|
|
286
293
|
data: {
|
|
287
|
-
|
|
294
|
+
ownerToken,
|
|
295
|
+
ciToken,
|
|
288
296
|
expiresAt,
|
|
289
297
|
variableDescription: buildDescription(ctx.bot, expiresAt)
|
|
290
298
|
}
|
|
@@ -297,7 +305,7 @@ async function checkTargetsAccess(ctx) {
|
|
|
297
305
|
const target = ctx.targets[index];
|
|
298
306
|
const endpoint = target.kind === 'группы' ? 'groups' : 'projects';
|
|
299
307
|
logSuccess('🔍', '[%s/%s] Проверяю доступ к %s %s', String(index + 1), String(total), target.kind, target.url);
|
|
300
|
-
const result = await gitlabRequest(ctx.origin, ctx.
|
|
308
|
+
const result = await gitlabRequest(ctx.origin, ctx.ownerToken, 'GET', `/${endpoint}/${target.encodedPath}`);
|
|
301
309
|
if (!result.ok) {
|
|
302
310
|
return { ok: false, reason: `${target.kind === 'группы' ? 'Группа' : 'Проект'} недоступен: ${target.url} (HTTP ${result.status}).` };
|
|
303
311
|
}
|
|
@@ -311,7 +319,7 @@ async function updateTargetVariable(ctx, target, index, total) {
|
|
|
311
319
|
const label = target.kind === 'группы' ? 'группе' : 'проекте';
|
|
312
320
|
logSuccess('📦', '[%s/%s] Обновляю CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
|
|
313
321
|
|
|
314
|
-
const listed = await gitlabGetAll(ctx.origin, ctx.
|
|
322
|
+
const listed = await gitlabGetAll(ctx.origin, ctx.ownerToken, `/${endpoint}/${target.encodedPath}/variables`);
|
|
315
323
|
if (!listed.ok) {
|
|
316
324
|
logError('❌', 'Не удалось получить CI variables для %s (HTTP %s).', target.path, String(listed.status));
|
|
317
325
|
return false;
|
|
@@ -320,7 +328,7 @@ async function updateTargetVariable(ctx, target, index, total) {
|
|
|
320
328
|
const exists = (listed.data || []).some((variable) => variable.key === TOKEN_NAME);
|
|
321
329
|
if (exists) {
|
|
322
330
|
logSuccess('🗑', '[%s/%s] Удаляю старую CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
|
|
323
|
-
const deleted = await gitlabRequest(ctx.origin, ctx.
|
|
331
|
+
const deleted = await gitlabRequest(ctx.origin, ctx.ownerToken, 'DELETE', `/${endpoint}/${target.encodedPath}/variables/${TOKEN_NAME}`);
|
|
324
332
|
if (!deleted.ok && deleted.status !== 204) {
|
|
325
333
|
logError('❌', 'Не удалось удалить CI variable в %s (HTTP %s).', target.path, String(deleted.status));
|
|
326
334
|
return false;
|
|
@@ -331,9 +339,9 @@ async function updateTargetVariable(ctx, target, index, total) {
|
|
|
331
339
|
}
|
|
332
340
|
|
|
333
341
|
logSuccess('📝', '[%s/%s] Создаю CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
|
|
334
|
-
const created = await gitlabRequest(ctx.origin, ctx.
|
|
342
|
+
const created = await gitlabRequest(ctx.origin, ctx.ownerToken, 'POST', `/${endpoint}/${target.encodedPath}/variables`, {
|
|
335
343
|
key: TOKEN_NAME,
|
|
336
|
-
value: ctx.
|
|
344
|
+
value: ctx.ciToken,
|
|
337
345
|
masked_and_hidden: true,
|
|
338
346
|
description: ctx.variableDescription
|
|
339
347
|
});
|
|
@@ -364,7 +372,7 @@ function tokenRotate() {
|
|
|
364
372
|
name: 'token rotate',
|
|
365
373
|
checks: [
|
|
366
374
|
{ name: 'load-config', run: loadOrCreateConfig },
|
|
367
|
-
{ name: 'ask-
|
|
375
|
+
{ name: 'ask-tokens', run: askTokens },
|
|
368
376
|
{ name: 'check-access', run: checkTargetsAccess }
|
|
369
377
|
],
|
|
370
378
|
steps: [
|
|
@@ -382,9 +390,9 @@ module.exports = {
|
|
|
382
390
|
addMonths,
|
|
383
391
|
buildDescription,
|
|
384
392
|
validateConfig,
|
|
385
|
-
|
|
393
|
+
promptHidden,
|
|
386
394
|
loadOrCreateConfig,
|
|
387
|
-
|
|
395
|
+
askTokens,
|
|
388
396
|
checkTargetsAccess,
|
|
389
397
|
updateCiVariables,
|
|
390
398
|
tokenRotate
|