From 28c70c709321760fde6d2c8af6732aa2b4668e10 Mon Sep 17 00:00:00 2001 From: vitya Date: Sun, 20 Sep 2026 23:51:26 +0300 Subject: [PATCH] =?UTF-8?q?scripts(schedjs):=20=D1=80=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=8F=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=B0=20OIDC-=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20(env=20npm-release=20+=20npm=20trust;=20ta?= =?UTF-8?q?sk:3130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/schedjs-npm-oidc-setup.ps1 | 150 +++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 scripts/schedjs-npm-oidc-setup.ps1 diff --git a/scripts/schedjs-npm-oidc-setup.ps1 b/scripts/schedjs-npm-oidc-setup.ps1 new file mode 100644 index 0000000..e8daabb --- /dev/null +++ b/scripts/schedjs-npm-oidc-setup.ps1 @@ -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 } +}