# Prepare TTRPG release: build Win/Linux, copy to release folder. # Run: prepare-release.cmd # # Recommended (Mac first, same version on all platforms): # 1) Bump version in package.json manually (npm version patch --no-git-tag-version) # 2) pack:mac on Mac, copy latest-mac.yml + *.zip to release folder # 3) prepare-release.cmd -AfterMac (or release-all.cmd) # # Options: # -AfterMac do not bump; require Mac files in release folder (default in release-all) # -Bump bump patch before build (Mac can be added later) # -Version 1.0.17 set explicit version (with -Bump workflow) # -Minor bump minor (with -Bump) # -SkipGit skip commit/push # -SkipLinux skip Linux build (WSL) # -NoBump do not change package.json (same as -AfterMac without Mac check) param( [string]$Version = '', [switch]$Patch, [switch]$Minor, [switch]$Bump, [switch]$AfterMac, [switch]$SkipGit, [switch]$SkipLinux, [switch]$NoBump ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ToolDir = $PSScriptRoot $ConfigPath = Join-Path $ToolDir 'release-config.json' function Write-Step([int]$n, [string]$text) { Write-Host '' Write-Host "----- Step $n : $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 Invoke-Npm { param( [string]$Label, [string[]]$NpmArgs, [string]$WorkingDirectory ) Write-Host " > $Label" Push-Location $WorkingDirectory try { & npm @NpmArgs if ($LASTEXITCODE -ne 0) { throw "$Label failed (exit $LASTEXITCODE)" } } finally { Pop-Location } } function Convert-ToWslPath([string]$winPath) { $full = [System.IO.Path]::GetFullPath($winPath) $p = $full -replace '\\', '/' if ($p -match '^([A-Za-z]):(.*)$') { $drive = $Matches[1].ToLower() return "/mnt/$drive$($Matches[2])" } return $p } function Get-WslLinuxBuildCommand([string]$WslProjectPath) { # WSL Debian often has node 18 on PATH; scripts/wsl-pack-linux.sh uses nvm + .nvmrc. return "cd '$WslProjectPath' && bash scripts/wsl-pack-linux.sh" } function Read-PackageVersion([string]$packageJsonPath) { $json = Get-Content -LiteralPath $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json return [string]$json.version } function Invoke-NpmVersion { param( [string]$ProjectRoot, [string]$Spec ) Invoke-Npm "npm version $Spec" @('version', $Spec, '--no-git-tag-version', '--allow-same-version') $ProjectRoot } function Get-GiteaPushUrl([string]$mcpConfigPath) { if (-not (Test-Path -LiteralPath $mcpConfigPath)) { return $null } $raw = Get-Content -LiteralPath $mcpConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json $token = $raw.mcpServers.'gitea-mailib'.env.GITEA_ACCESS_TOKEN if (-not $token) { return $null } return "https://ifontosh:${token}@git.mailib.ru/ifontosh/DndGamePlayer.git" } function Invoke-Git { param( [string]$ProjectRoot, [string[]]$GitArgs ) & git -C $ProjectRoot @GitArgs if ($LASTEXITCODE -ne 0) { throw "git $($GitArgs -join ' ') failed (exit $LASTEXITCODE)" } } function Write-GitLines($lines) { foreach ($line in $lines) { if ($line -is [System.Management.Automation.ErrorRecord]) { Write-Host $line.ToString() } else { Write-Host ([string]$line) } } } function Invoke-GitPush { param( [string]$ProjectRoot, [string[]]$PushArgs ) $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $output = & git -C $ProjectRoot push @PushArgs 2>&1 Write-GitLines $output return $LASTEXITCODE } finally { $ErrorActionPreference = $prevEap } } function Push-Git([string]$ProjectRoot, [string]$Remote, [string]$McpConfigPath) { $branch = (git -C $ProjectRoot rev-parse --abbrev-ref HEAD).Trim() Write-Host " > git push $Remote $branch" $exitCode = Invoke-GitPush $ProjectRoot @($Remote, $branch) if ($exitCode -eq 0) { return } $pushUrl = Get-GiteaPushUrl $McpConfigPath if (-not $pushUrl) { throw "git push failed (exit $exitCode) and no Gitea token in mcp config ($McpConfigPath)" } Write-Host " > git push via Gitea token URL" $exitCode = Invoke-GitPush $ProjectRoot @($pushUrl, "HEAD:${branch}") if ($exitCode -ne 0) { throw "git push failed (exit $exitCode)" } } function Get-YmlField([string]$ymlPath, [string]$fieldName) { $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8 $m = [regex]::Match($content, "(?m)^${fieldName}:\s*(\S+)\s*$") if ($m.Success) { return $m.Groups[1].Value.Trim() } return $null } function Test-MacReleaseReady { param( [string]$ReleaseDir, [string]$ExpectedVersion ) $macYml = Join-Path $ReleaseDir 'latest-mac.yml' if (-not (Test-Path -LiteralPath $macYml)) { throw @" AfterMac: missing latest-mac.yml in $ReleaseDir 1) Bump version in package.json (now $ExpectedVersion) 2) npm run pack:mac on Mac 3) Copy latest-mac.yml + TTRPGPlayer-*.zip into release folder 4) Run prepare-release.cmd -AfterMac again "@ } $macVersion = Get-YmlField $macYml 'version' if ($macVersion -and $macVersion -ne $ExpectedVersion) { throw "AfterMac: latest-mac.yml is v$macVersion but package.json is v$ExpectedVersion. Align versions before building Win/Linux." } $primary = Get-YmlField $macYml 'path' if (-not $primary) { throw 'AfterMac: latest-mac.yml has no path: entry' } $primaryPath = Join-Path $ReleaseDir $primary if (-not (Test-Path -LiteralPath $primaryPath)) { throw "AfterMac: missing Mac update file $primary (path in latest-mac.yml). Copy zip from Mac build." } Write-Ok "Mac feed v$ExpectedVersion, primary: $primary" } function Copy-ReleaseArtifacts { param( [string]$BuildReleaseDir, [string]$TargetDir ) if (-not (Test-Path -LiteralPath $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir | Out-Null } $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $fixed = @( 'latest.yml', 'TTRPGPlayer-Setup.exe', 'TTRPGPlayer-Setup.exe.blockmap' ) foreach ($n in $fixed) { [void]$names.Add($n) } foreach ($yml in Get-ChildItem -LiteralPath $BuildReleaseDir -Filter 'latest-linux*.yml' -File -ErrorAction SilentlyContinue) { [void]$names.Add($yml.Name) $content = Get-Content -LiteralPath $yml.FullName -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()) } } foreach ($img in Get-ChildItem -LiteralPath $BuildReleaseDir -Filter 'TTRPGPlayer-*.AppImage' -File -ErrorAction SilentlyContinue) { [void]$names.Add($img.Name) } $copied = 0 foreach ($name in ($names | Sort-Object)) { $src = Join-Path $BuildReleaseDir $name if (-not (Test-Path -LiteralPath $src)) { if ($name -match 'x64' -and $name -notmatch 'x86_64') { $alt = $name -replace 'x64', 'x86_64' $src = Join-Path $BuildReleaseDir $alt } } if (-not (Test-Path -LiteralPath $src)) { Write-Host " [--] skip (not built): $name" -ForegroundColor Yellow continue } $dest = Join-Path $TargetDir ([System.IO.Path]::GetFileName($src)) Copy-Item -LiteralPath $src -Destination $dest -Force Write-Ok "copied $([System.IO.Path]::GetFileName($src))" $copied += 1 } if ($copied -eq 0) { throw 'No release artifacts copied - check build output in project release folder' } } if (-not (Test-Path -LiteralPath $ConfigPath)) { throw "Missing release-config.json: $ConfigPath" } $config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json $projectRoot = [System.IO.Path]::GetFullPath([string]$config.projectRoot) $releaseDir = [System.IO.Path]::GetFullPath([string]$config.releaseDir) $gitRemote = [string]$config.gitRemote $mcpConfig = [string]$config.giteaMcpConfig $wslDistro = [string]$config.wslDistro $packageJson = Join-Path $projectRoot 'package.json' $buildReleaseDir = Join-Path $projectRoot 'release' if (-not (Test-Path -LiteralPath $packageJson)) { throw "package.json not found: $packageJson" } Write-Host '=== TTRPG Prepare Release ===' -ForegroundColor Cyan Write-Host "Project: $projectRoot" Write-Host "Release folder: $releaseDir" if ($AfterMac) { $NoBump = $true } if ($Bump -and $AfterMac) { throw 'Use either -Bump or -AfterMac, not both' } if ($Bump) { $Patch = $true } $currentVersion = Read-PackageVersion $packageJson if ($AfterMac) { Write-Step 0 'Mac artifacts check' Test-MacReleaseReady $releaseDir $currentVersion } # Step 1 - version if ($AfterMac) { Write-Step 1 'Version (unchanged, Mac-first workflow)' $newVersion = $currentVersion Write-Ok "building Win/Linux at v$newVersion (same as Mac feed)" } else { Write-Step 1 'Bump version' $newVersion = $currentVersion } if ($AfterMac) { # version step done above } elseif ($NoBump) { Write-Ok "version unchanged: $currentVersion" $newVersion = $currentVersion } elseif ($Version) { Invoke-NpmVersion $projectRoot $Version.Trim() $newVersion = Read-PackageVersion $packageJson Write-Ok "version set to $newVersion (was $currentVersion)" } elseif ($Minor) { Invoke-NpmVersion $projectRoot 'minor' $newVersion = Read-PackageVersion $packageJson Write-Ok "version $currentVersion -> $newVersion (minor)" } else { Invoke-NpmVersion $projectRoot 'patch' $newVersion = Read-PackageVersion $packageJson Write-Ok "version $currentVersion -> $newVersion (patch)" } # Step 2 - git if ($SkipGit) { Write-Step 2 'Git commit and push (skipped)' } else { Write-Step 2 'Git commit and push' Invoke-Git $projectRoot @('add', 'package.json') if (Test-Path -LiteralPath (Join-Path $projectRoot 'package-lock.json')) { Invoke-Git $projectRoot @('add', 'package-lock.json') } $commitMsg = "chore: release v$newVersion" $status = (git -C $projectRoot status --porcelain).Trim() if ($status) { Invoke-Git $projectRoot @('commit', '-m', $commitMsg) Write-Ok "committed: $commitMsg" } else { Write-Ok 'nothing to commit (version may already be committed)' } Push-Git $projectRoot $gitRemote $mcpConfig Write-Ok 'pushed to remote' } # Step 3 - Windows build Write-Step 3 'Build Windows (npm ci + pack:win)' Invoke-Npm 'npm ci' @('ci') $projectRoot Invoke-Npm 'npm run pack:win' @('run', 'pack:win') $projectRoot Write-Ok 'Windows build finished' # Step 4 - Linux build if ($SkipLinux) { Write-Step 4 'Build Linux (skipped)' } else { Write-Step 4 'Build Linux via WSL (npm ci + pack:linux)' $wslProject = Convert-ToWslPath $projectRoot $wslCmd = Get-WslLinuxBuildCommand $wslProject $wslArgs = @() if ($wslDistro) { $wslArgs += '-d', $wslDistro } $wslArgs += '-e', 'bash', '-lc', $wslCmd Write-Host " > wsl $($wslArgs -join ' ')" & wsl @wslArgs if ($LASTEXITCODE -ne 0) { throw "WSL Linux build failed (exit $LASTEXITCODE). Install WSL or use -SkipLinux" } Write-Ok 'Linux build finished' } # Step 5 - copy Write-Step 5 'Copy artifacts to release folder' if (-not (Test-Path -LiteralPath $buildReleaseDir)) { throw "Build output missing: $buildReleaseDir" } Copy-ReleaseArtifacts $buildReleaseDir $releaseDir Write-Ok "release folder updated: $releaseDir" Write-Host '' Write-Host '=== Prepare release done ===' -ForegroundColor Green Write-Host "Version: $newVersion" Write-Host '' Write-Host 'Next steps:' -ForegroundColor Yellow if ($AfterMac) { Write-Host ' Run publish.cmd (release-all continues to publish automatically)' } else { Write-Host ' 1) Copy Mac files (latest-mac.yml, TTRPGPlayer-*.zip) from Mac into release folder' Write-Host ' 2) Run release-all.cmd (default -AfterMac) or publish.cmd' } Write-Host '' exit 0