<# .SYNOPSIS Cuts a new OSTP release and pushes the tag that triggers the matching GitHub Actions build (see .github/workflows/release.yml, which only triggers on "v*" tag pushes + workflow_dispatch — a bare branch push does NOT start a build). .DESCRIPTION A release cycle has ONE fixed target version (e.g. "0.4.1") that stays in Cargo.toml/tauri.conf.json/package.json unchanged through every alpha and beta build — only a per-channel ITERATION counter increments, and that counter lives ONLY in the git tag, never in the manifests: v0.4.1-alpha.1 -> v0.4.1-alpha.2 -> ... -> v0.4.1-alpha.100 v0.4.1-beta.1 -> v0.4.1-beta.2 -> ... -> v0.4.1-beta.100 v0.4.1 <- master: iteration dropped This is deliberately NOT "0.4.1.5-alpha" (a 4th dot-separated component before the hyphen) — that is not valid semver, and Cargo's version parser rejects it outright. "0.4.1-alpha.5" (dot AFTER the hyphen, a semver pre-release identifier) is the only form that keeps Cargo.toml itself parseable, so that's the only place the iteration number is allowed to live: the git tag. Promoting to beta/master first fast-forwards that branch to `alpha` (--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 alpha's latest, not a stale branch. Switching to a channel for the first time in a cycle resets THAT channel's iteration counter to 1 (a fresh promotion starts its own count; it doesn't inherit wherever alpha's counter happened to be). Remembers {target_version, branch, alpha_iteration, beta_iteration} in .release-state.json at the repo root. Running with no arguments repeats last time's branch, bumping that channel's iteration by one — manifests are NOT touched (nothing to bump: the target version hasn't changed). -Switch starts a new target version line and resets both iteration counters to 0 — THIS is the one case that bumps every manifest. .PARAMETER Switch Set a new target version (e.g. "0.4.2") instead of continuing the current one. Resets both alpha_iteration and beta_iteration to 0. Defaults the channel back to alpha unless -Branch is also given this run. .PARAMETER Branch Which branch/channel to release from: master, pre-release (beta), or alpha. Defaults to whatever was used last time (see .release-state.json). .EXAMPLE .\scripts\gha.ps1 Bumps the current channel's iteration by one and pushes v{target}-{channel}.{N}. .EXAMPLE .\scripts\gha.ps1 -Switch 0.4.2 Starts a fresh 0.4.2 cycle: manifests -> 0.4.2, alpha iteration resets to 1, ships v0.4.2-alpha.1. .EXAMPLE .\scripts\gha.ps1 -Branch pre-release Promotes alpha -> pre-release (beta channel), resets beta_iteration to 1 (or bumps it if already mid-beta), ships v{target}-beta.{N}. .EXAMPLE .\scripts\gha.ps1 -Branch master Promotes to master and ships the bare v{target} stable tag — no iteration. #> [CmdletBinding()] param( [string]$Switch, [ValidateSet('master', 'pre-release', 'alpha')] [string]$Branch ) $ErrorActionPreference = "Stop" function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } 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 ---------------------------------------------------- $State = $null if (Test-Path $StateFile) { $State = Get-Content $StateFile -Raw | ConvertFrom-Json } $PrevBranch = if ($State) { $State.branch } else { $null } $IsNewTarget = [bool]$Switch $ResolvedBranch = if ($Branch) { $Branch } elseif ($PrevBranch) { $PrevBranch } else { "alpha" } # -- Resolve the target version + per-channel iteration counters ------------ if ($IsNewTarget) { if ($Switch -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { Fail "-Switch must be a bare X.Y.Z version, got '$Switch'." } $TargetVersion = $Switch $AlphaIter = 0 $BetaIter = 0 # A fresh target version starts a fresh cycle at the bottom of the chain, # unless the caller explicitly asked for a different branch this run. if (-not $Branch) { $ResolvedBranch = "alpha" } } else { # NOTE: a pre-migration state file (old schema: {version, branch, prefix}) # has no target_version property at all — PowerShell silently returns # $null for a missing property on a PSCustomObject rather than erroring, # so this must check for it explicitly or $TargetVersion would end up # $null and corrupt every manifest below. $TargetVersion = if ($State -and $State.target_version) { $State.target_version } else { (Select-String -Path (Join-Path $RepoRoot "Cargo.toml") -Pattern '^version = "([0-9]+\.[0-9]+\.[0-9]+)"').Matches[0].Groups[1].Value } $AlphaIter = if ($State -and $State.alpha_iteration) { [int]$State.alpha_iteration } else { 0 } $BetaIter = if ($State -and $State.beta_iteration) { [int]$State.beta_iteration } else { 0 } # Entering a channel that wasn't active last run (a promotion) starts # THAT channel's count fresh — it doesn't inherit alpha's iteration number. $BranchChanged = ($ResolvedBranch -ne $PrevBranch) if ($BranchChanged -and $ResolvedBranch -eq "pre-release") { $BetaIter = 0 } if ($BranchChanged -and $ResolvedBranch -eq "alpha") { $AlphaIter = 0 } } $Channel = switch ($ResolvedBranch) { "alpha" { "alpha" }; "pre-release" { "beta" }; "master" { "stable" } } if ($Channel -eq "alpha") { $AlphaIter++ } elseif ($Channel -eq "beta") { $BetaIter++ } $Iteration = if ($Channel -eq "alpha") { $AlphaIter } else { $BetaIter } $Tag = if ($Channel -eq "stable") { "v$TargetVersion" } else { "v$TargetVersion-$Channel.$Iteration" } Write-Step "Releasing $Tag on '$ResolvedBranch'" # -- Checkout the target branch, promoting it from alpha 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 "alpha") { Write-Step "Fast-forwarding $ResolvedBranch to alpha (promotion)" git merge alpha --ff-only 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { $msg = "'$ResolvedBranch' has diverged from alpha 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 manifests ONLY when the target version itself changes. A plain ----- # -- alpha/beta iteration touches nothing but the state file's counter. ----- if ($IsNewTarget -or -not $State) { Write-Step "Bumping target version -> $TargetVersion" 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 = `"$TargetVersion`"" Set-VersionLine "ostp-gui/src-tauri/Cargo.toml" '(?m)^version = "[0-9]+\.[0-9]+\.[0-9]+"' "version = `"$TargetVersion`"" Set-VersionLine "ostp-gui/src-tauri/tauri.conf.json" '"version": "[0-9]+\.[0-9]+\.[0-9]+"' "`"version`": `"$TargetVersion`"" Set-VersionLine "ostp-gui/package.json" '"version": "[0-9]+\.[0-9]+\.[0-9]+"' "`"version`": `"$TargetVersion`"" # 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` never touches. 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." } } # Flutter's build number (Android versionCode) must strictly increase on # every single build ever shipped — unlike the semantic version, it does NOT # stay fixed across alpha/beta iterations, so this runs every time, not just # on a target-version switch. $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: $TargetVersion+$nextBuild" [System.IO.File]::WriteAllText($pubspecPath, $pubspecText) } else { Fail "Version pattern not found in ostp-flutter/pubspec.yaml." } # -- Persist the new state --------------------------------------------------- [PSCustomObject]@{ target_version = $TargetVersion branch = $ResolvedBranch alpha_iteration = $AlphaIter beta_iteration = $BetaIter } | ConvertTo-Json | Set-Content $StateFile # -- Commit ------------------------------------------------------------------- $commitMsg = "chore: release $Tag 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. release.yml triggers ONLY on "v*" tag pushes (no branch trigger), - # -- so the tag push is what actually starts the build; the branch push is - # -- just so the promotion chain (alpha -> pre-release -> master) itself - # -- keeps moving forward for the next --ff-only. - Write-Step "Tagging $Tag and pushing $ResolvedBranch + tag" git tag $Tag git push origin $ResolvedBranch git push origin $Tag Write-Host "" Write-Host "Done. Watch the build: https://github.com/ospab/ostp/actions" -ForegroundColor Green