diff --git a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md index 362febb89..5c28ab884 100644 --- a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md +++ b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md @@ -96,6 +96,7 @@ Companion drill: `go test ./internal/hosted/... -count=1` `go test ./internal/api -run 'TestHostedLifecycle|TestHostedOrgAdminHandlers|TestHostedSignupSuccess|TestHostedSignupValidationFailures|TestHostedSignupHostedModeGate|TestHostedSignupRateLimit|TestHostedSignupRateLimit_NoProvisioningSideEffects|TestHostedSignupCleanupOnRBACFailure|TestHostedSignupFailsClosedWithoutPublicURL|TestStripeWebhook_' -count=1` `cd frontend-modern && npx vitest run src/pages/__tests__/HostedSignup.test.tsx src/components/Settings/__tests__/BillingAdminPanel.test.tsx src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx` + `EXPECT_PUBLIC_SIGNUP_ENABLED=false scripts/run_cloud_public_signup_smoke.sh` while public signup is intentionally closed, or `EXPECT_PUBLIC_SIGNUP_ENABLED=true scripts/run_cloud_public_signup_smoke.sh` during the GA launch window when public signup is expected to be live. - Manual scenario: 1. Start from a real hosted Pulse signup or an existing hosted tenant. 2. Confirm the user can authenticate into the hosted Pulse app and reach a diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 786183b35..60bbdea95 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -68,26 +68,27 @@ server-side update execution surfaces. 46. `scripts/release_control/release_promotion_policy_support.py` 47. `scripts/release_control/resolve_release_promotion.py` 48. `scripts/release_ldflags.sh` -49. `scripts/run_demo_public_browser_smoke.sh` -50. `scripts/demo_public_browser_smoke.cjs` -51. `scripts/run_hosted_staging_smoke.sh` -52. `scripts/trigger-release-dry-run.sh` -53. `scripts/trigger-release.sh` -54. `scripts/toggle-mock.sh` -55. `deploy/helm/pulse/` -56. `tests/integration/playwright.config.ts` -57. `tests/integration/QUICK_START.md` -58. `tests/integration/README.md` -59. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` -60. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` -61. `tests/integration/scripts/hosted-tenant-runtime.mjs` -62. `tests/integration/scripts/managed-dev-runtime.mjs` -63. `tests/integration/scripts/relay-mobile-token-helper.go` -64. `tests/integration/tests/helpers.ts` -65. `tests/integration/tests/runtime-defaults.ts` -66. `docker-compose.yml` -67. `scripts/install-docker.sh` -68. `scripts/validate-published-release.sh` +49. `scripts/run_cloud_public_signup_smoke.sh` +50. `scripts/run_demo_public_browser_smoke.sh` +51. `scripts/demo_public_browser_smoke.cjs` +52. `scripts/run_hosted_staging_smoke.sh` +53. `scripts/trigger-release-dry-run.sh` +54. `scripts/trigger-release.sh` +55. `scripts/toggle-mock.sh` +56. `deploy/helm/pulse/` +57. `tests/integration/playwright.config.ts` +58. `tests/integration/QUICK_START.md` +59. `tests/integration/README.md` +60. `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs` +61. `tests/integration/scripts/hosted-mobile-token-runtime.mjs` +62. `tests/integration/scripts/hosted-tenant-runtime.mjs` +63. `tests/integration/scripts/managed-dev-runtime.mjs` +64. `tests/integration/scripts/relay-mobile-token-helper.go` +65. `tests/integration/tests/helpers.ts` +66. `tests/integration/tests/runtime-defaults.ts` +67. `docker-compose.yml` +68. `scripts/install-docker.sh` +69. `scripts/validate-published-release.sh` 69. `scripts/validate-release.sh` 70. `scripts/release_asset_common.sh` 71. `scripts/backfill-release-assets.sh` @@ -174,8 +175,13 @@ server-side update execution surfaces. External helper binaries fetched by governed release workflows are part of the same supply-chain boundary and must be checksum-verified before they are executed. -8. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go` -9. Add or change the canonical hosted staging smoke operator path through `scripts/run_hosted_staging_smoke.sh`, `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs`, `tests/integration/scripts/hosted-mobile-token-runtime.mjs`, `tests/integration/scripts/hosted-tenant-runtime.mjs`, and `tests/integration/scripts/relay-mobile-token-helper.go` +8. Add or change the non-secret Pulse Cloud public signup route smoke through + `scripts/run_cloud_public_signup_smoke.sh`. That smoke must prove either + the open signup route contract or the intentionally closed redirect contract, + and valid magic-link probes must remain opt-in so routine public checks do + not send email accidentally. +9. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go` +10. Add or change the canonical hosted staging smoke operator path through `scripts/run_hosted_staging_smoke.sh`, `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs`, `tests/integration/scripts/hosted-mobile-token-runtime.mjs`, `tests/integration/scripts/hosted-tenant-runtime.mjs`, and `tests/integration/scripts/relay-mobile-token-helper.go` ## Forbidden Paths diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index c0e00f861..0854889c3 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2569,6 +2569,7 @@ "scripts/release_control/render_release_body.py", "scripts/release_control/resolve_release_promotion.py", "scripts/release_ldflags.sh", + "scripts/run_cloud_public_signup_smoke.sh", "scripts/run_demo_public_browser_smoke.sh", "scripts/run_hosted_staging_smoke.sh", "scripts/toggle-mock.sh", @@ -2698,6 +2699,19 @@ "scripts/installtests/release_ldflags_test.go" ] }, + { + "id": "cloud-public-signup-smoke-runtime", + "label": "Cloud public signup smoke proof", + "match_prefixes": [], + "match_files": [ + "scripts/run_cloud_public_signup_smoke.sh" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "scripts/tests/test-cloud-public-signup-smoke.sh" + ] + }, { "id": "demo-public-browser-smoke-runtime", "label": "demo public browser smoke proof", diff --git a/scripts/run_cloud_public_signup_smoke.sh b/scripts/run_cloud_public_signup_smoke.sh new file mode 100755 index 000000000..015dbb0d2 --- /dev/null +++ b/scripts/run_cloud_public_signup_smoke.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="${DOMAIN:-cloud.pulserelay.pro}" +PULSE_CLOUD_BASE_URL="${PULSE_CLOUD_BASE_URL:-https://${DOMAIN}}" +EXPECT_PUBLIC_SIGNUP_ENABLED="${EXPECT_PUBLIC_SIGNUP_ENABLED:-true}" +PUBLIC_SIGNUP_CLOSED_REDIRECT_URL="${PUBLIC_SIGNUP_CLOSED_REDIRECT_URL:-https://pulserelay.pro/}" +CHECK_MAGIC_LINK_VALID_PROBE="${CHECK_MAGIC_LINK_VALID_PROBE:-false}" +CURL_TIMEOUT="${CURL_TIMEOUT:-15}" + +BASE_URL="${PULSE_CLOUD_BASE_URL%/}" +FAILURES=0 + +pass() { + echo "[PASS] $*" +} + +fail() { + echo "[FAIL] $*" + FAILURES=$((FAILURES + 1)) +} + +info() { + echo "[INFO] $*" +} + +http_code() { + local method="$1" + local path="$2" + shift 2 + curl -sS --max-time "${CURL_TIMEOUT}" -o /dev/null -w "%{http_code}" -X "${method}" "$@" "${BASE_URL}${path}" +} + +http_status_and_redirect() { + local method="$1" + local path="$2" + shift 2 + curl -sS --max-time "${CURL_TIMEOUT}" -o /dev/null -w "%{http_code} %{redirect_url}" -X "${method}" "$@" "${BASE_URL}${path}" +} + +expect_code() { + local method="$1" + local path="$2" + local expected="$3" + shift 3 + local code + code="$(http_code "${method}" "${path}" "$@")" + if [[ "${code}" == "${expected}" ]]; then + pass "${method} ${path} returns ${expected}" + else + fail "${method} ${path} expected ${expected}, got ${code}" + fi +} + +expect_redirect() { + local method="$1" + local path="$2" + local expected_code="$3" + shift 3 + local code redirect_url + read -r code redirect_url <<<"$(http_status_and_redirect "${method}" "${path}" "$@")" + if [[ "${code}" == "${expected_code}" && "${redirect_url}" == "${PUBLIC_SIGNUP_CLOSED_REDIRECT_URL}" ]]; then + pass "${method} ${path} returns ${expected_code} to ${PUBLIC_SIGNUP_CLOSED_REDIRECT_URL}" + else + fail "${method} ${path} expected ${expected_code} to ${PUBLIC_SIGNUP_CLOSED_REDIRECT_URL}, got ${code} to ${redirect_url:-}" + fi +} + +check_shared_public_floor() { + info "Checking shared public control-plane floor at ${BASE_URL}" + expect_code GET /healthz 200 + expect_code GET /readyz 200 + expect_code POST /api/stripe/webhook 400 +} + +check_closed_signup_contract() { + info "Checking intentionally closed public signup contract" + expect_redirect GET /signup 302 + expect_redirect GET /cloud/signup 302 + expect_redirect GET /signup/complete 302 + expect_redirect GET /cloud/signup/complete 302 + expect_redirect GET /api/public/signup 302 + expect_redirect POST /api/public/signup 307 -H "Content-Type: application/json" --data '{"email":"not-an-email","org_name":"Acme"}' + expect_code POST /api/public/magic-link/request 400 -H "Content-Type: application/json" --data '{"email":"not-an-email"}' +} + +check_open_signup_contract() { + info "Checking live public signup contract" + expect_code GET /signup 200 + expect_code GET /cloud/signup 200 + expect_code GET /signup/complete 200 + expect_code GET /cloud/signup/complete 200 + expect_code GET /api/public/signup 405 + expect_code POST /api/public/signup 400 -H "Content-Type: application/json" --data '{"email":"not-an-email","org_name":"Acme"}' + expect_code POST /api/public/magic-link/request 400 -H "Content-Type: application/json" --data '{"email":"not-an-email"}' + + if [[ "${CHECK_MAGIC_LINK_VALID_PROBE}" == "true" ]]; then + expect_code POST /api/public/magic-link/request 200 -H "Content-Type: application/json" --data '{"email":"owner@example.com"}' + else + info "Skipping valid magic-link probe (CHECK_MAGIC_LINK_VALID_PROBE=${CHECK_MAGIC_LINK_VALID_PROBE})" + fi +} + +main() { + if ! command -v curl >/dev/null 2>&1; then + echo "curl is required for Pulse Cloud public signup smoke" >&2 + exit 1 + fi + + local expected_state + expected_state="$(echo "${EXPECT_PUBLIC_SIGNUP_ENABLED}" | tr '[:upper:]' '[:lower:]')" + check_shared_public_floor + + case "${expected_state}" in + true) + check_open_signup_contract + ;; + false) + check_closed_signup_contract + ;; + *) + fail "EXPECT_PUBLIC_SIGNUP_ENABLED must be true or false (got '${EXPECT_PUBLIC_SIGNUP_ENABLED}')" + ;; + esac + + echo + echo "Summary: failures=${FAILURES}" + if (( FAILURES > 0 )); then + exit 1 + fi +} + +main "$@" diff --git a/scripts/tests/test-cloud-public-signup-smoke.sh b/scripts/tests/test-cloud-public-signup-smoke.sh new file mode 100755 index 000000000..7127bea31 --- /dev/null +++ b/scripts/tests/test-cloud-public-signup-smoke.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SMOKE="${ROOT_DIR}/scripts/run_cloud_public_signup_smoke.sh" + +bash -n "${SMOKE}" + +grep -q 'EXPECT_PUBLIC_SIGNUP_ENABLED' "${SMOKE}" +grep -q 'PUBLIC_SIGNUP_CLOSED_REDIRECT_URL' "${SMOKE}" +grep -q 'CHECK_MAGIC_LINK_VALID_PROBE' "${SMOKE}" +grep -q 'check_closed_signup_contract' "${SMOKE}" +grep -q 'check_open_signup_contract' "${SMOKE}" + +echo "cloud public signup smoke checks passed"