458 lines
14 KiB
PowerShell
458 lines
14 KiB
PowerShell
# 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
|
|
}
|
|
|
|
$FfmpegStaticMirror = 'https://cdn.npmmirror.com/binaries/ffmpeg-static'
|
|
|
|
function Test-IsNpmCi([string[]]$NpmArgs) {
|
|
return ($NpmArgs.Count -eq 1 -and $NpmArgs[0] -eq 'ci')
|
|
}
|
|
|
|
function Invoke-NpmRaw {
|
|
param(
|
|
[string[]]$NpmArgs,
|
|
[string]$WorkingDirectory
|
|
)
|
|
Push-Location $WorkingDirectory
|
|
try {
|
|
& npm @NpmArgs
|
|
return $LASTEXITCODE
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
function Invoke-NpmCiWithFfmpegMirror {
|
|
param(
|
|
[string]$WorkingDirectory
|
|
)
|
|
$prevMirror = $env:FFMPEG_BINARIES_URL
|
|
$env:FFMPEG_BINARIES_URL = $FfmpegStaticMirror
|
|
try {
|
|
Write-Host " > npm ci (retry with FFMPEG_BINARIES_URL=$FfmpegStaticMirror)"
|
|
return (Invoke-NpmRaw -NpmArgs @('ci') -WorkingDirectory $WorkingDirectory)
|
|
} finally {
|
|
if ($null -eq $prevMirror) {
|
|
Remove-Item Env:\FFMPEG_BINARIES_URL -ErrorAction SilentlyContinue
|
|
} else {
|
|
$env:FFMPEG_BINARIES_URL = $prevMirror
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-Npm {
|
|
param(
|
|
[string]$Label,
|
|
[string[]]$NpmArgs,
|
|
[string]$WorkingDirectory
|
|
)
|
|
Write-Host " > $Label"
|
|
$exitCode = Invoke-NpmRaw -NpmArgs $NpmArgs -WorkingDirectory $WorkingDirectory
|
|
if ($exitCode -ne 0 -and (Test-IsNpmCi $NpmArgs)) {
|
|
Write-Host " [--] npm ci failed (exit $exitCode). Retrying via ffmpeg-static mirror..." -ForegroundColor Yellow
|
|
$exitCode = Invoke-NpmCiWithFfmpegMirror $WorkingDirectory
|
|
}
|
|
if ($exitCode -ne 0) {
|
|
throw "$Label failed (exit $exitCode)"
|
|
}
|
|
}
|
|
|
|
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 Get-GitTextOutput {
|
|
param(
|
|
[string]$ProjectRoot,
|
|
[string[]]$GitArgs
|
|
)
|
|
$prevEap = $ErrorActionPreference
|
|
$ErrorActionPreference = 'Continue'
|
|
try {
|
|
$output = & git -C $ProjectRoot @GitArgs 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "git $($GitArgs -join ' ') failed (exit $LASTEXITCODE)"
|
|
}
|
|
if ($null -eq $output) {
|
|
return ''
|
|
}
|
|
return (@($output | ForEach-Object {
|
|
if ($_ -is [System.Management.Automation.ErrorRecord]) {
|
|
$_.ToString()
|
|
} else {
|
|
[string]$_
|
|
}
|
|
}) -join "`n").Trim()
|
|
} finally {
|
|
$ErrorActionPreference = $prevEap
|
|
}
|
|
}
|
|
|
|
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 = Get-GitTextOutput $ProjectRoot @('rev-parse', '--abbrev-ref', 'HEAD')
|
|
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 = Get-GitTextOutput $projectRoot @('status', '--porcelain')
|
|
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
|