mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
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
293 lines
9.9 KiB
Bash
Executable file
293 lines
9.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Build the credential-free release payload that can run in parallel with
|
|
# platform-native signing. Packaging and release signing happen separately on
|
|
# a hosted runner after this payload's exact-SHA manifest is verified.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
cd "${REPO_ROOT}"
|
|
|
|
source "${SCRIPT_DIR}/release_build_targets.sh"
|
|
|
|
VERSION="${1:-$(tr -d '\n\r[:space:]' < VERSION)}"
|
|
OUTPUT_ROOT="${2:-}"
|
|
PROFILE="full"
|
|
if [[ "${3:-}" == "--profile" ]]; then
|
|
PROFILE="${4:-}"
|
|
if [[ -z "${PROFILE}" || -n "${5:-}" ]]; then
|
|
echo "Usage: $0 <version> <output-directory> [--profile full|pro-packaging]" >&2
|
|
exit 2
|
|
fi
|
|
elif [[ -n "${3:-}" ]]; then
|
|
echo "Usage: $0 <version> <output-directory> [--profile full|pro-packaging]" >&2
|
|
exit 2
|
|
fi
|
|
if [[ -z "${OUTPUT_ROOT}" ]]; then
|
|
echo "Usage: $0 <version> <output-directory> [--profile full|pro-packaging]" >&2
|
|
exit 2
|
|
fi
|
|
case "${PROFILE}" in
|
|
full|pro-packaging) ;;
|
|
*)
|
|
echo "Error: unsupported release compilation profile: ${PROFILE}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
OUTPUT_ROOT="$(python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "${OUTPUT_ROOT}")"
|
|
case "${OUTPUT_ROOT}" in
|
|
/|"${REPO_ROOT}"|"${REPO_ROOT}/scripts")
|
|
echo "Error: refusing unsafe release compilation output: ${OUTPUT_ROOT}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
required_go="go1.26.7"
|
|
current_go="$(go env GOVERSION 2>/dev/null || true)"
|
|
if [[ "${current_go}" != "${required_go}" ]]; then
|
|
echo "Error: Go toolchain must be ${required_go} (got ${current_go:-unknown})." >&2
|
|
exit 3
|
|
fi
|
|
for command_name in go npm python3 openssl getconf pgrep; do
|
|
command -v "${command_name}" >/dev/null 2>&1 || {
|
|
echo "Error: required release compilation command is missing: ${command_name}" >&2
|
|
exit 3
|
|
}
|
|
done
|
|
for variable_name in PULSE_LICENSE_PUBLIC_KEY PULSE_UPDATE_SIGNING_PUBLIC_KEY; do
|
|
if [[ -z "${!variable_name:-}" ]]; then
|
|
echo "Error: ${variable_name} is required for release compilation." >&2
|
|
exit 3
|
|
fi
|
|
done
|
|
|
|
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." >&2
|
|
exit 3
|
|
fi
|
|
|
|
SOURCE_SHA="$(git rev-parse HEAD)"
|
|
if [[ ! "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
|
|
echo "Error: release compilation requires an exact Git commit." >&2
|
|
exit 3
|
|
fi
|
|
if [[ "$(tr -d '\n\r[:space:]' < VERSION)" != "${VERSION}" ]]; then
|
|
echo "Error: requested version ${VERSION} does not match VERSION." >&2
|
|
exit 3
|
|
fi
|
|
|
|
PAYLOAD_DIR="${OUTPUT_ROOT}/payload"
|
|
BINARIES_DIR="${PAYLOAD_DIR}/binaries"
|
|
FRONTEND_DIR="${PAYLOAD_DIR}/frontend-dist"
|
|
MANIFEST_DIR="${OUTPUT_ROOT}/manifest"
|
|
if [[ -e "${OUTPUT_ROOT}" ]] && find "${OUTPUT_ROOT}" -mindepth 1 -print -quit | grep -q .; then
|
|
echo "Error: release compilation output must be absent or empty: ${OUTPUT_ROOT}" >&2
|
|
exit 2
|
|
fi
|
|
mkdir -p "${BINARIES_DIR}" "${MANIFEST_DIR}"
|
|
|
|
build_frontend() {
|
|
echo "Building exact-SHA frontend embed prerequisite..."
|
|
npm --prefix frontend-modern ci
|
|
npm --prefix frontend-modern run build
|
|
if [[ "${PROFILE}" == "full" ]]; then
|
|
mkdir -p "${FRONTEND_DIR}"
|
|
cp -a frontend-modern/dist/. "${FRONTEND_DIR}/"
|
|
fi
|
|
}
|
|
|
|
frontend_log="${OUTPUT_ROOT}/frontend.log"
|
|
build_frontend >"${frontend_log}" 2>&1 &
|
|
frontend_pid=$!
|
|
if [[ "${PROFILE}" == "full" ]]; then
|
|
echo "Building frontend bundle concurrently with the release binary matrix."
|
|
else
|
|
echo "Using Pro packaging profile: build the required frontend embed locally; transfer public Unified Agent binaries only."
|
|
fi
|
|
|
|
export CGO_ENABLED=0
|
|
release_go_build_args=(-buildvcs=false -trimpath)
|
|
agent_ldflags="$(./scripts/release_ldflags.sh agent \
|
|
--version "v${VERSION}" \
|
|
--update-public-keys "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}")"
|
|
build_time="$(date -u '+%Y-%m-%d_%H:%M:%S')"
|
|
git_commit="$(git rev-parse --short HEAD)"
|
|
server_ldflags="$(./scripts/release_ldflags.sh server \
|
|
--version "v${VERSION}" \
|
|
--build-time "${build_time}" \
|
|
--git-commit "${git_commit}" \
|
|
--license-public-key "${PULSE_LICENSE_PUBLIC_KEY}" \
|
|
--update-public-keys "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}")"
|
|
|
|
vcpus="$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')"
|
|
build_jobs="${PULSE_RELEASE_BUILD_JOBS:-$((vcpus / 2))}"
|
|
if (( build_jobs < 1 )); then build_jobs=1; fi
|
|
if (( build_jobs > 4 )); then build_jobs=4; fi
|
|
go_procs="${PULSE_RELEASE_GO_PROCS:-$((vcpus / build_jobs))}"
|
|
if (( go_procs < 1 )); then go_procs=1; fi
|
|
echo "Compiling release matrix with ${build_jobs} workers and GOMAXPROCS=${go_procs} per worker..."
|
|
|
|
declare -a task_components=()
|
|
declare -a task_targets=()
|
|
for target in "${PULSE_RELEASE_AGENT_TARGETS[@]}"; do
|
|
task_components+=(agent)
|
|
task_targets+=("${target}")
|
|
if [[ "${PROFILE}" == "full" ]]; then
|
|
task_components+=(mcp)
|
|
task_targets+=("${target}")
|
|
fi
|
|
done
|
|
if [[ "${PROFILE}" == "full" ]]; then
|
|
for target in "${PULSE_RELEASE_SERVER_TARGETS[@]}"; do
|
|
task_components+=(server)
|
|
task_targets+=("${target}")
|
|
done
|
|
for target in "${PULSE_RELEASE_CONTROL_PLANE_TARGETS[@]}"; do
|
|
task_components+=(control-plane)
|
|
task_targets+=("${target}")
|
|
done
|
|
fi
|
|
|
|
build_one() {
|
|
local component="$1"
|
|
local target="$2"
|
|
local target_env output package ldflags
|
|
local -a target_env_parts command
|
|
target_env="$(pulse_release_target_env "${target}")"
|
|
output="${BINARIES_DIR}/$(pulse_release_binary_filename "${component}" "${target}")"
|
|
case "${component}" in
|
|
agent)
|
|
package=./cmd/pulse-agent
|
|
ldflags="${agent_ldflags}"
|
|
;;
|
|
mcp)
|
|
package=./cmd/pulse-mcp
|
|
ldflags=""
|
|
;;
|
|
server)
|
|
package=./cmd/pulse
|
|
ldflags="${server_ldflags}"
|
|
;;
|
|
control-plane)
|
|
package=./cmd/pulse-control-plane
|
|
ldflags="${server_ldflags}"
|
|
;;
|
|
esac
|
|
read -r -a target_env_parts <<<"${target_env}"
|
|
command=(go build "${release_go_build_args[@]}")
|
|
if [[ "${component}" == server || "${component}" == control-plane ]]; then command+=(-tags release); fi
|
|
if [[ -n "${ldflags}" ]]; then command+=("-ldflags=${ldflags}"); fi
|
|
command+=(-o "${output}" "${package}")
|
|
env "${target_env_parts[@]}" GOMAXPROCS="${go_procs}" "${command[@]}"
|
|
}
|
|
|
|
declare -a active_pids=()
|
|
declare -A task_by_pid=()
|
|
terminate_tree() {
|
|
local parent_pid="$1"
|
|
local child_pid
|
|
while IFS= read -r child_pid; do
|
|
[[ -n "${child_pid}" ]] && terminate_tree "${child_pid}"
|
|
done < <(pgrep -P "${parent_pid}" || true)
|
|
kill -TERM "${parent_pid}" >/dev/null 2>&1 || true
|
|
}
|
|
terminate_active() {
|
|
local pid
|
|
for pid in "${active_pids[@]:-}"; do
|
|
[[ -n "${pid}" ]] && terminate_tree "${pid}"
|
|
done
|
|
wait "${active_pids[@]:-}" >/dev/null 2>&1 || true
|
|
if [[ -n "${frontend_pid:-}" ]]; then
|
|
terminate_tree "${frontend_pid}"
|
|
wait "${frontend_pid}" >/dev/null 2>&1 || true
|
|
frontend_pid=""
|
|
fi
|
|
}
|
|
trap terminate_active INT TERM
|
|
|
|
finish_frontend() {
|
|
local pid status
|
|
if [[ -z "${frontend_pid:-}" ]]; then
|
|
return
|
|
fi
|
|
pid="${frontend_pid}"
|
|
frontend_pid=""
|
|
if wait "${pid}"; then
|
|
status=0
|
|
else
|
|
status=$?
|
|
echo "Error: frontend embed prerequisite failed." >&2
|
|
cat "${frontend_log}" >&2
|
|
terminate_active
|
|
exit "${status}"
|
|
fi
|
|
rm -f "${frontend_log}"
|
|
echo "Built frontend embed prerequisite."
|
|
}
|
|
|
|
next_task=0
|
|
completed_tasks=0
|
|
total_tasks="${#task_components[@]}"
|
|
while (( completed_tasks < total_tasks )); do
|
|
while (( next_task < total_tasks && ${#active_pids[@]} < build_jobs )); do
|
|
component="${task_components[next_task]}"
|
|
target="${task_targets[next_task]}"
|
|
if [[ "${component}" == server || "${component}" == control-plane ]]; then
|
|
finish_frontend
|
|
fi
|
|
log_path="${OUTPUT_ROOT}/${component}-${target}.log"
|
|
build_one "${component}" "${target}" >"${log_path}" 2>&1 &
|
|
pid=$!
|
|
active_pids+=("${pid}")
|
|
task_by_pid["${pid}"]="${component}-${target}:${log_path}"
|
|
next_task=$((next_task + 1))
|
|
done
|
|
|
|
completed_pid=""
|
|
# Restrict wait -n to compilation children. The frontend build is also a
|
|
# child of this shell, and an unrestricted wait can reap it as if it were
|
|
# one of the matrix tasks. That advances completed_tasks early, leaves one
|
|
# binary build unjoined, and can publish an incomplete compiled manifest.
|
|
if wait -n -p completed_pid "${active_pids[@]}"; then
|
|
status=0
|
|
else
|
|
status=$?
|
|
fi
|
|
task_record="${task_by_pid[${completed_pid}]:-}"
|
|
if [[ -z "${task_record}" ]]; then
|
|
echo "Error: completed release compilation child is not in the active task set: ${completed_pid:-unknown}." >&2
|
|
terminate_active
|
|
exit 4
|
|
fi
|
|
task_name="${task_record%%:*}"
|
|
log_path="${task_record#*:}"
|
|
remaining_pids=()
|
|
for pid in "${active_pids[@]}"; do
|
|
if [[ "${pid}" != "${completed_pid}" ]]; then remaining_pids+=("${pid}"); fi
|
|
done
|
|
active_pids=("${remaining_pids[@]}")
|
|
unset "task_by_pid[${completed_pid}]"
|
|
if (( status != 0 )); then
|
|
echo "Error: release compilation task failed: ${task_name}" >&2
|
|
[[ -f "${log_path}" ]] && cat "${log_path}" >&2
|
|
terminate_active
|
|
exit "${status}"
|
|
fi
|
|
rm -f "${log_path}"
|
|
completed_tasks=$((completed_tasks + 1))
|
|
echo "Compiled ${task_name} (${completed_tasks}/${total_tasks})."
|
|
done
|
|
|
|
finish_frontend
|
|
trap - INT TERM
|
|
|
|
python3 scripts/release_candidate_manifest.py create \
|
|
--release-dir "${PAYLOAD_DIR}" \
|
|
--version "${VERSION}" \
|
|
--source-sha "${SOURCE_SHA}" \
|
|
--output "${MANIFEST_DIR}/release-compiled.json"
|
|
|
|
echo "Credential-free release compilation complete: ${OUTPUT_ROOT}"
|