mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
49412357af
Every v6 RC (rc.1 through rc.5, ~30 days) shipped the wrong install.sh. build-release.sh was copying the rendered AGENT installer into release/install.sh, but adapter_installsh, scripts/pulse-auto-update.sh, the root install.sh's own --rc/--stable/--version flows, and the README quickstart all fetch that asset and run `bash install.sh --version vX.Y.Z`. Since the agent installer rejects --version with "Unknown argument", the LXC quickstart, the in-product "Update Pulse" button on systemd/proxmoxve deployments, and the pulse-auto-update.sh systemd timer were all broken for every RC. Fix the build-release.sh copy to publish the root server installer. The agent installer continues to ship inside tarballs at ./scripts/install.sh and inside Docker images at /opt/pulse/scripts/install.sh, and is served at the running Pulse server's /install.sh endpoint — none of those paths change. Only the top-level GitHub Releases asset moves from agent to server. Update build_release_assets_test.go to lock in the new publishing rule and ban the reverse drift, replacing the March 18 "legacy root install.sh" guard that was the original mistake. Add a validate-release.sh smoke that catches the regression mode this hid: the published install.sh must have the Pulse server banner, the --version) arg handler, must not contain the agent banner, and `bash install.sh --help` must print the server installer's version-pinning help line. These checks run as part of validate-release-assets.yml against the post-publish asset bundle so a future swap back cannot slip through. Document the asset identity rule and the validate-release.sh guard in the deployment-installability contract so any future change to the publishing pipeline has to update the contract or trip the shape guard.
628 lines
25 KiB
Bash
Executable File
628 lines
25 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
|
|
}
|
|
|
|
section() {
|
|
echo ""
|
|
echo -e "${BLUE}=== ${1} ===${NC}"
|
|
}
|
|
|
|
success() {
|
|
echo -e "${GREEN}[✓]${NC} $*"
|
|
}
|
|
|
|
info() {
|
|
echo -e "${BLUE}[INFO]${NC} $*"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $*"
|
|
}
|
|
|
|
with_network_blocked() {
|
|
# Drop outbound traffic inside container by adding a reject route; avoids needing elevated host perms.
|
|
# Caller supplies: container name, command...
|
|
local container="$1"
|
|
shift
|
|
docker exec "$container" sh -c "ip route add blackhole 0.0.0.0/0 || true" && "$@"
|
|
}
|
|
|
|
check_tar_entries_nonempty() {
|
|
local tarball="$1"
|
|
shift
|
|
for entry in "$@"; do
|
|
if ! tar -tzf "$tarball" "$entry" >/dev/null 2>&1; then
|
|
error "$(basename "$tarball") missing entry: $entry"
|
|
exit 1
|
|
fi
|
|
# Examine type; skip size enforcement for symlinks
|
|
local type
|
|
type=$(tar -tvf "$tarball" "$entry" 2>/dev/null | awk 'NR==1 {print substr($0,1,1)}')
|
|
if [ "$type" = "l" ]; then
|
|
continue
|
|
fi
|
|
local size
|
|
size=$(tar -xOf "$tarball" "$entry" 2>/dev/null | wc -c | tr -d '[:space:]')
|
|
if [ -z "$size" ] || [ "$size" -le 0 ]; then
|
|
error "$(basename "$tarball") missing or empty entry: $entry"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
http_header_value() {
|
|
local header_name="$1"
|
|
local headers_file="$2"
|
|
awk -v needle="$(printf '%s' "$header_name" | tr '[:upper:]' '[:lower:]')" '
|
|
BEGIN { value = "" }
|
|
{
|
|
line = $0
|
|
sub(/\r$/, "", line)
|
|
sub(/^[[:space:]]+/, "", line)
|
|
lower = tolower(line)
|
|
if (index(lower, needle ":") == 1) {
|
|
sub(/^[^:]*:[[:space:]]*/, "", line)
|
|
value = line
|
|
}
|
|
}
|
|
END { print value }
|
|
' "$headers_file"
|
|
}
|
|
|
|
validate_download_binary_headers() {
|
|
local file_path="$1"
|
|
local headers_path="$2"
|
|
local label="$3"
|
|
local checksum_header signature_header ssh_signature_header actual_checksum
|
|
|
|
checksum_header="$(http_header_value "X-Checksum-Sha256" "$headers_path")"
|
|
signature_header="$(http_header_value "X-Signature-Ed25519" "$headers_path")"
|
|
ssh_signature_header="$(http_header_value "X-Signature-SSHSIG" "$headers_path")"
|
|
|
|
if [ -z "$checksum_header" ]; then
|
|
error "${label} missing X-Checksum-Sha256 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$signature_header" ]; then
|
|
error "${label} missing X-Signature-Ed25519 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$ssh_signature_header" ]; then
|
|
error "${label} missing X-Signature-SSHSIG header"
|
|
exit 1
|
|
fi
|
|
|
|
actual_checksum="$(sha256sum "$file_path" | awk '{print $1}')"
|
|
if [ "$actual_checksum" != "$checksum_header" ]; then
|
|
error "${label} checksum header mismatch: expected ${checksum_header}, got ${actual_checksum}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_download_script_headers() {
|
|
local headers_path="$1"
|
|
local label="$2"
|
|
local signature_header ssh_signature_header
|
|
|
|
signature_header="$(http_header_value "X-Signature-Ed25519" "$headers_path")"
|
|
ssh_signature_header="$(http_header_value "X-Signature-SSHSIG" "$headers_path")"
|
|
|
|
if [ -z "$signature_header" ]; then
|
|
error "${label} missing X-Signature-Ed25519 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$ssh_signature_header" ]; then
|
|
error "${label} missing X-Signature-SSHSIG header"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
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)
|
|
smoke_container=""
|
|
trap 'rm -rf "$tmp_root"; if [ -n "$smoke_container" ]; then docker rm -f "$smoke_container" >/dev/null 2>&1 || true; fi' 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; for path in /VERSION /app/VERSION; do if [ -f "$path" ]; then actual=$(cat "$path" | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] && exit 0 || { echo "VERSION mismatch at $path: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }; fi; done; echo "VERSION file not found in image" >&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-container-agent.sh install-docker.sh install.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-agent-linux-amd64 pulse-agent-linux-arm64 pulse-agent-linux-armv7 pulse-agent-linux-armv6 pulse-agent-linux-386 pulse-agent-darwin-amd64 pulse-agent-darwin-arm64 pulse-agent-windows-amd64.exe pulse-agent-windows-amd64 pulse-agent-windows-arm64.exe pulse-agent-windows-arm64 pulse-agent-windows-386.exe pulse-agent-windows-386 pulse-agent-freebsd-amd64 pulse-agent-freebsd-arm64"; 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-agent-windows-amd64)" = "pulse-agent-windows-amd64.exe" ] || { echo "unified agent windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-arm64)" = "pulse-agent-windows-arm64.exe" ] || { echo "unified agent windows arm64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-386)" = "pulse-agent-windows-386.exe" ] || { echo "unified agent windows 386 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
|
|
success "All downloadable binaries present"
|
|
|
|
# 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"
|
|
|
|
# Docker agent is embedded in the main pulse binary (check binary strings)
|
|
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /app/pulse >/dev/null' || { error "Docker agent version string not found"; exit 1; }
|
|
success "Docker agent version embedded: $PULSE_TAG"
|
|
|
|
# Unified agent binary
|
|
docker run --rm --entrypoint /opt/pulse/bin/pulse-agent-linux-amd64 "$IMAGE" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Unified agent version mismatch"; exit 1; }
|
|
success "Unified agent version: $PULSE_TAG"
|
|
|
|
# Smoke test download endpoints from a running container
|
|
info "Running download endpoint smoke tests..."
|
|
HOST_PORT=8765
|
|
SMOKE_CONTAINER="pulse-download-smoke-$$"
|
|
smoke_container="$SMOKE_CONTAINER"
|
|
|
|
docker run -d --rm \
|
|
--name "$SMOKE_CONTAINER" \
|
|
-p "$HOST_PORT:7655" \
|
|
-e PULSE_MOCK_MODE=true \
|
|
-e PULSE_ALLOW_DOCKER_UPDATES=true \
|
|
-e PULSE_AUTH_USER=admin \
|
|
-e PULSE_AUTH_PASS=admin \
|
|
"$IMAGE" >/dev/null
|
|
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS "http://127.0.0.1:${HOST_PORT}/api/health" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
if [ "$i" -eq 30 ]; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Pulse container did not become healthy for download smoke tests"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
download_matrix=(
|
|
"linux amd64"
|
|
"linux arm64"
|
|
"linux armv7"
|
|
"linux armv6"
|
|
"linux 386"
|
|
"darwin amd64"
|
|
"darwin arm64"
|
|
"windows amd64"
|
|
"windows arm64"
|
|
"windows 386"
|
|
)
|
|
|
|
for script_name in install.sh install.ps1; do
|
|
url="http://127.0.0.1:${HOST_PORT}/download/${script_name}"
|
|
tmp_file=$(mktemp)
|
|
tmp_headers=$(mktemp)
|
|
if ! curl -fsS -D "$tmp_headers" -o "$tmp_file" "$url"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Download failed for ${script_name}"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$tmp_file" ]; then
|
|
error "Downloaded empty ${script_name}"
|
|
exit 1
|
|
fi
|
|
validate_download_script_headers "$tmp_headers" "${script_name}"
|
|
rm -f "$tmp_file" "$tmp_headers"
|
|
done
|
|
success "Install script endpoints returned required signature headers"
|
|
|
|
for entry in "${download_matrix[@]}"; do
|
|
set -- $entry
|
|
platform=$1
|
|
arch=$2
|
|
url="http://127.0.0.1:${HOST_PORT}/download/pulse-agent?arch=${platform}-${arch}"
|
|
tmp_file=$(mktemp)
|
|
tmp_headers=$(mktemp)
|
|
if ! curl -fsS -D "$tmp_headers" -o "$tmp_file" "$url"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Download failed for $platform/$arch"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$tmp_file" ]; then
|
|
error "Downloaded empty binary for $platform/$arch"
|
|
exit 1
|
|
fi
|
|
validate_download_binary_headers "$tmp_file" "$tmp_headers" "${platform}/${arch}"
|
|
rm -f "$tmp_file" "$tmp_headers"
|
|
done
|
|
success "Download endpoints returned binaries with checksum and signature headers for all platforms/architectures"
|
|
|
|
docker rm -f "$SMOKE_CONTAINER" >/dev/null 2>&1 || true
|
|
smoke_container=""
|
|
|
|
echo ""
|
|
|
|
# Offline self-heal check: run with no outbound network and confirm download endpoint still serves binaries
|
|
section "Offline self-heal smoke test"
|
|
SMOKE_CONTAINER="pulse-offline-smoke-$$"
|
|
smoke_container="$SMOKE_CONTAINER"
|
|
docker run -d --rm \
|
|
--name "$SMOKE_CONTAINER" \
|
|
--network none \
|
|
-e PULSE_MOCK_MODE=true \
|
|
-e PULSE_ALLOW_DOCKER_UPDATES=true \
|
|
-e PULSE_AUTH_USER=admin \
|
|
-e PULSE_AUTH_PASS=admin \
|
|
"$IMAGE" >/dev/null
|
|
|
|
for i in $(seq 1 30); do
|
|
if docker exec "$SMOKE_CONTAINER" wget -qO- http://127.0.0.1:7655/api/health >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
if [ "$i" -eq 30 ]; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Pulse container did not become healthy for offline smoke tests"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
offline_tmp=$(mktemp)
|
|
offline_headers=$(mktemp)
|
|
if ! docker exec "$SMOKE_CONTAINER" sh -c "headers=\$(mktemp); wget -qS -O- 'http://127.0.0.1:7655/download/pulse-agent?arch=linux-amd64' 2>\"\$headers\"; status=\$?; cat \"\$headers\" >&2; rm -f \"\$headers\"; exit \$status" > "$offline_tmp" 2> "$offline_headers"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Offline self-heal failed: download endpoint returned error with no outbound network"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$offline_tmp" ]; then
|
|
error "Offline self-heal failed: downloaded binary is empty"
|
|
exit 1
|
|
fi
|
|
validate_download_binary_headers "$offline_tmp" "$offline_headers" "offline linux/amd64"
|
|
rm -f "$offline_tmp" "$offline_headers"
|
|
success "Offline self-heal: download endpoint works with checksum and signature headers without outbound network"
|
|
|
|
docker rm -f "$SMOKE_CONTAINER" >/dev/null 2>&1 || true
|
|
smoke_container=""
|
|
|
|
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
|
|
required_assets=(
|
|
"install.sh"
|
|
"checksums.txt"
|
|
"pulse-v${PULSE_VERSION}-release.sbom.spdx.json"
|
|
"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-agent-v${PULSE_VERSION}-darwin-amd64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-amd64.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-arm64.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-386.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-freebsd-amd64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-freebsd-arm64.tar.gz"
|
|
)
|
|
|
|
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 published install.sh is the Pulse SERVER installer.
|
|
# Across v6 rc.1 → rc.5 the rendered AGENT installer was published here by
|
|
# mistake, silently breaking the LXC quickstart, the in-product Update Pulse
|
|
# button, and the pulse-auto-update.sh systemd timer for 30 days before anyone
|
|
# noticed. This guard pins the asset identity so it cannot drift back.
|
|
info "Validating install.sh is the Pulse server installer..."
|
|
install_sh_path="install.sh"
|
|
if [ ! -s "$install_sh_path" ]; then
|
|
error "install.sh is missing or empty"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^# Pulse Installer Script' "$install_sh_path"; then
|
|
error "install.sh banner does not match the Pulse server installer"
|
|
error "If this fires, build-release.sh is publishing the wrong file (likely the agent installer)"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^[[:space:]]*--version\)' "$install_sh_path"; then
|
|
error "install.sh is missing the --version arg handler — required by adapter_installsh, pulse-auto-update.sh, and the README quickstart"
|
|
exit 1
|
|
fi
|
|
if grep -q 'Pulse Unified Agent Installer' "$install_sh_path"; then
|
|
error "install.sh is the agent installer, not the server installer — releases must publish the root install.sh"
|
|
exit 1
|
|
fi
|
|
# Smoke: actually invoke `bash install.sh --help` and confirm it prints the
|
|
# server-installer help text. Catches parse-time syntax breakage and confirms
|
|
# the script is structurally executable, not just textually correct.
|
|
install_help_output=$(bash "$install_sh_path" --help 2>&1 || true)
|
|
if ! echo "$install_help_output" | grep -qF "Install specific version (e.g."; then
|
|
error "bash install.sh --help did not print the server installer's version-pinning help line"
|
|
error "Help output captured: $install_help_output"
|
|
exit 1
|
|
fi
|
|
success "install.sh is the Pulse server installer (handles --version, --help prints server help)"
|
|
|
|
# Validate tarball contents
|
|
section "Validating tarball contents"
|
|
tar_arches=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386)
|
|
unified_agent_entries=(
|
|
./bin/pulse-agent-linux-amd64
|
|
./bin/pulse-agent-linux-arm64
|
|
./bin/pulse-agent-linux-armv7
|
|
./bin/pulse-agent-linux-armv6
|
|
./bin/pulse-agent-linux-386
|
|
./bin/pulse-agent-darwin-amd64
|
|
./bin/pulse-agent-darwin-arm64
|
|
./bin/pulse-agent-windows-amd64.exe
|
|
./bin/pulse-agent-windows-arm64.exe
|
|
./bin/pulse-agent-windows-386.exe
|
|
./bin/pulse-agent-windows-amd64
|
|
./bin/pulse-agent-windows-arm64
|
|
./bin/pulse-agent-windows-386
|
|
./bin/pulse-agent-freebsd-amd64
|
|
./bin/pulse-agent-freebsd-arm64
|
|
)
|
|
for arch in "${tar_arches[@]}"; do
|
|
tarball="pulse-v${PULSE_VERSION}-${arch}.tar.gz"
|
|
|
|
# Check binaries (note: tarballs use ./ prefix)
|
|
if ! tar -tzf "$tarball" ./bin/pulse >/dev/null 2>&1; then
|
|
error "$(basename $tarball) missing binaries"
|
|
exit 1
|
|
fi
|
|
|
|
check_tar_entries_nonempty "$tarball" "${unified_agent_entries[@]}"
|
|
|
|
# Check scripts
|
|
tar -tzf "$tarball" ./scripts/install-container-agent.sh ./scripts/install-docker.sh ./scripts/install.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 (including cross-platform unified agents)"
|
|
|
|
# Validate universal tarball
|
|
section "Validating 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 all agent binaries for download endpoint
|
|
info "Validating universal tarball contains all agent binaries..."
|
|
check_tar_entries_nonempty "pulse-v${PULSE_VERSION}.tar.gz" "${unified_agent_entries[@]}"
|
|
success "Universal tarball validated (includes cross-platform unified agents)"
|
|
|
|
# Validate macOS tarballs
|
|
tar -tzf "pulse-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz" pulse-agent-darwin-arm64 >/dev/null 2>&1 || { error "macOS unified-agent tarball validation failed"; exit 1; }
|
|
success "macOS agent tarballs 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"
|
|
|
|
release_sbom="pulse-${PULSE_TAG}-release.sbom.spdx.json"
|
|
if ! grep -F " ${release_sbom}" checksums.txt >/dev/null 2>&1; then
|
|
error "checksums.txt is missing ${release_sbom}"
|
|
exit 1
|
|
fi
|
|
success "Release SBOM is listed in checksums.txt"
|
|
|
|
# Validate release signature sidecars
|
|
info "Validating SSH signature sidecars..."
|
|
if [ ! -s "checksums.txt.sshsig" ]; then
|
|
error "Missing or empty checksums.txt.sshsig"
|
|
exit 1
|
|
fi
|
|
|
|
while IFS= read -r line; do
|
|
checksum=$(echo "$line" | awk '{print $1}')
|
|
filename=$(echo "$line" | awk '{print $2}')
|
|
|
|
[ -n "$checksum" ] || continue
|
|
[ -n "$filename" ] || continue
|
|
|
|
if [ ! -s "${filename}.sshsig" ]; then
|
|
error "Missing or empty ${filename}.sshsig"
|
|
exit 1
|
|
fi
|
|
done < checksums.txt
|
|
success "SSH signature sidecars 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}')
|
|
|
|
[ -n "$checksum" ] || continue
|
|
[ -n "$filename" ] || continue
|
|
|
|
# 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"
|
|
|
|
# Ensure extracted pulse binary contains embedded version string metadata.
|
|
grep -aF "$PULSE_TAG" "$extract_dir/bin/pulse" >/dev/null || { error "Extracted pulse binary does not contain expected version string"; exit 1; }
|
|
success "Extracted pulse binary contains expected version string: $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 ""
|