scripts(schedjs): разовая настройка OIDC-публикации (env npm-release + npm trust; task:3130)
This commit is contained in:
150
scripts/schedjs-npm-oidc-setup.ps1
Normal file
150
scripts/schedjs-npm-oidc-setup.ps1
Normal 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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user