mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
35d4cb0e97
Go 1.26.8 supersedes the prior patch release, so every release builder and local toolchain guard must move together to prevent candidate artifacts from retaining an older compiler and runtime. Contract-Neutral: toolchain-only patch update; no product or runtime contract changed 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.8"
|
|
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/
|