mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +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
310 lines
10 KiB
Bash
Executable File
310 lines
10 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.8"
|
|
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 agent-side 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
|
|
for target in "${PULSE_RELEASE_AGENT_HELPER_TARGETS[@]}"; do
|
|
task_components+=(agent-helper)
|
|
task_targets+=("${target}")
|
|
done
|
|
for target in "${PULSE_RELEASE_AGENT_RUNNER_TARGETS[@]}"; do
|
|
task_components+=(agent-runner)
|
|
task_targets+=("${target}")
|
|
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}"
|
|
;;
|
|
agent-helper)
|
|
package=./cmd/pulse-agent-helper
|
|
ldflags="${agent_ldflags}"
|
|
;;
|
|
agent-runner)
|
|
package=./cmd/pulse-agent-runner
|
|
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}"
|