From cc28ac5d9a1b3d4f7ab026c6e24cde5afff71e9f Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Thu, 30 Jul 2026 02:24:29 +0300 Subject: [PATCH 1/4] fix: validate selected simulation entrypoint config --- .github/request-simulation-model-versions.sh | 17 +- .github/scripts/deploy_cloud_run_candidate.sh | 13 +- .github/scripts/prepare_app_engine_bundle.sh | 3 + .github/scripts/simulation_entrypoint_env.sh | 67 +++ .../scripts/validate_app_engine_deploy_env.sh | 10 +- .../scripts/validate_cloud_run_deploy_env.sh | 10 +- .github/simulation-entrypoint-mode | 1 + .github/workflows/pr.yml | 4 +- .github/workflows/push.yml | 27 +- README.md | 5 +- ...onal-simulation-entrypoint-config.fixed.md | 1 + docs/migration/cloud-run-operations.md | 18 + gcp/export.py | 21 +- gcp/policyengine_api/Dockerfile | 6 +- tests/unit/test_cloud_run_deploy_scripts.py | 396 ++++++++++++++++-- 15 files changed, 511 insertions(+), 88 deletions(-) create mode 100644 .github/scripts/simulation_entrypoint_env.sh create mode 100644 .github/simulation-entrypoint-mode create mode 100644 changelog.d/conditional-simulation-entrypoint-config.fixed.md diff --git a/.github/request-simulation-model-versions.sh b/.github/request-simulation-model-versions.sh index 454574745..e606fa76a 100755 --- a/.github/request-simulation-model-versions.sh +++ b/.github/request-simulation-model-versions.sh @@ -2,13 +2,18 @@ set -euo pipefail -# Modal gateway version check script. +# Simulation gateway version check script. # Verifies that the PolicyEngine .py bundle used by API v1 is deployed in the -# simulation gateway before allowing API v1 deployment to proceed. US/UK -# versions are optional compatibility checks that should resolve to the same -# gateway app as the .py bundle route. +# selected simulation gateway before allowing API v1 deployment to proceed. +# US/UK versions are optional compatibility checks that should resolve to the +# same gateway app as the .py bundle route. -GATEWAY_URL="${SIMULATION_ENTRYPOINT_URL:-https://policyengine--policyengine-simulation-gateway-web-app.modal.run}" +source .github/scripts/simulation_entrypoint_env.sh +simulation_entrypoint_load_git_selection + +GATEWAY_URL="$( + simulation_entrypoint_selected_url "${SIM_ENTRYPOINT:-}" +)" usage() { echo "Usage: $0 -py [-us ] [-uk ]" @@ -55,7 +60,7 @@ if [ -z "$POLICYENGINE_VERSION" ]; then usage fi -echo "Checking Modal simulation API versions..." +echo "Checking selected simulation API versions..." echo " Gateway: $GATEWAY_URL" echo " Expected PolicyEngine .py bundle: $POLICYENGINE_VERSION" if [ -n "$US_VERSION" ]; then diff --git a/.github/scripts/deploy_cloud_run_candidate.sh b/.github/scripts/deploy_cloud_run_candidate.sh index eb2fc5fc0..0fb0fd5eb 100755 --- a/.github/scripts/deploy_cloud_run_candidate.sh +++ b/.github/scripts/deploy_cloud_run_candidate.sh @@ -3,6 +3,8 @@ set -euo pipefail source .github/scripts/cloud_run_env.sh +source .github/scripts/simulation_entrypoint_env.sh +simulation_entrypoint_load_git_selection cloud_run_set_defaults bash .github/scripts/validate_cloud_run_deploy_env.sh @@ -11,20 +13,25 @@ env_vars=( "POLICYENGINE_DB_INSTANCE_CONNECTION_NAME=${CLOUD_RUN_CLOUD_SQL_INSTANCE}" "POLICYENGINE_DB_USER=${POLICYENGINE_DB_USER:-policyengine}" "POLICYENGINE_DB_NAME=${POLICYENGINE_DB_NAME:-policyengine}" - "SIMULATION_ENTRYPOINT_URL=${SIMULATION_ENTRYPOINT_URL}" - "OLD_SIMULATION_GATEWAY_URL=${OLD_SIMULATION_GATEWAY_URL}" "GATEWAY_AUTH_REQUIRED=1" "GATEWAY_AUTH_ISSUER=${GATEWAY_AUTH_ISSUER}" "GATEWAY_AUTH_AUDIENCE=${GATEWAY_AUTH_AUDIENCE}" "GATEWAY_AUTH_CLIENT_ID=${GATEWAY_AUTH_CLIENT_ID}" "GATEWAY_AUTH_CLIENT_SECRET_RESOURCE=${GATEWAY_AUTH_CLIENT_SECRET_RESOURCE}" "API_HOST_BACKEND=cloud_run" - "SIM_ENTRYPOINT=${SIM_ENTRYPOINT:-old_gateway_direct}" + "SIM_ENTRYPOINT=${SIM_ENTRYPOINT}" "SIM_COMPUTE_ECONOMY=old_gateway" "CLOUD_RUN_REVISION_TAG=${CLOUD_RUN_TAG}" "WEB_CONCURRENCY=${CLOUD_RUN_WEB_CONCURRENCY}" ) +if [[ -n "${OLD_SIMULATION_GATEWAY_URL:-}" ]]; then + env_vars+=("OLD_SIMULATION_GATEWAY_URL=${OLD_SIMULATION_GATEWAY_URL}") +fi +if [[ -n "${SIMULATION_ENTRYPOINT_URL:-}" ]]; then + env_vars+=("SIMULATION_ENTRYPOINT_URL=${SIMULATION_ENTRYPOINT_URL}") +fi + secret_vars=( "POLICYENGINE_DB_PASSWORD=${CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET}" "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN=${CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET}" diff --git a/.github/scripts/prepare_app_engine_bundle.sh b/.github/scripts/prepare_app_engine_bundle.sh index e7b8f96f6..a6c49577d 100644 --- a/.github/scripts/prepare_app_engine_bundle.sh +++ b/.github/scripts/prepare_app_engine_bundle.sh @@ -2,6 +2,9 @@ set -euo pipefail +source .github/scripts/simulation_entrypoint_env.sh +simulation_entrypoint_load_git_selection + python gcp/export.py cp gcp/policyengine_api/app.yaml . cp gcp/policyengine_api/Dockerfile . diff --git a/.github/scripts/simulation_entrypoint_env.sh b/.github/scripts/simulation_entrypoint_env.sh new file mode 100644 index 000000000..3731bead2 --- /dev/null +++ b/.github/scripts/simulation_entrypoint_env.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Shared selected-mode configuration for deployment and compatibility checks. +# The legacy SIMULATION_API_URL secret is intentionally unsupported: its value +# is opaque and must not influence which upstream API v1 calls. + +simulation_entrypoint_load_git_selection() { + local config_file + + if [[ -n "${SIM_ENTRYPOINT:-}" ]]; then + return + fi + + config_file="${SIM_ENTRYPOINT_CONFIG_FILE:-.github/simulation-entrypoint-mode}" + if [[ ! -r "${config_file}" ]]; then + printf 'Unable to read Git-controlled simulation entrypoint mode from %s\n' \ + "${config_file}" >&2 + return 1 + fi + + IFS= read -r SIM_ENTRYPOINT <"${config_file}" + if [[ -z "${SIM_ENTRYPOINT}" ]]; then + printf 'Git-controlled simulation entrypoint mode is empty in %s\n' \ + "${config_file}" >&2 + return 1 + fi + + export SIM_ENTRYPOINT +} + +simulation_entrypoint_url_env_name() { + local entrypoint="${1:-}" + + case "${entrypoint}" in + old_gateway_direct) + printf '%s\n' "OLD_SIMULATION_GATEWAY_URL" + ;; + cloud_run_simulation_entrypoint) + printf '%s\n' "SIMULATION_ENTRYPOINT_URL" + ;; + "") + echo "SIM_ENTRYPOINT is required" >&2 + return 1 + ;; + *) + printf 'SIM_ENTRYPOINT=%q is invalid; expected old_gateway_direct or cloud_run_simulation_entrypoint\n' \ + "${entrypoint}" >&2 + return 1 + ;; + esac +} + +simulation_entrypoint_selected_url() { + local url_env_name + local url + + url_env_name="$(simulation_entrypoint_url_env_name "${1:-}")" || return + url="${!url_env_name:-}" + + if [[ -z "${url}" ]]; then + printf '%s is required when SIM_ENTRYPOINT=%s\n' \ + "${url_env_name}" "${1:-}" >&2 + return 1 + fi + + printf '%s\n' "${url}" +} diff --git a/.github/scripts/validate_app_engine_deploy_env.sh b/.github/scripts/validate_app_engine_deploy_env.sh index 60393abcd..53cf7975d 100644 --- a/.github/scripts/validate_app_engine_deploy_env.sh +++ b/.github/scripts/validate_app_engine_deploy_env.sh @@ -2,10 +2,16 @@ set -euo pipefail +source .github/scripts/simulation_entrypoint_env.sh +simulation_entrypoint_load_git_selection + +selected_url_env="$( + simulation_entrypoint_url_env_name "${SIM_ENTRYPOINT:-}" +)" + required=( - SIMULATION_ENTRYPOINT_URL - OLD_SIMULATION_GATEWAY_URL SIM_ENTRYPOINT + "${selected_url_env}" GATEWAY_AUTH_ISSUER GATEWAY_AUTH_AUDIENCE GATEWAY_AUTH_CLIENT_ID diff --git a/.github/scripts/validate_cloud_run_deploy_env.sh b/.github/scripts/validate_cloud_run_deploy_env.sh index f69463c85..99100e78b 100755 --- a/.github/scripts/validate_cloud_run_deploy_env.sh +++ b/.github/scripts/validate_cloud_run_deploy_env.sh @@ -3,6 +3,8 @@ set -euo pipefail source .github/scripts/cloud_run_env.sh +source .github/scripts/simulation_entrypoint_env.sh +simulation_entrypoint_load_git_selection cloud_run_set_defaults # Cloud Run rejects deploys where the traffic tag and service name together @@ -28,9 +30,13 @@ cloud_run_require_env \ CLOUD_RUN_ANTHROPIC_API_KEY_SECRET \ CLOUD_RUN_OPENAI_API_KEY_SECRET \ CLOUD_RUN_HUGGING_FACE_TOKEN_SECRET \ - SIMULATION_ENTRYPOINT_URL \ - OLD_SIMULATION_GATEWAY_URL \ + SIM_ENTRYPOINT \ GATEWAY_AUTH_ISSUER \ GATEWAY_AUTH_AUDIENCE \ GATEWAY_AUTH_CLIENT_ID \ GATEWAY_AUTH_CLIENT_SECRET_RESOURCE + +selected_url_env="$( + simulation_entrypoint_url_env_name "${SIM_ENTRYPOINT}" +)" +cloud_run_require_env "${selected_url_env}" diff --git a/.github/simulation-entrypoint-mode b/.github/simulation-entrypoint-mode new file mode 100644 index 000000000..659198541 --- /dev/null +++ b/.github/simulation-entrypoint-mode @@ -0,0 +1 @@ +old_gateway_direct diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cb2cc45b5..32bc88204 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,6 +4,8 @@ on: pull_request env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} + SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }} jobs: ensure-policyengine-bundle-supported-by-simulation-api: @@ -19,8 +21,6 @@ jobs: - name: Install jq run: sudo apt-get install -y jq - name: Check simulation API supports updated PolicyEngine bundle - env: - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} run: bash .github/check-policyengine-bundle-supported.sh --if-changed-from-base "${{ github.base_ref }}" lint: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 04f53ffee..2aa7def3f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -7,6 +7,8 @@ on: env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} + SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }} concurrency: group: deploy @@ -41,8 +43,6 @@ jobs: - name: Install jq run: sudo apt-get install -y jq - name: Check simulation API supports PolicyEngine bundle - env: - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} run: bash .github/check-policyengine-bundle-supported.sh versioning: @@ -152,9 +152,6 @@ jobs: # Transitional: these values are still passed into the deploy bundle # by gcp/export.py. Long-term target is a generic image plus runtime # config / Secret Manager lookups instead of image bake-in. - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -171,9 +168,6 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -191,9 +185,6 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -270,9 +261,6 @@ jobs: CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT: ${{ secrets.GCP_CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT }} POLICYENGINE_DB_USER: ${{ vars.POLICYENGINE_DB_USER }} POLICYENGINE_DB_NAME: ${{ vars.POLICYENGINE_DB_NAME }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -434,8 +422,6 @@ jobs: - name: Install jq run: sudo apt-get install -y jq - name: Check simulation API supports PolicyEngine bundle - env: - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} run: bash .github/check-policyengine-bundle-supported.sh deploy-production-candidate: @@ -476,9 +462,6 @@ jobs: # Transitional: these values are still passed into the deploy bundle # by gcp/export.py. Long-term target is a generic image plus runtime # config / Secret Manager lookups instead of image bake-in. - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -496,9 +479,6 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} @@ -620,9 +600,6 @@ jobs: CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT: ${{ secrets.GCP_CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT }} POLICYENGINE_DB_USER: ${{ vars.POLICYENGINE_DB_USER }} POLICYENGINE_DB_NAME: ${{ vars.POLICYENGINE_DB_NAME }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} - OLD_SIMULATION_GATEWAY_URL: ${{ secrets.OLD_SIMULATION_GATEWAY_URL }} - SIM_ENTRYPOINT: old_gateway_direct GATEWAY_AUTH_ISSUER: ${{ secrets.GATEWAY_AUTH_ISSUER }} GATEWAY_AUTH_AUDIENCE: ${{ secrets.GATEWAY_AUTH_AUDIENCE }} GATEWAY_AUTH_CLIENT_ID: ${{ secrets.GATEWAY_AUTH_CLIENT_ID }} diff --git a/README.md b/README.md index 41bf22f83..724a0e408 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,9 @@ Keep that commented unless you are pointing at a real local credential file. The If you are running against an auth-protected simulation gateway outside the managed deploy path, you may also need: - `SIM_ENTRYPOINT` (`old_gateway_direct` or `cloud_run_simulation_entrypoint`) -- `OLD_SIMULATION_GATEWAY_URL` -- `SIMULATION_ENTRYPOINT_URL` +- the URL selected by `SIM_ENTRYPOINT`: + - `OLD_SIMULATION_GATEWAY_URL` for `old_gateway_direct`; or + - `SIMULATION_ENTRYPOINT_URL` for `cloud_run_simulation_entrypoint` - `GATEWAY_AUTH_REQUIRED` - `GATEWAY_AUTH_ISSUER` - `GATEWAY_AUTH_AUDIENCE` diff --git a/changelog.d/conditional-simulation-entrypoint-config.fixed.md b/changelog.d/conditional-simulation-entrypoint-config.fixed.md new file mode 100644 index 000000000..7e4c162c9 --- /dev/null +++ b/changelog.d/conditional-simulation-entrypoint-config.fixed.md @@ -0,0 +1 @@ +Require only the Git-selected simulation upstream URL during deployment, without relying on the inaccessible legacy `SIMULATION_API_URL` secret. diff --git a/docs/migration/cloud-run-operations.md b/docs/migration/cloud-run-operations.md index 6e844ada1..469d978e8 100644 --- a/docs/migration/cloud-run-operations.md +++ b/docs/migration/cloud-run-operations.md @@ -241,6 +241,24 @@ control-plane traffic and stable health, and then leaves the deployment red. This rollback only covers immediate deployment failures; later operational rollback remains an explicit incident-response action. +## Simulation entrypoint deployment configuration + +`SIM_ENTRYPOINT` is read from the single Git-controlled +`.github/simulation-entrypoint-mode` file. Deployment validation, App Engine +packaging, Cloud Run configuration, and the model-version compatibility guard +all require only the URL selected by that value: + +- `old_gateway_direct` requires `OLD_SIMULATION_GATEWAY_URL`; +- `cloud_run_simulation_entrypoint` requires + `SIMULATION_ENTRYPOINT_URL`. + +Both URLs are non-secret repository-level GitHub Actions variables shared by +the PR, staging, and production workflows. If both are configured, both are +retained on a revision, but an unselected future URL does not block a +direct-Modal deployment. The legacy `SIMULATION_API_URL` secret is +intentionally unsupported: its value is opaque, cannot be audited, and must +not silently choose a deployment upstream. + ## IAM and bootstrap constraints - The GitHub deploy service account holds `roles/run.developer`: it can deploy to diff --git a/gcp/export.py b/gcp/export.py index 025d9d390..a56bcbf39 100644 --- a/gcp/export.py +++ b/gcp/export.py @@ -5,9 +5,26 @@ ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"] OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] HUGGING_FACE_TOKEN = os.environ["HUGGING_FACE_TOKEN"] -SIMULATION_ENTRYPOINT_URL = os.environ["SIMULATION_ENTRYPOINT_URL"] -OLD_SIMULATION_GATEWAY_URL = os.environ["OLD_SIMULATION_GATEWAY_URL"] SIM_ENTRYPOINT = os.environ["SIM_ENTRYPOINT"] +SIMULATION_URL_ENV_BY_ENTRYPOINT = { + "old_gateway_direct": "OLD_SIMULATION_GATEWAY_URL", + "cloud_run_simulation_entrypoint": "SIMULATION_ENTRYPOINT_URL", +} +try: + selected_url_env = SIMULATION_URL_ENV_BY_ENTRYPOINT[SIM_ENTRYPOINT] +except KeyError as error: + raise ValueError( + "SIM_ENTRYPOINT must be old_gateway_direct or cloud_run_simulation_entrypoint" + ) from error + +selected_url = os.environ.get(selected_url_env, "") +if not selected_url: + raise ValueError( + f"{selected_url_env} is required when SIM_ENTRYPOINT={SIM_ENTRYPOINT}" + ) + +SIMULATION_ENTRYPOINT_URL = os.environ.get("SIMULATION_ENTRYPOINT_URL", "") +OLD_SIMULATION_GATEWAY_URL = os.environ.get("OLD_SIMULATION_GATEWAY_URL", "") GATEWAY_AUTH_ISSUER = os.environ["GATEWAY_AUTH_ISSUER"] GATEWAY_AUTH_AUDIENCE = os.environ["GATEWAY_AUTH_AUDIENCE"] GATEWAY_AUTH_CLIENT_ID = os.environ["GATEWAY_AUTH_CLIENT_ID"] diff --git a/gcp/policyengine_api/Dockerfile b/gcp/policyengine_api/Dockerfile index 56298b847..87697c4d0 100644 --- a/gcp/policyengine_api/Dockerfile +++ b/gcp/policyengine_api/Dockerfile @@ -7,9 +7,9 @@ ENV POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN .github_microdata_token ENV ANTHROPIC_API_KEY .anthropic_api_key ENV OPENAI_API_KEY .openai_api_key ENV HUGGING_FACE_TOKEN .hugging_face_token -ENV SIMULATION_ENTRYPOINT_URL .simulation_entrypoint_url -ENV OLD_SIMULATION_GATEWAY_URL .old_simulation_gateway_url -ENV SIM_ENTRYPOINT .sim_entrypoint +ENV SIMULATION_ENTRYPOINT_URL=".simulation_entrypoint_url" +ENV OLD_SIMULATION_GATEWAY_URL=".old_simulation_gateway_url" +ENV SIM_ENTRYPOINT=".sim_entrypoint" ENV CREDENTIALS_JSON_API_V2 .credentials_json_api_v2 ENV GATEWAY_AUTH_REQUIRED 1 ENV GATEWAY_AUTH_ISSUER .gateway_auth_issuer diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index bcc72f75a..07ec4d2ee 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -4,7 +4,9 @@ import os import re import shlex +import shutil import subprocess +import sys from pathlib import Path import pytest @@ -50,6 +52,17 @@ def _script_env(**overrides: str) -> dict[str, str]: return env +def _gateway_auth_env() -> dict[str, str]: + return { + "GATEWAY_AUTH_ISSUER": "https://issuer.example.test", + "GATEWAY_AUTH_AUDIENCE": "simulation-gateway", + "GATEWAY_AUTH_CLIENT_ID": "client-id", + "GATEWAY_AUTH_CLIENT_SECRET_RESOURCE": ( + "projects/policyengine-api/secrets/gateway-client-secret/versions/latest" + ), + } + + def _required_runtime_env() -> dict[str, str]: return { "POLICYENGINE_DB_PASSWORD": "raw-db-secret-value", @@ -60,12 +73,7 @@ def _required_runtime_env() -> dict[str, str]: "SIMULATION_ENTRYPOINT_URL": "https://simulation.example.test", "OLD_SIMULATION_GATEWAY_URL": "https://old-gateway.example.test", "SIM_ENTRYPOINT": "cloud_run_simulation_entrypoint", - "GATEWAY_AUTH_ISSUER": "https://issuer.example.test", - "GATEWAY_AUTH_AUDIENCE": "simulation-gateway", - "GATEWAY_AUTH_CLIENT_ID": "client-id", - "GATEWAY_AUTH_CLIENT_SECRET_RESOURCE": ( - "projects/policyengine-api/secrets/gateway-client-secret/versions/latest" - ), + **_gateway_auth_env(), } @@ -184,7 +192,9 @@ def _fake_gcloud_env(gcloud_path: Path, state_path: Path) -> dict[str, str]: def _run_simulation_version_guard( - versions_response: dict, *args: str + versions_response: dict, + *args: str, + entrypoint: str = "old_gateway_direct", ) -> subprocess.CompletedProcess[str]: versions_json = json.dumps(versions_response) command = ( @@ -195,7 +205,11 @@ def _run_simulation_version_guard( return subprocess.run( ["bash", "-c", command, "request-simulation-model-versions.sh", *args], cwd=REPO, - env=_script_env(SIMULATION_ENTRYPOINT_URL="https://simulation.example.test"), + env=_script_env( + SIM_ENTRYPOINT=entrypoint, + SIMULATION_ENTRYPOINT_URL="https://simulation.example.test", + OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", + ), text=True, capture_output=True, check=False, @@ -206,6 +220,10 @@ def _push_workflow() -> str: return (REPO / ".github/workflows/push.yml").read_text(encoding="utf-8") +def _pr_workflow() -> str: + return (REPO / ".github/workflows/pr.yml").read_text(encoding="utf-8") + + def _sync_secrets_workflow() -> str: return (REPO / ".github/workflows/sync-cloud-run-secrets.yml").read_text( encoding="utf-8" @@ -315,6 +333,61 @@ def test_simulation_version_guard_accepts_policyengine_bundle_route_only(): assert "SUCCESS: PolicyEngine bundle route is deployed and ready" in result.stdout +@pytest.mark.parametrize( + ("entrypoint", "expected_url"), + [ + ("old_gateway_direct", "https://old-gateway.example.test"), + ( + "cloud_run_simulation_entrypoint", + "https://simulation.example.test", + ), + ], +) +def test_simulation_version_guard_uses_selected_endpoint(entrypoint, expected_url): + result = _run_simulation_version_guard( + { + "policyengine": { + "4.18.5": "policyengine-simulation-py4-18-5", + }, + }, + "-py", + "4.18.5", + entrypoint=entrypoint, + ) + + assert result.returncode == 0, result.stderr + assert f"Gateway: {expected_url}" in result.stdout + + +@pytest.mark.parametrize( + ("entrypoint", "missing_url"), + [ + ("old_gateway_direct", "OLD_SIMULATION_GATEWAY_URL"), + ("cloud_run_simulation_entrypoint", "SIMULATION_ENTRYPOINT_URL"), + ], +) +def test_simulation_version_guard_rejects_missing_selected_endpoint( + entrypoint, + missing_url, +): + command = ( + "curl() { printf '{}'; }; . .github/request-simulation-model-versions.sh \"$@\"" + ) + result = subprocess.run( + ["bash", "-c", command, "request-simulation-model-versions.sh", "-py", "1.0"], + cwd=REPO, + env=_script_env(SIM_ENTRYPOINT=entrypoint), + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 1 + assert ( + f"{missing_url} is required when SIM_ENTRYPOINT={entrypoint}" in result.stderr + ) + + def test_policyengine_bundle_support_check_passes_pyproject_pin_to_guard(tmp_path): capture_path = tmp_path / "guard-args.txt" guard_script = tmp_path / "request-simulation-model-versions.sh" @@ -396,43 +469,192 @@ def test_cloud_run_startup_supervises_redis_and_server_children(): assert re.search(r"(?m)^ *wait 2>/dev/null", start_script) is None -def test_validate_cloud_run_deploy_env_reports_missing_runtime_config(): +def test_validate_cloud_run_deploy_env_requires_git_controlled_selector(): result = _run_script( ".github/scripts/validate_cloud_run_deploy_env.sh", - _script_env(), + _script_env(SIM_ENTRYPOINT_CONFIG_FILE="/missing/entrypoint-mode"), ) assert result.returncode == 1 - assert "Missing required Cloud Run deployment configuration" in result.stderr - assert "SIMULATION_ENTRYPOINT_URL" in result.stderr - assert "OLD_SIMULATION_GATEWAY_URL" in result.stderr - assert "GATEWAY_AUTH_CLIENT_SECRET_RESOURCE" in result.stderr - assert "POLICYENGINE_DB_PASSWORD" not in result.stderr + assert "Unable to read Git-controlled simulation entrypoint mode" in result.stderr + + +def test_validate_cloud_run_deploy_env_loads_git_controlled_direct_mode(): + result = _run_script( + ".github/scripts/validate_cloud_run_deploy_env.sh", + _script_env( + OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", + **_gateway_auth_env(), + ), + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize( + ("entrypoint", "selected_url_env", "selected_url"), + [ + ( + "old_gateway_direct", + "OLD_SIMULATION_GATEWAY_URL", + "https://old-gateway.example.test", + ), + ( + "cloud_run_simulation_entrypoint", + "SIMULATION_ENTRYPOINT_URL", + "https://simulation.example.test", + ), + ], +) +def test_validate_cloud_run_deploy_env_requires_only_selected_url( + entrypoint, + selected_url_env, + selected_url, +): + env = _script_env( + SIM_ENTRYPOINT=entrypoint, + **_gateway_auth_env(), + ) + missing_result = _run_script( + ".github/scripts/validate_cloud_run_deploy_env.sh", + env, + ) + valid_result = _run_script( + ".github/scripts/validate_cloud_run_deploy_env.sh", + {**env, selected_url_env: selected_url}, + ) + assert missing_result.returncode == 1 + assert selected_url_env in missing_result.stderr + assert valid_result.returncode == 0, valid_result.stderr -def test_validate_app_engine_deploy_env_requires_both_simulation_urls_and_selector(): + +def test_validate_app_engine_deploy_env_requires_git_controlled_selector(): result = _run_script( ".github/scripts/validate_app_engine_deploy_env.sh", - _script_env(), + _script_env(SIM_ENTRYPOINT_CONFIG_FILE="/missing/entrypoint-mode"), ) assert result.returncode == 1 - assert "SIMULATION_ENTRYPOINT_URL" in result.stderr - assert "OLD_SIMULATION_GATEWAY_URL" in result.stderr - assert "SIM_ENTRYPOINT" in result.stderr + assert "Unable to read Git-controlled simulation entrypoint mode" in result.stderr + + +def test_validate_app_engine_deploy_env_loads_git_controlled_direct_mode(): + result = _run_script( + ".github/scripts/validate_app_engine_deploy_env.sh", + _script_env( + OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", + **_gateway_auth_env(), + ), + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize( + ("entrypoint", "selected_url_env", "selected_url"), + [ + ( + "old_gateway_direct", + "OLD_SIMULATION_GATEWAY_URL", + "https://old-gateway.example.test", + ), + ( + "cloud_run_simulation_entrypoint", + "SIMULATION_ENTRYPOINT_URL", + "https://simulation.example.test", + ), + ], +) +def test_validate_app_engine_deploy_env_requires_only_selected_url( + entrypoint, + selected_url_env, + selected_url, +): + env = _script_env( + SIM_ENTRYPOINT=entrypoint, + **_gateway_auth_env(), + ) + missing_result = _run_script( + ".github/scripts/validate_app_engine_deploy_env.sh", + env, + ) + valid_result = _run_script( + ".github/scripts/validate_app_engine_deploy_env.sh", + {**env, selected_url_env: selected_url}, + ) + + assert missing_result.returncode == 1 + assert selected_url_env in missing_result.stderr + assert valid_result.returncode == 0, valid_result.stderr def test_app_engine_image_contains_git_controlled_simulation_routing_placeholders(): dockerfile = (REPO / "gcp/policyengine_api/Dockerfile").read_text(encoding="utf-8") export_script = (REPO / "gcp/export.py").read_text(encoding="utf-8") - assert "ENV SIMULATION_ENTRYPOINT_URL .simulation_entrypoint_url" in dockerfile - assert "ENV OLD_SIMULATION_GATEWAY_URL .old_simulation_gateway_url" in dockerfile - assert "ENV SIM_ENTRYPOINT .sim_entrypoint" in dockerfile + assert 'ENV SIMULATION_ENTRYPOINT_URL=".simulation_entrypoint_url"' in dockerfile + assert 'ENV OLD_SIMULATION_GATEWAY_URL=".old_simulation_gateway_url"' in dockerfile + assert 'ENV SIM_ENTRYPOINT=".sim_entrypoint"' in dockerfile assert '".old_simulation_gateway_url", OLD_SIMULATION_GATEWAY_URL' in export_script assert '".sim_entrypoint", SIM_ENTRYPOINT' in export_script +@pytest.mark.parametrize( + ("entrypoint", "selected_url_env", "selected_url", "unselected_url_env"), + [ + ( + "old_gateway_direct", + "OLD_SIMULATION_GATEWAY_URL", + "https://old-gateway.example.test", + "SIMULATION_ENTRYPOINT_URL", + ), + ( + "cloud_run_simulation_entrypoint", + "SIMULATION_ENTRYPOINT_URL", + "https://simulation.example.test", + "OLD_SIMULATION_GATEWAY_URL", + ), + ], +) +def test_app_engine_export_requires_only_selected_url( + tmp_path, + entrypoint, + selected_url_env, + selected_url, + unselected_url_env, +): + (tmp_path / "gcp/policyengine_api").mkdir(parents=True) + shutil.copy2(REPO / "gcp/export.py", tmp_path / "gcp/export.py") + shutil.copy2( + REPO / "gcp/policyengine_api/Dockerfile", + tmp_path / "gcp/policyengine_api/Dockerfile", + ) + env = { + **_script_env(), + **_required_runtime_env(), + "SIM_ENTRYPOINT": entrypoint, + selected_url_env: selected_url, + } + env.pop(unselected_url_env) + + result = subprocess.run( + [sys.executable, "gcp/export.py"], + cwd=tmp_path, + env=env, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + rendered = (tmp_path / "gcp/policyengine_api/Dockerfile").read_text( + encoding="utf-8" + ) + assert f'ENV {selected_url_env}="{selected_url}"' in rendered + assert f'ENV {unselected_url_env}=""' in rendered + + def test_build_cloud_run_image_dry_run_uses_cloud_run_dockerfile(): dockerignore = REPO / "gcp/cloud_run/Dockerfile.dockerignore" @@ -493,6 +715,73 @@ def test_deploy_cloud_run_candidate_dry_run_never_shifts_traffic(): assert "SIM_ENTRYPOINT=cloud_run_simulation_entrypoint" in result.stdout +@pytest.mark.parametrize( + ("entrypoint", "selected_url_env", "selected_url", "unselected_url_env"), + [ + ( + "old_gateway_direct", + "OLD_SIMULATION_GATEWAY_URL", + "https://old-gateway.example.test", + "SIMULATION_ENTRYPOINT_URL", + ), + ( + "cloud_run_simulation_entrypoint", + "SIMULATION_ENTRYPOINT_URL", + "https://simulation.example.test", + "OLD_SIMULATION_GATEWAY_URL", + ), + ], +) +def test_deploy_cloud_run_candidate_passes_only_configured_simulation_urls( + entrypoint, + selected_url_env, + selected_url, + unselected_url_env, +): + env = { + **_script_env(), + **_required_runtime_env(), + "SIM_ENTRYPOINT": entrypoint, + selected_url_env: selected_url, + "CLOUD_RUN_IMAGE_URI": ("us-central1-docker.pkg.dev/project/repo/api:sha"), + "CLOUD_RUN_TAG": "stage3-test", + } + env.pop(unselected_url_env) + + result = _run_script( + ".github/scripts/deploy_cloud_run_candidate.sh", + env, + ) + + assert result.returncode == 0, result.stderr + assert f"{selected_url_env}={selected_url}" in result.stdout + assert f"{unselected_url_env}=" not in result.stdout + + +def test_deploy_cloud_run_candidate_loads_git_controlled_direct_mode(): + env = { + **_script_env(), + **_required_runtime_env(), + "OLD_SIMULATION_GATEWAY_URL": "https://old-gateway.example.test", + "CLOUD_RUN_IMAGE_URI": "us-central1-docker.pkg.dev/project/repo/api:sha", + "CLOUD_RUN_TAG": "stage3-test", + } + env.pop("SIM_ENTRYPOINT") + env.pop("SIMULATION_ENTRYPOINT_URL") + + result = _run_script( + ".github/scripts/deploy_cloud_run_candidate.sh", + env, + ) + + assert result.returncode == 0, result.stderr + assert "SIM_ENTRYPOINT=old_gateway_direct" in result.stdout + assert ( + "OLD_SIMULATION_GATEWAY_URL=https://old-gateway.example.test" in result.stdout + ) + assert "SIMULATION_ENTRYPOINT_URL=" not in result.stdout + + def test_manual_simulation_entrypoint_ramp_is_removed(): assert not (REPO / ".github/scripts/ramp_simulation_entrypoint.sh").exists() assert not (REPO / ".github/workflows/ramp-simulation-entrypoint.yml").exists() @@ -644,7 +933,7 @@ def test_deploy_cloud_run_candidate_pins_runtime_shape(): # Stage 2-qualified values, pinned on every deploy — rationale in # docs/migration/cloud-run-operations.md ("Runtime shape and scaling"). assert "--concurrency 6 " in result.stdout - assert "WEB_CONCURRENCY=2 " in result.stdout + assert "WEB_CONCURRENCY=2" in result.stdout # Revision-level floor (--min-instances) stays 0; the warm floor is applied # service-level (--min), defaulting to 0 unless a job overrides it. assert "--min-instances 0 " in result.stdout @@ -991,26 +1280,51 @@ def test_push_workflow_serializes_deployments_without_cancelling_in_progress_run def test_push_workflow_pins_direct_modal_selector_in_git_for_initial_release(): workflow = _push_workflow() - staging_deploy = _workflow_job_block(workflow, "deploy-cloud-run-staging") - production_deploy = _workflow_job_block(workflow, "deploy-cloud-run-candidate") - app_engine_staging = _workflow_job_block(workflow, "deploy-staging") - app_engine_production = _workflow_job_block( - workflow, - "deploy-production-candidate", - ) + pr_workflow = _pr_workflow() + mode = (REPO / ".github/simulation-entrypoint-mode").read_text(encoding="utf-8") - for job in ( - staging_deploy, - production_deploy, - app_engine_staging, - app_engine_production, - ): - assert "SIM_ENTRYPOINT: old_gateway_direct" in job - assert "${{ vars.SIM_ENTRYPOINT" not in job + assert mode.strip() == "old_gateway_direct" + for workflow_text in (workflow, pr_workflow): + assert "SIM_ENTRYPOINT:" not in workflow_text assert ( - "OLD_SIMULATION_GATEWAY_URL: " - "${{ secrets.OLD_SIMULATION_GATEWAY_URL }}" in job + workflow_text.count( + "OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }}" + ) + == 1 ) + assert ( + workflow_text.count( + "SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }}" + ) + == 1 + ) + assert "${{ vars.SIM_ENTRYPOINT" not in workflow_text + + +def test_deployment_consumers_load_single_git_controlled_selector(): + consumers = ( + ".github/request-simulation-model-versions.sh", + ".github/scripts/deploy_cloud_run_candidate.sh", + ".github/scripts/prepare_app_engine_bundle.sh", + ".github/scripts/validate_app_engine_deploy_env.sh", + ".github/scripts/validate_cloud_run_deploy_env.sh", + ) + + for consumer in consumers: + script = (REPO / consumer).read_text(encoding="utf-8") + assert "source .github/scripts/simulation_entrypoint_env.sh" in script + assert "simulation_entrypoint_load_git_selection" in script + + +def test_workflows_never_depend_on_opaque_legacy_simulation_url_secret(): + workflows = "\n".join( + workflow.read_text(encoding="utf-8") + for workflow in (REPO / ".github/workflows").glob("*.y*ml") + ) + + assert "SIMULATION_API_URL" not in workflows + assert "secrets.SIMULATION_ENTRYPOINT_URL" not in workflows + assert "secrets.OLD_SIMULATION_GATEWAY_URL" not in workflows def test_push_workflow_uses_dedicated_cloud_run_runtime_service_account(): From 7a2d97af41f1789b01d823a921944bfe7f209a4b Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Thu, 30 Jul 2026 02:55:41 +0300 Subject: [PATCH 2/4] fix: read simulation entrypoint URL from secret --- .github/workflows/pr.yml | 2 +- .github/workflows/push.yml | 2 +- docs/migration/cloud-run-operations.md | 14 ++++++++------ tests/unit/test_cloud_run_deploy_scripts.py | 5 +++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 32bc88204..daac28cbc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,7 +5,7 @@ on: pull_request env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} - SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }} + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} jobs: ensure-policyengine-bundle-supported-by-simulation-api: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 2aa7def3f..b8e7b20af 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,7 +8,7 @@ on: env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} - SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }} + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} concurrency: group: deploy diff --git a/docs/migration/cloud-run-operations.md b/docs/migration/cloud-run-operations.md index 469d978e8..6cdc2da17 100644 --- a/docs/migration/cloud-run-operations.md +++ b/docs/migration/cloud-run-operations.md @@ -252,12 +252,14 @@ all require only the URL selected by that value: - `cloud_run_simulation_entrypoint` requires `SIMULATION_ENTRYPOINT_URL`. -Both URLs are non-secret repository-level GitHub Actions variables shared by -the PR, staging, and production workflows. If both are configured, both are -retained on a revision, but an unselected future URL does not block a -direct-Modal deployment. The legacy `SIMULATION_API_URL` secret is -intentionally unsupported: its value is opaque, cannot be audited, and must -not silently choose a deployment upstream. +`OLD_SIMULATION_GATEWAY_URL` is a non-secret repository-level GitHub Actions +variable. `SIMULATION_ENTRYPOINT_URL` is a repository-level GitHub Actions +secret, exposed to the workflow and deployed service as an environment +variable with the same name. If both are configured, both are retained on a +revision, but an unselected future URL does not block a direct-Modal +deployment. The legacy `SIMULATION_API_URL` secret is intentionally +unsupported: its value is opaque, cannot be audited, and must not silently +choose a deployment upstream. ## IAM and bootstrap constraints diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index 07ec4d2ee..ef7674c3b 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -1294,7 +1294,7 @@ def test_push_workflow_pins_direct_modal_selector_in_git_for_initial_release(): ) assert ( workflow_text.count( - "SIMULATION_ENTRYPOINT_URL: ${{ vars.SIMULATION_ENTRYPOINT_URL }}" + "SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}" ) == 1 ) @@ -1323,7 +1323,8 @@ def test_workflows_never_depend_on_opaque_legacy_simulation_url_secret(): ) assert "SIMULATION_API_URL" not in workflows - assert "secrets.SIMULATION_ENTRYPOINT_URL" not in workflows + assert "vars.SIMULATION_ENTRYPOINT_URL" not in workflows + assert "secrets.SIMULATION_ENTRYPOINT_URL" in workflows assert "secrets.OLD_SIMULATION_GATEWAY_URL" not in workflows From bb0c6a07873753fad035377a4f1dbce45a28f958 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Thu, 30 Jul 2026 03:05:21 +0300 Subject: [PATCH 3/4] fix: scope entrypoint secret to deploy environments --- .github/workflows/pr.yml | 4 +- .github/workflows/push.yml | 11 +++++- docs/migration/cloud-run-operations.md | 15 +++---- tests/unit/test_cloud_run_deploy_scripts.py | 43 ++++++++++++++++++--- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index daac28cbc..408b12997 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,12 +5,14 @@ on: pull_request env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} jobs: ensure-policyengine-bundle-supported-by-simulation-api: name: Ensure PolicyEngine bundle is supported by simulation API runs-on: ubuntu-latest + environment: staging + env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read steps: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index b8e7b20af..9f4767a83 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,7 +8,6 @@ on: env: ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }} - SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} concurrency: group: deploy @@ -37,6 +36,8 @@ jobs: (github.repository == 'PolicyEngine/policyengine-api') && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging + env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} steps: - name: Checkout repo uses: actions/checkout@v4 @@ -108,6 +109,8 @@ jobs: (github.repository == 'PolicyEngine/policyengine-api') && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging + env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read id-token: write @@ -210,6 +213,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} CLOUD_RUN_SERVICE: policyengine-api-staging # Staging stays scale-to-zero, single instance: it exists for per-push # validation, not capacity. Both the revision-level (--min-instances) and @@ -416,6 +420,8 @@ jobs: (github.repository == 'PolicyEngine/policyengine-api') && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production + env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} steps: - name: Checkout repo uses: actions/checkout@v4 @@ -432,6 +438,8 @@ jobs: (github.repository == 'PolicyEngine/policyengine-api') && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production + env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read id-token: write @@ -559,6 +567,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production env: + SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} CLOUD_RUN_SERVICE: policyengine-api # Sized by the Stage 2 qualification and the PR 4 host cutover — rationale # and numbers in docs/migration/cloud-run-operations.md ("Runtime shape and diff --git a/docs/migration/cloud-run-operations.md b/docs/migration/cloud-run-operations.md index 6cdc2da17..872e311a8 100644 --- a/docs/migration/cloud-run-operations.md +++ b/docs/migration/cloud-run-operations.md @@ -253,13 +253,14 @@ all require only the URL selected by that value: `SIMULATION_ENTRYPOINT_URL`. `OLD_SIMULATION_GATEWAY_URL` is a non-secret repository-level GitHub Actions -variable. `SIMULATION_ENTRYPOINT_URL` is a repository-level GitHub Actions -secret, exposed to the workflow and deployed service as an environment -variable with the same name. If both are configured, both are retained on a -revision, but an unselected future URL does not block a direct-Modal -deployment. The legacy `SIMULATION_API_URL` secret is intentionally -unsupported: its value is opaque, cannot be audited, and must not silently -choose a deployment upstream. +variable. `SIMULATION_ENTRYPOINT_URL` is a GitHub Actions Environment secret +configured separately in the existing `staging` and `production` environments. +Each environment-bound job exposes it as the same-named environment variable +and passes it to the deployed service as runtime configuration. If both URLs +are configured, both are retained on a revision, but an unselected future URL +does not block a direct-Modal deployment. The legacy `SIMULATION_API_URL` +secret is intentionally unsupported: its value is opaque, cannot be audited, +and must not silently choose a deployment upstream. ## IAM and bootstrap constraints diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index ef7674c3b..2a4e598e5 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -1292,15 +1292,46 @@ def test_push_workflow_pins_direct_modal_selector_in_git_for_initial_release(): ) == 1 ) - assert ( - workflow_text.count( - "SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}" - ) - == 1 - ) assert "${{ vars.SIM_ENTRYPOINT" not in workflow_text +def test_workflows_scope_simulation_entrypoint_secret_to_github_environments(): + secret_env = ( + "SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}" + ) + pr_workflow = _pr_workflow() + push_workflow = _push_workflow() + + assert secret_env not in pr_workflow.split("jobs:", maxsplit=1)[0] + assert secret_env not in push_workflow.split("jobs:", maxsplit=1)[0] + + environment_jobs = ( + ( + pr_workflow, + "ensure-policyengine-bundle-supported-by-simulation-api", + "staging", + ), + ( + push_workflow, + "ensure-staging-model-version-aligns-with-sim-api", + "staging", + ), + (push_workflow, "deploy-staging", "staging"), + (push_workflow, "deploy-cloud-run-staging", "staging"), + ( + push_workflow, + "ensure-production-model-version-aligns-with-sim-api", + "production", + ), + (push_workflow, "deploy-production-candidate", "production"), + (push_workflow, "deploy-cloud-run-candidate", "production"), + ) + for workflow_text, job_name, environment in environment_jobs: + job = _workflow_job_block(workflow_text, job_name) + assert f"environment: {environment}" in job + assert secret_env in job + + def test_deployment_consumers_load_single_git_controlled_selector(): consumers = ( ".github/request-simulation-model-versions.sh", From c435b46cbb5391ada4c8266cae637506cc150dc7 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Thu, 30 Jul 2026 03:25:27 +0300 Subject: [PATCH 4/4] refactor: configure simulation selector by environment --- .github/request-simulation-model-versions.sh | 1 - .github/scripts/deploy_cloud_run_candidate.sh | 2 - .github/scripts/prepare_app_engine_bundle.sh | 3 - .github/scripts/simulation_entrypoint_env.sh | 26 +------- .../scripts/validate_app_engine_deploy_env.sh | 1 - .../scripts/validate_cloud_run_deploy_env.sh | 1 - .github/simulation-entrypoint-mode | 1 - .github/workflows/pr.yml | 1 + .github/workflows/push.yml | 6 ++ ...onal-simulation-entrypoint-config.fixed.md | 2 +- docs/migration/cloud-run-operations.md | 23 +++---- tests/unit/test_cloud_run_deploy_scripts.py | 65 ++++++++++--------- 12 files changed, 56 insertions(+), 76 deletions(-) delete mode 100644 .github/simulation-entrypoint-mode diff --git a/.github/request-simulation-model-versions.sh b/.github/request-simulation-model-versions.sh index e606fa76a..27a68062c 100755 --- a/.github/request-simulation-model-versions.sh +++ b/.github/request-simulation-model-versions.sh @@ -9,7 +9,6 @@ set -euo pipefail # same gateway app as the .py bundle route. source .github/scripts/simulation_entrypoint_env.sh -simulation_entrypoint_load_git_selection GATEWAY_URL="$( simulation_entrypoint_selected_url "${SIM_ENTRYPOINT:-}" diff --git a/.github/scripts/deploy_cloud_run_candidate.sh b/.github/scripts/deploy_cloud_run_candidate.sh index 0fb0fd5eb..4f1a0dcf1 100755 --- a/.github/scripts/deploy_cloud_run_candidate.sh +++ b/.github/scripts/deploy_cloud_run_candidate.sh @@ -3,8 +3,6 @@ set -euo pipefail source .github/scripts/cloud_run_env.sh -source .github/scripts/simulation_entrypoint_env.sh -simulation_entrypoint_load_git_selection cloud_run_set_defaults bash .github/scripts/validate_cloud_run_deploy_env.sh diff --git a/.github/scripts/prepare_app_engine_bundle.sh b/.github/scripts/prepare_app_engine_bundle.sh index a6c49577d..e7b8f96f6 100644 --- a/.github/scripts/prepare_app_engine_bundle.sh +++ b/.github/scripts/prepare_app_engine_bundle.sh @@ -2,9 +2,6 @@ set -euo pipefail -source .github/scripts/simulation_entrypoint_env.sh -simulation_entrypoint_load_git_selection - python gcp/export.py cp gcp/policyengine_api/app.yaml . cp gcp/policyengine_api/Dockerfile . diff --git a/.github/scripts/simulation_entrypoint_env.sh b/.github/scripts/simulation_entrypoint_env.sh index 3731bead2..a3e8389ee 100644 --- a/.github/scripts/simulation_entrypoint_env.sh +++ b/.github/scripts/simulation_entrypoint_env.sh @@ -1,33 +1,9 @@ #!/usr/bin/env bash -# Shared selected-mode configuration for deployment and compatibility checks. +# Shared selected-mode helpers for deployment and compatibility checks. # The legacy SIMULATION_API_URL secret is intentionally unsupported: its value # is opaque and must not influence which upstream API v1 calls. -simulation_entrypoint_load_git_selection() { - local config_file - - if [[ -n "${SIM_ENTRYPOINT:-}" ]]; then - return - fi - - config_file="${SIM_ENTRYPOINT_CONFIG_FILE:-.github/simulation-entrypoint-mode}" - if [[ ! -r "${config_file}" ]]; then - printf 'Unable to read Git-controlled simulation entrypoint mode from %s\n' \ - "${config_file}" >&2 - return 1 - fi - - IFS= read -r SIM_ENTRYPOINT <"${config_file}" - if [[ -z "${SIM_ENTRYPOINT}" ]]; then - printf 'Git-controlled simulation entrypoint mode is empty in %s\n' \ - "${config_file}" >&2 - return 1 - fi - - export SIM_ENTRYPOINT -} - simulation_entrypoint_url_env_name() { local entrypoint="${1:-}" diff --git a/.github/scripts/validate_app_engine_deploy_env.sh b/.github/scripts/validate_app_engine_deploy_env.sh index 53cf7975d..4e081a4b0 100644 --- a/.github/scripts/validate_app_engine_deploy_env.sh +++ b/.github/scripts/validate_app_engine_deploy_env.sh @@ -3,7 +3,6 @@ set -euo pipefail source .github/scripts/simulation_entrypoint_env.sh -simulation_entrypoint_load_git_selection selected_url_env="$( simulation_entrypoint_url_env_name "${SIM_ENTRYPOINT:-}" diff --git a/.github/scripts/validate_cloud_run_deploy_env.sh b/.github/scripts/validate_cloud_run_deploy_env.sh index 99100e78b..23b2654e9 100755 --- a/.github/scripts/validate_cloud_run_deploy_env.sh +++ b/.github/scripts/validate_cloud_run_deploy_env.sh @@ -4,7 +4,6 @@ set -euo pipefail source .github/scripts/cloud_run_env.sh source .github/scripts/simulation_entrypoint_env.sh -simulation_entrypoint_load_git_selection cloud_run_set_defaults # Cloud Run rejects deploys where the traffic tag and service name together diff --git a/.github/simulation-entrypoint-mode b/.github/simulation-entrypoint-mode deleted file mode 100644 index 659198541..000000000 --- a/.github/simulation-entrypoint-mode +++ /dev/null @@ -1 +0,0 @@ -old_gateway_direct diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 408b12997..2dd9b5235 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest environment: staging env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 9f4767a83..2c64e3b21 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -37,6 +37,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} steps: - name: Checkout repo @@ -110,6 +111,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read @@ -213,6 +215,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: staging env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} CLOUD_RUN_SERVICE: policyengine-api-staging # Staging stays scale-to-zero, single instance: it exists for per-push @@ -421,6 +424,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} steps: - name: Checkout repo @@ -439,6 +443,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} permissions: contents: read @@ -567,6 +572,7 @@ jobs: && (github.event.head_commit.message == 'Update PolicyEngine API') environment: production env: + SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }} SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }} CLOUD_RUN_SERVICE: policyengine-api # Sized by the Stage 2 qualification and the PR 4 host cutover — rationale diff --git a/changelog.d/conditional-simulation-entrypoint-config.fixed.md b/changelog.d/conditional-simulation-entrypoint-config.fixed.md index 7e4c162c9..bc7354b79 100644 --- a/changelog.d/conditional-simulation-entrypoint-config.fixed.md +++ b/changelog.d/conditional-simulation-entrypoint-config.fixed.md @@ -1 +1 @@ -Require only the Git-selected simulation upstream URL during deployment, without relying on the inaccessible legacy `SIMULATION_API_URL` secret. +Require only the simulation upstream URL selected by each deployment environment, without relying on the inaccessible legacy `SIMULATION_API_URL` secret. diff --git a/docs/migration/cloud-run-operations.md b/docs/migration/cloud-run-operations.md index 872e311a8..d4c2b3321 100644 --- a/docs/migration/cloud-run-operations.md +++ b/docs/migration/cloud-run-operations.md @@ -243,10 +243,11 @@ rollback remains an explicit incident-response action. ## Simulation entrypoint deployment configuration -`SIM_ENTRYPOINT` is read from the single Git-controlled -`.github/simulation-entrypoint-mode` file. Deployment validation, App Engine -packaging, Cloud Run configuration, and the model-version compatibility guard -all require only the URL selected by that value: +`SIM_ENTRYPOINT` is a GitHub Actions Environment variable configured separately +in the existing `staging` and `production` environments. Environment-bound +jobs expose it as the same-named environment variable. Deployment validation, +App Engine packaging, Cloud Run configuration, and the model-version +compatibility guard all require only the URL selected by that value: - `old_gateway_direct` requires `OLD_SIMULATION_GATEWAY_URL`; - `cloud_run_simulation_entrypoint` requires @@ -254,13 +255,13 @@ all require only the URL selected by that value: `OLD_SIMULATION_GATEWAY_URL` is a non-secret repository-level GitHub Actions variable. `SIMULATION_ENTRYPOINT_URL` is a GitHub Actions Environment secret -configured separately in the existing `staging` and `production` environments. -Each environment-bound job exposes it as the same-named environment variable -and passes it to the deployed service as runtime configuration. If both URLs -are configured, both are retained on a revision, but an unselected future URL -does not block a direct-Modal deployment. The legacy `SIMULATION_API_URL` -secret is intentionally unsupported: its value is opaque, cannot be audited, -and must not silently choose a deployment upstream. +configured separately in those same environments. Each environment-bound job +exposes both values under their same-named environment variables and passes +them to the deployed service as runtime configuration. If both URLs are +configured, both are retained on a revision, but an unselected future URL does +not block a direct-Modal deployment. The legacy `SIMULATION_API_URL` secret is +intentionally unsupported: its value is opaque, cannot be audited, and must +not silently choose a deployment upstream. ## IAM and bootstrap constraints diff --git a/tests/unit/test_cloud_run_deploy_scripts.py b/tests/unit/test_cloud_run_deploy_scripts.py index 2a4e598e5..e46e486f4 100644 --- a/tests/unit/test_cloud_run_deploy_scripts.py +++ b/tests/unit/test_cloud_run_deploy_scripts.py @@ -469,20 +469,24 @@ def test_cloud_run_startup_supervises_redis_and_server_children(): assert re.search(r"(?m)^ *wait 2>/dev/null", start_script) is None -def test_validate_cloud_run_deploy_env_requires_git_controlled_selector(): +def test_validate_cloud_run_deploy_env_requires_selector_environment_variable(): result = _run_script( ".github/scripts/validate_cloud_run_deploy_env.sh", - _script_env(SIM_ENTRYPOINT_CONFIG_FILE="/missing/entrypoint-mode"), + _script_env( + OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", + **_gateway_auth_env(), + ), ) assert result.returncode == 1 - assert "Unable to read Git-controlled simulation entrypoint mode" in result.stderr + assert "SIM_ENTRYPOINT" in result.stderr -def test_validate_cloud_run_deploy_env_loads_git_controlled_direct_mode(): +def test_validate_cloud_run_deploy_env_accepts_direct_mode_from_environment(): result = _run_script( ".github/scripts/validate_cloud_run_deploy_env.sh", _script_env( + SIM_ENTRYPOINT="old_gateway_direct", OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", **_gateway_auth_env(), ), @@ -529,20 +533,24 @@ def test_validate_cloud_run_deploy_env_requires_only_selected_url( assert valid_result.returncode == 0, valid_result.stderr -def test_validate_app_engine_deploy_env_requires_git_controlled_selector(): +def test_validate_app_engine_deploy_env_requires_selector_environment_variable(): result = _run_script( ".github/scripts/validate_app_engine_deploy_env.sh", - _script_env(SIM_ENTRYPOINT_CONFIG_FILE="/missing/entrypoint-mode"), + _script_env( + OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", + **_gateway_auth_env(), + ), ) assert result.returncode == 1 - assert "Unable to read Git-controlled simulation entrypoint mode" in result.stderr + assert "SIM_ENTRYPOINT is required" in result.stderr -def test_validate_app_engine_deploy_env_loads_git_controlled_direct_mode(): +def test_validate_app_engine_deploy_env_accepts_direct_mode_from_environment(): result = _run_script( ".github/scripts/validate_app_engine_deploy_env.sh", _script_env( + SIM_ENTRYPOINT="old_gateway_direct", OLD_SIMULATION_GATEWAY_URL="https://old-gateway.example.test", **_gateway_auth_env(), ), @@ -589,7 +597,7 @@ def test_validate_app_engine_deploy_env_requires_only_selected_url( assert valid_result.returncode == 0, valid_result.stderr -def test_app_engine_image_contains_git_controlled_simulation_routing_placeholders(): +def test_app_engine_image_contains_simulation_routing_environment_placeholders(): dockerfile = (REPO / "gcp/policyengine_api/Dockerfile").read_text(encoding="utf-8") export_script = (REPO / "gcp/export.py").read_text(encoding="utf-8") @@ -758,7 +766,7 @@ def test_deploy_cloud_run_candidate_passes_only_configured_simulation_urls( assert f"{unselected_url_env}=" not in result.stdout -def test_deploy_cloud_run_candidate_loads_git_controlled_direct_mode(): +def test_deploy_cloud_run_candidate_requires_selector_environment_variable(): env = { **_script_env(), **_required_runtime_env(), @@ -774,12 +782,8 @@ def test_deploy_cloud_run_candidate_loads_git_controlled_direct_mode(): env, ) - assert result.returncode == 0, result.stderr - assert "SIM_ENTRYPOINT=old_gateway_direct" in result.stdout - assert ( - "OLD_SIMULATION_GATEWAY_URL=https://old-gateway.example.test" in result.stdout - ) - assert "SIMULATION_ENTRYPOINT_URL=" not in result.stdout + assert result.returncode == 1 + assert "SIM_ENTRYPOINT" in result.stderr def test_manual_simulation_entrypoint_ramp_is_removed(): @@ -1278,32 +1282,34 @@ def test_push_workflow_serializes_deployments_without_cancelling_in_progress_run assert "concurrency:\n group: deploy\n cancel-in-progress: false" in workflow -def test_push_workflow_pins_direct_modal_selector_in_git_for_initial_release(): +def test_workflows_do_not_store_simulation_selector_in_git(): workflow = _push_workflow() pr_workflow = _pr_workflow() - mode = (REPO / ".github/simulation-entrypoint-mode").read_text(encoding="utf-8") - assert mode.strip() == "old_gateway_direct" + assert not (REPO / ".github/simulation-entrypoint-mode").exists() for workflow_text in (workflow, pr_workflow): - assert "SIM_ENTRYPOINT:" not in workflow_text assert ( workflow_text.count( "OLD_SIMULATION_GATEWAY_URL: ${{ vars.OLD_SIMULATION_GATEWAY_URL }}" ) == 1 ) - assert "${{ vars.SIM_ENTRYPOINT" not in workflow_text + assert ( + "SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }}" + not in workflow_text.split("jobs:", maxsplit=1)[0] + ) -def test_workflows_scope_simulation_entrypoint_secret_to_github_environments(): - secret_env = ( - "SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}" - ) +def test_workflows_scope_simulation_routing_config_to_github_environments(): + selector_env = "SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }}" + secret_env = "SIMULATION_ENTRYPOINT_URL: ${{ secrets.SIMULATION_ENTRYPOINT_URL }}" pr_workflow = _pr_workflow() push_workflow = _push_workflow() assert secret_env not in pr_workflow.split("jobs:", maxsplit=1)[0] assert secret_env not in push_workflow.split("jobs:", maxsplit=1)[0] + assert selector_env not in pr_workflow.split("jobs:", maxsplit=1)[0] + assert selector_env not in push_workflow.split("jobs:", maxsplit=1)[0] environment_jobs = ( ( @@ -1329,14 +1335,13 @@ def test_workflows_scope_simulation_entrypoint_secret_to_github_environments(): for workflow_text, job_name, environment in environment_jobs: job = _workflow_job_block(workflow_text, job_name) assert f"environment: {environment}" in job + assert selector_env in job assert secret_env in job -def test_deployment_consumers_load_single_git_controlled_selector(): +def test_deployment_consumers_require_selector_from_environment(): consumers = ( ".github/request-simulation-model-versions.sh", - ".github/scripts/deploy_cloud_run_candidate.sh", - ".github/scripts/prepare_app_engine_bundle.sh", ".github/scripts/validate_app_engine_deploy_env.sh", ".github/scripts/validate_cloud_run_deploy_env.sh", ) @@ -1344,7 +1349,7 @@ def test_deployment_consumers_load_single_git_controlled_selector(): for consumer in consumers: script = (REPO / consumer).read_text(encoding="utf-8") assert "source .github/scripts/simulation_entrypoint_env.sh" in script - assert "simulation_entrypoint_load_git_selection" in script + assert "simulation_entrypoint_load_git_selection" not in script def test_workflows_never_depend_on_opaque_legacy_simulation_url_secret(): @@ -1478,7 +1483,7 @@ def test_push_workflow_promotes_production_cloud_run_after_candidate_smoke(): ) assert smoke_index < promote_index - assert "${{ vars.SIM_ENTRYPOINT" not in cloud_run_production + assert "SIM_ENTRYPOINT: ${{ vars.SIM_ENTRYPOINT }}" in cloud_run_production assert ( "CLOUD_RUN_TARGET_REVISION: ${{ steps.candidate.outputs.revision }}" in cloud_run_production