mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-11-01 08:20:01 +00:00
02e53bc777
Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate CAs in crypto/x509 (CVE-2026-111943). Binaries built with 1.26.9 still accept chains through them, so every verified outbound TLS connection Pulse makes (licence, updates, webhooks, AI providers) stays open to interception by a holder of those keys. govulncheck does not flag a trust-store change as a called symbol, so the scheduled scan stayed green while the gap existed. Advance the governed toolchain floor and every pin together: go.mod, the toolchain installer and release-script guards, the source-built container stages (manifest-list digests for golang:1.26.10-alpine and the Debian dev container image), the root-pair diagnostic, and their tests. Vite 6.4.3 bundles a launch-editor with three advisories (GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw, GHSA-698v-4r3x-3vcv) that failed the daily advisory watch. 6.4.4 bundles the fixed launch-editor and 6.4.5 adds the dev-server server.fs query check. Both reach only the local dev server, not the built product. The lockfile changes only the vite entry, and dependencySecurity.test.ts now pins the floor.
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.10"
|
|
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}"
|