scripts(schedjs): разовая настройка OIDC-публикации (env npm-release + npm trust; task:3130)

This commit is contained in:
2026-09-20 23:51:26 +03:00
parent 560711af5c
commit 28c70c7093

View File

@@ -0,0 +1,150 @@
# schedjs-npm-oidc-setup.ps1 — разовая настройка OIDC-публикации @schedjs/* (task:3130).
#
# Зачем: npm не даёт automation-токену публиковать/настраивать без 2FA — версию выкладывает
# человек. Trusted publisher (OIDC) снимает это: публикует GitHub Actions, токена нет вовсе.
#
# Фазы (можно по одной):
# -Phase github — создать environment `npm-release` + branch policy «только main» (без 2FA)
# -Phase npm — trusted publisher у 9 пакетов scope (НУЖЕН web-логин с 2FA, см. ниже)
# -Phase verify — показать, что trust реально встал
#
# Предусловия для -Phase npm (иначе будет 403 «Two-factor authentication is required»):
# 1) npm >= 11.15.0
# 2) npm login --auth-type=web (заменить automation-токен npm_… на web-сессию с 2FA)
# 3) 2FA на аккаунте включена; режим «Authorization and writes»
#
# Примеры (на машине только PowerShell 5.1, `pwsh` нет):
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase github
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase npm
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase npm -Otp 123456 # код из аутентификатора
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase verify
[CmdletBinding()]
param(
[ValidateSet('github', 'npm', 'verify')]
[string]$Phase = 'github',
[string]$Repo = 'schedjs/schedjs',
[string]$WorkflowFile = 'release-npm.yml',
[string]$Environment = 'npm-release',
# 6-значный TOTP, если npm просит его неинтерактивно (или возьми код из аутентификатора на экран)
[string]$Otp = ''
)
$ErrorActionPreference = 'Stop'
$pkgs = @('core', 'storage-mongo', 'storage-mysql', 'storage-postgres', 'admin-api', 'mcp', 'ui', 'daemon', 'cli')
function Say([string]$m) { Write-Host $m }
function Fail([string]$m) { Write-Host "FAIL: $m" -ForegroundColor Red; exit 1 }
# npm пишет notice в stderr; при $ErrorActionPreference='Stop' PS 5.1 делает из этого
# завершающую ошибку (NativeCommandError) — глушим только на время вызова и смотрим код.
function Invoke-Npm([string[]]$NpmArgs) {
$old = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$text = (& npm @NpmArgs 2>&1 | Out-String)
$code = $LASTEXITCODE
}
finally { $ErrorActionPreference = $old }
[pscustomobject]@{ Code = $code; Text = $text }
}
function Get-BranchPolicies {
# до включения custom_branch_policies эндпоинт отдаёт 404 — это норма, не ошибка
@(gh api "repos/$Repo/environments/$Environment/deployment-branch-policies" --jq '.branch_policies[]?.name' 2>$null)
}
function Phase-GitHub {
Say "== GitHub environment '$Environment' в $Repo"
# 1) создать/обновить environment и включить custom branch policies
'{"deployment_branch_policy":{"protected_branches":false,"custom_branch_policies":true}}' |
gh api -X PUT "repos/$Repo/environments/$Environment" --input - --jq '{name,deployment_branch_policy}' |
ForEach-Object { Say " environment: $_" }
# 2) policy «только main» (идемпотентно)
if ((Get-BranchPolicies) -notcontains 'main') {
Say " добавляю policy: branch main"
gh api -X POST "repos/$Repo/environments/$Environment/deployment-branch-policies" -f name=main -f type=branch --jq '{name,type}' 2>$null | ForEach-Object { Say " policy: $_" }
}
else {
Say " policy 'main' уже есть — идемпотентно"
}
# 3) verify живьём
$now = Get-BranchPolicies
if ($now -notcontains 'main') { Fail "policy main не встала (получили: $($now -join ', '))" }
Say " verify: $(($now | ForEach-Object { "branch:$_" }) -join ', ')"
Say "OK: environment готов (секреты не нужны — OIDC)"
}
function Assert-NpmAuth {
$npmver = (Invoke-Npm @('-v')).Text.Trim()
Say "== npm $npmver (нужно >= 11.15.0)"
if ([version]($npmver -replace '[^0-9.].*$', '') -lt [version]'11.15.0') { Fail "npm старый: $npmver" }
$who = (Invoke-Npm @('whoami')).Text.Trim()
if (-not $who -or $who -match 'npm error') { Fail "npm не залогинен. Сделай: npm login --auth-type=web — и повтори -Phase npm" }
Say " whoami: $who"
$npmrc = Join-Path $env:USERPROFILE '.npmrc'
if (Test-Path $npmrc) {
$content = Get-Content $npmrc -Raw
$backup = Join-Path $env:USERPROFILE '.npmrc.bak-npmtrust'
if (-not (Test-Path $backup)) {
Copy-Item $npmrc $backup
Say " бэкап .npmrc -> $backup"
}
if ($content -match 'registry\.npmjs\.org/:_authToken=npm_') {
Fail @"
в .npmrc лежит automation-токен npm_ по нему trust всегда даёт 403 'Two-factor authentication is required'.
Лечение (однажды):
1) npm login --auth-type=web (браузер + код из аутентификатора; перезапишет токен в .npmrc)
2) повторить: -Phase npm
3) вернуть старый токен: Copy-Item `$env:USERPROFILE\.npmrc.bak-npmtrust `$env:USERPROFILE\.npmrc -Force
"@
}
}
}
function Phase-Npm {
Assert-NpmAuth
Say "== trusted publisher: $($pkgs.Count) пакетов @schedjs/*"
$ok = @(); $bad = @()
foreach ($p in $pkgs) {
$argsList = @('trust', 'github', "@schedjs/$p", '--repo', $Repo, '--file', $WorkflowFile,
'--env', $Environment, '--allow-publish', '--yes')
if ($Otp) { $argsList += "--otp=$Otp" }
Write-Host (" " + "@schedjs/$p") -NoNewline
$r = Invoke-Npm $argsList
if ($r.Code -eq 0) { Write-Host 'OK' -ForegroundColor Green; $ok += $p }
else {
Write-Host 'FAIL' -ForegroundColor Red
$bad += $p
($r.Text.Trim() -split "`n" | Select-Object -Last 3) | ForEach-Object { Write-Host " $_" }
}
}
Say ""
Say "итог: ok $($ok.Count)/$($pkgs.Count)$(if ($bad) { ' — упали: ' + ($bad -join ', ') })"
if ($bad.Count) { Say "подсказка: 403 → истёк OTP или не web-логин; 404 → пакета нет на npm"; exit 1 }
}
function Phase-Verify {
Say "== npm trust list по 9 пакетам (ищем $WorkflowFile / $Environment)"
$miss = @()
foreach ($p in $pkgs) {
$out = (Invoke-Npm @('trust', 'list', "@schedjs/$p")).Text
$hit = ($out -match [regex]::Escape($WorkflowFile)) -and ($out -match [regex]::Escape($Environment))
if ($hit) { Write-Host (" " + "@schedjs/$p : OK") -ForegroundColor Green }
else { Write-Host (" " + "@schedjs/$p : НЕТ trust") -ForegroundColor Yellow; $miss += $p }
}
if ($miss.Count) { Say "нет trust у: $($miss -join ', ')"; exit 1 }
Say "OK: trust у всех 9"
}
switch ($Phase) {
'github' { Phase-GitHub }
'npm' { Phase-Npm }
'verify' { Phase-Verify }
}