mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
9a5af6ff79
Change-source: pulse-maintainer
629 lines
28 KiB
Bash
Executable File
629 lines
28 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[@]}")
|
|
agent_helper_build_order=("${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}")
|
|
agent_runner_build_order=("${PULSE_RELEASE_AGENT_RUNNER_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
|
|
|
|
echo "Building privileged agent helpers for Linux..."
|
|
for target in "${agent_helper_build_order[@]}"; do
|
|
build_env="$(pulse_release_target_env "${target}")"
|
|
output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-helper "${target}")"
|
|
env ${build_env} go build \
|
|
-ldflags="${agent_ldflags}" \
|
|
"${release_go_build_args[@]}" \
|
|
-o "${output_path}" \
|
|
./cmd/pulse-agent-helper
|
|
done
|
|
|
|
echo "Building action runners for Linux..."
|
|
for target in "${agent_runner_build_order[@]}"; do
|
|
build_env="$(pulse_release_target_env "${target}")"
|
|
output_path="${BUILD_DIR}/$(pulse_release_binary_filename agent-runner "${target}")"
|
|
env ${build_env} go build \
|
|
"${release_go_build_args[@]}" \
|
|
-o "${output_path}" \
|
|
./cmd/pulse-agent-runner
|
|
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 "${agent_helper_build_order[@]}"; do
|
|
test -f "${BUILD_DIR}/$(pulse_release_binary_filename agent-helper "${target}")" || {
|
|
echo "Error: release payload is missing agent helper binary for ${target}." >&2
|
|
exit 1
|
|
}
|
|
done
|
|
for target in "${agent_runner_build_order[@]}"; do
|
|
test -f "${BUILD_DIR}/$(pulse_release_binary_filename agent-runner "${target}")" || {
|
|
echo "Error: release payload is missing agent runner 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
|
|
for target in "${agent_helper_build_order[@]}"; do
|
|
cp "$BUILD_DIR/pulse-agent-helper-${target}" "$universal_dir/bin/pulse-agent-helper-${target}"
|
|
done
|
|
for target in "${agent_runner_build_order[@]}"; do
|
|
cp "$BUILD_DIR/pulse-agent-runner-${target}" "$universal_dir/bin/pulse-agent-runner-${target}"
|
|
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. Sign the completed universal payload only after every
|
|
# cross-platform agent has been staged below.
|
|
echo "$VERSION" > "$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 privileged agent helpers (Linux only).
|
|
for target in "${agent_helper_build_order[@]}"; do
|
|
tar -czf "$RELEASE_DIR/pulse-agent-helper-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-helper-${target}"
|
|
done
|
|
|
|
# Package the separately enabled action runner (Linux only).
|
|
for target in "${agent_runner_build_order[@]}"; do
|
|
tar -czf "$RELEASE_DIR/pulse-agent-runner-v${VERSION}-${target}.tar.gz" -C "$BUILD_DIR" "pulse-agent-runner-${target}"
|
|
done
|
|
|
|
# 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 privileged helper binaries for installer/download compatibility.
|
|
for target in "${agent_helper_build_order[@]}"; do
|
|
cp "$BUILD_DIR/pulse-agent-helper-${target}" "$RELEASE_DIR/"
|
|
done
|
|
|
|
# Copy bare action runner binaries for the signed installer download endpoint.
|
|
for target in "${agent_runner_build_order[@]}"; do
|
|
cp "$BUILD_DIR/pulse-agent-runner-${target}" "$RELEASE_DIR/"
|
|
done
|
|
|
|
# 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/"
|
|
|
|
# Sign all regular payload files, then create the extensionless Windows aliases
|
|
# as complete binary/signature triplets required by the download endpoint.
|
|
pulse_release_sign_directory_assets "$universal_dir/bin"
|
|
pulse_release_sign_directory_assets "$universal_dir/scripts"
|
|
pulse_release_sign_file "$universal_dir/VERSION"
|
|
pulse_release_link_windows_agent_aliases "$universal_dir/bin"
|
|
|
|
# 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}"
|
|
|
|
# Import the small GitHub-hosted secure-runtime qualification packet before
|
|
# SBOM, checksum, and signature generation. The ordinary release payload stays
|
|
# canonical for current binaries: hosted compiler output is accepted only when
|
|
# collector-v4, helper, and runner reproduce those exact bytes.
|
|
if [[ -n "${PULSE_SECURE_RUNTIME_QUALIFICATION_DIR:-}" ]]; then
|
|
qualification_dir="$(cd "${PULSE_SECURE_RUNTIME_QUALIFICATION_DIR}" && pwd)"
|
|
qualification_expected=(
|
|
pulse-secure-runtime-collector-v1-linux-amd64
|
|
pulse-secure-runtime-collector-v2-linux-amd64
|
|
pulse-secure-runtime-collector-v3-linux-amd64
|
|
pulse-agent-linux-amd64
|
|
pulse-agent-helper-linux-amd64
|
|
pulse-agent-runner-linux-amd64
|
|
secure-runtime-build-contract-v1.json
|
|
secure-runtime-compiler-provenance.sigstore.json
|
|
secure-runtime-compiler-subjects.sha256
|
|
)
|
|
mapfile -t qualification_actual < <(find "${qualification_dir}" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort)
|
|
mapfile -t qualification_expected_sorted < <(printf '%s\n' "${qualification_expected[@]}" | sort)
|
|
if [[ "$(printf '%s\n' "${qualification_actual[@]}")" != "$(printf '%s\n' "${qualification_expected_sorted[@]}")" ]]; then
|
|
echo "Error: hosted secure-runtime qualification packet has an unexpected file set." >&2
|
|
printf 'Expected:\n%s\nActual:\n%s\n' \
|
|
"$(printf '%s\n' "${qualification_expected_sorted[@]}")" \
|
|
"$(printf '%s\n' "${qualification_actual[@]}")" >&2
|
|
exit 1
|
|
fi
|
|
for qualification_name in "${qualification_expected[@]}"; do
|
|
if [[ ! -f "${qualification_dir}/${qualification_name}" || -L "${qualification_dir}/${qualification_name}" ]]; then
|
|
echo "Error: hosted secure-runtime qualification subject is not a regular non-symlink file: ${qualification_name}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
cmp "${qualification_dir}/pulse-agent-linux-amd64" "${BUILD_DIR}/pulse-agent-linux-amd64" || {
|
|
echo "Error: hosted secure-runtime collector-v4 does not reproduce the release collector." >&2
|
|
exit 1
|
|
}
|
|
cmp "${qualification_dir}/pulse-agent-helper-linux-amd64" "${BUILD_DIR}/pulse-agent-helper-linux-amd64" || {
|
|
echo "Error: hosted secure-runtime helper does not reproduce the release helper." >&2
|
|
exit 1
|
|
}
|
|
cmp "${qualification_dir}/pulse-agent-runner-linux-amd64" "${BUILD_DIR}/pulse-agent-runner-linux-amd64" || {
|
|
echo "Error: hosted secure-runtime runner does not reproduce the release runner." >&2
|
|
exit 1
|
|
}
|
|
for qualification_name in \
|
|
pulse-secure-runtime-collector-v1-linux-amd64 \
|
|
pulse-secure-runtime-collector-v2-linux-amd64 \
|
|
pulse-secure-runtime-collector-v3-linux-amd64 \
|
|
secure-runtime-build-contract-v1.json \
|
|
secure-runtime-compiler-provenance.sigstore.json; do
|
|
install -m 0644 "${qualification_dir}/${qualification_name}" "${RELEASE_DIR}/${qualification_name}"
|
|
done
|
|
chmod 0755 "${RELEASE_DIR}"/pulse-secure-runtime-collector-v*-linux-amd64
|
|
echo "Imported hosted secure-runtime qualification packet."
|
|
elif [[ "${PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION:-false}" == "true" ]]; then
|
|
echo "Error: release requires the hosted secure-runtime qualification packet." >&2
|
|
exit 1
|
|
fi
|
|
|
|
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/
|