fix(release): harden cross-platform offline installers (#131)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# Wrap each platform-specific Release archive with the matching installer and checksum.
|
||||
# Relative artifact paths are resolved from the repository root.
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd "$(dirname "$0")/.." && pwd)"
|
||||
ARTIFACT_INPUT="${1:-dist-artifacts}"
|
||||
|
||||
case "$ARTIFACT_INPUT" in
|
||||
/*) ARTIFACT_DIR="$ARTIFACT_INPUT" ;;
|
||||
*) ARTIFACT_DIR="$ROOT_DIR/$ARTIFACT_INPUT" ;;
|
||||
esac
|
||||
|
||||
[ -d "$ARTIFACT_DIR" ] || {
|
||||
echo "error: artifact directory not found: $ARTIFACT_DIR" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v tar >/dev/null 2>&1 || {
|
||||
echo "error: tar is required" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v zip >/dev/null 2>&1 || {
|
||||
echo "error: zip is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_sha256() {
|
||||
file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "$(dirname "$file")" && sha256sum "$(basename "$file")" > "$(basename "$file").sha256")
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "$(dirname "$file")" && shasum -a 256 "$(basename "$file")" > "$(basename "$file").sha256")
|
||||
else
|
||||
echo "error: sha256sum or shasum is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_payload() {
|
||||
payload="$1"
|
||||
[ -f "$ARTIFACT_DIR/$payload" ] || {
|
||||
echo "error: missing payload: $ARTIFACT_DIR/$payload" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$ARTIFACT_DIR/$payload.sha256" ] || {
|
||||
echo "error: missing payload checksum: $ARTIFACT_DIR/$payload.sha256" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
|
||||
payload="penguin-$target.tar.gz"
|
||||
output="$ARTIFACT_DIR/penguin-$target-offline.tar.gz"
|
||||
bundle="$WORK_DIR/$target"
|
||||
require_payload "$payload"
|
||||
mkdir -p "$bundle"
|
||||
cp "$ARTIFACT_DIR/$payload" "$ARTIFACT_DIR/$payload.sha256" "$bundle/"
|
||||
cp "$ROOT_DIR/install.sh" "$bundle/penguin-installer.sh"
|
||||
{
|
||||
printf '%s\n' '#!/bin/sh'
|
||||
printf '%s\n' '# Offline bundle entry point. The payload path is explicit so the online installer never scans its directory.'
|
||||
printf '%s\n' 'set -eu'
|
||||
printf '%s\n' 'SCRIPT_DIR="$(CDPATH= cd "$(dirname "$0")" && pwd)"'
|
||||
printf 'exec sh "$SCRIPT_DIR/penguin-installer.sh" --archive "$SCRIPT_DIR/%s" "$@"\n' "$payload"
|
||||
} > "$bundle/install.sh"
|
||||
chmod +x "$bundle/install.sh" "$bundle/penguin-installer.sh"
|
||||
rm -f "$output" "$output.sha256"
|
||||
tar -czf "$output" -C "$bundle" .
|
||||
write_sha256 "$output"
|
||||
echo "Created $(basename "$output")"
|
||||
done
|
||||
|
||||
payload="penguin-win32-x64.zip"
|
||||
output="$ARTIFACT_DIR/penguin-win32-x64-offline.zip"
|
||||
bundle="$WORK_DIR/win32-x64"
|
||||
require_payload "$payload"
|
||||
mkdir -p "$bundle"
|
||||
cp "$ARTIFACT_DIR/$payload" "$ARTIFACT_DIR/$payload.sha256" \
|
||||
"$ROOT_DIR/install.ps1" "$ROOT_DIR/install.cmd" "$bundle/"
|
||||
rm -f "$output" "$output.sha256"
|
||||
(cd "$bundle" && zip -qr "$output" .)
|
||||
write_sha256 "$output"
|
||||
echo "Created $(basename "$output")"
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# Smoke-test five offline wrappers and POSIX upgrade rollback with tiny fixtures.
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
ARTIFACT_DIR="$WORK_DIR/artifacts"
|
||||
TEST_HOME="$WORK_DIR/home"
|
||||
INSTALL_DIR="$TEST_HOME/.penguin"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT HUP INT TERM
|
||||
|
||||
fail_test() {
|
||||
echo "test failure: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_sha256() {
|
||||
file="$1"
|
||||
(cd "$(dirname "$file")" && sha256sum "$(basename "$file")" > "$(basename "$file").sha256")
|
||||
}
|
||||
|
||||
make_posix_payload() {
|
||||
target="$1"
|
||||
output="$2"
|
||||
behavior="${3:-success}"
|
||||
payload="$WORK_DIR/payload"
|
||||
rm -rf "$payload"
|
||||
mkdir -p "$payload/penguin/bin" "$payload/penguin/lib" "$payload/penguin/web"
|
||||
if [ "$behavior" = "final-failure" ]; then
|
||||
{
|
||||
printf '%s\n' '#!/bin/sh'
|
||||
printf '%s\n' 'case "$0" in'
|
||||
printf '%s\n' ' */.staging.*/bin/penguin) echo fixture-new; exit 0 ;;'
|
||||
printf '%s\n' ' *) echo "fixture final-path failure" >&2; exit 42 ;;'
|
||||
printf '%s\n' 'esac'
|
||||
} > "$payload/penguin/bin/penguin"
|
||||
else
|
||||
{
|
||||
printf '%s\n' '#!/bin/sh'
|
||||
printf '%s\n' 'echo fixture-old'
|
||||
} > "$payload/penguin/bin/penguin"
|
||||
fi
|
||||
chmod +x "$payload/penguin/bin/penguin"
|
||||
printf '%s\n' fixture > "$payload/penguin/lib/fixture.txt"
|
||||
printf '%s\n' fixture > "$payload/penguin/web/index.html"
|
||||
printf '{"schemaVersion":1,"target":"%s"}\n' "$target" > "$payload/penguin/package-manifest.json"
|
||||
tar -czf "$output" -C "$payload" penguin
|
||||
write_sha256 "$output"
|
||||
}
|
||||
|
||||
verify_bundle() {
|
||||
bundle="$1"
|
||||
shift
|
||||
[ -f "$bundle" ] || fail_test "missing $(basename "$bundle")"
|
||||
[ -f "$bundle.sha256" ] || fail_test "missing $(basename "$bundle").sha256"
|
||||
(cd "$(dirname "$bundle")" && sha256sum -c "$(basename "$bundle").sha256" >/dev/null)
|
||||
members="$("$@" | sed 's#^\./##' | sed '/^$/d')"
|
||||
count="$(printf '%s\n' "$members" | wc -l | tr -d ' ')"
|
||||
[ "$count" -eq 4 ] || fail_test "$(basename "$bundle") should contain exactly four files"
|
||||
}
|
||||
|
||||
command -v sha256sum >/dev/null 2>&1 || fail_test "sha256sum is required"
|
||||
command -v unzip >/dev/null 2>&1 || fail_test "unzip is required"
|
||||
mkdir -p "$ARTIFACT_DIR" "$TEST_HOME"
|
||||
|
||||
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
|
||||
make_posix_payload "$target" "$ARTIFACT_DIR/penguin-$target.tar.gz"
|
||||
done
|
||||
|
||||
windows_payload="$WORK_DIR/windows/penguin"
|
||||
mkdir -p "$windows_payload/bin"
|
||||
printf '%s\r\n' '@echo off' 'echo fixture-old' > "$windows_payload/bin/penguin.cmd"
|
||||
printf '%s\n' '{"schemaVersion":1,"target":"win32-x64"}' > "$windows_payload/package-manifest.json"
|
||||
(cd "$WORK_DIR/windows" && zip -qr "$ARTIFACT_DIR/penguin-win32-x64.zip" penguin)
|
||||
write_sha256 "$ARTIFACT_DIR/penguin-win32-x64.zip"
|
||||
|
||||
sh "$ROOT_DIR/scripts/package-offline-bundles.sh" "$ARTIFACT_DIR"
|
||||
|
||||
for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
|
||||
bundle="$ARTIFACT_DIR/penguin-$target-offline.tar.gz"
|
||||
verify_bundle "$bundle" tar -tzf "$bundle"
|
||||
tar -tzf "$bundle" | grep -q "penguin-$target.tar.gz.sha256" \
|
||||
|| fail_test "$(basename "$bundle") is missing its payload checksum"
|
||||
done
|
||||
|
||||
windows_bundle="$ARTIFACT_DIR/penguin-win32-x64-offline.zip"
|
||||
verify_bundle "$windows_bundle" unzip -Z1 "$windows_bundle"
|
||||
unzip -Z1 "$windows_bundle" | grep -q "penguin-win32-x64.zip.sha256" \
|
||||
|| fail_test "Windows bundle is missing its payload checksum"
|
||||
|
||||
HOME="$TEST_HOME" PENGUIN_INSTALL_DIR="$INSTALL_DIR" \
|
||||
sh "$ROOT_DIR/install.sh" --archive "$ARTIFACT_DIR/penguin-linux-x64.tar.gz" >/dev/null
|
||||
|
||||
failure_archive="$WORK_DIR/final-failure.tar.gz"
|
||||
make_posix_payload linux-x64 "$failure_archive" final-failure
|
||||
set +e
|
||||
HOME="$TEST_HOME" PENGUIN_INSTALL_DIR="$INSTALL_DIR" \
|
||||
sh "$ROOT_DIR/install.sh" --archive "$failure_archive" >/dev/null 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
[ "$status" -ne 0 ] || fail_test "failing POSIX upgrade unexpectedly succeeded"
|
||||
[ "$("$INSTALL_DIR/bin/penguin" --version)" = "fixture-old" ] \
|
||||
|| fail_test "previous POSIX installation was not restored"
|
||||
|
||||
echo "Offline bundle and POSIX rollback smoke tests passed."
|
||||
@@ -0,0 +1,65 @@
|
||||
# Smoke-test Windows offline upgrade rollback with tiny local archives.
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$Installer = Join-Path $RepoRoot "install.ps1"
|
||||
$WorkDir = Join-Path ([IO.Path]::GetTempPath()) "penguin-offline-install-tests-$PID"
|
||||
$OriginalPath = $env:Path
|
||||
$OriginalOs = $env:OS
|
||||
|
||||
function Assert-True([bool]$Condition, [string]$Message) {
|
||||
if (-not $Condition) { throw "test failure: $Message" }
|
||||
}
|
||||
|
||||
function New-FixtureArchive([string]$Name, [bool]$Fails = $false) {
|
||||
$SourceDir = Join-Path $WorkDir "$Name-source"
|
||||
$PenguinDir = Join-Path $SourceDir "penguin"
|
||||
New-Item -ItemType Directory -Path (Join-Path $PenguinDir "bin") -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path (Join-Path $PenguinDir "lib") -Force | Out-Null
|
||||
$VersionLines = if ($Fails) {
|
||||
@("echo fixture runtime failure 1>&2", "exit /b 42")
|
||||
} else {
|
||||
@("echo fixture-old", "exit /b 0")
|
||||
}
|
||||
@("@echo off", "if `"%~1`"==`"--version`" (") + $VersionLines + @(")", "exit /b 0") |
|
||||
Set-Content -LiteralPath (Join-Path $PenguinDir "bin\penguin.cmd") -Encoding ascii
|
||||
"fixture" | Set-Content -LiteralPath (Join-Path $PenguinDir "lib\fixture.txt") -Encoding ascii
|
||||
@{ schemaVersion = 1; target = "win32-x64" } | ConvertTo-Json -Compress |
|
||||
Set-Content -LiteralPath (Join-Path $PenguinDir "package-manifest.json") -Encoding ascii
|
||||
$Archive = Join-Path $WorkDir "$Name.zip"
|
||||
Compress-Archive -Path $PenguinDir -DestinationPath $Archive -CompressionLevel Fastest
|
||||
$Hash = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash
|
||||
"$Hash $([IO.Path]::GetFileName($Archive))" |
|
||||
Set-Content -LiteralPath "$Archive.sha256" -Encoding ascii
|
||||
return $Archive
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
|
||||
# Keep the fixture test away from the runner's user registry Path.
|
||||
$env:OS = "PenguinInstallerFixtureTest"
|
||||
$InstallDir = Join-Path $WorkDir "installed"
|
||||
$GoodArchive = New-FixtureArchive "valid"
|
||||
& $Installer -InstallDir $InstallDir -ArchivePath $GoodArchive *>&1 | Out-Null
|
||||
|
||||
$FailedArchive = New-FixtureArchive "failure" $true
|
||||
$Failed = $false
|
||||
try {
|
||||
& $Installer -InstallDir $InstallDir -ArchivePath $FailedArchive *>&1 | Out-Null
|
||||
} catch {
|
||||
$Failed = $true
|
||||
}
|
||||
Assert-True $Failed "failing Windows upgrade unexpectedly succeeded"
|
||||
$Version = & (Join-Path $InstallDir "bin\penguin.cmd") --version
|
||||
Assert-True ($Version -eq "fixture-old") "previous Windows installation was not restored"
|
||||
Write-Host "Windows offline rollback smoke test passed."
|
||||
} finally {
|
||||
$env:Path = $OriginalPath
|
||||
$env:OS = $OriginalOs
|
||||
if (Test-Path -LiteralPath $WorkDir) {
|
||||
Remove-Item -LiteralPath $WorkDir -Recurse -Force
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user