CI: переход публикации npm-пакетов на Trusted Publishing (OIDC)
Что такое Trusted Publishing
Trusted Publishing (TP) — механизм при котором публикация выполняется без npm-токена. При каждом релизе CI получает от GitHub короткоживущий OIDC-токен — подписанный JWT с данными о репозитории и workflow, — а реестр обменивает его на одноразовый publish-credential, если эти данные совпадают с конфигурацией Trusted Publisher пакета (org/repo + имя workflow-файла).
Преимущества:
- Нет долгоживущего секрета.
NPM_REGISTRY_TOKEN становится не нужен: нечего красть, ротировать и нечему утекать. Компрометация CI или секретов не даёт постоянного доступа к публикации — именно через украденные npm-токены проходили крупные supply-chain-атаки последних лет.
- Минимальная область действия. Обменянный credential живёт минуты и действует на один пакет — в отличие от общего токена с правом публикации во все пакеты организации.
- Provenance-аттестации автоматически. Каждая версия получает подписанное подтверждение (SLSA), из какого репозитория, коммита и workflow она собрана; на странице пакета появляется бейдж Provenance, потребители могут проверять подписи (
npm audit signatures).
- Публикация только из зафиксированного workflow. После включения на пакетах «Require 2FA and disallow tokens» опубликовать версию мимо CI — в том числе украденным токеном — невозможно.
Что сделано
- Единый входной workflow
publish-npm.yml, объединивший publish-latest / publish-next / publish-rc / publish-canary (старые файлы удалены). Вся логика публикации по-прежнему в reusable publish-common.yml:
push в master → latest-релиз (+ changelog и комментарии в PR);
pull_request → canary-версии;
workflow_dispatch с input release → rc (с текущей ветки) или next-ветка.
permissions: id-token: write в publish-jobs входного workflow и в publish-common.yml — право на выпуск OIDC-токена (npm требует его в обоих звеньях цепочки).
- Поле
repository во всех публичных package.json (33 файла): канонический git+https://github.com/salute-developers/plasma.git + directory. Добавлено 11 отсутствовавших, исправлены битые: чужой репозиторий (pashka) в plasma-tokens-utils, неверные directory в plasma-hope и sdds-finai, невалидный ssh://-формат в остальных. Это требование provenance: реестр сверяет поле с фактическим репозиторием сборки, при расхождении публикация падает с 422.
- Нормализовано
contributors (9 пакетов) → ["salute.developers@gmail.com"].
- Убран
pull_request_target из publish-цепочки: canary публикуется только для PR из основного репозитория. Выдавать id-token: write в контексте, где исполняется код из форка, недопустимо.
Почему именно так
Почему один входной workflow. У пакета может быть ровно одна TP-конфигурация, и npm валидирует workflow, запущенный событием (claim workflow_ref), а не файл, где физически выполняется npm publish. Подтверждено экспериментом на @salutejs/plasma-web: регистрация publish-common.yml → все обмены падают с OIDC token exchange error - package not found; регистрация входного файла → обмен успешен. Покрыть четыре старых входа одной конфигурацией невозможно, поэтому вход один.
Почему миграция ничего не ломает. lerna 9 при неудачном OIDC-обмене молча продолжает публикацию с прежним токеном из .npmrc. Пакеты без настроенного TP публикуются по-старому — раскатывать конфигурации можно постепенно, релизы не блокируются.
Дальнейшие шаги
CI: переход публикации npm-пакетов на Trusted Publishing (OIDC)
Что такое Trusted Publishing
Trusted Publishing (TP) — механизм при котором публикация выполняется без npm-токена. При каждом релизе CI получает от GitHub короткоживущий OIDC-токен — подписанный JWT с данными о репозитории и workflow, — а реестр обменивает его на одноразовый publish-credential, если эти данные совпадают с конфигурацией Trusted Publisher пакета (org/repo + имя workflow-файла).
Преимущества:
NPM_REGISTRY_TOKENстановится не нужен: нечего красть, ротировать и нечему утекать. Компрометация CI или секретов не даёт постоянного доступа к публикации — именно через украденные npm-токены проходили крупные supply-chain-атаки последних лет.npm audit signatures).Что сделано
publish-npm.yml, объединившийpublish-latest/publish-next/publish-rc/publish-canary(старые файлы удалены). Вся логика публикации по-прежнему в reusablepublish-common.yml:pushв master → latest-релиз (+ changelog и комментарии в PR);pull_request→ canary-версии;workflow_dispatchс inputrelease→ rc (с текущей ветки) или next-ветка.permissions: id-token: writeв publish-jobs входного workflow и вpublish-common.yml— право на выпуск OIDC-токена (npm требует его в обоих звеньях цепочки).repositoryво всех публичных package.json (33 файла): каноническийgit+https://github.com/salute-developers/plasma.git+directory. Добавлено 11 отсутствовавших, исправлены битые: чужой репозиторий (pashka) вplasma-tokens-utils, неверныеdirectoryвplasma-hopeиsdds-finai, невалидныйssh://-формат в остальных. Это требование provenance: реестр сверяет поле с фактическим репозиторием сборки, при расхождении публикация падает с 422.contributors(9 пакетов) →["salute.developers@gmail.com"].pull_request_targetиз publish-цепочки: canary публикуется только для PR из основного репозитория. Выдаватьid-token: writeв контексте, где исполняется код из форка, недопустимо.Почему именно так
Почему один входной workflow. У пакета может быть ровно одна TP-конфигурация, и npm валидирует workflow, запущенный событием (claim
workflow_ref), а не файл, где физически выполняетсяnpm publish. Подтверждено экспериментом на@salutejs/plasma-web: регистрацияpublish-common.yml→ все обмены падают сOIDC token exchange error - package not found; регистрация входного файла → обмен успешен. Покрыть четыре старых входа одной конфигурацией невозможно, поэтому вход один.Почему миграция ничего не ломает. lerna 9 при неудачном OIDC-обмене молча продолжает публикацию с прежним токеном из
.npmrc. Пакеты без настроенного TP публикуются по-старому — раскатывать конфигурации можно постепенно, релизы не блокируются.Дальнейшие шаги
NPM_REGISTRY_TOKENиз publish-цепочки и шагnpm whoamiизpublish-common.ymlnpm trust→ проверка аттестации