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 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-private-token` (`GITLAB_PRIVATE_TOKEN` только в памяти), `check-access` (все группы и проекты из конфига доступны).
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
- - `GITLAB_PRIVATE_TOKEN` запрашивается скрытым вводом и живет только в памяти на время команды. Им же ходим в API.
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. Запрашивает `GITLAB_PRIVATE_TOKEN`
219
- 3. Проверяет доступ ко всем группам и проектам
220
- 4. Обновляет CI variables во всех целях
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "🚀 Modern CLI for development workflow automation - release management, versioning, and changelog generation",
5
5
  "main": "index.js",
6
6
  "bin": {
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 promptPrivateToken() {
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('🔑 Введите GITLAB_PRIVATE_TOKEN: ');
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, privateToken, method, apiPath, body) {
168
+ async function gitlabRequest(origin, accessToken, method, apiPath, body) {
169
169
  const url = `${origin}/api/v4${apiPath}`;
170
- const headers = { 'PRIVATE-TOKEN': privateToken };
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, privateToken, apiPath) {
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, privateToken, 'GET', `${apiPath}${separator}per_page=100&page=${page}`);
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 askPrivateToken(ctx) {
276
- logSuccess('🔑', 'Запрашиваю %s. Значение останется только в памяти.', TOKEN_NAME);
277
- const privateToken = await module.exports.promptPrivateToken();
278
- if (!privateToken) {
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
- privateToken,
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.privateToken, 'GET', `/${endpoint}/${target.encodedPath}`);
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.privateToken, `/${endpoint}/${target.encodedPath}/variables`);
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.privateToken, 'DELETE', `/${endpoint}/${target.encodedPath}/variables/${TOKEN_NAME}`);
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.privateToken, 'POST', `/${endpoint}/${target.encodedPath}/variables`, {
342
+ const created = await gitlabRequest(ctx.origin, ctx.ownerToken, 'POST', `/${endpoint}/${target.encodedPath}/variables`, {
335
343
  key: TOKEN_NAME,
336
- value: ctx.privateToken,
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-private-token', run: askPrivateToken },
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
- promptPrivateToken,
393
+ promptHidden,
386
394
  loadOrCreateConfig,
387
- askPrivateToken,
395
+ askTokens,
388
396
  checkTargetsAccess,
389
397
  updateCiVariables,
390
398
  tokenRotate