Files
Pulse/scripts/validate-release.sh
rcourtman 535ae1c70f Fix Windows/macOS host agent downloads for bare metal installs (related to #684)
Bare metal installations couldn't serve Windows host agent downloads because
the Windows and macOS binaries weren't included in the universal tarball. The
download endpoint would return 404 when Windows users tried to install the
host agent from a bare metal Pulse deployment (Proxmox LXC, Debian VM, etc.).

Changes:
- build-release.sh: Copy Windows/macOS host agent binaries into universal tarball
- build-release.sh: Create symlinks for Windows binaries without .exe extension
- validate-release.sh: Add Windows 386 binary and symlink to Docker validation
- validate-release.sh: Add explicit validation that universal tarball contains all Windows/macOS binaries

The universal tarball now matches the Docker image, ensuring both deployment
methods can serve the complete set of downloadable binaries for the /download/
endpoint.
2025-11-11 21:26:33 +00:00

330 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pulse Release Validation Script
# Comprehensive artifact validation to prevent missing files/binaries in releases
#
# Usage: ./scripts/validate-release.sh <pulse-version> [image] [release-dir] [--skip-docker]
# Example: ./scripts/validate-release.sh 4.26.2
# ./scripts/validate-release.sh 4.26.2 rcourtman/pulse:v4.26.2 release
# ./scripts/validate-release.sh 4.26.2 --skip-docker
set -euo pipefail
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
success() {
echo -e "${GREEN}[✓]${NC} $*"
}
info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
if [ $# -lt 1 ]; then
error "Usage: $0 <pulse-version> [image] [release-dir] [--skip-docker]"
exit 1
fi
PULSE_VERSION=$1
PULSE_TAG="v${PULSE_VERSION}"
SKIP_DOCKER=false
# Parse arguments
shift
while [ $# -gt 0 ]; do
case "$1" in
--skip-docker)
SKIP_DOCKER=true
shift
;;
*)
if [ -z "${IMAGE:-}" ]; then
IMAGE="$1"
elif [ -z "${RELEASE_DIR:-}" ]; then
RELEASE_DIR="$1"
fi
shift
;;
esac
done
# Set defaults
IMAGE=${IMAGE:-"rcourtman/pulse:${PULSE_TAG}"}
RELEASE_DIR=${RELEASE_DIR:-"release"}
# Validate prerequisites
if [ "$SKIP_DOCKER" = false ]; then
command -v docker >/dev/null || { error "docker is required (use --skip-docker to skip Docker validation)"; exit 1; }
fi
[ -d "$RELEASE_DIR" ] || { error "release dir not found: $RELEASE_DIR"; exit 1; }
# Create temp directory for extractions
tmp_root=$(mktemp -d)
trap 'rm -rf "$tmp_root"' EXIT
info "Validating Pulse $PULSE_TAG release artifacts"
info "Image: $IMAGE"
info "Release directory: $RELEASE_DIR"
echo ""
#=============================================================================
# DOCKER IMAGE VALIDATION
#=============================================================================
if [ "$SKIP_DOCKER" = false ]; then
info "=== Docker Image Validation ==="
# Validate VERSION file in container
info "Checking VERSION file in Docker image..."
docker run --rm --entrypoint /bin/sh -e EXPECTED_VERSION="$PULSE_VERSION" "$IMAGE" -c 'set -euo pipefail; actual=$(cat /VERSION | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] || { echo "VERSION mismatch: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }' || { error "VERSION file mismatch in Docker image"; exit 1; }
success "VERSION file correct: $PULSE_VERSION"
# Validate all required scripts exist and are executable
info "Checking installer/uninstaller scripts in /opt/pulse/scripts/..."
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/scripts; required="install-docker-agent.sh install-container-agent.sh install-host-agent.sh install-host-agent.ps1 uninstall-host-agent.sh uninstall-host-agent.ps1 install-sensor-proxy.sh install-docker.sh"; for f in $required; do [ -f "$f" ] || { echo "missing script $f" >&2; exit 1; }; case "$f" in *.sh|*.ps1) [ -x "$f" ] || { echo "$f not executable" >&2; exit 1; };; esac; done; echo "All scripts present and executable"' || { error "Script validation failed"; exit 1; }
success "All installer/uninstaller scripts present and executable"
# Validate all required binaries exist and are non-empty
info "Checking downloadable binaries in /opt/pulse/bin/..."
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-docker-agent pulse-docker-agent-linux-amd64 pulse-docker-agent-linux-arm64 pulse-docker-agent-linux-armv7 pulse-docker-agent-linux-armv6 pulse-docker-agent-linux-386 pulse-host-agent-linux-amd64 pulse-host-agent-linux-arm64 pulse-host-agent-linux-armv7 pulse-host-agent-linux-armv6 pulse-host-agent-linux-386 pulse-host-agent-darwin-amd64 pulse-host-agent-darwin-arm64 pulse-host-agent-windows-amd64.exe pulse-host-agent-windows-amd64 pulse-host-agent-windows-arm64.exe pulse-host-agent-windows-arm64 pulse-host-agent-windows-386.exe pulse-host-agent-windows-386 pulse-sensor-proxy pulse-sensor-proxy-linux-amd64 pulse-sensor-proxy-linux-arm64 pulse-sensor-proxy-linux-armv7 pulse-sensor-proxy-linux-armv6 pulse-sensor-proxy-linux-386"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-host-agent-windows-amd64)" = "pulse-host-agent-windows-amd64.exe" ] || { echo "windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-host-agent-windows-arm64)" = "pulse-host-agent-windows-arm64.exe" ] || { echo "windows arm64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-host-agent-windows-386)" = "pulse-host-agent-windows-386.exe" ] || { echo "windows 386 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
success "All downloadable binaries present (26 binaries + 3 Windows symlinks)"
# Validate version embedding in Docker image binaries
info "Validating version embedding in Docker image binaries..."
# Pulse server binary
docker run --rm --entrypoint /app/pulse "$IMAGE" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Pulse server version mismatch"; exit 1; }
success "Pulse server version: $PULSE_TAG"
# Host agent binary
docker run --rm --entrypoint /opt/pulse/bin/pulse-host-agent-linux-amd64 "$IMAGE" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Host agent version mismatch"; exit 1; }
success "Host agent version: $PULSE_TAG"
# Sensor proxy binary
docker run --rm --entrypoint /opt/pulse/bin/pulse-sensor-proxy-linux-amd64 "$IMAGE" version 2>/dev/null | grep -Fx "pulse-sensor-proxy $PULSE_TAG" >/dev/null || { error "Sensor proxy version mismatch"; exit 1; }
success "Sensor proxy version: $PULSE_TAG"
# Docker agent binary (no CLI flag, check binary strings)
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /opt/pulse/bin/pulse-docker-agent-linux-amd64 >/dev/null' || { error "Docker agent version string not found"; exit 1; }
success "Docker agent version embedded: $PULSE_TAG"
echo ""
else
warn "=== Skipping Docker Image Validation (--skip-docker flag provided) ==="
echo ""
fi
#=============================================================================
# RELEASE TARBALL VALIDATION
#=============================================================================
info "=== Release Tarball Validation ==="
pushd "$RELEASE_DIR" >/dev/null
# Validate all expected release assets exist
info "Checking required release assets..."
# NOTE: Standalone binaries are NOT in GitHub releases
# They are only included in Docker images for /download/ endpoints
# NOTE: Linux host-agent binaries are in the main pulse tarballs, not separate archives
required_assets=(
"install.sh"
"checksums.txt"
"pulse-v${PULSE_VERSION}.tar.gz"
"pulse-v${PULSE_VERSION}-linux-amd64.tar.gz"
"pulse-v${PULSE_VERSION}-linux-arm64.tar.gz"
"pulse-v${PULSE_VERSION}-linux-armv7.tar.gz"
"pulse-v${PULSE_VERSION}-linux-armv6.tar.gz"
"pulse-v${PULSE_VERSION}-linux-386.tar.gz"
"pulse-host-agent-v${PULSE_VERSION}-darwin-amd64.tar.gz"
"pulse-host-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz"
"pulse-host-agent-v${PULSE_VERSION}-windows-amd64.zip"
"pulse-host-agent-v${PULSE_VERSION}-windows-arm64.zip"
"pulse-host-agent-v${PULSE_VERSION}-windows-386.zip"
)
missing_count=0
for asset in "${required_assets[@]}"; do
if [ ! -e "$asset" ]; then
error "Missing release asset: $asset"
missing_count=$((missing_count + 1))
fi
done
if [ $missing_count -gt 0 ]; then
error "$missing_count required assets missing"
exit 1
fi
success "All ${#required_assets[@]} required release assets present"
# Validate tarball contents
info "Validating tarball contents..."
for arch in linux-amd64 linux-arm64 linux-armv7; do
tarball="pulse-v${PULSE_VERSION}-${arch}.tar.gz"
# Check binaries (note: tarballs use ./ prefix)
if ! tar -tzf "$tarball" ./bin/pulse ./bin/pulse-docker-agent ./bin/pulse-host-agent ./bin/pulse-sensor-proxy >/dev/null 2>&1; then
error "$(basename $tarball) missing binaries"
exit 1
fi
# Check scripts
tar -tzf "$tarball" ./scripts/install-docker-agent.sh ./scripts/install-container-agent.sh ./scripts/install-host-agent.sh ./scripts/install-host-agent.ps1 ./scripts/uninstall-host-agent.sh ./scripts/uninstall-host-agent.ps1 ./scripts/install-sensor-proxy.sh ./scripts/install-docker.sh >/dev/null 2>&1 || { error "$(basename $tarball) missing scripts"; exit 1; }
# Check VERSION file
tar -tzf "$tarball" ./VERSION >/dev/null 2>&1 || { error "$(basename $tarball) missing VERSION file"; exit 1; }
done
success "Platform-specific tarballs contain all required files"
# Validate universal tarball
tar -tzf "pulse-v${PULSE_VERSION}.tar.gz" ./VERSION >/dev/null 2>&1 || { error "Universal tarball missing VERSION file"; exit 1; }
# Validate universal tarball contains Windows/macOS binaries for download endpoint
info "Validating universal tarball contains Windows/macOS binaries..."
for binary in \
./bin/pulse-host-agent-darwin-amd64 \
./bin/pulse-host-agent-darwin-arm64 \
./bin/pulse-host-agent-windows-amd64.exe \
./bin/pulse-host-agent-windows-arm64.exe \
./bin/pulse-host-agent-windows-386.exe \
./bin/pulse-host-agent-windows-amd64 \
./bin/pulse-host-agent-windows-arm64 \
./bin/pulse-host-agent-windows-386; do
tar -tzf "pulse-v${PULSE_VERSION}.tar.gz" "$binary" >/dev/null 2>&1 || { error "Universal tarball missing $binary"; exit 1; }
done
success "Universal tarball validated (includes Windows/macOS binaries)"
# Validate macOS tarball
tar -tzf "pulse-host-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz" pulse-host-agent-darwin-arm64 >/dev/null 2>&1 || { error "macOS tarball validation failed"; exit 1; }
success "macOS host-agent tarball validated"
# Validate checksums.txt
info "Validating checksums..."
sha256sum -c checksums.txt >/dev/null 2>&1 || { error "checksums.txt validation failed"; exit 1; }
success "checksums.txt validated"
# Validate individual .sha256 files exist and match checksums.txt
info "Validating individual .sha256 files..."
while IFS= read -r line; do
checksum=$(echo "$line" | awk '{print $1}')
filename=$(echo "$line" | awk '{print $2}')
# Check .sha256 file exists
if [ ! -f "${filename}.sha256" ]; then
error "Missing ${filename}.sha256"
exit 1
fi
# Check .sha256 file content matches checksums.txt
sha256_content=$(cat "${filename}.sha256")
expected_content="${checksum} ${filename}"
if [ "$sha256_content" != "$expected_content" ]; then
error "${filename}.sha256 content mismatch"
exit 1
fi
done < checksums.txt
success "Individual .sha256 files validated"
popd >/dev/null
echo ""
#=============================================================================
# VERSION EMBEDDING VALIDATION (EXTRACTED TARBALL)
#=============================================================================
info "=== Version Embedding Validation (Extracted Binaries) ==="
# Extract linux-amd64 tarball for testing
extract_dir="$tmp_root/linux-amd64"
mkdir -p "$extract_dir"
tar -xzf "$RELEASE_DIR/pulse-v${PULSE_VERSION}-linux-amd64.tar.gz" -C "$extract_dir"
info "Testing extracted binaries from linux-amd64 tarball..."
# Test Pulse server
"$extract_dir/bin/pulse" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Extracted pulse binary version mismatch"; exit 1; }
success "Extracted pulse binary: $PULSE_TAG"
# Test host agent
"$extract_dir/bin/pulse-host-agent" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Extracted host-agent version mismatch"; exit 1; }
success "Extracted host-agent binary: $PULSE_TAG"
# Test sensor proxy
"$extract_dir/bin/pulse-sensor-proxy" version 2>/dev/null | grep -Fx "pulse-sensor-proxy $PULSE_TAG" >/dev/null || { error "Extracted sensor-proxy version mismatch"; exit 1; }
success "Extracted sensor-proxy binary: $PULSE_TAG"
# Test docker agent (no CLI flag)
grep -aF "$PULSE_TAG" "$extract_dir/bin/pulse-docker-agent" >/dev/null || { error "Extracted docker-agent version string not found"; exit 1; }
success "Extracted docker-agent binary contains: $PULSE_TAG"
# Test VERSION file
grep -Fx "$PULSE_VERSION" "$extract_dir/VERSION" >/dev/null || { error "Extracted VERSION file mismatch"; exit 1; }
success "Extracted VERSION file: $PULSE_VERSION"
echo ""
# NOTE: Standalone binary validation removed - they are NOT in GitHub releases
# They are only included in Docker images for /download/ endpoints
echo ""
#=============================================================================
# OPTIONAL: HELM CHART VALIDATION
#=============================================================================
if [ -f "$RELEASE_DIR/pulse-${PULSE_VERSION}.tgz" ]; then
info "=== Helm Chart Validation ==="
if command -v helm >/dev/null 2>&1; then
# Extract and validate Helm chart
helm_extract="$tmp_root/helm"
mkdir -p "$helm_extract"
tar -xzf "$RELEASE_DIR/pulse-${PULSE_VERSION}.tgz" -C "$helm_extract"
# Validate Chart.yaml
if [ -f "$helm_extract/pulse/Chart.yaml" ]; then
chart_version=$(grep '^version:' "$helm_extract/pulse/Chart.yaml" | awk '{print $2}')
app_version=$(grep '^appVersion:' "$helm_extract/pulse/Chart.yaml" | awk '{print $2}' | tr -d '"')
if [ "$chart_version" = "$PULSE_VERSION" ]; then
success "Helm chart version: $chart_version"
else
error "Helm chart version mismatch: expected=$PULSE_VERSION actual=$chart_version"
fi
if [ "$app_version" = "$PULSE_VERSION" ]; then
success "Helm appVersion: $app_version"
else
error "Helm appVersion mismatch: expected=$PULSE_VERSION actual=$app_version"
fi
else
warn "Helm Chart.yaml not found in extracted chart"
fi
else
warn "Helm not installed, skipping chart validation"
fi
else
info "Helm chart not found (pulse-${PULSE_VERSION}.tgz) - skipping Helm validation"
fi
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ ✓ RELEASE VALIDATION PASSED FOR ${PULSE_TAG}${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ All required artifacts, scripts, binaries, and version ║${NC}"
echo -e "${GREEN}║ strings validated successfully. ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""