spectrum-cli 5.0.1 → 5.1.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 CHANGED
@@ -9,6 +9,27 @@ This is the changelog of the `spectrum-cli` npm package. Its version comes from
9
9
  `package.json`; the release commands use Git tags and `CHANGELOG.md` in the
10
10
  **target project** and do not update that project's `package.json`.
11
11
 
12
+ ## 🚀 [5.1.0]
13
+
14
+ ### Changed
15
+
16
+ - Deployment-адреса и GitLab-проекты задаются окружением: встроенные URL/namespace удалены, примеры и fixtures используют example-домены. Добавлены preflight наличия настроек, доступности, GitLab token scope/роли, YouTrack identity/проектов/полей и registry login/чтения. Права записи не объявляются доказанными по успешному GET.
17
+
18
+ ### Added
19
+
20
+ - Все новые automation-команды поддерживают `-d, --dry` и `-s, --silence`: dry выполняет только предпроверки без изменения исходников, отчётов и удалённых сущностей; silence скрывает успешные предпроверки, сохраняя ошибки и результаты.
21
+
22
+ - Команды `nextjs licenses`, `sbom catalog resolve/populate` и `security scan/track/reconcile/repair` перенесены из CI-компонентов в JavaScript-модули CLI. Сохранены OCI provenance/integrity, CVE и проектные задачи, Priority, изоляция проверок, проверки исправлений и отчёты для Mattermost.
23
+ - Автоматизация работает без интерактивных запросов и проверки обновлений; версии CLI в CI закрепляются служебными образами. Python для новых команд не требуется.
24
+
25
+ ## 🚀 [5.0.2] - 2026-10-23
26
+
27
+ ### Changed
28
+
29
+ - При baseline `0.x` обычные релизы с breaking/removed повышают minor, сохраняя тип записей; исправления повышают patch. Первый `1.0.0` требует явного `release start --stable`; другой открытый релиз блокирует переход. После `1.0.0` действует прежний SemVer.
30
+
31
+ - Команды `spectrum chart` используют `SC_DEVELOPER_PAT` вместо `SC_OWNER_PAT` для Helm-registry. Новое имя имеет приоритет. `SC_OWNER_PAT` сохранена как deprecated fallback с предупреждением при использовании; поддержка старого имени будет удалена в следующей версии. Owner для проверки registry не требуется. Запрос owner PAT в `token rotate` для управления групповыми CI/CD variables не изменён.
32
+
12
33
  ## 🚀 [5.0.1] - 2026-10-02
13
34
 
14
35
  ### 🛠 Changed
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g spectrum-cli
14
14
  spectrum --help
