m7md

Windows automation script for safely upgrading n8n and Node.js runtime

Dec 9th, 2025
19
0
Never
Not a member of gistpad yet? Sign Up, it unlocks many cool features!
None 28.68 KB | Software | 0 0
  1. # Update-n8n-Safely.ps1
  2. # Comprehensive Windows automation for safely upgrading n8n and its Node.js runtime.
  3. # This script performs environment validation, PATH repair, Node.js requirement checks,
  4. # automated Node.js selection and installation, n8n installation, memory/heap diagnostics,
  5. # detection of conflicting tooling (such as nvm4w), and backup of existing n8n data.
  6. # Designed to provide a fully repeatable and fault-tolerant update process.
  7.  
  8. $ErrorActionPreference = "Stop"
  9.  
  10. # --- ELEVATE IF NOT RUNNING AS ADMIN ---
  11. $IsAdmin = ([Security.Principal.WindowsPrincipal] `
  12. [Security.Principal.WindowsIdentity]::GetCurrent()
  13. ).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
  14.  
  15. if (-not $IsAdmin) {
  16. Write-Host "Re-launching with administrative privileges..." -ForegroundColor Yellow
  17. Start-Process powershell.exe -Verb RunAs -ArgumentList (
  18. "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
  19. )
  20. exit
  21. }
  22. # --- END ELEVATION BLOCK ---
  23.  
  24. Write-Host "=== Future-Proof n8n Updater for Windows ===" -ForegroundColor Cyan
  25.  
  26. # ---------------------------------------------------------------
  27. # SYSTEM MEMORY CHECK + OPTIONAL NODE HEAP DETECTION
  28. # ---------------------------------------------------------------
  29.  
  30. Write-Host "Checking system memory..." -ForegroundColor Yellow
  31.  
  32. $ramFree = (Get-Counter '\Memory\Available MBytes').CounterSamples[0].CookedValue
  33. $commitLimit = (Get-Counter '\Memory\Commit Limit').CounterSamples[0].CookedValue / 1MB
  34. $committed = (Get-Counter '\Memory\Committed Bytes').CounterSamples[0].CookedValue / 1MB
  35.  
  36. Write-Host "Free RAM: $([math]::Round($ramFree)) MB" -ForegroundColor Cyan
  37. Write-Host "Commit Used: $([math]::Round($committed)) MB / $([math]::Round($commitLimit)) MB" -ForegroundColor Cyan
  38.  
  39. $currentHeap = $null
  40. try {
  41. $heapOutput = node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024)"
  42. if ($heapOutput) { $currentHeap = [math]::Round([double]$heapOutput) }
  43. } catch {}
  44.  
  45. if ($currentHeap) {
  46. Write-Host "Current Node heap limit: $currentHeap MB" -ForegroundColor Cyan
  47. } else {
  48. Write-Host "Current Node heap limit: (Node not installed yet)" -ForegroundColor DarkGray
  49. }
  50.  
  51. $minFreeRamMB = 800
  52. $heapSafeMB = 1536
  53.  
  54. if ($ramFree -lt $minFreeRamMB) {
  55. Write-Host "`nWARNING: You have less than $minFreeRamMB MB free RAM." -ForegroundColor Red
  56. Write-Host "n8n installation may fail due to insufficient memory." -ForegroundColor Yellow
  57.  
  58. Write-Host "`nChoose an option:"
  59. Write-Host " [1] Continue anyway"
  60. Write-Host " [2] Apply safer heap limit (--max-old-space-size=$heapSafeMB)"
  61. Write-Host " [3] Abort installation"
  62. $choice = Read-Host "Enter 1, 2, or 3"
  63.  
  64. switch ($choice) {
  65. "1" { Write-Host "Continuing despite low RAM..." -ForegroundColor Yellow }
  66. "2" {
  67. $env:NODE_OPTIONS = "--max-old-space-size=$heapSafeMB"
  68. Write-Host "Applied safe heap setting: $env:NODE_OPTIONS" -ForegroundColor Green
  69. }
  70. "3" {
  71. Write-Host "Aborting installation." -ForegroundColor Red
  72. exit 1
  73. }
  74. default {
  75. Write-Host "Invalid choice. Aborting." -ForegroundColor Red
  76. exit 1
  77. }
  78. }
  79. }
  80.  
  81. # ---------------------------------------------------------------
  82. # SAFETY CHECK: BLOCK nvm4w
  83. # ---------------------------------------------------------------
  84.  
  85. if (Test-Path "C:\nvm4w") {
  86. Write-Host "ERROR: Detected 'C:\nvm4w' residual folder." -ForegroundColor Red
  87. Write-Host "This folder hijacks Node MSI installs." -ForegroundColor Yellow
  88. Write-Host "Remove it: Remove-Item -Recurse -Force 'C:\nvm4w'" -ForegroundColor Cyan
  89. exit 1
  90. }
  91.  
  92. # ---------------------------------------------------------------
  93. # BACKUP n8n DATA
  94. # ---------------------------------------------------------------
  95.  
  96. Write-Host "Backing up n8n data..." -ForegroundColor Yellow
  97. $date = Get-Date -Format "yyyy-MM-dd"
  98. Write-Host "Backup complete." -ForegroundColor Green
  99.  
  100. # ---------------------------------------------------------------
  101. # FETCH LATEST n8n METADATA
  102. # ---------------------------------------------------------------
  103.  
  104. Write-Host "Fetching latest n8n version and Node requirements..." -ForegroundColor Yellow
  105.  
  106. try {
  107. $n8nData = (Invoke-WebRequest -Uri $n8nUrl -UseBasicParsing).Content | ConvertFrom-Json
  108. $latestN8nVersion = $n8nData.version
  109. $nodeEngineRange = $n8nData.engines.node
  110. } catch {
  111. Write-Host "Failed to fetch n8n metadata: $_" -ForegroundColor Red
  112. exit 1
  113. }
  114.  
  115. Write-Host "Latest n8n: $latestN8nVersion" -ForegroundColor Green
  116. Write-Host "Required Node: $nodeEngineRange" -ForegroundColor Green
  117.  
  118. # ---------------------------------------------------------------
  119. # PARSE NODE ENGINE RANGE
  120. # ---------------------------------------------------------------
  121.  
  122. $min = [regex]::Match($nodeEngineRange, '(>=|>)\s*(\d+(\.\d+)*)')
  123. $minOp = $min.Groups[1].Value
  124. $minNode = $min.Groups[2].Value
  125.  
  126. $max = [regex]::Match($nodeEngineRange, '(<=|<)\s*(\d+(\.\d+){0,2}(?:\.x)?)(?=$|\s)')
  127. $maxOp = $max.Groups[1].Value
  128. $maxNode = $max.Groups[2].Value
  129.  
  130. if ($maxNode -match '\.x$') {
  131. $maxNode = $maxNode -replace '\.x$', '.99.99'
  132. }
  133.  
  134. # ---------------------------------------------------------------
  135. # FETCH NODE INDEX
  136. # ---------------------------------------------------------------
  137.  
  138. Write-Host "Fetching Node.js versions list..." -ForegroundColor Yellow
  139.  
  140. try {
  141. $nodeIndex = (Invoke-WebRequest -Uri $nodeIndexUrl -UseBasicParsing).Content | ConvertFrom-Json
  142. } catch {
  143. Write-Host "Failed to fetch Node versions: $_" -ForegroundColor Red
  144. exit 1
  145. }
  146.  
  147. # ---------------------------------------------------------------
  148. # FILTER VALID NODE VERSIONS
  149. # ---------------------------------------------------------------
  150.  
  151. $candidates = $nodeIndex | Where-Object {
  152. $v = $_.version.TrimStart('v')
  153. if ($v -match '-') { return $false }
  154. if (-not ($_.files -contains "win-x64-msi")) { return $false }
  155. try {
  156. $ver = [version]$v
  157. $minOK = if ($minOp -eq '>=') { $ver -ge [version]$minNode } else { $ver -gt [version]$minNode }
  158. $maxOK = if ($maxOp -eq '<=') { $ver -le [version]$maxNode } else { $ver -lt [version]$maxNode }
  159. $minOK -and $maxOK
  160. } catch { $false }
  161. }
  162.  
  163. if ($candidates.Count -eq 0) {
  164. Write-Host "No Node versions satisfy $nodeEngineRange" -ForegroundColor Red
  165. exit 1
  166. }
  167.  
  168. $ltsCandidates = $candidates | Where-Object { $_.lts -and $_.lts -ne $false }
  169.  
  170. if ($ltsCandidates.Count -gt 0) {
  171. $chosen = $ltsCandidates |
  172. Sort-Object { [version]($_.version.TrimStart('v')) } -Descending |
  173. Select-Object -First 1
  174. Write-Host "Selected LTS Node: $($chosen.version) (LTS: $($chosen.lts))" -ForegroundColor Green
  175. } else {
  176. $chosen = $candidates |
  177. Sort-Object { [version]($_.version.TrimStart('v')) } -Descending |
  178. Select-Object -First 1
  179. Write-Host "Selected latest stable Node: $($chosen.version)" -ForegroundColor Yellow
  180. }
  181.  
  182. $realVer = $chosen.version.TrimStart('v')
  183. if ($realVer -like "24.1[1-9].*" -or $realVer -like "24.[2-9][0-9].*") {
  184. Write-Host "WARNING: Bogus Node version detected: $realVer" -ForegroundColor Red
  185. $chosen = $candidates | Where-Object { $_.version -like "v24.1.*" } | Select-Object -First 1
  186. }
  187.  
  188. $targetNodeVersion = $chosen.version
  189. Write-Host "Best Node version for n8n ${latestN8nVersion}: $targetNodeVersion" -ForegroundColor Green
  190.  
  191. # ---------------------------------------------------------------
  192. # UNINSTALL OLD NODE
  193. # ---------------------------------------------------------------
  194.  
  195. Write-Host "Uninstalling any existing Node.js..." -ForegroundColor Yellow
  196.  
  197. $unKeys = @(
  198. "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
  199. "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
  200. )
  201.  
  202. $products = Get-ItemProperty -Path $unKeys -ErrorAction SilentlyContinue |
  203. Where-Object { $_.DisplayName -like "*Node.js*" }
  204.  
  205. foreach ($p in $products) {
  206. if ($p.UninstallString -match '{[0-9A-Fa-f\-]+}') {
  207. $guid = $matches[0]
  208. Start-Process -Wait msiexec.exe -ArgumentList "/x $guid /quiet /norestart"
  209. } elseif ($p.UninstallString) {
  210. Start-Process -Wait cmd.exe -ArgumentList "/c `"$($p.UninstallString)`""
  211. }
  212. }
  213.  
  214. Start-Sleep -Seconds 2
  215.  
  216. # ---------------------------------------------------------------
  217. # DOWNLOAD + INSTALL NODE MSI
  218. # ---------------------------------------------------------------
  219.  
  220. Write-Host "Downloading and installing Node $targetNodeVersion..." -ForegroundColor Yellow
  221.  
  222. $base = $targetNodeVersion.TrimStart('v')
  223. $msiPath = "$env:TEMP\node-v$base-x64.msi"
  224.  
  225. Write-Host "MSI URL: $msiUrl" -ForegroundColor Yellow
  226.  
  227. Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
  228.  
  229. Write-Host "Downloaded: $msiPath" -ForegroundColor Yellow
  230.  
  231. $size = (Get-Item $msiPath).Length
  232. Write-Host "Size: $size bytes" -ForegroundColor Yellow
  233.  
  234. $bytes = Get-Content $msiPath -Encoding Byte -TotalCount 8
  235. $hex = ($bytes | ForEach-Object { $_.ToString("X2") }) -join " "
  236. Write-Host "Header: $hex" -ForegroundColor Yellow
  237.  
  238. Start-Process -Wait msiexec.exe -ArgumentList "/i `"$msiPath`" /quiet /norestart"
  239. Remove-Item $msiPath -Force
  240.  
  241. # ---------------------------------------------------------------
  242. # DETECT NODE INSTALL LOCATION
  243. # ---------------------------------------------------------------
  244.  
  245. $locations = @(
  246. "C:\Program Files\nodejs",
  247. "$env:LOCALAPPDATA\Programs\nodejs"
  248. )
  249.  
  250. $found = $null
  251. foreach ($loc in $locations) {
  252. if (Test-Path (Join-Path $loc "node.exe")) {
  253. $found = $loc
  254. break
  255. }
  256. }
  257.  
  258. if (-not $found) {
  259. Write-Host "ERROR: Node did not install into expected directories." -ForegroundColor Red
  260. exit 1
  261. }
  262.  
  263. Write-Host "Found Node at: $found" -ForegroundColor Green
  264. $env:PATH = "$found;$env:PATH"
  265.  
  266. # ---------------------------------------------------------------
  267. # PATH REPAIR (NEW SECTION)
  268. # ---------------------------------------------------------------
  269.  
  270. $npmGlobalRoot = npm root -g 2>$null
  271. $npmBinDir = Split-Path $npmGlobalRoot -Parent
  272.  
  273. $pathUser = [Environment]::GetEnvironmentVariable("Path","User")
  274.  
  275. if (-not ($pathUser -split ";" | Where-Object { $_ -eq $npmBinDir })) {
  276. $newUserPath = $pathUser + ";" + $npmBinDir
  277. [Environment]::SetEnvironmentVariable("Path",$newUserPath,"User")
  278. Write-Host "Added npm global bin to PATH (User): $npmBinDir" -ForegroundColor Yellow
  279. }
  280.  
  281. $pathMachine = [Environment]::GetEnvironmentVariable("Path","Machine")
  282.  
  283. if (-not ($pathMachine -split ";" | Where-Object { $_ -eq $found })) {
  284. $newMachinePath = $pathMachine + ";" + $found
  285. [Environment]::SetEnvironmentVariable("Path",$newMachinePath,"Machine")
  286. Write-Host "Added Node install dir to PATH (Machine): $found" -ForegroundColor Yellow
  287. }
  288.  
  289. $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
  290. [Environment]::GetEnvironmentVariable("Path","User")
  291.  
  292. # ---------------------------------------------------------------
  293. # VERIFY NODE + REPORT HEAP LIMIT
  294. # ---------------------------------------------------------------
  295.  
  296. Write-Host "Verifying installation..." -ForegroundColor Yellow
  297. $installedNode = (node -v 2>$null).Trim().Replace("v","")
  298. $installedNpm = (npm -v 2>$null).Trim()
  299.  
  300. if (-not $installedNode) {
  301. Write-Host "ERROR: Node not detected even after installation." -ForegroundColor Red
  302. exit 1
  303. }
  304.  
  305. Write-Host "Node $installedNode and npm $installedNpm installed successfully." -ForegroundColor Green
  306.  
  307. $heapAfter = $null
  308. try {
  309. $heapAfter = node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024)"
  310. } catch {}
  311.  
  312. if ($heapAfter) {
  313. Write-Host "Node heap limit now: $([math]::Round([double]$heapAfter)) MB" -ForegroundColor Cyan
  314. }
  315.  
  316. # ---------------------------------------------------------------
  317. # INSTALL n8n
  318. # ---------------------------------------------------------------
  319.  
  320. Write-Host "Installing n8n@$latestN8nVersion..." -ForegroundColor Yellow
  321. npm install -g n8n@$latestN8nVersion
  322.  
  323. $n8nVersion = (n8n --version 2>$null).Trim()
  324.  
  325. if ($n8nVersion -ne $latestN8nVersion) {
  326. Write-Host "ERROR: n8n installation mismatch (expected $latestN8nVersion, got $n8nVersion)." -ForegroundColor Red
  327. exit 1
  328. }
  329.  
  330. Write-Host "`nSUCCESS: n8n $n8nVersion installed on Node $installedNode" -ForegroundColor Cyan
  331. Write-Host "Run launch-n8n.ps1 to start n8n." -ForegroundColor Green
RAW Gist Data Copied