# Copy of D:\TTRPG-Release\publish.ps1 - keep in sync when updating the release folder tool. param( [switch]$CheckOnly, [switch]$SkipMac ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ReleaseDir = $PSScriptRoot $ConfigPath = Join-Path $ReleaseDir 'publish-config.json' function Expand-ConfigPath([string]$value) { if ($value -match '%([^%]+)%') { $envName = $Matches[1] $envVal = [Environment]::GetEnvironmentVariable($envName) if ($null -ne $envVal) { return $value.Replace("%$envName%", $envVal) } } return $value } function Write-Title([string]$text) { Write-Host '' Write-Host "=== $text ===" -ForegroundColor Cyan } function Write-Ok([string]$text) { Write-Host " [OK] $text" -ForegroundColor Green } function Write-Fail([string]$text) { Write-Host " [!!] $text" -ForegroundColor Red } function Write-Warn([string]$text) { Write-Host " [--] $text" -ForegroundColor Yellow } function Get-YmlVersion([string]$ymlPath) { $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8 $m = [regex]::Match($content, '(?m)^version:\s*(\S+)\s*$') if ($m.Success) { return $m.Groups[1].Value.Trim() } return $null } function Get-YmlPrimaryPath([string]$ymlPath) { $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8 $m = [regex]::Match($content, '(?m)^path:\s*(\S+)\s*$') if ($m.Success) { return $m.Groups[1].Value.Trim() } return $null } function Get-YmlReferencedFiles([string]$ymlPath) { $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8 foreach ($m in [regex]::Matches($content, '(?m)^(?:\s*-\s*)?(?:url|path):\s*(\S+)\s*$')) { [void]$names.Add($m.Groups[1].Value.Trim()) } return @($names) } function Get-YmlOptionalUrls([string]$ymlPath) { $primary = Get-YmlPrimaryPath $ymlPath $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8 foreach ($m in [regex]::Matches($content, '(?m)^\s*-\s*url:\s*(\S+)\s*$')) { $name = $m.Groups[1].Value.Trim() if ($primary -and $name.Equals($primary, [StringComparison]::OrdinalIgnoreCase)) { continue } [void]$names.Add($name) } return @($names) } function Resolve-ReleaseFile([string]$name) { $direct = Join-Path $ReleaseDir $name if (Test-Path -LiteralPath $direct) { return Get-Item -LiteralPath $direct } if ($name -match 'x64' -and $name -notmatch 'x86_64') { $alt = $name -replace 'x64', 'x86_64' $altPath = Join-Path $ReleaseDir $alt if (Test-Path -LiteralPath $altPath) { return Get-Item -LiteralPath $altPath } } if ($name -match 'x86_64') { $alt = $name -replace 'x86_64', 'x64' $altPath = Join-Path $ReleaseDir $alt if (Test-Path -LiteralPath $altPath) { return Get-Item -LiteralPath $altPath } } return $null } function Add-FileToUploadSet { param( [System.Collections.Generic.HashSet[string]]$set, [System.IO.FileInfo]$file ) [void]$set.Add($file.FullName) } Write-Title 'TTRPG Release Publisher' Write-Host "Release folder: $ReleaseDir" if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Missing publish-config.json: $ConfigPath" } $config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json $sshKey = Expand-ConfigPath $config.sshKey $sshTarget = [string]$config.sshTarget $remoteDir = [string]$config.remoteDir $feedUrl = [string]$config.feedUrl if (-not (Test-Path -LiteralPath $sshKey)) { throw "SSH key not found: $sshKey" } $errors = [System.Collections.Generic.List[string]]::new() $warnings = [System.Collections.Generic.List[string]]::new() $uploadFiles = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) Write-Title 'Windows (required)' $winYml = Join-Path $ReleaseDir 'latest.yml' if (-not (Test-Path -LiteralPath $winYml)) { $errors.Add('Missing latest.yml') } else { Write-Ok 'latest.yml' [void]$uploadFiles.Add($winYml) foreach ($name in (Get-YmlReferencedFiles $winYml)) { $file = Resolve-ReleaseFile $name if ($null -eq $file) { $errors.Add("Windows: missing file $name (from latest.yml)") } else { Write-Ok $file.Name Add-FileToUploadSet $uploadFiles $file } } $blockmap = Resolve-ReleaseFile 'TTRPGPlayer-Setup.exe.blockmap' if ($null -eq $blockmap) { $warnings.Add('Missing TTRPGPlayer-Setup.exe.blockmap (recommended)') } else { Write-Ok $blockmap.Name Add-FileToUploadSet $uploadFiles $blockmap } } Write-Title 'Linux (if latest-linux*.yml present)' $linuxYmls = Get-ChildItem -LiteralPath $ReleaseDir -Filter 'latest-linux*.yml' -File -ErrorAction SilentlyContinue if ($linuxYmls.Count -eq 0) { Write-Warn 'No latest-linux*.yml - skipping Linux' } else { foreach ($yml in $linuxYmls) { Write-Ok $yml.Name [void]$uploadFiles.Add($yml.FullName) foreach ($name in (Get-YmlReferencedFiles $yml.FullName)) { $file = Resolve-ReleaseFile $name if ($null -eq $file) { $errors.Add("Linux ($($yml.Name)): missing file $name") } else { if ($file.Name -ne $name) { Write-Warn "$($yml.Name): yml expects $name, disk has $($file.Name) - will upload $($file.Name)" } else { Write-Ok "$($yml.Name) -> $name" } Add-FileToUploadSet $uploadFiles $file } } } } Write-Title 'macOS (if latest-mac.yml present)' $macYml = Join-Path $ReleaseDir 'latest-mac.yml' if ($SkipMac) { Write-Warn 'macOS skipped (-SkipMac)' } elseif (-not (Test-Path -LiteralPath $macYml)) { Write-Warn 'No latest-mac.yml - skipping macOS' } else { Write-Ok 'latest-mac.yml' [void]$uploadFiles.Add($macYml) $macVersion = Get-YmlVersion $macYml $winYml = Join-Path $ReleaseDir 'latest.yml' $winVersion = $null if (Test-Path -LiteralPath $winYml) { $winVersion = Get-YmlVersion $winYml } if ($macVersion -and $winVersion -and $macVersion -ne $winVersion) { $errors.Add( "macOS: latest-mac.yml is v$macVersion but latest.yml is v$winVersion (stale Mac feed). " + 'On Mac run: npm run pack:mac, then copy latest-mac.yml + TTRPGPlayer-*.zip (+ optional *.dmg). ' + 'Or publish Win/Linux only: publish.cmd -SkipMac' ) } $primaryName = Get-YmlPrimaryPath $macYml if (-not $primaryName) { $errors.Add('macOS: latest-mac.yml has no path: entry') } else { $primaryFile = Resolve-ReleaseFile $primaryName if ($null -eq $primaryFile) { $errors.Add( "macOS: missing primary update file $primaryName (path in latest-mac.yml). " + 'electron-updater on macOS needs the .zip from pack:mac, not only .dmg. Rebuild on Mac or use -SkipMac.' ) } else { Write-Ok "primary update: $($primaryFile.Name)" Add-FileToUploadSet $uploadFiles $primaryFile } } foreach ($name in (Get-YmlOptionalUrls $macYml)) { $file = Resolve-ReleaseFile $name if ($null -eq $file) { $warnings.Add("macOS: optional file missing (not uploaded): $name") } else { Write-Ok "optional: $($file.Name)" Add-FileToUploadSet $uploadFiles $file } } } Write-Title 'Summary' foreach ($w in $warnings) { Write-Warn $w } if ($errors.Count -gt 0) { foreach ($e in $errors) { Write-Fail $e } Write-Host '' Write-Host 'Upload cancelled. Fix the release folder and run again.' -ForegroundColor Red exit 1 } Write-Host '' Write-Host "Files to upload: $($uploadFiles.Count)" -ForegroundColor Green foreach ($path in ($uploadFiles | Sort-Object)) { Write-Host " - $([System.IO.Path]::GetFileName($path))" } if ($CheckOnly) { Write-Host '' Write-Host 'CheckOnly: upload skipped.' -ForegroundColor Yellow exit 0 } Write-Title 'Upload' Write-Host "Target: ${sshTarget}:${remoteDir}" Write-Host "Feed: $feedUrl" foreach ($path in ($uploadFiles | Sort-Object)) { $name = [System.IO.Path]::GetFileName($path) Write-Host " -> $name" & scp -i $sshKey -q $path "${sshTarget}:${remoteDir}/" if ($LASTEXITCODE -ne 0) { throw "scp failed for $name (exit $LASTEXITCODE)" } } Write-Host '' Write-Host 'Setting www-data ownership on server...' & ssh -i $sshKey $sshTarget "chown -R www-data:www-data '$remoteDir'" if ($LASTEXITCODE -ne 0) { throw "ssh chown failed (exit $LASTEXITCODE)" } Write-Title 'Done' Write-Host 'Verify:' Write-Host " ${feedUrl}latest.yml" if (Test-Path -LiteralPath (Join-Path $ReleaseDir 'latest-linux.yml')) { Write-Host " ${feedUrl}latest-linux.yml" } if (Test-Path -LiteralPath (Join-Path $ReleaseDir 'latest-mac.yml')) { Write-Host " ${feedUrl}latest-mac.yml" } exit 0