15
15
  ```
16
16
 
17
- Перед выполнением каждой команды CLI проверяет опубликованную версию `spectrum-cli`
17
+ Перед выполнением команд workflow CLI проверяет опубликованную версию `spectrum-cli`
18
18
  в npm Registry. Если доступна новая версия, показывает текущую и новую версии и
19
19
  команду `spectrum upgrade`. Она запускает `npm install -g spectrum-cli`.
20
20
  Проверка ограничена 200 мс; недоступность Registry не влияет на выполнение
@@ -43,7 +43,7 @@ spectrum --help
43
43
  spectrum -v # или spectrum --version
44
44
 
45
45
  # 🚀 Управление релизами
46
- spectrum release start # Схлопнуть fragments и атомарно отправить release commit в dev и main/master
46
+ spectrum release start [--stable] # Схлопнуть fragments и атомарно отправить release commit в dev и main/master
47
47
  spectrum release deploy # Создать стабильный тег release/X.Y.Z из заголовка CHANGELOG
48
48
  spectrum release close # Свести stable main/master в dev
49
49
 
@@ -172,7 +172,14 @@ spectrum-cli/
172
172
  - `spectrum release close`: чистая и актуальная main/master, версия из верхнего заголовка `CHANGELOG.md` и remote stable-тег поддерживаемой формы, указывающий на текущий commit.
173
173
  - `spectrum changelog append <message>`: `git-repo`, `changelog-exists`, валидные ID задачи, git identity и тип fragment. Команда не изменяет общий `CHANGELOG.md`.
174
174
  - `spectrum changelog check`: `git-repo`, `changelog-exists`, `changelog-prettier-check`, наличие и формат всех changelog fragments.
175
- - `spectrum chart start`: `git-repo`, `clean-working-tree`, `on-main-branch`, `single-chart` (ровно один `charts/<chart-name>/Chart.yaml`), валидный верхний SemVer-заголовок в `charts/<chart-name>/CHANGELOG.md`, отсутствие тега `chart-<name>-<version>` локально и на `origin`, отсутствие этой версии чарта в Helm-registry. Для запроса registry нужен `SC_OWNER_PAT`; ошибка доступа блокирует команду. `-f, --force` обходит только проверку повышения версии относительно последнего remote-тега. После push команда по умолчанию ждёт версию в registry; `-n, --no-wait` отключает ожидание.
175
+ - `spectrum chart start`: `git-repo`, `clean-working-tree`, `on-main-branch`, `single-chart` (ровно один `charts/<chart-name>/Chart.yaml`), валидный верхний SemVer-заголовок в `charts/<chart-name>/CHANGELOG.md`, отсутствие тега `chart-<name>-<version>` локально и на `origin`, отсутствие этой версии чарта в Helm-registry. Для запроса registry нужен `SC_DEVELOPER_PAT` (PAT со scope `api` и доступом к проекту; роль Owner не требуется); ошибка доступа блокирует команду. Право отправки chart-тега определяется отдельно правилами protected tags. `-f, --force` обходит только проверку повышения версии относительно последнего remote-тега. После push команда по умолчанию ждёт версию в registry; `-n, --no-wait` отключает ожидание.
176
+
177
+ Для обратной совместимости chart-команды принимают `SC_OWNER_PAT`, если
178
+ `SC_DEVELOPER_PAT` не задана или пуста. `SC_OWNER_PAT` deprecated: поддержка
179
+ будет удалена в следующей версии. При её использовании CLI один раз за процесс
180
+ выводит предупреждение с просьбой перейти на `SC_DEVELOPER_PAT`. Если заданы обе
181
+ переменные, используется `SC_DEVELOPER_PAT` без предупреждения.
182
+
176
183
  - `spectrum chart deploy`: `git-repo`, `clean-working-tree`, `on-main-branch` (текущая ветка `main`), `remote-origin` (настроен `origin`), `remote-reachable` (доступен `origin`), `single-chart`, `helmrelease-files` (найдены `helmrelease.yaml`).
177
184
  - `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`.
178
185
  - `spectrum token rotate`: `load-config` (есть валидный `~/.config/spectrum-cli/config.yaml`), `ask-tokens` (owner PAT и `SC_BOT_PAT` только в памяти), `check-access` (все группы и проекты доступны owner PAT).
@@ -185,10 +192,23 @@ spectrum-cli/
185
192
 
186
193
  1. Проверяет чистую актуальную dev-ветку, включающую `origin/main` или `origin/master`, changelog и fragments.
187
194
  2. После `git fetch origin --prune --tags` находит максимальный stable-тег среди `release/X.Y.Z`, `hotfix/X.Y.Z` и `vX.Y.Z`, достижимый из production-ветки.
188
- 3. Если открытого релиза нет, вычисляет версию по fragments: `breaking` и `removed` → major, `added` → minor, остальные → patch. Если раздел уже открыт в main/master, сохраняет его версию и дату, объединяет прежние записи с новыми по разделам без дублей. Тип новых fragments не повышает уже выбранную версию.
195
+ 3. Если открытого релиза нет, вычисляет версию по fragments. До `1.0.0`: `breaking`, `removed` и `added` → minor, остальные → patch. Начиная с `1.0.0`: `breaking` и `removed` → major, `added` → minor, остальные → patch. Если раздел уже открыт в main/master, сохраняет его версию и дату, объединяет прежние записи с новыми по разделам без дублей. Тип новых fragments не повышает уже выбранную версию.
189
196
  4. Форматирует и проверяет `CHANGELOG.md`, затем удаляет использованные fragments.
190
197
  5. Коммитит изменения и атомарно пушит dev в `origin/dev` и `origin/main` или `origin/master`, без Merge Request. Если новых записей нет, пустой коммит не создаётся; можно повторить отправку после её сбоя.
191
198
 
199
+ Первый стабильный выпуск создаётся явно: `spectrum release start --stable`
200
+ вычисляет ровно `1.0.0` от опубликованного `0.x`. Другой открытый релиз блокирует
201
+ переход; повтор подготовки открытого `1.0.0` допустим. Флаг запрещён после
202
+ перехода на `1.x`. Он выполняет обычный workflow `release start`, включая commit
203
+ и push; `--dry` запускает только предпроверки. Обычный запуск от `0.3.7` с
204
+ breaking/removed даёт `0.4.0`, с исправлением — `0.3.8`; исходные типы записей
205
+ сохраняются. Alpha CI должен применять ту же политику к своей базовой версии.
206
+
207
+ Сброс прежней высокой нумерации на `0.x` не выполняется этой командой: это
208
+ отдельная миграция с архивом changelog и ссылок тегов, согласованием dev/master и
209
+ исключением старых тегов из расчёта версии. Нельзя запускать `release start`,
210
+ пока опубликованная история в dev и production различается.
211
+
192
212
  Например, повторный `release start` дополняет открытый `1.4.0`; после публикации
193
213
  `release/1.4.0` новые fragments уже формируют следующий релиз. Изменение
194
214
  опубликованной истории и конфликтующие открытые версии блокируют команду.
@@ -378,13 +398,13 @@ projects: []
378
398
 
379
399
  **📋 Типы fragment и SemVer:**
380
400
 
381
- | Суффикс | Раздел | Повышение версии |
382
- | --- | --- | --- |
383
- | `breaking` | Breaking change | major |
384
- | `removed` | Removed | major |
385
- | `added` | Added | minor |
386
- | `changed`, `deprecated` | Соответствующий раздел | patch |
387
- | `fixed`, `security`, `support` | Соответствующий раздел | patch |
401
+ | Суффикс | Раздел | До `1.0.0` | С `1.0.0` |
402
+ | --- | --- | --- | --- |
403
+ | `breaking` | Breaking change | minor | major |
404
+ | `removed` | Removed | minor | major |
405
+ | `added` | Added | minor | minor |
406
+ | `changed`, `deprecated` | Соответствующий раздел | patch | patch |
407
+ | `fixed`, `security`, `support` | Соответствующий раздел | patch | patch |
388
408
 
389
409
  **📌 Пример использования:**
390
410
 
@@ -456,3 +476,134 @@ MIT License - см. [LICENSE](./LICENSE)
456
476
  ---
457
477
 
458
478
  _Создано для автоматизации повседневных задач разработки_ 🛠️
479
+
480
+ ## Команды для CI: Next.js, SBOM и security
481
+
482
+ Начиная с 5.1.0 логика команд написана на JavaScript и входит в npm-пакет.
483
+ Команды не задают вопросов и не проверяют обновления CLI. Они не запускают
484
+ release/hotfix preflight, рассчитанный на интерактивную работу разработчика.
485
+
486
+ | Команда | Назначение | Результат |
487
+ | --- | --- | --- |
488
+ | `spectrum nextjs licenses` | Сохранить исходные LICENSE уже включённых compiled-пакетов Next.js | JSON summary в stdout; код 2 при неполном результате |
489
+ | `spectrum sbom catalog resolve` | Найти Next в immutable образе, проверить OCI-каталоги, запросить отсутствующие версии | `sbom-catalog/<prefix>/{selection.json,report.json,report.md}` |
490
+ | `spectrum sbom catalog populate` | Построить каталог по официальному npm tarball и историческому Git tree | `sbom-catalog-updater/{report.json,report.md}` |
491
+ | `spectrum security scan` | Проверить `npm audit --omit=dev`, runtime и фактическую версию Node | `security-report/findings.json`, raw evidence, `scan.env` |
492
+ | `spectrum security reconcile` | Запустить центральный tracker и получить ссылки на задачи | `security-report/tracked.json` |
493
+ | `spectrum security track` | Найти/создать общие CVE и проектные задачи | `security-report/tracked.json` в центральном pipeline |
494
+ | `spectrum security repair` | Исправить одну проектную задачу за MR, выполнить проверки и повторный аудит | `security-report/{final.json,message.json}` |
495
+
496
+ Код 0 означает успешное выполнение, 1 — ошибку security/запуска, 2 — неполный
497
+ результат SBOM или LICENSE. Команды работают в текущем каталоге. Отсутствующие
498
+ credentials/некорректные отчёты приводят к ошибке, а не к чистому аудиту.
499
+ Автоматическое слияние MR отключено; сетевые записи не повторяются вслепую.
500
+
501
+ Компоненты передают свои inputs через job-local окружение. `security scan`
502
+ читает `NIGHTLY_YOUTRACK_PROJECT`, `NIGHTLY_YOUTRACK_TIME_TRACKING_ARTICLE`,
503
+ `NIGHTLY_YOUTRACK_TESTER`, необязательный `NIGHTLY_YOUTRACK_SUBSYSTEM`,
504
+ `NIGHTLY_DOCKERFILE`, `NIGHTLY_PLATFORMS` и `NIGHTLY_CHECK_COMMANDS` (JSON argv).
505
+ Токены `GLAB_BOT_PAT` и `YOUTRACK_PRIVATE_TOKEN` остаются секретными CI variables;
506
+ YouTrack-токен нужен только центральному tracker. Для registry используются
507
+ `NEXUS_USER`/`NEXUS_PASSWORD` либо стандартные CI registry credentials.
508
+ Секреты автоматизации исключаются из окружения выполняемых проверок проекта.
509
+
510
+ `sbom catalog resolve` читает `SBOM_CATALOG_CLIENT_ENABLED=true`,
511
+ `SBOM_CATALOG_FRAMEWORK=nextjs`, `SBOM_CATALOG_PREFIX` (по умолчанию `site:`),
512
+ `SBOM_CATALOG_REPOSITORY`, `DOCKER_IMAGE`, `APP_VERSION` и стандартные GitLab
513
+ `CI_*` variables. Чтение каталога использует `SBOM_CATALOG_USER` и
514
+ `SBOM_CATALOG_PASSWORD`, если заданы. `populate` требует защищённый `main`,
515
+ `NEXT_CATALOG_VERSIONS` (JSON-массив 1..10 точных версий) и центральные
516
+ `NEXUS_USER`/`NEXUS_PASSWORD`. Namespace записи задаёт только доверенная
517
+ project variable `SBOM_CATALOG_PUBLISH_REPOSITORY`.
518
+
519
+ Внешние инструменты: Linux amd64 для SBOM bootstrap; Git для каталога,
520
+ Trivy/Skopeo/Buildah для runtime-аудита и исправлений. SBOM-команды скачивают
521
+ закреплённые ORAS/Syft с проверкой SHA-256, ограничениями размера и времени.
522
+ Python для этих команд не требуется. Они не выполняют код из npm tarball
523
+ или upstream build-рецептов. Неоднозначное происхождение остаётся unresolved.
524
+ Поля CVE, Severity, Priority и Type=Group проверяются до создания подзадач;
525
+ CRITICAL получает Stopper (Неотложная), HIGH — Critical (Критическая).
526
+
527
+ Образы и адреса сервисов задаёт CI-конфигурация конкретной установки.
528
+ CLI не содержит дефолтов для YouTrack, registry или внутренних GitLab-проектов.
529
+ Само наличие команды `track` не сериализует процессы: job должен оставаться
530
+ в одном центральном проекте с `resource_group: nightly-youtrack-global`.
531
+ Отдельный consumer lock `nightly-security-fix` защищает исправления проекта.
532
+
533
+ Для проверки миграции:
534
+
535
+ ```sh
536
+ npm ci
537
+ npm test -- --runInBand
538
+ npm pack --dry-run
539
+ ```
540
+
541
+ Тесты используют локальные Git-репозитории и fake API. Зафиксированные parity
542
+ fixtures получены из прежнего Python-кода; исполняемый Python в пакет не входит.
543
+ Публикация CLI, образов, компонентов и запуск живого pipeline — отдельные шаги.
544
+
545
+ ### Preflight для команд автоматизации
546
+
547
+ Обязательные deployment-настройки задаются явно:
548
+
549
+ | Команда | Настройки |
550
+ | --- | --- |
551
+ | `security track` | `YOUTRACK_URL`, `YOUTRACK_PRIVATE_TOKEN`, `CI_API_V4_URL`, `GLAB_BOT_PAT`, идентификаторы consumer job/project и component SHA |
552
+ | `security reconcile` | `NIGHTLY_TRACKER_PROJECT`, `NIGHTLY_COMPONENT_SHA`, `NIGHTLY_SCAN_JOB_ID`, `CI_PROJECT_ID`, GitLab API/token; при пустом отчёте сеть не нужна |
553
+ | `security repair` | GitLab API/token и результаты scan/tracking; при пустом отчёте запись и проверка токена не нужны |
554
+ | `sbom catalog resolve` | `SBOM_CATALOG_REPOSITORY`, `DOCKER_IMAGE`, `APP_VERSION`; `SBOM_CATALOG_UPDATER_PROJECT` для запроса отсутствующих версий |
555
+ | `sbom catalog populate` | `SBOM_CATALOG_PUBLISH_REPOSITORY`, `NEXUS_USER`, `NEXUS_PASSWORD`, защищённый main и точные `NEXT_CATALOG_VERSIONS` |
556
+ | `security scan/repair` с `NEXUS_*` | Дополнительно явный `NIGHTLY_REGISTRY_HOSTS`; один логин без пароля или наоборот считается ошибкой |
557
+
558
+ В GitLab выполняются GET `/user`, `/personal_access_tokens/self` и чтение
559
+ целевых проектов. Проверяются активность токена, scope `api` для записи
560
+ (`api`/`read_api` для чтения), эффективная роль Developer/Reporter и состояние
561
+ проекта. Fine-grained/прочие токены, для которых introspection не подтверждает
562
+ эти scope, не считаются проверенными. Защита веток/тегов дополнительно
563
+ проверяется сервером при конкретной операции.
564
+
565
+ В YouTrack выполняются GET `/users/me`, чтение двух проектов, их полей и
566
+ поиск задач. До создания первой задачи проверяются активность пользователя,
567
+ доступность проектов и обязательные поля. Ответы 401/403, недоступность и
568
+ неверный TLS приводят к ошибке с названием проверки; секреты/тела ответов
569
+ не выводятся. Инспекция полей не требует тестового изменения проекта.
570
+
571
+ Writer выполняет ORAS login и чтение точного namespace до скачивания исходников
572
+ и построения каталога. Preflight сохраняется в отчёте. Чтение/логин не доказывают
573
+ права Create/Update/Link Issue, доступ к приватным полям, workflow-переходам или
574
+ registry push: эти права проверяются соответствующими операциями. Они явно
575
+ помечены `checked_by_operations`/`checked_by_publish`, без ложного статуса
576
+ «запись проверена». Preflight не создаёт задач, MR, upload-сессий или пробных
577
+ каталогов. Ошибка preflight останавливает следующие изменения и остаётся
578
+ в JSON/Markdown/notification-артефактах, когда исходный отчёт уже доступен.
579
+
580
+ Источник контрактов: [GitLab token scopes](https://docs.gitlab.com/security/tokens/access_token_scopes/),
581
+ [YouTrack My User Profile](https://www.jetbrains.com/help/youtrack/devportal/resource-api-users-me.html),
582
+ [YouTrack project fields](https://www.jetbrains.com/help/youtrack/devportal/resource-api-admin-projects-projectID-customFields.html).
583
+
584
+ ### Dry и silence для команд автоматизации
585
+
586
+ Все семь команд `nextjs licenses`, `sbom catalog resolve/populate` и
587
+ `security scan/track/reconcile/repair` поддерживают `-d, --dry` и
588
+ `-s, --silence`, включая сочетание `-ds`.
589
+
590
+ ```sh
591
+ spectrum nextjs licenses --dry
592
+ spectrum sbom catalog populate --dry --silence
593
+ spectrum security repair -ds
594
+ ```
595
+
596
+ `--dry` выполняет предпроверки настроек, файлов, инструментов и доступов,
597
+ после чего останавливается перед действиями команды. Он не копирует LICENSE,
598
+ не выполняет npm audit/Trivy/Syft scan или исправления, не создаёт задачи,
599
+ MR и downstream pipeline, не публикует каталоги и не перезаписывает отчёты.
600
+ Для проверки используются GET-запросы, чтение image descriptor и metadata
601
+ Git/проектов; SBOM preflight может загрузить закреплённые инструменты во
602
+ временную папку и выполнить registry login с временным auth-файлом.
603
+ Временные файлы удаляются; исходники, lockfiles и существующие артефакты
604
+ сохраняются. При неполных входных отчётах dry завершается с ошибкой.
605
+
606
+ `--silence` скрывает только сообщения об успешно пройденных предпроверках.
607
+ Ошибки, предупреждения, результаты команды и итог dry остаются видимыми.
608
+ Это тот же смысл флага, что у release/hotfix команд. Без dry выполняются
609
+ обычные действия и сохраняются прежние коды завершения 0/1/2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-cli",
3
- "version": "5.0.1",
3
+ "version": "5.1.0",
4
4
  "description": "🚀 Modern CLI for development workflow automation - release management, versioning, and changelog generation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -49,7 +49,9 @@
49
49
  "url": "https://github.com/dnwSilver/spectrum-cli/issues"
50
50
  },
51
51
  "dependencies": {
52
- "commander": "^11.1.0"
52
+ "commander": "^11.1.0",
53
+ "semver": "7.8.5",
54
+ "yaml": "2.9.1"
53
55
  },
54
56
  "engines": {
55
57
  "node": ">=20.0.0",
@@ -0,0 +1,297 @@
1
+ // Shared bounded IO for noninteractive CI commands. Never execute shell strings.
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const crypto = require("node:crypto");
6
+ const { spawnSync } = require("node:child_process");
7
+ const { gunzipSync } = require("node:zlib");
8
+ const { isDeepStrictEqual } = require("node:util");
9
+
10
+ const SEMVER =
11
+ /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?![\s\S])/;
12
+ const DIGEST = /^sha256:[a-f0-9]{64}(?![\s\S])/;
13
+ const HEX64 = /^[a-f0-9]{64}(?![\s\S])/;
14
+ const COMMIT = /^[a-f0-9]{40}(?![\s\S])/;
15
+ function ensure(condition, message) {
16
+ if (!condition) throw new Error(message);
17
+ }
18
+ function sha(data, algorithm = "sha256") {
19
+ return crypto.createHash(algorithm).update(data).digest("hex");
20
+ }
21
+ function sorted(value) {
22
+ if (Array.isArray(value)) return value.map(sorted);
23
+ if (value && typeof value === "object" && !Buffer.isBuffer(value))
24
+ return Object.fromEntries(
25
+ Object.keys(value)
26
+ .sort()
27
+ .map((key) => [key, sorted(value[key])]),
28
+ );
29
+ return value;
30
+ }
31
+ function stable(value) {
32
+ return JSON.stringify(sorted(value));
33
+ }
34
+ function json(value) {
35
+ try {
36
+ return JSON.parse(Buffer.isBuffer(value) ? value.toString("utf8") : value);
37
+ } catch {
38
+ throw new Error("Invalid JSON data");
39
+ }
40
+ }
41
+ function read(file) {
42
+ return json(fs.readFileSync(file));
43
+ }
44
+ function save(file, value) {
45
+ fs.mkdirSync(path.dirname(file), { recursive: true });
46
+ const tmp = file + ".tmp";
47
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
48
+ fs.renameSync(tmp, file);
49
+ }
50
+ function temp(prefix) {
51
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
52
+ }
53
+ function remove(directory) {
54
+ fs.rmSync(directory, { recursive: true, force: true });
55
+ }
56
+ async function temporary(prefix, action) {
57
+ const dir = temp(prefix);
58
+ try {
59
+ return await action(dir);
60
+ } finally {
61
+ remove(dir);
62
+ }
63
+ }
64
+ function safePath(value, absolute = false) {
65
+ return (
66
+ typeof value === "string" &&
67
+ !!value &&
68
+ !/[\\\x00-\x1f]/.test(value) &&
69
+ value.startsWith("/") === absolute &&
70
+ value
71
+ .replace(/^\//, "")
72
+ .split("/")
73
+ .every((part) => part && part !== "." && part !== "..")
74
+ );
75
+ }
76
+ function repositoryPath(root, relative) {
77
+ ensure(safePath(relative), "Expected a repository-relative path");
78
+ const base = fs.realpathSync(root);
79
+ const target = path.resolve(base, relative);
80
+ let cursor = base;
81
+ for (const part of relative.split("/")) {
82
+ cursor = path.join(cursor, part);
83
+ if (
84
+ fs.existsSync(cursor) ||
85
+ (() => {
86
+ try {
87
+ fs.lstatSync(cursor);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ })()
93
+ ) {
94
+ const real = fs.realpathSync(cursor);
95
+ ensure(real.startsWith(base + path.sep), "Path leaves repository");
96
+ }
97
+ }
98
+ return target;
99
+ }
100
+ function exec(
101
+ args,
102
+ {
103
+ cwd,
104
+ env = process.env,
105
+ input,
106
+ allowed = [0],
107
+ timeout = 1800000,
108
+ maxBuffer = 32 * 1024 * 1024,
109
+ binary = false,
110
+ label,
111
+ } = {},
112
+ ) {
113
+ ensure(
114
+ Array.isArray(args) &&
115
+ args.length &&
116
+ args.every((arg) => typeof arg === "string" && !arg.includes("\0")),
117
+ "Expected command argv",
118
+ );
119
+ const result = spawnSync(args[0], args.slice(1), {
120
+ cwd,
121
+ env,
122
+ input,
123
+ timeout,
124
+ maxBuffer,
125
+ encoding: binary ? null : "utf8",
126
+ windowsHide: true,
127
+ });
128
+ ensure(
129
+ !result.error && allowed.includes(result.status),
130
+ `${label || path.basename(args[0]) + " " + (args[1] || "")} failed (exit ${result.status})`,
131
+ );
132
+ return {
133
+ stdout: result.stdout,
134
+ stderr: result.stderr,
135
+ status: result.status,
136
+ };
137
+ }
138
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
139
+ async function responseBytes(response, limit) {
140
+ const chunks = [];
141
+ let size = 0;
142
+ for await (const chunk of response.body || []) {
143
+ size += chunk.length;
144
+ if (size > limit) {
145
+ await response.body?.cancel?.().catch(() => {});
146
+ throw new Error("Download limit exceeded");
147
+ }
148
+ chunks.push(Buffer.from(chunk));
149
+ }
150
+ return Buffer.concat(chunks);
151
+ }
152
+ async function request(
153
+ url,
154
+ {
155
+ method = "GET",
156
+ headers = {},
157
+ body,
158
+ limit = 32 * 1024 * 1024,
159
+ timeout = 60000,
160
+ fetchImpl = fetch,
161
+ } = {},
162
+ ) {
163
+ const controller = new AbortController();
164
+ const timer = setTimeout(() => controller.abort(), timeout);
165
+ try {
166
+ const response = await fetchImpl(url, {
167
+ method,
168
+ headers,
169
+ body,
170
+ redirect: "manual",
171
+ signal: controller.signal,
172
+ });
173
+ const bytes = await responseBytes(response, limit);
174
+ return { status: response.status, headers: response.headers, body: bytes };
175
+ } finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
179
+ // Extract data only. No archive entry is ever written to the filesystem.
180
+ function tarEntries(
181
+ gzip,
182
+ {
183
+ maxUnpacked = 1024 * 1024 * 1024,
184
+ maxFile = 32 * 1024 * 1024,
185
+ maxEntries = 30000,
186
+ } = {},
187
+ ) {
188
+ const bytes = gunzipSync(gzip, {
189
+ maxOutputLength: maxUnpacked + maxEntries * 1024,
190
+ });
191
+ const entries = [];
192
+ let offset = 0;
193
+ let total = 0;
194
+ let pax = {};
195
+ let longName;
196
+ const str = (data) => data.toString("utf8").replace(/\0.*(?![\s\S])/s, "");
197
+ while (offset + 512 <= bytes.length) {
198
+ const header = bytes.subarray(offset, offset + 512);
199
+ offset += 512;
200
+ if (header.every((byte) => byte === 0)) break;
201
+ const sizeText = str(header.subarray(124, 136)).trim();
202
+ ensure(/^[0-7]+(?![\s\S])/.test(sizeText), "Invalid tar entry size");
203
+ const size = parseInt(sizeText, 8);
204
+ const expected = parseInt(str(header.subarray(148, 156)).trim(), 8);
205
+ const sum = header.reduce(
206
+ (sum, b, i) => sum + (i >= 148 && i < 156 ? 32 : b),
207
+ 0,
208
+ );
209
+ ensure(
210
+ expected === sum && size <= maxFile && offset + size <= bytes.length,
211
+ "Invalid or oversized tar entry",
212
+ );
213
+ const data = bytes.subarray(offset, offset + size);
214
+ offset += Math.ceil(size / 512) * 512;
215
+ const type = String.fromCharCode(header[156] || 48);
216
+ if (type === "x") {
217
+ pax = {};
218
+ let pos = 0;
219
+ while (pos < data.length) {
220
+ const space = data.indexOf(32, pos);
221
+ ensure(space > pos, "Invalid PAX record");
222
+ const len = Number(data.subarray(pos, space).toString());
223
+ ensure(
224
+ Number.isInteger(len) &&
225
+ len > space - pos + 1 &&
226
+ pos + len <= data.length &&
227
+ data[pos + len - 1] === 10,
228
+ "Invalid PAX length",
229
+ );
230
+ const row = data.subarray(space + 1, pos + len - 1).toString();
231
+ const split = row.indexOf("=");
232
+ ensure(split > 0, "Invalid PAX value");
233
+ pax[row.slice(0, split)] = row.slice(split + 1);
234
+ pos += len;
235
+ }
236
+ continue;
237
+ }
238
+ if (type === "L") {
239
+ longName = str(data).replace(/\n(?![\s\S])/, "");
240
+ continue;
241
+ }
242
+ const prefix =
243
+ str(header.subarray(257, 263)) === "ustar"
244
+ ? str(header.subarray(345, 500))
245
+ : "";
246
+ const name =
247
+ pax.path ||
248
+ longName ||
249
+ (prefix ? prefix + "/" : "") + str(header.subarray(0, 100));
250
+ ensure(
251
+ pax.size === undefined || Number(pax.size) === size,
252
+ "Unsupported PAX size",
253
+ );
254
+ pax = {};
255
+ longName = undefined;
256
+ ensure(
257
+ type === "0" || type === "5",
258
+ "Links and special archive entries are forbidden",
259
+ );
260
+ total += size;
261
+ ensure(
262
+ total <= maxUnpacked && entries.length < maxEntries,
263
+ "Archive expansion limit exceeded",
264
+ );
265
+ entries.push({
266
+ name: name.replace(/\/(?![\s\S])/, ""),
267
+ directory: type === "5",
268
+ data,
269
+ });
270
+ }
271
+ return entries;
272
+ }
273
+ module.exports = {
274
+ fs,
275
+ path,
276
+ SEMVER,
277
+ DIGEST,
278
+ HEX64,
279
+ COMMIT,
280
+ ensure,
281
+ sha,
282
+ stable,
283
+ sorted,
284
+ json,
285
+ read,
286
+ save,
287
+ temp,
288
+ remove,
289
+ temporary,
290
+ safePath,
291
+ repositoryPath,
292
+ exec,
293
+ sleep,
294
+ request,
295
+ tarEntries,
296
+ equal: isDeepStrictEqual,
297
+ };
@@ -3,6 +3,7 @@ const { parseGitlabUrl } = require('../integrations/gitlab-url');
3
3
  const HELM_REGISTRY_CHANNEL = 'stable';
4
4
  const REGISTRY_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
5
5
  const REGISTRY_WAIT_INTERVAL_MS = 15 * 1000;
6
+ let legacyTokenWarningShown = false;
6
7
 
7
8
  function sleep(ms) {
8
9
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -88,11 +89,15 @@ async function fetchChartVersionFromRegistry(chartName, version) {
88
89
  return { ok: false, reason: 'Не удалось определить GitLab-проект из remote "origin".' };
89
90
  }
90
91
 
91
- const accessToken = process.env.SC_OWNER_PAT;
92
+ const accessToken = process.env.SC_DEVELOPER_PAT || process.env.SC_OWNER_PAT;
93
+ if (!process.env.SC_DEVELOPER_PAT && accessToken && !legacyTokenWarningShown) {
94
+ logError('⚠️', 'SC_OWNER_PAT устарела (deprecated) и будет удалена в следующей версии spectrum-cli. Замените её на SC_DEVELOPER_PAT.');
95
+ legacyTokenWarningShown = true;
96
+ }
92
97
  if (!accessToken) {
93
98
  return {
94
99
  ok: false,
95
- reason: 'Не задана переменная окружения SC_OWNER_PAT. Она нужна для проверки публикации чарта в Helm-registry.'
100
+ reason: 'Не задана переменная окружения SC_DEVELOPER_PAT. Она нужна для проверки публикации чарта в Helm-registry.'
96
101
  };
97
102
  }
98
103