zscripts-token-savers/zrelease.ps1
KellyMichels 502c0e8b29 feat(zversion, zrelease): toolkit versioning + downloadable release zips
Implements the versioning rule (SmartPlant's 5-segment scheme, now the global
standard; currently only sp and zscripts are on it at v1.x):

    v{major}.{rc}.{beta}.{alpha}.{build}

zversion: get / bump / bump-stage / set. A stage bump zeroes every lower
segment including build. 'bump' is one per PR and one per defect fix, not per
file. Any write rewrites three things together, because they are only useful
when they agree: build-version.json (source of truth), a '# Version:' line in
all 42 script headers (a lone copied script still says which release it came
from), and CHECKSUMS.txt (stamping changes every file).

zrelease: packages the current version as releases/zscripts-<version>.zip with
a sibling .sha256, for people who want the toolkit without cloning. One hash
verifies the download; the bundled CHECKSUMS.txt verifies the extracted
contents. Refuses to overwrite an existing version's zip (released = immutable;
bump instead), and refuses to package when zchecksums fails. tests/ excluded
from the zip; releases/ never packages itself.

First release included: releases/zscripts-v1.0.0.0.0.zip (42 scripts + 7
support files) and its .sha256.

.gitattributes: releases/*.sha256 pinned LF (sha256sum treats a trailing CR as
part of the filename), releases/*.zip marked binary.

Verified end-to-end as a downloader would experience it, in WSL: sha256sum -c
on the zip passes, unzip, sha256sum -c CHECKSUMS.txt inside gives 42 OK / 0
FAILED, and the extracted zdeploy.ps1 header and build-version.json both read
v1.0.0.0.0. Double-release guard and -Verify mode exercised. Full Pester suite
219/219 (the checksum tests absorb the new files automatically).
2026-07-28 13:27:50 -05:00

147 lines
5.9 KiB
PowerShell

# Evomedia.net — https://github.com/kellymichels/zscripts-token-savers
# Created by Kelly Michels · dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
# Version: v1.0.0.0.0
# zrelease.ps1 - package the current version as a downloadable zip.
#
# Usage:
# zrelease build releases/zscripts-<version>.zip
# zrelease -Force overwrite an existing zip for this version
# zrelease -Verify re-check the zip already on disk for this version
#
# For people who want the toolkit without cloning: one zip, one hash to check.
#
# WHAT GOES IN
# ------------
# every *.ps1 / *.cmd the commands themselves
# CHECKSUMS.txt per-file manifest, so the contents can be
# re-verified after unzipping
# zconfig.example.json you need it to configure anything
# README.md, CHANGELOG.md, LICENSE, TOKEN_SAVINGS.md
# build-version.json which release this is
#
# Left out: tests/ (a user does not need the suite to run the commands) and
# releases/ (never package the packages).
#
# WHAT COMES OUT
# releases/zscripts-v1.0.0.0.1.zip
# releases/zscripts-v1.0.0.0.1.zip.sha256 <- sha256sum format, one line
#
# Two layers on purpose: the .sha256 verifies you downloaded the zip intact,
# and CHECKSUMS.txt inside verifies the individual scripts after extraction.
# Both are integrity checks, not signatures - see the note in zchecksums.ps1.
[CmdletBinding()]
param(
[switch]$Force,
[switch]$Verify
)
$ErrorActionPreference = "Stop"
$ReleasesDir = Join-Path $PSScriptRoot "releases"
$VersionFile = Join-Path $PSScriptRoot "build-version.json"
if (-not (Test-Path -LiteralPath $VersionFile)) {
Write-Host "ERROR: build-version.json not found. Run 'zversion set v1.0.0.0.0' first." -ForegroundColor Red
exit 1
}
$version = (Get-Content -LiteralPath $VersionFile -Raw -Encoding UTF8 | ConvertFrom-Json).version
if ($version -notmatch '^v\d+\.\d+\.\d+\.\d+\.\d+$') {
Write-Host "ERROR: build-version.json holds an invalid version '$version'." -ForegroundColor Red
exit 1
}
$zipName = "zscripts-$version.zip"
$zipPath = Join-Path $ReleasesDir $zipName
$shaPath = "$zipPath.sha256"
function Get-Sha256([string]$Path) {
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
# ---- verify mode -------------------------------------------------------------
if ($Verify) {
if (-not (Test-Path -LiteralPath $zipPath)) {
Write-Host "ERROR: $zipName not found in releases/." -ForegroundColor Red
exit 1
}
if (-not (Test-Path -LiteralPath $shaPath)) {
Write-Host "ERROR: $zipName.sha256 not found." -ForegroundColor Red
exit 1
}
$expected = ((Get-Content -LiteralPath $shaPath -Raw) -split '\s+')[0].ToLowerInvariant()
$actual = Get-Sha256 $zipPath
Write-Host ""
Write-Host "=== zrelease (verify) ===" -ForegroundColor Cyan
if ($actual -eq $expected) {
Write-Host " OK - $zipName matches its .sha256." -ForegroundColor Green
Write-Host ""
exit 0
}
Write-Host " FAILED - $zipName does not match its .sha256." -ForegroundColor Red
Write-Host ""
exit 1
}
# ---- build mode --------------------------------------------------------------
if ((Test-Path -LiteralPath $zipPath) -and -not $Force) {
Write-Host ""
Write-Host "ERROR: releases/$zipName already exists." -ForegroundColor Red
Write-Host " A released version is immutable - bump instead: zversion bump" -ForegroundColor DarkGray
Write-Host " (or pass -Force if you are rebuilding one that was never published)" -ForegroundColor DarkGray
Write-Host ""
exit 1
}
# Refuse to package scripts that disagree with the manifest.
& (Join-Path $PSScriptRoot "zchecksums.ps1") -Quiet | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "ERROR: checksum verification failed - refusing to package." -ForegroundColor Red
Write-Host " Run 'zchecksums' to see what differs, then 'zversion bump' to restamp." -ForegroundColor DarkGray
Write-Host ""
exit 1
}
New-Item -ItemType Directory -Force -Path $ReleasesDir | Out-Null
$staging = Join-Path ([IO.Path]::GetTempPath()) ("zrel-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $staging -Force | Out-Null
try {
$scripts = @(Get-ChildItem -LiteralPath $PSScriptRoot -File | Where-Object { $_.Extension -in @('.ps1', '.cmd') })
foreach ($f in $scripts) { Copy-Item -LiteralPath $f.FullName -Destination $staging }
$extras = @('CHECKSUMS.txt', 'zconfig.example.json', 'README.md', 'CHANGELOG.md', 'LICENSE', 'TOKEN_SAVINGS.md', 'build-version.json')
$included = @()
foreach ($name in $extras) {
$p = Join-Path $PSScriptRoot $name
if (Test-Path -LiteralPath $p) { Copy-Item -LiteralPath $p -Destination $staging; $included += $name }
}
if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
[IO.Compression.ZipFile]::CreateFromDirectory($staging, $zipPath, [IO.Compression.CompressionLevel]::Optimal, $false)
}
finally {
Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
}
$hash = Get-Sha256 $zipPath
# sha256sum format, LF ending, so `sha256sum -c` works on Linux/macOS.
[IO.File]::WriteAllText($shaPath, "$hash $zipName`n", (New-Object Text.UTF8Encoding($false)))
$sizeKb = [math]::Round((Get-Item -LiteralPath $zipPath).Length / 1KB, 1)
Write-Host ""
Write-Host "=== zrelease ===" -ForegroundColor Cyan
Write-Host " Version: $version" -ForegroundColor Green
Write-Host " Zip: releases/$zipName ($sizeKb KB, $($scripts.Count) scripts + $($included.Count) support file(s))" -ForegroundColor Gray
Write-Host " SHA-256: $hash" -ForegroundColor Gray
Write-Host " Digest: releases/$zipName.sha256" -ForegroundColor Gray
Write-Host ""
Write-Host " Commit both files - a release lives in the repo under releases/." -ForegroundColor DarkGray
Write-Host ""
exit 0