From fbcb77d635198e39ca7c37287ec4ecf2a675dbab Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 14 May 2026 21:40:07 +0100 Subject: [PATCH] Stabilize managed hot-dev startup --- .../subsystems/deployment-installability.md | 83 ++++++++++--------- .../v6/internal/subsystems/registry.json | 21 +++-- scripts/hot-dev-bg.sh | 1 + scripts/hot-dev.sh | 43 +++++++++- scripts/lib/hot-dev-auth.sh | 2 +- scripts/tests/test-hot-dev-auth.sh | 29 +++++++ scripts/tests/test-hot-dev-bg.sh | 1 + scripts/tests/test-hot-dev-runtime.sh | 10 +++ 8 files changed, 138 insertions(+), 52 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 77d962d1c..62d6c3c8c 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -63,46 +63,47 @@ server-side update execution surfaces. 38. `scripts/hot-dev-bg.sh` 39. `scripts/hot-dev.sh` 40. `scripts/lib/hot-dev-runtime.sh` -40. `scripts/install-container-agent.sh` -41. `install.sh` -42. `scripts/install.ps1` -43. `scripts/install.sh` -44. `scripts/install-mcp.sh` -45. `scripts/install-mcp.ps1` -46. `cmd/pulse-mcp/` -47. `scripts/pulse-auto-update.sh` -48. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py` -49. `scripts/release_control/record_rc_to_ga_rehearsal.py` -50. `scripts/release_control/release_promotion_policy_support.py` -51. `scripts/release_control/resolve_release_promotion.py` -52. `scripts/release_ldflags.sh` -53. `scripts/run_cloud_public_signup_smoke.sh` -54. `scripts/run_demo_public_browser_smoke.sh` -55. `scripts/demo_public_browser_smoke.cjs` -56. `scripts/run_hosted_staging_smoke.sh` -57. `scripts/trigger-release-dry-run.sh` -58. `scripts/trigger-release.sh` -59. `scripts/toggle-mock.sh` -60. `deploy/helm/pulse/` -61. `tests/integration/playwright.config.ts` -62. `tests/integration/QUICK_START.md` -63. `tests/integration/README.md` -64. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` -65. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` -66. `tests/integration/scripts/hosted-tenant-approval-store.mjs` -67. `tests/integration/scripts/hosted-tenant-runtime.mjs` -68. `tests/integration/scripts/hosted-tenant-runtime-restart.mjs` -69. `tests/integration/scripts/managed-dev-runtime.mjs` -70. `tests/integration/scripts/relay-mobile-token-helper.go` -71. `tests/integration/tests/helpers.ts` -72. `tests/integration/tests/runtime-defaults.ts` -73. `docker-compose.yml` -74. `scripts/install-docker.sh` -75. `scripts/validate-published-release.sh` -76. `scripts/validate-release.sh` -77. `scripts/release_asset_common.sh` -78. `scripts/backfill-release-assets.sh` -79. `.github/workflows/backfill-release-assets.yml` +41. `scripts/lib/hot-dev-auth.sh` +42. `scripts/install-container-agent.sh` +43. `install.sh` +44. `scripts/install.ps1` +45. `scripts/install.sh` +46. `scripts/install-mcp.sh` +47. `scripts/install-mcp.ps1` +48. `cmd/pulse-mcp/` +49. `scripts/pulse-auto-update.sh` +50. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py` +51. `scripts/release_control/record_rc_to_ga_rehearsal.py` +52. `scripts/release_control/release_promotion_policy_support.py` +53. `scripts/release_control/resolve_release_promotion.py` +54. `scripts/release_ldflags.sh` +55. `scripts/run_cloud_public_signup_smoke.sh` +56. `scripts/run_demo_public_browser_smoke.sh` +57. `scripts/demo_public_browser_smoke.cjs` +58. `scripts/run_hosted_staging_smoke.sh` +59. `scripts/trigger-release-dry-run.sh` +60. `scripts/trigger-release.sh` +61. `scripts/toggle-mock.sh` +62. `deploy/helm/pulse/` +63. `tests/integration/playwright.config.ts` +64. `tests/integration/QUICK_START.md` +65. `tests/integration/README.md` +66. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` +67. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` +68. `tests/integration/scripts/hosted-tenant-approval-store.mjs` +69. `tests/integration/scripts/hosted-tenant-runtime.mjs` +70. `tests/integration/scripts/hosted-tenant-runtime-restart.mjs` +71. `tests/integration/scripts/managed-dev-runtime.mjs` +72. `tests/integration/scripts/relay-mobile-token-helper.go` +73. `tests/integration/tests/helpers.ts` +74. `tests/integration/tests/runtime-defaults.ts` +75. `docker-compose.yml` +76. `scripts/install-docker.sh` +77. `scripts/validate-published-release.sh` +78. `scripts/validate-release.sh` +79. `scripts/release_asset_common.sh` +80. `scripts/backfill-release-assets.sh` +81. `.github/workflows/backfill-release-assets.yml` ## Shared Boundaries @@ -170,7 +171,7 @@ server-side update execution surfaces. enrollment runtime tokens while keeping deploy binding metadata limited to deploy facts such as cluster, job, target, source agent, and expected node. 4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts` -5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `scripts/lib/hot-dev-runtime.sh`, `scripts/dev-deploy-agent.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md` +5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, deterministic dev auth seeding, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `scripts/lib/hot-dev-runtime.sh`, `scripts/lib/hot-dev-auth.sh`, `scripts/dev-deploy-agent.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md` 6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh` That release-promotion boundary also owns prerelease note packet lineage: shipped RC notes must remain historically accurate, the top-level diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 8ae347e90..61e5ee630 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2716,6 +2716,7 @@ "scripts/install-docker.sh", "scripts/install.ps1", "scripts/install.sh", + "scripts/lib/hot-dev-auth.sh", "scripts/lib/hot-dev-runtime.sh", "scripts/pulse-auto-update.sh", "scripts/release_asset_common.sh", @@ -2970,11 +2971,12 @@ "scripts/dev-deploy-agent.sh", "scripts/dev-launchd-setup.sh", "scripts/dev-launchd-wrapper.sh", - "scripts/hot-dev-bg.sh", - "scripts/hot-dev.sh", - "scripts/lib/hot-dev-runtime.sh", - "scripts/toggle-mock.sh", - "tests/integration/playwright.config.ts", + "scripts/hot-dev-bg.sh", + "scripts/hot-dev.sh", + "scripts/lib/hot-dev-auth.sh", + "scripts/lib/hot-dev-runtime.sh", + "scripts/toggle-mock.sh", + "tests/integration/playwright.config.ts", "tests/integration/QUICK_START.md", "tests/integration/README.md", "tests/integration/scripts/managed-dev-runtime.mjs", @@ -2984,10 +2986,11 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ - "scripts/release_control/ssh_host_key_policy_test.py", - "scripts/tests/test-hot-dev-bg.sh", - "scripts/tests/test-hot-dev-runtime.sh", - "scripts/tests/test-toggle-mock.sh", + "scripts/release_control/ssh_host_key_policy_test.py", + "scripts/tests/test-hot-dev-auth.sh", + "scripts/tests/test-hot-dev-bg.sh", + "scripts/tests/test-hot-dev-runtime.sh", + "scripts/tests/test-toggle-mock.sh", "tests/integration/tests/16-dev-runtime-recovery.spec.ts" ] }, diff --git a/scripts/hot-dev-bg.sh b/scripts/hot-dev-bg.sh index 4e3537204..28ea57214 100755 --- a/scripts/hot-dev-bg.sh +++ b/scripts/hot-dev-bg.sh @@ -431,6 +431,7 @@ start_hot_dev_child() { PULSE_DEV_API_PORT="${PULSE_DEV_API_PORT}" \ PULSE_DEV_API_URL="${PULSE_DEV_API_URL}" \ PULSE_DEV_WS_URL="${PULSE_DEV_WS_URL}" \ + HOT_DEV_SKIP_NPM_CLEANUP=true \ ROOT_DIR="${ROOT_DIR}" \ python3 - <<'PY' & import os diff --git a/scripts/hot-dev.sh b/scripts/hot-dev.sh index 529896aa1..cf4886a6a 100755 --- a/scripts/hot-dev.sh +++ b/scripts/hot-dev.sh @@ -258,6 +258,47 @@ kill_port() { lsof -i :"${port}" 2>/dev/null | awk 'NR>1 {print $2}' | xargs -r kill -9 2>/dev/null || true } +process_parent_id() { + local pid=$1 + ps -o ppid= -p "${pid}" 2>/dev/null | tr -d '[:space:]' +} + +is_current_shell_descendant_of() { + local target_pid=$1 + local current_pid=$$ + local parent_pid + + [[ -n "${target_pid}" ]] || return 1 + + while [[ -n "${current_pid}" && "${current_pid}" != "1" ]]; do + parent_pid="$(process_parent_id "${current_pid}")" + [[ -n "${parent_pid}" && "${parent_pid}" != "${current_pid}" ]] || break + if [[ "${parent_pid}" == "${target_pid}" ]]; then + return 0 + fi + current_pid="${parent_pid}" + done + + return 1 +} + +kill_stale_npm_dev_wrappers() { + local pid + + if [[ "${HOT_DEV_SKIP_NPM_CLEANUP:-false}" == "true" ]]; then + return 0 + fi + + while IFS= read -r pid; do + [[ -n "${pid}" ]] || continue + [[ "${pid}" != "$$" ]] || continue + if is_current_shell_descendant_of "${pid}"; then + continue + fi + kill "${pid}" 2>/dev/null || true + done < <(pgrep -f "npm run dev" 2>/dev/null || true) +} + log_info "Cleaning up existing processes..." # OS-Specific Cleanup @@ -271,7 +312,7 @@ fi pkill -f "backend-watch.sh" 2>/dev/null || true # Only kill vite/npm processes that look like ours (simple check) pkill -f "vite" 2>/dev/null || true -pkill -f "npm run dev" 2>/dev/null || true +kill_stale_npm_dev_wrappers pkill -x "pulse" 2>/dev/null || true sleep 1 diff --git a/scripts/lib/hot-dev-auth.sh b/scripts/lib/hot-dev-auth.sh index 415f1fd80..a8d1c944a 100644 --- a/scripts/lib/hot-dev-auth.sh +++ b/scripts/lib/hot-dev-auth.sh @@ -56,7 +56,7 @@ hot_dev_sync_auth_env_file() { printf "PULSE_AUTH_PASS='%s'\n" "$(hot_dev_single_quote "${auth_pass}")" if [[ -f "${runtime_env}" ]]; then - grep -v -E '^(# Managed by hot-dev\.sh for deterministic dev auth|PULSE_AUTH_USER=|PULSE_AUTH_PASS=)' "${runtime_env}" + grep -v -E '^(# Managed by hot-dev\.sh for deterministic dev auth|PULSE_AUTH_USER=|PULSE_AUTH_PASS=)' "${runtime_env}" || true fi } > "${tmp_file}" diff --git a/scripts/tests/test-hot-dev-auth.sh b/scripts/tests/test-hot-dev-auth.sh index db6034db8..9899227c8 100755 --- a/scripts/tests/test-hot-dev-auth.sh +++ b/scripts/tests/test-hot-dev-auth.sh @@ -104,10 +104,39 @@ EOF assert_contains "sync preserves mock settings" "${output}" "PULSE_MOCK_MODE=false" } +test_sync_auth_env_file_handles_managed_only_env_under_errexit() { + local state_dir runtime_env output + state_dir="$(make_temp_dir)" + runtime_env="${state_dir}/.env" + + cat > "${runtime_env}" <<'EOF' +# Managed by hot-dev.sh for deterministic dev auth +PULSE_AUTH_USER='admin' +PULSE_AUTH_PASS='stale-pass' +EOF + + output="$( + HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \ + RUNTIME_ENV_PATH="${runtime_env}" \ + bash -lc ' + set -euo pipefail + source "${HOT_DEV_AUTH_LIB}" + hot_dev_sync_auth_env_file "${RUNTIME_ENV_PATH}" "admin" "${HOT_DEV_DEFAULT_AUTH_HASH}" + printf "survived=yes\n" + cat "${RUNTIME_ENV_PATH}" + ' + )" + + assert_contains "managed-only env does not trip errexit" "${output}" "survived=yes" + assert_contains "managed-only sync keeps auth user" "${output}" "PULSE_AUTH_USER='admin'" + assert_contains "managed-only sync rewrites auth password hash" "${output}" "PULSE_AUTH_PASS='${HOT_DEV_DEFAULT_AUTH_HASH}'" +} + source "${HOT_DEV_AUTH_LIB}" test_default_auth_contract test_custom_auth_banner_contract test_sync_auth_env_file_preserves_non_auth_settings +test_sync_auth_env_file_handles_managed_only_env_under_errexit if (( failures > 0 )); then echo "FAIL: ${failures} hot-dev auth assertions failed" >&2 diff --git a/scripts/tests/test-hot-dev-bg.sh b/scripts/tests/test-hot-dev-bg.sh index 09a50562e..1d29335bf 100755 --- a/scripts/tests/test-hot-dev-bg.sh +++ b/scripts/tests/test-hot-dev-bg.sh @@ -720,6 +720,7 @@ test_hot_dev_bg_script_advertises_managed_entrypoint() { assert_contains "hot-dev-bg routes log guidance to managed wrapper" "${output}" "Check logs with: npm run dev:logs" assert_contains "hot-dev-bg routes verify guidance to managed wrapper" "${output}" "Rerun with: npm run dev:verify" assert_contains "hot-dev-bg routes launchd supervision guidance to managed wrapper" "${output}" "Rerun with: npm run dev" + assert_contains "hot-dev-bg managed child skips npm wrapper cleanup" "${output}" "HOT_DEV_SKIP_NPM_CLEANUP=true" } test_hot_dev_bg_usage_prefers_managed_wrappers() { diff --git a/scripts/tests/test-hot-dev-runtime.sh b/scripts/tests/test-hot-dev-runtime.sh index 96d7404a2..f01ca66ee 100755 --- a/scripts/tests/test-hot-dev-runtime.sh +++ b/scripts/tests/test-hot-dev-runtime.sh @@ -127,11 +127,21 @@ test_hot_dev_keeps_backend_launch_errors_in_debug_log() { assert_contains "backend LOG_FILE follows debug log override" "${output}" "LOG_FILE=\"\${BACKEND_DEBUG_LOG}\"" } +test_hot_dev_avoids_self_killing_npm_wrapper() { + local output + output="$(sed -n '1,330p' "${HOT_DEV}")" + + assert_contains "hot-dev routes npm cleanup through a guarded helper" "${output}" "kill_stale_npm_dev_wrappers" + assert_contains "hot-dev supports managed skip for npm cleanup" "${output}" "HOT_DEV_SKIP_NPM_CLEANUP" + assert_not_contains "hot-dev no longer broad-kills npm dev wrappers" "${output}" 'pkill -f "npm run dev"' +} + source "${HOT_DEV_RUNTIME_LIB}" test_pulse_process_count_handles_zero_matches_under_pipefail test_pulse_process_count_counts_matching_processes test_hot_dev_uses_resilient_backend_process_count test_hot_dev_keeps_backend_launch_errors_in_debug_log +test_hot_dev_avoids_self_killing_npm_wrapper if (( failures > 0 )); then echo "FAIL: ${failures} hot-dev runtime assertions failed" >&2