Files
admin/scripts/schedjs-npm-oidc-setup.ps1

165 lines
8.0 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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', 'login', 'npm', 'verify', 'all')]
[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 Ensure-WebLogin {
$npmrc = Join-Path $env:USERPROFILE '.npmrc'
$content = if (Test-Path $npmrc) { Get-Content $npmrc -Raw } else { '' }
if ($content -notmatch 'registry\.npmjs\.org/:_authToken=npm_') { Say '== .npmrc: automation-токена нет — логин уже web-сессия (или токен иной формы)'; return }
Say '== .npmrc: automation-токен npm_… — для trust нужна web-сессия. Откроется браузер, введи код 2FA.'
$old = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
try { & npm login --auth-type=web } finally { $ErrorActionPreference = $old }
if ($LASTEXITCODE -ne 0) { Fail "npm login завершился с кодом $LASTEXITCODE — повтори вручную: npm login --auth-type=web" }
Say ' логин ок'
}
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 {
Ensure-WebLogin
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 }
'login' { Ensure-WebLogin }
'npm' { Phase-Npm }
'verify' { Phase-Verify }
'all' { Phase-Npm; Say ''; Phase-Verify }
}