Pulse/scripts/build-release.sh
rcourtman 135b378820 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

360 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
# Build script for Pulse releases
# Creates release archives for different architectures
set -euo pipefail
# Use Go 1.24 if available
if [ -x /usr/local/go/bin/go ]; then
export PATH=/usr/local/go/bin:$PATH
fi
# Force static binaries so release artifacts run on older glibc hosts
export CGO_ENABLED=0
VERSION=${1:-$(cat VERSION)}
BUILD_DIR="build"
RELEASE_DIR="release"
echo "Building Pulse v${VERSION}..."
# Clean previous builds
rm -rf $BUILD_DIR $RELEASE_DIR
mkdir -p $BUILD_DIR $RELEASE_DIR
# Build frontend
echo "Building frontend..."
npm --prefix frontend-modern ci
npm --prefix frontend-modern run build
# Copy frontend dist for embedding (required for Go embed)
echo "Copying frontend dist for embedding..."
rm -rf internal/api/frontend-modern
mkdir -p internal/api/frontend-modern
cp -r frontend-modern/dist internal/api/frontend-modern/
# Build for different architectures
declare -A builds=(
["linux-amd64"]="GOOS=linux GOARCH=amd64"
["linux-arm64"]="GOOS=linux GOARCH=arm64"
["linux-armv7"]="GOOS=linux GOARCH=arm GOARM=7"
["linux-armv6"]="GOOS=linux GOARCH=arm GOARM=6"
["linux-386"]="GOOS=linux GOARCH=386"
)
for build_name in "${!builds[@]}"; do
echo "Building for $build_name..."
# Get build environment
build_env="${builds[$build_name]}"
build_time=$(date -u '+%Y-%m-%d_%H:%M:%S')
git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')
# Build backend binary with version info
env $build_env go build \
-ldflags="-s -w -X main.Version=v${VERSION} -X main.BuildTime=${build_time} -X main.GitCommit=${git_commit} -X github.com/rcourtman/pulse-go-rewrite/internal/dockeragent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-$build_name" \
./cmd/pulse
# Build docker agent binary
env $build_env go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/dockeragent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-docker-agent-$build_name" \
./cmd/pulse-docker-agent
# Build host agent binary
env $build_env go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-$build_name" \
./cmd/pulse-host-agent
# Build temperature proxy binary
env $build_env go build \
-ldflags="-s -w -X main.Version=v${VERSION} -X main.BuildTime=${build_time} -X main.GitCommit=${git_commit}" \
-trimpath \
-o "$BUILD_DIR/pulse-sensor-proxy-$build_name" \
./cmd/pulse-sensor-proxy
# Create release archive with proper structure
tar_name="pulse-v${VERSION}-${build_name}.tar.gz"
# Create staging directory
staging_dir="$BUILD_DIR/staging-$build_name"
rm -rf "$staging_dir"
mkdir -p "$staging_dir/bin"
mkdir -p "$staging_dir/scripts"
# Copy binaries and VERSION file
cp "$BUILD_DIR/pulse-$build_name" "$staging_dir/bin/pulse"
cp "$BUILD_DIR/pulse-docker-agent-$build_name" "$staging_dir/bin/pulse-docker-agent"
cp "$BUILD_DIR/pulse-host-agent-$build_name" "$staging_dir/bin/pulse-host-agent"
cp "$BUILD_DIR/pulse-sensor-proxy-$build_name" "$staging_dir/bin/pulse-sensor-proxy"
cp "scripts/install-docker-agent.sh" "$staging_dir/scripts/install-docker-agent.sh"
cp "scripts/install-container-agent.sh" "$staging_dir/scripts/install-container-agent.sh"
cp "scripts/install-host-agent.sh" "$staging_dir/scripts/install-host-agent.sh"
cp "scripts/install-host-agent.ps1" "$staging_dir/scripts/install-host-agent.ps1"
cp "scripts/uninstall-host-agent.sh" "$staging_dir/scripts/uninstall-host-agent.sh"
cp "scripts/uninstall-host-agent.ps1" "$staging_dir/scripts/uninstall-host-agent.ps1"
cp "scripts/install-sensor-proxy.sh" "$staging_dir/scripts/install-sensor-proxy.sh"
cp "scripts/install-docker.sh" "$staging_dir/scripts/install-docker.sh"
chmod 755 "$staging_dir/scripts/"*.sh "$staging_dir/scripts/"*.ps1
echo "$VERSION" > "$staging_dir/VERSION"
# Create tarball from staging directory
cd "$staging_dir"
tar -czf "../../$RELEASE_DIR/$tar_name" .
cd ../..
# Cleanup staging
rm -rf "$staging_dir"
echo "Created $RELEASE_DIR/$tar_name"
done
# Create universal tarball with all binaries
echo "Creating universal tarball..."
universal_dir="$BUILD_DIR/universal"
rm -rf "$universal_dir"
mkdir -p "$universal_dir/bin"
mkdir -p "$universal_dir/scripts"
# Copy all binaries to bin/ directory to maintain consistent structure
for build_name in "${!builds[@]}"; do
cp "$BUILD_DIR/pulse-$build_name" "$universal_dir/bin/pulse-${build_name}"
cp "$BUILD_DIR/pulse-docker-agent-$build_name" "$universal_dir/bin/pulse-docker-agent-${build_name}"
cp "$BUILD_DIR/pulse-host-agent-$build_name" "$universal_dir/bin/pulse-host-agent-${build_name}"
cp "$BUILD_DIR/pulse-sensor-proxy-$build_name" "$universal_dir/bin/pulse-sensor-proxy-${build_name}"
done
cp "scripts/install-docker-agent.sh" "$universal_dir/scripts/install-docker-agent.sh"
cp "scripts/install-container-agent.sh" "$universal_dir/scripts/install-container-agent.sh"
cp "scripts/install-host-agent.sh" "$universal_dir/scripts/install-host-agent.sh"
cp "scripts/install-host-agent.ps1" "$universal_dir/scripts/install-host-agent.ps1"
cp "scripts/uninstall-host-agent.sh" "$universal_dir/scripts/uninstall-host-agent.sh"
cp "scripts/uninstall-host-agent.ps1" "$universal_dir/scripts/uninstall-host-agent.ps1"
cp "scripts/install-sensor-proxy.sh" "$universal_dir/scripts/install-sensor-proxy.sh"
cp "scripts/install-docker.sh" "$universal_dir/scripts/install-docker.sh"
chmod 755 "$universal_dir/scripts/"*.sh "$universal_dir/scripts/"*.ps1
# Create a detection script that creates the pulse symlink based on architecture
cat > "$universal_dir/bin/pulse" << 'EOF'
#!/bin/sh
# Auto-detect architecture and run appropriate binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
exec "$(dirname "$0")/pulse-linux-amd64" "$@"
;;
aarch64|arm64)
exec "$(dirname "$0")/pulse-linux-arm64" "$@"
;;
armv7l|armhf)
exec "$(dirname "$0")/pulse-linux-armv7" "$@"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
EOF
chmod +x "$universal_dir/bin/pulse"
cat > "$universal_dir/bin/pulse-docker-agent" << 'EOF'
#!/bin/sh
# Auto-detect architecture and run appropriate pulse-docker-agent binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
exec "$(dirname "$0")/pulse-docker-agent-linux-amd64" "$@"
;;
aarch64|arm64)
exec "$(dirname "$0")/pulse-docker-agent-linux-arm64" "$@"
;;
armv7l|armhf)
exec "$(dirname "$0")/pulse-docker-agent-linux-armv7" "$@"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
EOF
chmod +x "$universal_dir/bin/pulse-docker-agent"
cat > "$universal_dir/bin/pulse-sensor-proxy" << 'EOF'
#!/bin/sh
# Auto-detect architecture and run appropriate pulse-sensor-proxy binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
exec "$(dirname "$0")/pulse-sensor-proxy-linux-amd64" "$@"
;;
aarch64|arm64)
exec "$(dirname "$0")/pulse-sensor-proxy-linux-arm64" "$@"
;;
armv7l|armhf)
exec "$(dirname "$0")/pulse-sensor-proxy-linux-armv7" "$@"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
EOF
chmod +x "$universal_dir/bin/pulse-sensor-proxy"
cat > "$universal_dir/bin/pulse-host-agent" << 'EOF'
#!/bin/sh
# Auto-detect architecture and run appropriate pulse-host-agent binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
exec "$(dirname "$0")/pulse-host-agent-linux-amd64" "$@"
;;
aarch64|arm64)
exec "$(dirname "$0")/pulse-host-agent-linux-arm64" "$@"
;;
armv7l|armhf)
exec "$(dirname "$0")/pulse-host-agent-linux-armv7" "$@"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
EOF
chmod +x "$universal_dir/bin/pulse-host-agent"
# Add VERSION file
echo "$VERSION" > "$universal_dir/VERSION"
# Build host agent for macOS
echo "Building host agent for macOS amd64..."
env GOOS=darwin GOARCH=amd64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-darwin-amd64" \
./cmd/pulse-host-agent
echo "Building host agent for macOS arm64..."
env GOOS=darwin GOARCH=arm64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-darwin-arm64" \
./cmd/pulse-host-agent
# Build host agent for Windows
echo "Building host agent for Windows amd64..."
env GOOS=windows GOARCH=amd64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-amd64.exe" \
./cmd/pulse-host-agent
echo "Building host agent for Windows arm64..."
env GOOS=windows GOARCH=arm64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-arm64.exe" \
./cmd/pulse-host-agent
echo "Building host agent for Windows 386..."
env GOOS=windows GOARCH=386 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-386.exe" \
./cmd/pulse-host-agent
# Package standalone host agent binaries
tar -czf "$RELEASE_DIR/pulse-host-agent-v${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" pulse-host-agent-darwin-amd64
tar -czf "$RELEASE_DIR/pulse-host-agent-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-host-agent-darwin-arm64
zip -j "$RELEASE_DIR/pulse-host-agent-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-host-agent-windows-amd64.exe"
zip -j "$RELEASE_DIR/pulse-host-agent-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-host-agent-windows-arm64.exe"
zip -j "$RELEASE_DIR/pulse-host-agent-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-host-agent-windows-386.exe"
# Copy Windows and macOS binaries into universal tarball for /download/ endpoint
echo "Adding Windows and macOS binaries to universal tarball..."
cp "$BUILD_DIR/pulse-host-agent-darwin-amd64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-host-agent-darwin-arm64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-host-agent-windows-amd64.exe" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-host-agent-windows-arm64.exe" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-host-agent-windows-386.exe" "$universal_dir/bin/"
# Create symlinks for Windows binaries without .exe extension (required for download endpoint)
ln -s pulse-host-agent-windows-amd64.exe "$universal_dir/bin/pulse-host-agent-windows-amd64"
ln -s pulse-host-agent-windows-arm64.exe "$universal_dir/bin/pulse-host-agent-windows-arm64"
ln -s pulse-host-agent-windows-386.exe "$universal_dir/bin/pulse-host-agent-windows-386"
# Create universal tarball
cd "$universal_dir"
tar -czf "../../$RELEASE_DIR/pulse-v${VERSION}.tar.gz" .
cd ../..
# Cleanup
rm -rf "$universal_dir"
# Optionally package Helm chart
if [ "${SKIP_HELM_PACKAGE:-0}" != "1" ]; then
if command -v helm >/dev/null 2>&1; then
echo "Packaging Helm chart..."
./scripts/package-helm-chart.sh "$VERSION"
if [ -f "dist/pulse-$VERSION.tgz" ]; then
cp "dist/pulse-$VERSION.tgz" "$RELEASE_DIR/"
fi
else
echo "Helm not found on PATH; skipping Helm chart packaging. Install Helm 3.9+ or set SKIP_HELM_PACKAGE=1 to silence this message."
fi
fi
# Copy install.sh to release directory (required for GitHub releases)
echo "Copying install.sh to release directory..."
cp install.sh "$RELEASE_DIR/"
# Generate checksums (include tarballs, zip files, helm chart, and install.sh)
cd "$RELEASE_DIR"
shopt -s nullglob extglob
# Match all tarballs, zip files, and install.sh
checksum_files=( *.tar.gz *.zip install.sh )
if compgen -G "pulse-*.tgz" > /dev/null; then
checksum_files+=( pulse-*.tgz )
fi
if [ ${#checksum_files[@]} -eq 0 ]; then
echo "Warning: no release artifacts found to checksum."
else
# Generate checksums from a single sha256sum run for deterministic results (prevents #671 checksum mismatches)
checksum_output="$(sha256sum "${checksum_files[@]}" | sort -k 2)"
printf '%s\n' "$checksum_output" > checksums.txt
# Emit per-file .sha256 artifacts for backward compatibility while legacy installers transition off them
while IFS= read -r checksum filename; do
printf '%s %s\n' "$checksum" "$filename" > "${filename}.sha256"
done <<< "$checksum_output"
if [ -n "${SIGNING_KEY_ID:-}" ]; then
if command -v gpg >/dev/null 2>&1; then
echo "Signing checksums with GPG key ${SIGNING_KEY_ID}..."
gpg --batch --yes --detach-sign --armor \
--local-user "${SIGNING_KEY_ID}" \
--output checksums.txt.asc \
checksums.txt
else
echo "SIGNING_KEY_ID is set but gpg is not installed; skipping signature."
fi
fi
fi
shopt -u nullglob
cd ..
echo
echo "Release build complete!"
echo "Archives created in $RELEASE_DIR/"
ls -lh $RELEASE_DIR/