# Prepare TTRPG release: bump version, git push, build Win/Linux, copy to release folder. # Run: prepare-release.cmd # Options: # -Version 1.0.17 explicit version (skip auto bump) # -Patch bump patch (default) # -Minor bump minor # -SkipGit skip commit/push # -SkipLinux skip Linux build (WSL) # -NoBump do not change package.json version param( [string]$Version = '', [switch]$Patch, [switch]$Minor, [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 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 Push-Git([string]$ProjectRoot, [string]$Remote, [string]$McpConfigPath) { $branch = (git -C $ProjectRoot rev-parse --abbrev-ref HEAD).Trim() Write-Host " > git push $Remote $branch" & git -C $ProjectRoot push $Remote $branch 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -eq 0) { return } $pushUrl = Get-GiteaPushUrl $McpConfigPath if (-not $pushUrl) { throw "git push failed and no Gitea token in mcp config" } Write-Host " > git push via Gitea token URL" & git -C $ProjectRoot push $pushUrl "HEAD:${branch}" 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { throw "git push failed (exit $LASTEXITCODE)" } } 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" # Step 1 - version Write-Step 1 'Bump version' $currentVersion = Read-PackageVersion $packageJson $newVersion = $currentVersion if ($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 = "cd '$wslProject' && npm ci && npm run pack:linux" $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 Write-Host ' 1) Copy Mac files (latest-mac.yml, *.dmg) from Mac into release folder if needed' Write-Host ' 2) Run publish.cmd to validate and upload to updates.mailib.ru' Write-Host '' exit 0