#!/usr/bin/env bash # Pulse Release Validation Script # Comprehensive artifact validation to prevent missing files/binaries in releases # # Usage: ./scripts/validate-release.sh [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 } if [ $# -lt 1 ]; then error "Usage: $0 [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-docker-agent.sh install-container-agent.sh install-host-agent.ps1 uninstall-host-agent.sh uninstall-host-agent.ps1 install-sensor-proxy.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-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-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-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; }; [ "$(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 (39 binaries + 6 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" # 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 entry in "${download_matrix[@]}"; do set -- $entry platform=$1 arch=$2 url="http://127.0.0.1:${HOST_PORT}/download/pulse-host-agent?platform=${platform}&arch=${arch}" tmp_file=$(mktemp) if ! curl -fsS -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 rm -f "$tmp_file" done success "Download endpoints returned binaries for all platforms/architectures" checksum_url="http://127.0.0.1:${HOST_PORT}/download/pulse-host-agent.sha256?platform=linux&arch=amd64" checksum_tmp=$(mktemp) if curl -fsS -o "$checksum_tmp" "$checksum_url"; then if ! grep -Eq '^[0-9a-f]{64}$' "$checksum_tmp"; then error "Invalid checksum response from $checksum_url" exit 1 fi success "Checksum endpoint responded with SHA256" else warn "Checksum endpoint unavailable (non-blocking): $checksum_url" fi rm -f "$checksum_tmp" 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) if ! docker exec "$SMOKE_CONTAINER" wget -qO- "http://127.0.0.1:7655/download/pulse-host-agent?platform=linux&arch=amd64" > "$offline_tmp"; 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 rm -f "$offline_tmp" success "Offline self-heal: download endpoint works 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 # NOTE: Linux host-agent binaries are in the main pulse tarballs, not separate archives required_assets=( "install.sh" "checksums.txt" "host-agent-manifest.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-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" "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" ) 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 host-agent manifest matches expected set section "Validating host-agent manifest" host_agent_manifest="host-agent-manifest.json" python3 - "$host_agent_manifest" "$PULSE_VERSION" <<'EOF' || { error "Host-agent manifest validation failed"; exit 1; } import json import sys import os manifest_path = sys.argv[1] version = sys.argv[2] with open(manifest_path, "r", encoding="utf-8") as handle: manifest = json.load(handle) expected_agents = { "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-arm64.exe", "pulse-host-agent-windows-386.exe", "pulse-host-agent-windows-amd64", "pulse-host-agent-windows-arm64", "pulse-host-agent-windows-386", } def check_set(name, found): missing = expected_agents - set(found) extra = set(found) - expected_agents if missing or extra: msg = [] if missing: msg.append(f"{name} missing: {sorted(missing)}") if extra: msg.append(f"{name} unexpected: {sorted(extra)}") print(" ; ".join(msg)) return False return True ok = True if manifest.get("version") != version: print(f"Manifest version mismatch: expected {version}, got {manifest.get('version')}") ok = False universal = manifest.get("universal", []) if not check_set("universal", universal): ok = False for arch in ["linux-amd64","linux-arm64","linux-armv7","linux-armv6","linux-386"]: found = manifest.get("tarballs", {}).get(arch) if found is None: print(f"Missing tarball entry in manifest for {arch}") ok = False continue if not check_set(arch, found): ok = False if not ok: sys.exit(1) EOF success "Host-agent manifest matches expected platform/arch matrix" # Validate tarball contents section "Validating tarball contents" tar_arches=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386) host_agent_entries=( ./bin/pulse-host-agent-linux-amd64 ./bin/pulse-host-agent-linux-arm64 ./bin/pulse-host-agent-linux-armv7 ./bin/pulse-host-agent-linux-armv6 ./bin/pulse-host-agent-linux-386 ./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 ) 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 ) 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 ./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_tar_entries_nonempty "$tarball" "${host_agent_entries[@]}" check_tar_entries_nonempty "$tarball" "${unified_agent_entries[@]}" # Check scripts tar -tzf "$tarball" ./scripts/install-docker-agent.sh ./scripts/install-container-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 ./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 host 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" "${host_agent_entries[@]}" check_tar_entries_nonempty "pulse-v${PULSE_VERSION}.tar.gz" "${unified_agent_entries[@]}" success "Universal tarball validated (includes cross-platform host and unified agents)" # Validate macOS tarballs tar -tzf "pulse-host-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz" pulse-host-agent-darwin-arm64 >/dev/null 2>&1 || { error "macOS host-agent tarball validation failed"; exit 1; } 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" # 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 ""