165 lines
8.0 KiB
PowerShell
165 lines
8.0 KiB
PowerShell
# 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 }
|
||
}
|