From 9235714fd06ef569d8087e3f13303cc3f12b5740 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:08:03 +0800 Subject: [PATCH] fix: parse nested Cloud Run deployment readback Co-Authored-By: Codex --- .github/workflows/sync-cloud-run-env.yml | 16 +++---- tests/test_sync_cloud_run_env_workflow.py | 58 +++++++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 584b509..3f96da9 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -281,14 +281,14 @@ jobs: --project="${GCP_PROJECT_ID}" \ --region="${CLOUD_RUN_REGION}" \ --format='json(status.traffic)' \ - | python3 -c 'import json, sys; traffic = json.load(sys.stdin) or []; active = [{"revisionName": row.get("revisionName"), "percent": int(row.get("percent", 0))} for row in traffic if isinstance(row, dict) and int(row.get("percent", 0)) > 0]; print(json.dumps(sorted(active, key=lambda row: (str(row["revisionName"]), row["percent"])), separators=(",", ":")))' \ + | python3 -c 'import json, sys; traffic = (json.load(sys.stdin).get("status", {}).get("traffic") or []); active = [{"revisionName": row.get("revisionName"), "percent": int(row.get("percent", 0))} for row in traffic if isinstance(row, dict) and int(row.get("percent", 0)) > 0]; print(json.dumps(sorted(active, key=lambda row: (str(row["revisionName"]), row["percent"])), separators=(",", ":")))' \ | sha256sum | cut -d ' ' -f1 } { echo "traffic=$(active_traffic_digest)" - echo "config=$(summarize gcloud run services describe "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(spec.template.spec.serviceAccountName,spec.template.spec.containerConcurrency,spec.template.spec.timeoutSeconds,spec.template.spec.containers.resources,spec.template.spec.containers.env.name,spec.template.spec.containers.env.valueFrom.secretKeyRef.name)')" + echo "config=$(summarize gcloud run services describe "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(spec.template.spec.serviceAccountName,spec.template.spec.containerConcurrency,spec.template.spec.timeoutSeconds,spec.template.spec.containers[].resources,spec.template.spec.containers[].env[].name,spec.template.spec.containers[].env[].valueFrom.secretKeyRef)')" echo "scheduler=$(summarize gcloud scheduler jobs list --project="${GCP_PROJECT_ID}" --location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}" --sort-by=name --format='json(name,state,schedule,timeZone,httpTarget.uri,httpTarget.httpMethod,httpTarget.oidcToken.serviceAccountEmail,httpTarget.oidcToken.audience)')" - echo "iam=$(summarize gcloud run services get-iam-policy "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(bindings.role,bindings.members)')" + echo "iam=$(summarize gcloud run services get-iam-policy "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(bindings[].role,bindings[].members,bindings[].condition)')" } > "${BASELINE_PATH}" echo "path=${BASELINE_PATH}" >> "${GITHUB_OUTPUT}" @@ -367,7 +367,7 @@ jobs: --project="${GCP_PROJECT_ID}" \ --region="${CLOUD_RUN_REGION}" \ --format='json(status.traffic)' \ - | python3 -c 'import json, sys; traffic = json.load(sys.stdin) or []; active = [{"revisionName": row.get("revisionName"), "percent": int(row.get("percent", 0))} for row in traffic if isinstance(row, dict) and int(row.get("percent", 0)) > 0]; print(json.dumps(sorted(active, key=lambda row: (str(row["revisionName"]), row["percent"])), separators=(",", ":")))' \ + | python3 -c 'import json, sys; traffic = (json.load(sys.stdin).get("status", {}).get("traffic") or []); active = [{"revisionName": row.get("revisionName"), "percent": int(row.get("percent", 0))} for row in traffic if isinstance(row, dict) and int(row.get("percent", 0)) > 0]; print(json.dumps(sorted(active, key=lambda row: (str(row["revisionName"]), row["percent"])), separators=(",", ":")))' \ | sha256sum | cut -d ' ' -f1 } fail_readback() { echo "No-traffic deployment readback failed: $1" >&2; exit 1; } @@ -382,7 +382,7 @@ jobs: for _ in $(seq 1 30); do revision_ready="$( { gcloud run revisions describe "${revision_name}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(status.conditions)' 2>/dev/null \ - | python3 -c 'import json, sys; conditions = json.load(sys.stdin) or []; print("True" if any(isinstance(condition, dict) and condition.get("type") == "Ready" and condition.get("status") == "True" for condition in conditions) else "False")' 2>/dev/null; } \ + | python3 -c 'import json, sys; conditions = (json.load(sys.stdin).get("status", {}).get("conditions") or []); print("True" if any(isinstance(condition, dict) and condition.get("type") == "Ready" and condition.get("status") == "True" for condition in conditions) else "False")' 2>/dev/null; } \ || printf 'False' )" [ "${revision_ready}" = "True" ] && break @@ -390,7 +390,7 @@ jobs: done revision_ready_category="$( { gcloud run revisions describe "${revision_name}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(status.conditions)' 2>/dev/null \ - | python3 -c 'import json, sys; conditions = json.load(sys.stdin) or []; ready = next((condition for condition in conditions if isinstance(condition, dict) and condition.get("type") == "Ready"), {}); reason = str(ready.get("reason") or "").upper(); print("HEALTHCHECK_FAILURE") if "HEALTH" in reason or "PROBE" in reason else print("CONTAINER_FAILURE") if "CONTAINER" in reason or "IMAGE" in reason else print("RESOURCE_FAILURE") if "RESOURCE" in reason or "QUOTA" in reason else print("PROGRESS_DEADLINE") if "DEADLINE" in reason or "TIMEOUT" in reason else print("PLATFORM_FAILURE") if "INTERNAL" in reason or "PLATFORM" in reason else print("UNKNOWN")' 2>/dev/null; } \ + | python3 -c 'import json, sys; conditions = (json.load(sys.stdin).get("status", {}).get("conditions") or []); ready = next((condition for condition in conditions if isinstance(condition, dict) and condition.get("type") == "Ready"), {}); reason = str(ready.get("reason") or "").upper(); print("HEALTHCHECK_FAILURE") if "HEALTH" in reason or "PROBE" in reason else print("CONTAINER_FAILURE") if "CONTAINER" in reason or "IMAGE" in reason else print("RESOURCE_FAILURE") if "RESOURCE" in reason or "QUOTA" in reason else print("PROGRESS_DEADLINE") if "DEADLINE" in reason or "TIMEOUT" in reason else print("PLATFORM_FAILURE") if "INTERNAL" in reason or "PLATFORM" in reason else print("UNKNOWN")' 2>/dev/null; } \ || printf 'UNKNOWN' )" if [ "${revision_ready}" != "True" ]; then @@ -400,9 +400,9 @@ jobs: [ "${revision_sha}" = "${EXPECTED_SHA}" ] || fail_readback "revision commit SHA mismatch" [[ "${revision_image}" == *"${EXPECTED_IMAGE_DIGEST}"* ]] || fail_readback "revision image digest mismatch" [ "${traffic}" = "$(active_traffic_digest)" ] || fail_readback "effective traffic changed" - [ "${config}" = "$(summarize gcloud run services describe "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(spec.template.spec.serviceAccountName,spec.template.spec.containerConcurrency,spec.template.spec.timeoutSeconds,spec.template.spec.containers.resources,spec.template.spec.containers.env.name,spec.template.spec.containers.env.valueFrom.secretKeyRef.name)')" ] || fail_readback "configuration changed" + [ "${config}" = "$(summarize gcloud run services describe "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(spec.template.spec.serviceAccountName,spec.template.spec.containerConcurrency,spec.template.spec.timeoutSeconds,spec.template.spec.containers[].resources,spec.template.spec.containers[].env[].name,spec.template.spec.containers[].env[].valueFrom.secretKeyRef)')" ] || fail_readback "configuration changed" [ "${scheduler}" = "$(summarize gcloud scheduler jobs list --project="${GCP_PROJECT_ID}" --location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}" --sort-by=name --format='json(name,state,schedule,timeZone,httpTarget.uri,httpTarget.httpMethod,httpTarget.oidcToken.serviceAccountEmail,httpTarget.oidcToken.audience)')" ] || fail_readback "Scheduler changed" - [ "${iam}" = "$(summarize gcloud run services get-iam-policy "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(bindings.role,bindings.members)')" ] || fail_readback "IAM changed" + [ "${iam}" = "$(summarize gcloud run services get-iam-policy "${CLOUD_RUN_SERVICE}" --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='json(bindings[].role,bindings[].members,bindings[].condition)')" ] || fail_readback "IAM changed" { echo "## No-traffic deployment readback" echo "- Revision commit SHA and image digest verified." diff --git a/tests/test_sync_cloud_run_env_workflow.py b/tests/test_sync_cloud_run_env_workflow.py index ac6ea2f..11c75e0 100644 --- a/tests/test_sync_cloud_run_env_workflow.py +++ b/tests/test_sync_cloud_run_env_workflow.py @@ -1,7 +1,13 @@ from __future__ import annotations +import json +import re +import subprocess +import sys from pathlib import Path +import pytest + def test_sync_cloud_run_env_workflow_requires_manual_dispatch(): workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml" @@ -304,3 +310,55 @@ def test_sync_cloud_run_env_workflow_is_fail_closed_and_no_traffic_by_default(): assert 'inputs.allow_configuration_sync }}" != "true"' in workflow assert 'inputs.allow_traffic_promotion == true' in workflow assert 'inputs.allow_cleanup == true' in workflow + + +def _run_readback_python(marker, payload): + workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml").read_text() + commands = [code for code in re.findall(r"python3 -c '([^']+)'", workflow) if marker in code] + assert commands + results = [subprocess.run([sys.executable, "-c", code], input=json.dumps(payload), + text=True, capture_output=True, check=True).stdout.strip() + for code in commands] + assert len(set(results)) == 1 # capture and verify must interpret traffic identically + return results[0] + + +@pytest.mark.parametrize("ready,expected", [("True", "True"), ("False", "False"), ("Unknown", "False")]) +def test_readback_parses_nested_ready_conditions(ready, expected): + assert _run_readback_python('print("True" if any(', { + "status": {"conditions": [{"type": "Ready", "status": ready}]}, + }) == expected + + +def test_readback_does_not_invent_readiness_when_conditions_missing(): + assert _run_readback_python('print("True" if any(', {}) == "False" + + +def test_readback_classifies_nested_failure_without_exposing_message(): + assert _run_readback_python('reason = str(ready.get(', { + "status": {"conditions": [{"type": "Ready", "status": "False", + "reason": "HealthCheckContainerError", + "message": "synthetic private diagnostic"}]}, + }) == "HEALTHCHECK_FAILURE" + + +def test_readback_traffic_changes_are_not_hashed_as_empty_lists(): + def traffic(revision, percent=100): + return {"status": {"traffic": [{"revisionName": revision, "percent": percent}]}} + + before = _run_readback_python("active = [", traffic("old-revision")) + after = _run_readback_python("active = [", traffic("new-revision")) + assert json.loads(before) == [{"revisionName": "old-revision", "percent": 100}] + assert before != after + tagged = traffic("old-revision") + tagged["status"]["traffic"].append({"revisionName": "zero-traffic", "percent": 0, "tag": "test"}) + assert _run_readback_python("active = [", tagged) == before + + +def test_readback_baseline_projections_include_array_members(): + workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml").read_text() + for field in ("spec.template.spec.containers[].resources", + "spec.template.spec.containers[].env[].name", + "spec.template.spec.containers[].env[].valueFrom.secretKeyRef", + "bindings[].role", "bindings[].members", "bindings[].condition"): + assert workflow.count(field) == 2 # baseline capture and comparison