diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index a8b988e5b..385dbba06 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -560,8 +560,12 @@ upgrade, update, release, or artifact-selection behavior. release signing, R2 upload, private-registry publication, or paid-runtime staging. Full public compilation may likewise overlap the frontend with agent and - MCP targets, but it must join successful frontend completion before any - server or control-plane target that consumes the embed directory starts. + MCP targets, but its matrix wait set must include only active compilation + children so the independent frontend child cannot be counted as a completed + binary task. Every matrix child must be joined successfully before the + compiled manifest is created, and successful frontend completion must be + joined before any server or control-plane target that consumes the embed + directory starts. Public and Pro server archives must use the shared canonical staging helper and may assemble independent target archives concurrently with a bounded worker count. The verified dual-SHA Pro path must stage its five archives diff --git a/scripts/build-release-binaries.sh b/scripts/build-release-binaries.sh index d37eb3dad..85c9bde55 100755 --- a/scripts/build-release-binaries.sh +++ b/scripts/build-release-binaries.sh @@ -247,12 +247,21 @@ while (( completed_tasks < total_tasks )); do done completed_pid="" - if wait -n -p completed_pid; then + # 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}]:-unknown:}" + 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=() diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 0a26bc9a1..2a135ae73 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -157,6 +157,8 @@ func TestProPackagingBuildsFrontendEmbedWithoutTransferringBundle(t *testing.T) `cp -a frontend-modern/dist/. "${FRONTEND_DIR}/"`, `if [[ "${component}" == server || "${component}" == control-plane ]]; then`, `finish_frontend`, + `wait -n -p completed_pid "${active_pids[@]}"`, + `completed release compilation child is not in the active task set`, `transfer public Unified Agent binaries only`, } { if !strings.Contains(script, needle) { @@ -169,6 +171,9 @@ func TestProPackagingBuildsFrontendEmbedWithoutTransferringBundle(t *testing.T) npm --prefix frontend-modern ci`) { t.Fatal("Pro packaging must build the frontend embed prerequisite") } + if strings.Contains(script, `wait -n -p completed_pid;`) { + t.Fatal("release compilation must not let wait -n consume the independent frontend child") + } serverGate := strings.Index(script, `if [[ "${component}" == server || "${component}" == control-plane ]]; then`) serverLaunch := strings.Index(script, `build_one "${component}" "${target}"`) if serverGate < 0 || serverLaunch < 0 || serverGate > serverLaunch ||