ostp/scripts/gha.ps1

202 lines
9.3 KiB
PowerShell

<#
.SYNOPSIS
Cuts a new OSTP release and pushes it to the channel that triggers the
matching GitHub Actions build (see .github/workflows/release.yml).
.DESCRIPTION
Three release channels, in increasing order of stability:
nightly -> pushes the `nightly` branch -> tag "{version}-nightly"
pre-release -> pushes the `pre-release` branch -> tag "{version}-beta"
master -> pushes an actual "v{version}" tag -> real stable release
Promoting to pre-release/master first fast-forwards that branch to
`nightly` (--ff-only — this always succeeds cleanly as long as nobody ever
commits directly to pre-release/master, per CONTRIBUTING.md's branch
strategy), so a release always ships nightly's latest, not a stale branch.
Remembers the last {version, branch, prefix} it used in .release-state.json
at the repo root. Running with no arguments repeats last time's branch and
prefix, auto-incrementing the patch version. -Switch starts a new version
line (e.g. 0.3.x -> 0.4.0) without changing branch/prefix. -Branch/-Prefix
override just that one setting for this run (and become the new default).
.PARAMETER Switch
Set an exact version (e.g. "0.4.0") instead of auto-incrementing the patch
of the last released version. Becomes the new baseline for future bare runs.
.PARAMETER Branch
Which branch to release from: master, pre-release, or nightly.
Defaults to whatever was used last time (see .release-state.json).
.PARAMETER Prefix
Tag suffix for non-stable channels: beta or nightly. Ignored (forced empty)
when -Branch master, since stable releases are bare "vX.Y.Z" tags.
Defaults to whatever was used last time.
.EXAMPLE
.\scripts\gha.ps1
Re-releases the same branch/prefix as last time, with the patch version bumped by 1.
.EXAMPLE
.\scripts\gha.ps1 -Switch 0.4.0
Starts releasing the 0.4.x line from now on; this run ships exactly 0.4.0.
.EXAMPLE
.\scripts\gha.ps1 -Branch pre-release -Prefix beta
Promotes nightly -> pre-release and ships "{version}-beta".
#>
[CmdletBinding()]
param(
[string]$Switch,
[ValidateSet('master', 'beta', 'nightly')]
[string]$Branch,
[ValidateSet('beta', 'nightly')]
[string]$Prefix
)
$ErrorActionPreference = "Stop"
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Write-Warn2($msg) { Write-Host "!! $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "ERROR: $msg" -ForegroundColor Red; exit 1 }
# ── Locate repo root, regardless of where this script was invoked from ──────
$RepoRoot = (git rev-parse --show-toplevel 2>$null)
if (-not $RepoRoot) { Fail "Not inside a git repository." }
Set-Location $RepoRoot
$StateFile = Join-Path $RepoRoot ".release-state.json"
# ── Refuse to run on a dirty tree: this script commits, and an autocommit ──
# ── silently sweeping up unrelated WIP changes would be a nasty surprise. ──
$dirty = git status --porcelain
if ($dirty) {
Write-Host $dirty
Fail "Working tree has uncommitted changes. Commit or stash them first."
}
# ── Load remembered state (branch/prefix/version from the last release) ────
$State = $null
if (Test-Path $StateFile) {
$State = Get-Content $StateFile -Raw | ConvertFrom-Json
}
$ResolvedBranch = if ($Branch) { $Branch } elseif ($State) { $State.branch } else { "nightly" }
$ResolvedPrefix = if ($Prefix) { $Prefix } elseif ($State) { $State.prefix } else { "nightly" }
# Stable releases are always a bare "vX.Y.Z" tag, never suffixed — master
# never carries a prefix regardless of what was remembered or passed in.
if ($ResolvedBranch -eq "master") {
if ($Prefix) { Write-Warn2 "-Prefix is ignored for -Branch master (stable releases are bare 'vX.Y.Z' tags)." }
$ResolvedPrefix = ""
}
# ── Resolve the version: exact via -Switch, else auto-increment the patch ──
$CurrentVersion = if ($State) { $State.version } else {
(Select-String -Path (Join-Path $RepoRoot "Cargo.toml") -Pattern '^version = "([0-9]+\.[0-9]+\.[0-9]+)"').Matches[0].Groups[1].Value
}
if ($Switch) {
if ($Switch -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { Fail "-Switch must be a bare X.Y.Z version, got '$Switch'." }
$NewVersion = $Switch
} else {
$parts = $CurrentVersion.Split('.')
$NewVersion = "{0}.{1}.{2}" -f $parts[0], $parts[1], ([int]$parts[2] + 1)
}
Write-Step "Releasing $NewVersion on '$ResolvedBranch'$(if ($ResolvedPrefix) { " (tag suffix: -$ResolvedPrefix)" } else { " (stable, tag v$NewVersion)" })"
# ── Checkout the target branch, promoting it from nightly first ────────────
$CurrentBranch = git rev-parse --abbrev-ref HEAD
if ($CurrentBranch -ne $ResolvedBranch) {
Write-Step "Checking out $ResolvedBranch"
git checkout $ResolvedBranch 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { Fail "Could not check out branch '$ResolvedBranch'." }
}
if ($ResolvedBranch -ne "nightly") {
Write-Step "Fast-forwarding $ResolvedBranch to nightly (promotion)"
git merge nightly --ff-only 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
$msg = "'$ResolvedBranch' has diverged from nightly and can't fast-forward. " +
"Per CONTRIBUTING.md, nothing should ever be committed directly to " +
"$ResolvedBranch — check what's there before forcing anything."
Fail $msg
}
}
# ── Bump the version across every manifest that carries one ────────────────
Write-Step "Bumping version $CurrentVersion -> $NewVersion"
function Set-VersionLine($Path, $Pattern, $Replacement) {
$full = Join-Path $RepoRoot $Path
$text = Get-Content $full -Raw
$updated = $text -replace $Pattern, $Replacement
if ($updated -eq $text) { Fail "Version pattern not found in $Path — refusing to proceed with a stale file." }
[System.IO.File]::WriteAllText($full, $updated)
}
Set-VersionLine "Cargo.toml" '(?m)^version = "[0-9]+\.[0-9]+\.[0-9]+"' "version = `"$NewVersion`""
Set-VersionLine "ostp-gui/src-tauri/Cargo.toml" '(?m)^version = "[0-9]+\.[0-9]+\.[0-9]+"' "version = `"$NewVersion`""
Set-VersionLine "ostp-gui/src-tauri/tauri.conf.json" '"version": "[0-9]+\.[0-9]+\.[0-9]+"' "`"version`": `"$NewVersion`""
Set-VersionLine "ostp-gui/package.json" '"version": "[0-9]+\.[0-9]+\.[0-9]+"' "`"version`": `"$NewVersion`""
# Flutter build number must increase monotonically (Android versionCode) —
# bump it alongside the version string, don't just rewrite the version part.
$pubspecPath = Join-Path $RepoRoot "ostp-flutter/pubspec.yaml"
$pubspecText = Get-Content $pubspecPath -Raw
if ($pubspecText -match 'version: [0-9]+\.[0-9]+\.[0-9]+\+([0-9]+)') {
$nextBuild = [int]$Matches[1] + 1
$pubspecText = $pubspecText -replace 'version: [0-9]+\.[0-9]+\.[0-9]+\+[0-9]+', "version: $NewVersion+$nextBuild"
[System.IO.File]::WriteAllText($pubspecPath, $pubspecText)
} else {
Fail "Version pattern not found in ostp-flutter/pubspec.yaml."
}
# ── Refresh Cargo.lock's per-package version entries ────────────────────────
# ostp-gui/src-tauri is excluded from the main workspace (its own Tauri build
# graph), so it has its own separate Cargo.lock that the main `cargo check`
# below never touches — needs its own pass or it'd drift from Cargo.toml.
Write-Step "Running cargo check to refresh Cargo.lock (main workspace)"
cargo check --workspace --exclude ostp-jni --quiet
if ($LASTEXITCODE -ne 0) { Fail "cargo check failed after the version bump — not committing a broken build." }
Write-Step "Running cargo check to refresh Cargo.lock (ostp-gui/src-tauri)"
Push-Location (Join-Path $RepoRoot "ostp-gui/src-tauri")
cargo check --quiet
$tauriCheckExit = $LASTEXITCODE
Pop-Location
if ($tauriCheckExit -ne 0) { Fail "cargo check failed in ostp-gui/src-tauri after the version bump." }
# ── Persist the new state ───────────────────────────────────────────────────
[PSCustomObject]@{
version = $NewVersion
branch = $ResolvedBranch
prefix = $ResolvedPrefix
} | ConvertTo-Json | Set-Content $StateFile
# ── Commit ───────────────────────────────────────────────────────────────────
$suffixLabel = if ($ResolvedPrefix) { "-$ResolvedPrefix" } else { "" }
$commitMsg = "chore: release $NewVersion$suffixLabel on $ResolvedBranch"
Write-Step "Committing: $commitMsg"
git add Cargo.toml Cargo.lock ostp-gui/src-tauri/Cargo.toml ostp-gui/src-tauri/Cargo.lock `
ostp-gui/src-tauri/tauri.conf.json ostp-gui/package.json ostp-flutter/pubspec.yaml `
.release-state.json
git commit -m $commitMsg | Out-Null
# ── Push: branch push for nightly/pre-release (CI computes the tag itself), ─
# ── a real "vX.Y.Z" tag for master (the only path that yields a stable ─
# ── release per release.yml's resolve-channel job). ─
if ($ResolvedBranch -eq "master") {
$tag = "v$NewVersion"
Write-Step "Tagging $tag and pushing master + tag"
git tag $tag
git push origin master
git push origin $tag
} else {
Write-Step "Pushing $ResolvedBranch"
git push origin $ResolvedBranch
}
Write-Host ""
Write-Host "Done. Watch the build: https://github.com/ospab/ostp/actions" -ForegroundColor Green