From b4db1c242c7ea98497ed80f0aebe638536609c02 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Mon, 3 Aug 2026 14:17:47 +0800 Subject: [PATCH] fix(release): kill broader locks before npm ci (rolldown EPERM) Co-authored-by: Cursor --- src/release/pipeline.mjs | 60 ++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/release/pipeline.mjs b/src/release/pipeline.mjs index 8558f68..3f4107c 100644 --- a/src/release/pipeline.mjs +++ b/src/release/pipeline.mjs @@ -3,18 +3,60 @@ import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); -/** Stop vite/electron dev servers that lock dnd_player node_modules during npm ci. */ +/** + * Stop Vite/Electron/dev.mjs/TTRPGPlayer that lock native .node files under + * dnd_player/node_modules (EPERM unlink on @rolldown during npm ci). + * @param {string} projectRoot + */ export async function killBlockingDevProcesses(projectRoot) { if (process.platform !== 'win32') return; + + const rootEsc = projectRoot.replace(/'/g, "''"); + const script = ` +$ErrorActionPreference = 'SilentlyContinue' +$root = '${rootEsc}' +$killed = [System.Collections.Generic.HashSet[int]]::new() +foreach ($name in @('TTRPGPlayer','electron')) { + Get-Process -Name $name | ForEach-Object { + Stop-Process -Id $_.Id -Force + [void]$killed.Add([int]$_.Id) + } +} +Get-CimInstance Win32_Process -Filter "Name='node.exe'" | ForEach-Object { + $cmd = [string]$_.CommandLine + if (-not $cmd) { return } + $touches = ($cmd -like ('*' + $root + '*')) -or ($cmd -match 'dnd_player') + if (-not $touches) { return } + if ($cmd -match 'dev\\.mjs|vite|electron|esbuild|rolldown|tsx|scripts[/\\\\]dev') { + Stop-Process -Id $_.ProcessId -Force + [void]$killed.Add([int]$_.ProcessId) + } +} +foreach ($port in @(5173, 5174)) { + try { + Get-NetTCPConnection -LocalPort $port -ErrorAction Stop | ForEach-Object { + $ownerPid = [int]$_.OwningProcess + if ($ownerPid -gt 0) { + Stop-Process -Id $ownerPid -Force + [void]$killed.Add($ownerPid) + } + } + } catch {} +} +Start-Sleep -Seconds 2 +$rolldown = Join-Path $root 'node_modules\\@rolldown\\binding-win32-x64-msvc' +if (Test-Path -LiteralPath $rolldown) { + Remove-Item -LiteralPath $rolldown -Recurse -Force -ErrorAction SilentlyContinue +} +Start-Sleep -Seconds 1 +Write-Output ('killed=' + $killed.Count) +`; + try { const { stdout } = await execFileAsync( 'powershell', - [ - '-NoProfile', - '-Command', - `Get-CimInstance Win32_Process -Filter "name='node.exe'" | Where-Object { $_.CommandLine -match 'dnd_player' -and ($_.CommandLine -match 'vite dev|electron') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`, - ], - { windowsHide: true }, + ['-NoProfile', '-Command', script], + { windowsHide: true, timeout: 30000 }, ); void stdout; } catch { @@ -24,6 +66,7 @@ export async function killBlockingDevProcesses(projectRoot) { /** * @param {string} cwd + * @param {string} scriptName * @param {string[]} args * @param {(line: string) => void} onLine */ @@ -62,7 +105,7 @@ export function runPowerShellScript(cwd, scriptName, args, onLine) { }); } -/** @param {string} projectRoot */ +/** @param {string} projectRoot @param {(line: string) => void} onLine */ export async function gitPull(projectRoot, onLine) { onLine('git pull…'); await new Promise((resolve, reject) => { @@ -88,6 +131,7 @@ export async function gitPull(projectRoot, onLine) { /** @param {string} line @returns {number | null} */ export function progressFromLogLine(line) { if (/Mac artifacts check/i.test(line)) return 12; + if (/Stopping processes that may lock/i.test(line)) return 20; if (/Build Windows/i.test(line)) return 22; if (/npm ci/i.test(line)) return 28; if (/pack:win/i.test(line)) return 38;