Files
pulse/scripts/build-release.sh
T
pulse-triage[bot] aa50087c08 security(build): advance release toolchain to Go 1.26.7
Use the current supported Go 1.26 security patch across source, release, dev, and production container build surfaces. Keep the immutable official builder pin and governance proofs aligned with the exact toolchain.

Change-source: pulse-maintainer
2026-08-27 05:04:03 +01:00

509 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build script for Pulse releases
# Creates release archives for different architectures
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PULSE_SCRIPTS_DIR="${SCRIPT_DIR}"
PULSE_REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PULSE_REPO_ROOT}"
source "${SCRIPT_DIR}/release_asset_common.sh"
source "${SCRIPT_DIR}/release_build_targets.sh"
# Prefer the pinned toolchain from go.mod (toolchain directive).
# If /usr/local/go exists (typical in CI images), prepend it to PATH.
if [ -x /usr/local/go/bin/go ]; then
export PATH=/usr/local/go/bin:$PATH
fi
# Release artifacts must be built with the vetted toolchain to match security-gate evidence.
required_go="go1.26.7"
current_go="$(go env GOVERSION 2>/dev/null || true)"
if [[ "${PULSE_SKIP_GO_VERSION_CHECK:-false}" != "true" ]]; then
if [[ "${current_go}" != "${required_go}" ]]; then
echo "Error: Go toolchain must be ${required_go} (got ${current_go:-unknown})." >&2
echo "Tip: set GOTOOLCHAIN=auto to allow automatic toolchain download." >&2
echo "Override: PULSE_SKIP_GO_VERSION_CHECK=true (not recommended)." >&2
exit 1
fi
fi
# Force static binaries so release artifacts run on older glibc hosts
export CGO_ENABLED=0
release_go_build_args=(-buildvcs=false -trimpath)
VERSION=${1:-$(cat VERSION)}
BUILD_DIR="build"
RELEASE_DIR="release"
RENDERED_INSTALLERS_DIR="${BUILD_DIR}/rendered-installers"
RELEASE_PACKET_SBOM="pulse-v${VERSION}-release.sbom.spdx.json"
echo "Building Pulse v${VERSION}..."
# Require public key embedding for release-grade license validation.
# Explicitly opt out with PULSE_ALLOW_MISSING_LICENSE_KEY=true (not recommended).
license_ldflags_args=()
if [[ -z "${PULSE_LICENSE_PUBLIC_KEY:-}" ]]; then
if [[ "${PULSE_ALLOW_MISSING_LICENSE_KEY:-false}" == "true" ]]; then
echo "Warning: PULSE_LICENSE_PUBLIC_KEY not set; continuing because PULSE_ALLOW_MISSING_LICENSE_KEY=true."
else
echo "Error: PULSE_LICENSE_PUBLIC_KEY is required for release builds." >&2
echo "Set PULSE_ALLOW_MISSING_LICENSE_KEY=true only for local non-release debugging." >&2
exit 1
fi
else
decoded_key_len=$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | openssl base64 -d -A 2>/dev/null | wc -c | tr -d ' ')
if [[ "${decoded_key_len}" != "32" ]]; then
echo "Error: PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes (Ed25519 public key)." >&2
exit 1
fi
if [[ -n "${PULSE_LICENSE_PUBLIC_KEY_FINGERPRINT:-}" ]]; then
expected_fingerprint="${PULSE_LICENSE_PUBLIC_KEY_FINGERPRINT#SHA256:}"
actual_fingerprint=$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | openssl base64 -d -A 2>/dev/null | openssl dgst -sha256 -binary | openssl base64 -A)
if [[ -z "${actual_fingerprint}" ]]; then
echo "Error: Failed to compute fingerprint for PULSE_LICENSE_PUBLIC_KEY." >&2
exit 1
fi
if [[ "${actual_fingerprint}" != "${expected_fingerprint}" ]]; then
echo "Error: PULSE_LICENSE_PUBLIC_KEY fingerprint mismatch." >&2
echo "Expected: SHA256:${expected_fingerprint}" >&2
echo "Actual: SHA256:${actual_fingerprint}" >&2
exit 1
fi
echo "Verified license public key fingerprint: SHA256:${actual_fingerprint}"
fi
license_ldflags_args=(--license-public-key "${PULSE_LICENSE_PUBLIC_KEY}")
fi
# Require update signing for release-grade agent and installer verification.
# Explicitly opt out with PULSE_ALLOW_MISSING_UPDATE_SIGNING_KEY=true for local-only debugging.
update_ldflags_args=()
pulse_release_prepare_signing_state "pulse-installer" "pulse-install"
trap 'pulse_release_cleanup_signing_state' EXIT
if [[ -n "${PULSE_RELEASE_UPDATE_PUBLIC_KEY:-}" ]]; then
update_ldflags_args=(--update-public-keys "${PULSE_RELEASE_UPDATE_PUBLIC_KEY}")
fi
render_release_installers() {
local output_dir="$1"
mkdir -p "${output_dir}"
go run ./scripts/render_installers.go \
--source-dir ./scripts \
--output-dir "${output_dir}" \
--installer-ssh-public-key "${PULSE_RELEASE_UPDATE_SSH_PUBLIC_KEY}"
}
# Clean previous builds
rm -rf $BUILD_DIR $RELEASE_DIR
mkdir -p $BUILD_DIR $RELEASE_DIR
render_release_installers "${RENDERED_INSTALLERS_DIR}"
# Build the frontend locally, or consume the exact-SHA payload produced by the
# credential-free compilation lane.
if [[ -n "${PULSE_RELEASE_COMPILED_PAYLOAD_DIR:-}" ]]; then
compiled_payload_dir="$(cd "${PULSE_RELEASE_COMPILED_PAYLOAD_DIR}" && pwd)"
test -d "${compiled_payload_dir}/frontend-dist" || {
echo "Error: compiled release payload is missing frontend-dist." >&2
exit 1
}
rm -rf frontend-modern/dist
mkdir -p frontend-modern/dist
cp -a "${compiled_payload_dir}/frontend-dist/." frontend-modern/dist/
echo "Applied exact-SHA precompiled frontend bundle."
else
echo "Building frontend..."
npm --prefix frontend-modern ci
npm --prefix frontend-modern run build
fi
agent_ldflags="$(./scripts/release_ldflags.sh agent --version "v${VERSION}" "${update_ldflags_args[@]}")"
# Build unified agents for every supported platform/architecture
echo "Building unified agents for all platforms..."
agent_build_order=("${PULSE_RELEASE_AGENT_TARGETS[@]}")
if [[ -n "${compiled_payload_dir:-}" ]]; then
test -d "${compiled_payload_dir}/binaries" || {
echo "Error: compiled release payload is missing binaries." >&2
exit 1
}
cp -a "${compiled_payload_dir}/binaries/." "${BUILD_DIR}/"
else
for target in "${agent_build_order[@]}"; do
build_env="$(pulse_release_target_env "${target}")"
output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent "${target}")"
env ${build_env} go build \
-ldflags="${agent_ldflags}" \
"${release_go_build_args[@]}" \
-o "${output_path}" \
./cmd/pulse-agent
done
fi
# Platform-native signing jobs may supply replacement desktop binaries. They
# are copied before packaging, checksums, SBOM generation, and release signing
# so the immutable candidate manifest covers the exact signed bytes users get.
if [[ -n "${PULSE_AGENT_NATIVE_BINARIES_DIR:-}" ]]; then
native_dir="${PULSE_AGENT_NATIVE_BINARIES_DIR}"
native_targets=()
if [[ "${PULSE_REQUIRE_MACOS_SIGNING:-false}" == "true" ]]; then
native_targets+=(darwin-amd64 darwin-arm64)
fi
if [[ "${PULSE_REQUIRE_WINDOWS_SIGNING:-false}" == "true" ]]; then
native_targets+=(windows-amd64 windows-arm64 windows-386)
fi
for target in "${native_targets[@]}"; do
filename="pulse-agent-${target}"
if [[ "$target" == windows-* ]]; then
filename="${filename}.exe"
fi
if [[ ! -f "${native_dir}/${filename}" ]]; then
echo "Error: native agent binary override is missing ${filename}." >&2
exit 1
fi
cp "${native_dir}/${filename}" "${BUILD_DIR}/${filename}"
done
echo "Applied required platform-native signed Unified Agent binaries."
elif [[ "${PULSE_REQUIRE_MACOS_SIGNING:-false}" == "true" || "${PULSE_REQUIRE_WINDOWS_SIGNING:-false}" == "true" ]]; then
echo "Error: required native signing is enabled but PULSE_AGENT_NATIVE_BINARIES_DIR is empty." >&2
exit 1
fi
# Build pulse-mcp (Model Context Protocol adapter) for the same
# multi-OS matrix as the unified agent. The MCP server runs on
# the integrator's machine (Mac, Windows, Linux desktop) and
# speaks stdio to a local MCP client like Claude Desktop, so it
# needs the full desktop-OS matrix even though the Pulse server
# itself only ships for Linux. The binary takes no version
# ldflags: it reads the manifest from whichever Pulse instance
# it points at, so its own build identity is intentionally minimal.
echo "Building pulse-mcp for all platforms..."
mcp_build_order=("${agent_build_order[@]}")
if [[ -z "${compiled_payload_dir:-}" ]]; then
for target in "${mcp_build_order[@]}"; do
build_env="$(pulse_release_target_env "${target}")"
output_path="${BUILD_DIR}/$(pulse_release_binary_filename mcp "${target}")"
env ${build_env} go build \
"${release_go_build_args[@]}" \
-o "${output_path}" \
./cmd/pulse-mcp
done
fi
# Build for different architectures (server + agents)
build_order=("${PULSE_RELEASE_SERVER_TARGETS[@]}")
if [[ -z "${compiled_payload_dir:-}" ]]; then
for build_name in "${build_order[@]}"; do
echo "Building for $build_name..."
build_env="$(pulse_release_target_env "${build_name}")"
build_time=$(date -u '+%Y-%m-%d_%H:%M:%S')
git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')
server_ldflags="$(./scripts/release_ldflags.sh server --version "v${VERSION}" --build-time "${build_time}" --git-commit "${git_commit}" "${license_ldflags_args[@]}" "${update_ldflags_args[@]}")"
# Build backend binary with version info. The release tag disables
# development-only feature-gating and signature-validation bypasses.
env $build_env go build \
-tags release \
-ldflags="${server_ldflags}" \
"${release_go_build_args[@]}" \
-o "$BUILD_DIR/pulse-$build_name" \
./cmd/pulse
done
fi
for target in "${agent_build_order[@]}"; do
test -f "${BUILD_DIR}/$(pulse_release_binary_filename agent "${target}")" || {
echo "Error: release payload is missing agent binary for ${target}." >&2
exit 1
}
test -f "${BUILD_DIR}/$(pulse_release_binary_filename mcp "${target}")" || {
echo "Error: release payload is missing MCP binary for ${target}." >&2
exit 1
}
done
for target in "${build_order[@]}"; do
test -f "${BUILD_DIR}/$(pulse_release_binary_filename server "${target}")" || {
echo "Error: release payload is missing server binary for ${target}." >&2
exit 1
}
done
# Create platform-specific tarballs that include all unified agent binaries for
# download endpoints. The archives are independent, so stage them concurrently
# while keeping the default bounded for hosted runners.
package_server_target() {
local build_name="$1"
local archive_path="${PULSE_REPO_ROOT}/${RELEASE_DIR}/pulse-v${VERSION}-${build_name}.tar.gz"
local staging_dir="${PULSE_REPO_ROOT}/${BUILD_DIR}/staging-${build_name}"
echo "Packaging release for ${build_name}..."
pulse_release_stage_server_archive \
"${archive_path}" \
"${staging_dir}" \
"${PULSE_REPO_ROOT}/${BUILD_DIR}/pulse-${build_name}" \
"${VERSION}" \
"${PULSE_REPO_ROOT}/${BUILD_DIR}" \
"${PULSE_REPO_ROOT}/${RENDERED_INSTALLERS_DIR}"
rm -rf "${staging_dir}"
echo "Created ${RELEASE_DIR}/pulse-v${VERSION}-${build_name}.tar.gz"
}
package_workers="${PULSE_RELEASE_PACKAGE_WORKERS:-4}"
if [[ ! "${package_workers}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: PULSE_RELEASE_PACKAGE_WORKERS must be a positive integer." >&2
exit 1
fi
package_pids=()
for build_name in "${build_order[@]}"; do
package_server_target "${build_name}" &
package_pids+=("$!")
if [[ ${#package_pids[@]} -ge ${package_workers} ]]; then
package_failed=0
for package_pid in "${package_pids[@]}"; do
if ! wait "${package_pid}"; then
package_failed=1
fi
done
[[ ${package_failed} -eq 0 ]] || exit 1
package_pids=()
fi
done
package_failed=0
for package_pid in "${package_pids[@]}"; do
if ! wait "${package_pid}"; then
package_failed=1
fi
done
[[ ${package_failed} -eq 0 ]] || exit 1
# 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 "${build_order[@]}"; do
cp "$BUILD_DIR/pulse-$build_name" "$universal_dir/bin/pulse-${build_name}"
cp "$BUILD_DIR/pulse-agent-$build_name" "$universal_dir/bin/pulse-agent-${build_name}"
done
cp "scripts/install-container-agent.sh" "$universal_dir/scripts/install-container-agent.sh"
cp "scripts/install-docker.sh" "$universal_dir/scripts/install-docker.sh"
cp "${RENDERED_INSTALLERS_DIR}/install.sh" "$universal_dir/scripts/install.sh"
[ -f "${RENDERED_INSTALLERS_DIR}/install.ps1" ] && cp "${RENDERED_INSTALLERS_DIR}/install.ps1" "$universal_dir/scripts/install.ps1"
chmod 755 "$universal_dir/scripts/"*.sh
chmod 755 "$universal_dir/scripts/"*.ps1 2>/dev/null || true
# 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-agent" << 'EOF'
#!/bin/sh
# Auto-detect architecture and run appropriate pulse-agent binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
exec "$(dirname "$0")/pulse-agent-linux-amd64" "$@"
;;
aarch64|arm64)
exec "$(dirname "$0")/pulse-agent-linux-arm64" "$@"
;;
armv7l|armhf)
exec "$(dirname "$0")/pulse-agent-linux-armv7" "$@"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
EOF
chmod +x "$universal_dir/bin/pulse-agent"
# Add VERSION file
echo "$VERSION" > "$universal_dir/VERSION"
pulse_release_sign_directory_assets "$universal_dir/bin"
pulse_release_sign_directory_assets "$universal_dir/scripts"
pulse_release_sign_file "$universal_dir/VERSION"
# Package standalone unified agent binaries (all platforms)
# Linux
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-agent-linux-amd64
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-linux-arm64.tar.gz" -C "$BUILD_DIR" pulse-agent-linux-arm64
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-linux-armv7.tar.gz" -C "$BUILD_DIR" pulse-agent-linux-armv7
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-linux-armv6.tar.gz" -C "$BUILD_DIR" pulse-agent-linux-armv6
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-linux-386.tar.gz" -C "$BUILD_DIR" pulse-agent-linux-386
# Darwin
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" pulse-agent-darwin-amd64
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-agent-darwin-arm64
# FreeBSD
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-freebsd-amd64.tar.gz" -C "$BUILD_DIR" pulse-agent-freebsd-amd64
tar -czf "$RELEASE_DIR/pulse-agent-v${VERSION}-freebsd-arm64.tar.gz" -C "$BUILD_DIR" pulse-agent-freebsd-arm64
# Windows (zip archives with version in filename)
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-agent-windows-amd64.exe"
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-agent-windows-arm64.exe"
zip -j "$RELEASE_DIR/pulse-agent-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-agent-windows-386.exe"
# Package standalone pulse-mcp binaries (all platforms). Mirrors
# the pulse-agent packaging shape exactly so the release-asset
# upload step does not need per-binary special cases.
# Linux
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-amd64
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-arm64
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-armv7.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-armv7
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-armv6.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-armv6
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-linux-386.tar.gz" -C "$BUILD_DIR" pulse-mcp-linux-386
# Darwin
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-amd64
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-darwin-arm64
# FreeBSD
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-freebsd-amd64.tar.gz" -C "$BUILD_DIR" pulse-mcp-freebsd-amd64
tar -czf "$RELEASE_DIR/pulse-mcp-v${VERSION}-freebsd-arm64.tar.gz" -C "$BUILD_DIR" pulse-mcp-freebsd-arm64
# Windows (zip archives with version in filename)
zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-amd64.zip" "$BUILD_DIR/pulse-mcp-windows-amd64.exe"
zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-arm64.zip" "$BUILD_DIR/pulse-mcp-windows-arm64.exe"
zip -j "$RELEASE_DIR/pulse-mcp-v${VERSION}-windows-386.zip" "$BUILD_DIR/pulse-mcp-windows-386.exe"
# Also copy bare binaries for /releases/latest/download/ redirect compatibility
# These allow LXC/barebone installs to redirect to GitHub without needing versioned URLs
echo "Copying bare binaries to release directory for redirect compatibility..."
cp "$BUILD_DIR/pulse-agent-linux-amd64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-linux-arm64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-linux-armv7" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-linux-armv6" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-linux-386" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-windows-amd64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-windows-arm64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-windows-386.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-freebsd-amd64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-agent-freebsd-arm64" "$RELEASE_DIR/"
# Copy bare pulse-mcp binaries for /releases/latest/download/ redirect
# compatibility. The install-mcp.sh installer fetches these directly from
# the GitHub Releases endpoint without needing a versioned URL.
cp "$BUILD_DIR/pulse-mcp-linux-amd64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-linux-arm64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-linux-armv7" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-linux-armv6" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-linux-386" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-darwin-amd64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-darwin-arm64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-windows-amd64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-windows-arm64.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-windows-386.exe" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-freebsd-amd64" "$RELEASE_DIR/"
cp "$BUILD_DIR/pulse-mcp-freebsd-arm64" "$RELEASE_DIR/"
# Copy Windows, macOS, and FreeBSD binaries into universal tarball for /download/ endpoint
echo "Adding Windows, macOS, and FreeBSD binaries to universal tarball..."
cp "$BUILD_DIR/pulse-agent-darwin-amd64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-darwin-arm64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-freebsd-amd64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-freebsd-arm64" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-windows-amd64.exe" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-windows-arm64.exe" "$universal_dir/bin/"
cp "$BUILD_DIR/pulse-agent-windows-386.exe" "$universal_dir/bin/"
# Create symlinks for Windows binaries without .exe extension (required for download endpoint)
ln -s pulse-agent-windows-amd64.exe "$universal_dir/bin/pulse-agent-windows-amd64"
ln -s pulse-agent-windows-arm64.exe "$universal_dir/bin/pulse-agent-windows-arm64"
ln -s pulse-agent-windows-386.exe "$universal_dir/bin/pulse-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 scripts to release directory (required for GitHub releases)
# These are uploaded as standalone assets so users can:
# curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.sh | bash
# instead of pulling from main branch (which may have newer, incompatible changes)
echo "Copying install scripts to release directory..."
# The published install.sh is the Pulse SERVER installer (LXC/systemd/Proxmox VE).
# scripts/pulse-auto-update.sh, the root install.sh's own --rc/--stable/--version flows,
# and the README quickstart all fetch this asset and run `bash install.sh --version vX.Y.Z`. The rendered AGENT installer
# (scripts/install.sh) ships inside tarballs and Docker images at ./scripts/install.sh and
# is served at the running server's /install.sh endpoint — it is not a GitHub Releases asset.
cp install.sh "$RELEASE_DIR/install.sh"
[ -f "${RENDERED_INSTALLERS_DIR}/install.ps1" ] && cp "${RENDERED_INSTALLERS_DIR}/install.ps1" "$RELEASE_DIR/install.ps1"
cp scripts/install-docker.sh "$RELEASE_DIR/"
cp scripts/pulse-auto-update.sh "$RELEASE_DIR/"
cp scripts/install-mcp.sh "$RELEASE_DIR/install-mcp.sh"
[ -f scripts/install-mcp.ps1 ] && cp scripts/install-mcp.ps1 "$RELEASE_DIR/install-mcp.ps1"
# Package the provider-hosted MSP deploy surface as its own versioned release
# asset. The source-tree archive is not an installation channel: this bundle is
# covered by the immutable candidate manifest, checksums, and detached release
# signatures below. Its Pulse image refs are exact-version tags which setup.sh
# resolves to immutable registry digests before Compose is allowed to run.
provider_msp_bundle_root="pulse-provider-msp-v${VERSION}"
provider_msp_bundle_dir="${BUILD_DIR}/${provider_msp_bundle_root}"
provider_msp_bundle_asset="${RELEASE_DIR}/${provider_msp_bundle_root}.tar.gz"
rm -rf "${provider_msp_bundle_dir}"
mkdir -p "${provider_msp_bundle_dir}"
cp -a deploy/provider-msp/. "${provider_msp_bundle_dir}/"
sed -i "s|^CONTROL_PLANE_IMAGE=.*|CONTROL_PLANE_IMAGE=ghcr.io/rcourtman/pulse-control-plane:v${VERSION}|" "${provider_msp_bundle_dir}/.env.example"
sed -i "s|^CP_PULSE_IMAGE=.*|CP_PULSE_IMAGE=ghcr.io/rcourtman/pulse:v${VERSION}|" "${provider_msp_bundle_dir}/.env.example"
printf '%s\n' "${VERSION}" > "${provider_msp_bundle_dir}/VERSION"
tar -czf "${provider_msp_bundle_asset}" -C "${BUILD_DIR}" "${provider_msp_bundle_root}"
pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}"
mapfile -t checksum_files < <(pulse_release_collect_checksum_files "${RELEASE_DIR}")
pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"
echo
echo "Release build complete!"
echo "Archives created in $RELEASE_DIR/"
ls -lh $RELEASE_DIR/