1111DEDICATED_CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT = (
1212 "policyengine-api-cr-runtime@policyengine-api.iam.gserviceaccount.com"
1313)
14+ CLOUD_RUN_SECRET_MAPPINGS = {
15+ "POLICYENGINE_DB_PASSWORD" : "policyengine-api-prod-db-password:latest" ,
16+ "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN" : (
17+ "policyengine-api-prod-github-microdata-token:latest"
18+ ),
19+ "ANTHROPIC_API_KEY" : "policyengine-api-prod-anthropic-api-key:latest" ,
20+ "OPENAI_API_KEY" : "policyengine-api-prod-openai-api-key:latest" ,
21+ "HUGGING_FACE_TOKEN" : "policyengine-api-prod-hugging-face-token:latest" ,
22+ }
23+ RAW_CLOUD_RUN_SECRET_VALUES = (
24+ "raw-db-secret-value" ,
25+ "raw-github-secret-value" ,
26+ "raw-anthropic-secret-value" ,
27+ "raw-openai-secret-value" ,
28+ "raw-hf-secret-value" ,
29+ )
1430
1531
1632def _script_env (** overrides : str ) -> dict [str , str ]:
@@ -25,11 +41,11 @@ def _script_env(**overrides: str) -> dict[str, str]:
2541
2642def _required_runtime_env () -> dict [str , str ]:
2743 return {
28- "POLICYENGINE_DB_PASSWORD" : "db-password " ,
29- "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN" : " github-token" ,
30- "ANTHROPIC_API_KEY" : "anthropic-key " ,
31- "OPENAI_API_KEY" : "openai-key " ,
32- "HUGGING_FACE_TOKEN" : "hf-token " ,
44+ "POLICYENGINE_DB_PASSWORD" : "raw- db-secret-value " ,
45+ "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN" : ( "raw- github-secret-value" ) ,
46+ "ANTHROPIC_API_KEY" : "raw- anthropic-secret-value " ,
47+ "OPENAI_API_KEY" : "raw- openai-secret-value " ,
48+ "HUGGING_FACE_TOKEN" : "raw- hf-secret-value " ,
3349 "SIMULATION_API_URL" : "https://simulation.example.test" ,
3450 "GATEWAY_AUTH_ISSUER" : "https://issuer.example.test" ,
3551 "GATEWAY_AUTH_AUDIENCE" : "simulation-gateway" ,
@@ -55,6 +71,12 @@ def _push_workflow() -> str:
5571 return (REPO / ".github/workflows/push.yml" ).read_text (encoding = "utf-8" )
5672
5773
74+ def _sync_secrets_workflow () -> str :
75+ return (REPO / ".github/workflows/sync-cloud-run-secrets.yml" ).read_text (
76+ encoding = "utf-8"
77+ )
78+
79+
5880def _workflow_job_block (workflow : str , job_name : str ) -> str :
5981 match = re .search (
6082 rf"^ { re .escape (job_name )} :\n(?P<body>.*?)(?=^ [a-zA-Z0-9_-]+:|\Z)" ,
@@ -118,8 +140,9 @@ def test_validate_cloud_run_deploy_env_reports_missing_runtime_config():
118140
119141 assert result .returncode == 1
120142 assert "Missing required Cloud Run deployment configuration" in result .stderr
121- assert "POLICYENGINE_DB_PASSWORD " in result .stderr
143+ assert "SIMULATION_API_URL " in result .stderr
122144 assert "GATEWAY_AUTH_CLIENT_SECRET_RESOURCE" in result .stderr
145+ assert "POLICYENGINE_DB_PASSWORD" not in result .stderr
123146
124147
125148def test_build_cloud_run_image_dry_run_uses_cloud_run_dockerfile ():
@@ -168,6 +191,11 @@ def test_deploy_cloud_run_candidate_dry_run_never_shifts_traffic():
168191 f"POLICYENGINE_DB_INSTANCE_CONNECTION_NAME={ PRODUCTION_CLOUD_SQL_INSTANCE } "
169192 in result .stdout
170193 )
194+ assert "--set-secrets" in result .stdout
195+ for env_name , secret_ref in CLOUD_RUN_SECRET_MAPPINGS .items ():
196+ assert f"{ env_name } ={ secret_ref } " in result .stdout
197+ for raw_secret_value in RAW_CLOUD_RUN_SECRET_VALUES :
198+ assert raw_secret_value not in result .stdout
171199 assert "CLOUD_RUN_INTERNAL_PROBES" not in result .stdout
172200 assert "--to-latest" not in result .stdout
173201 assert "update-traffic" not in result .stdout
@@ -278,6 +306,56 @@ def test_push_workflow_uses_dedicated_cloud_run_runtime_service_account():
278306 assert deploy_account_secret not in cloud_run_production
279307
280308
309+ def test_push_workflow_does_not_pass_raw_secrets_to_cloud_run_deploy_jobs ():
310+ workflow = _push_workflow ()
311+ cloud_run_staging = _workflow_job_block (workflow , "deploy-cloud-run-staging" )
312+ cloud_run_production = _workflow_job_block (workflow , "deploy-cloud-run-candidate" )
313+ raw_secret_envs = (
314+ "POLICYENGINE_DB_PASSWORD: ${{ secrets.POLICYENGINE_DB_PASSWORD }}" ,
315+ (
316+ "POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN: "
317+ "${{ secrets.POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN }}"
318+ ),
319+ "ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}" ,
320+ "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" ,
321+ "HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}" ,
322+ )
323+
324+ for raw_secret_env in raw_secret_envs :
325+ assert raw_secret_env not in cloud_run_staging
326+ assert raw_secret_env not in cloud_run_production
327+
328+
329+ def test_sync_cloud_run_secrets_workflow_is_manual_and_environment_gated ():
330+ workflow = _sync_secrets_workflow ()
331+
332+ assert "workflow_dispatch:" in workflow
333+ assert "pull_request:" not in workflow
334+ assert "push:" not in workflow
335+ assert "environment: production" in workflow
336+ assert "id-token: write" in workflow
337+ assert "github.ref != 'refs/heads/master'" in workflow
338+ assert "google-github-actions/auth@v2" in workflow
339+ assert (
340+ 'workload_identity_provider: "${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}"'
341+ in workflow
342+ )
343+ assert 'service_account: "${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }}"' in workflow
344+
345+
346+ def test_sync_cloud_run_secrets_workflow_writes_expected_secret_versions ():
347+ workflow = _sync_secrets_workflow ()
348+
349+ assert "set +x" in workflow
350+ assert "--data-file=-" in workflow
351+ assert "gcloud secrets add-iam-policy-binding" in workflow
352+ assert "roles/secretmanager.secretAccessor" in workflow
353+ for env_name , secret_ref in CLOUD_RUN_SECRET_MAPPINGS .items ():
354+ secret_name = secret_ref .removesuffix (":latest" )
355+ assert f"{ env_name } : ${{{{ secrets.{ env_name } }}}}" in workflow
356+ assert f"sync_secret { env_name } { secret_name } " in workflow
357+
358+
281359def test_push_workflow_promotes_production_cloud_run_after_candidate_smoke ():
282360 workflow = _push_workflow ()
283361 cloud_run_production = _workflow_job_block (workflow , "deploy-cloud-run-candidate" )
0 commit comments