Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
options:
- configured
- hk-verify
configured_service:
description: "Optional exact inventory service_name for configured target; blank keeps all configured targets."
required: false
type: string
cloud_run_region:
description: "Cloud Run region for hk-verify. Leave blank to use repository/environment variables."
required: false
Expand Down Expand Up @@ -70,6 +74,7 @@ jobs:
QSL_ENABLE_CLOUD_RUN_AUTOMATION: ${{ vars.QSL_ENABLE_CLOUD_RUN_AUTOMATION }}
CLOUD_RUN_CLEANUP_ENABLED: ${{ vars.CLOUD_RUN_CLEANUP_ENABLED }}
WORKFLOW_TARGET: ${{ inputs.target || 'configured' }}
INPUT_CONFIGURED_SERVICE: ${{ inputs.configured_service }}
INPUT_CLOUD_RUN_REGION: ${{ inputs.cloud_run_region }}
INPUT_CLOUD_RUN_SERVICE: ${{ inputs.cloud_run_service }}
INPUT_ACCOUNT_GROUP: ${{ inputs.account_group }}
Expand Down Expand Up @@ -342,11 +347,31 @@ jobs:
run: |
set -euo pipefail
sync_plan_json="$(uv run --no-sync python scripts/build_cloud_run_env_sync_plan.py --json)"
{
echo "sync_plan_json<<__SYNC_PLAN_JSON__"
printf '%s\n' "${sync_plan_json}"
echo "__SYNC_PLAN_JSON__"
} >> "$GITHUB_OUTPUT"
export SYNC_PLAN_JSON="${sync_plan_json}"
python - <<'PY'
import json
import os
import re
from pathlib import Path

plan = json.loads(os.environ["SYNC_PLAN_JSON"])
selected = os.environ.get("INPUT_CONFIGURED_SERVICE", "")
if os.environ.get("WORKFLOW_TARGET", "configured") == "configured" and selected:
if not re.fullmatch(r"[a-z][a-z0-9-]{0,62}", selected) or plan.get("mode") != "per_service":
raise SystemExit("configured_service requires an exact private inventory service_name")
matches = [target for target in plan["targets"] if target.get("service_name") == selected]
if len(matches) != 1:
raise SystemExit("configured_service must match exactly one admitted inventory target")
plan["targets"] = matches
# Reconciliation also reads inventory and legacy env; scope every source.
inventory = json.dumps({"targets": [{"service_name": selected}]})
with Path(os.environ["GITHUB_ENV"]).open("a") as handle:
handle.write(f"CLOUD_RUN_SERVICE_TARGETS_JSON={inventory}\n")
handle.write(f"CLOUD_RUN_SERVICE={selected}\nCLOUD_RUN_SERVICES={selected}\n")
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as handle:
handle.write("sync_plan_json<<__SYNC_PLAN_JSON__\n")
handle.write(json.dumps(plan) + "\n__SYNC_PLAN_JSON__\n")
PY

- name: Validate env sync inputs
if: steps.config.outputs.env_sync_enabled == 'true'
Expand Down Expand Up @@ -1289,7 +1314,9 @@ jobs:
--scheduler-location="${scheduler_location}"
--delete-legacy-schedulers
)
python3 scripts/reconcile_cloud_runtime.py "${reconcile_args[@]}"
if [ "${WORKFLOW_TARGET:-configured}" != "configured" ] || [ -z "${INPUT_CONFIGURED_SERVICE:-}" ]; then
python3 scripts/reconcile_cloud_runtime.py "${reconcile_args[@]}"
fi

for update in "${scheduler_updates[@]}"; do
IFS=$'\t' read -r cloud_run_service _ <<< "${update}"
Expand All @@ -1309,7 +1336,7 @@ jobs:
done

- name: Prune old Cloud Run revisions
if: steps.config.outputs.deploy_enabled == 'true' && env.CLOUD_RUN_CLEANUP_ENABLED == 'true'
if: steps.config.outputs.deploy_enabled == 'true' && env.CLOUD_RUN_CLEANUP_ENABLED == 'true' && (env.WORKFLOW_TARGET != 'configured' || env.INPUT_CONFIGURED_SERVICE == '')
env:
SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_json }}
run: |
Expand Down Expand Up @@ -1398,7 +1425,7 @@ jobs:
done

- name: Clean up old Cloud Run images
if: steps.config.outputs.deploy_enabled == 'true' && env.CLOUD_RUN_CLEANUP_ENABLED == 'true'
if: steps.config.outputs.deploy_enabled == 'true' && env.CLOUD_RUN_CLEANUP_ENABLED == 'true' && (env.WORKFLOW_TARGET != 'configured' || env.INPUT_CONFIGURED_SERVICE == '')
run: |
set -euo pipefail

Expand Down
100 changes: 100 additions & 0 deletions tests/test_cloud_run_warmup_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,103 @@ def test_lifecycle_observes_the_exact_existing_service_binding() -> None:
assert "cloud-run-region: ${{ env.CLOUD_RUN_REGION }}" in publish
assert "cloud-run-service: ${{ matrix.target.service }}" in publish
assert "scheduler-location: ${{ env.RUNTIME_HEARTBEAT_SCHEDULER_LOCATION }}" in publish


def _resolve_sync_plan(tmp_path, monkeypatch, *, selector="service-b", target="configured", services=None, mode="per_service"):
import json
import os
import subprocess
import sys
import textwrap

workflow = Path(".github/workflows/sync-cloud-run-env.yml").read_text(encoding="utf-8")
step = workflow.split(" - name: Resolve admissible Cloud Run targets\n", 1)[1].split(" - name:", 1)[0]
script = textwrap.dedent(step.split(" run: |\n", 1)[1])
plan = {"mode": mode, "targets": [{"service_name": service, "env": {"RUNTIME_TARGET_ENABLED": "false"}} for service in (services or ["service-a", "service-b"])]}
stub = tmp_path / "uv"
stub.write_text(f"#!{sys.executable}\nimport os\nprint(os.environ['TEST_PLAN'])\n")
stub.chmod(0o700)
(tmp_path / "python").symlink_to(sys.executable)
output = tmp_path / "output"
github_env = tmp_path / "env"
output.touch()
github_env.touch()
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
env = {
"PATH": os.environ["PATH"],
"TEST_PLAN": json.dumps(plan),
"WORKFLOW_TARGET": target,
"INPUT_CONFIGURED_SERVICE": selector,
"GITHUB_OUTPUT": str(output),
"GITHUB_ENV": str(github_env),
}
result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True)
return result, output.read_text(), github_env.read_text(), plan


def test_configured_service_selects_one_target_and_all_downstream_sources(tmp_path, monkeypatch) -> None:
import json
from scripts.reconcile_cloud_runtime import load_targets

result, output, github_env, _ = _resolve_sync_plan(tmp_path, monkeypatch)
assert result.returncode == 0, result.stderr
plan = json.loads(output.split("\n", 1)[1].split("\n", 1)[0])
assert [target["service_name"] for target in plan["targets"]] == ["service-b"]
assert "CLOUD_RUN_SERVICES=service-b\n" in github_env
assert "CLOUD_RUN_SERVICE=service-b\n" in github_env
inventory = json.loads(github_env.split("CLOUD_RUN_SERVICE_TARGETS_JSON=", 1)[1].split("\n", 1)[0])
assert inventory == {"targets": [{"service_name": "service-b"}]}
scoped_env = dict(line.split("=", 1) for line in github_env.splitlines())
scoped_env["SYNC_PLAN_JSON"] = json.dumps(plan)
assert [target.service_name for target in load_targets(env=scoped_env)] == ["service-b"]
assert plan["targets"][0]["env"] == {"RUNTIME_TARGET_ENABLED": "false"}
assert result.stdout == ""


def test_configured_service_rejects_unmatched_duplicate_or_noninventory(tmp_path, monkeypatch) -> None:
for index, kwargs in enumerate((
{"selector": "missing"},
{"services": ["service-b", "service-b"]},
{"selector": " service-b"},
{"selector": "service-b\nOTHER=value"},
{"mode": "legacy"},
)):
case_path = tmp_path / str(index)
case_path.mkdir()
result, output, github_env, _ = _resolve_sync_plan(case_path, monkeypatch, **kwargs)
assert result.returncode != 0
assert output == ""
assert github_env == ""
assert "service-b" not in result.stderr


def test_empty_selector_and_hk_verify_preserve_existing_targets(tmp_path, monkeypatch) -> None:
import json

for index, kwargs in enumerate(({"selector": ""}, {"target": "hk-verify"})):
case_path = tmp_path / str(index)
case_path.mkdir()
result, output, github_env, original = _resolve_sync_plan(case_path, monkeypatch, **kwargs)
assert result.returncode == 0, result.stderr
assert json.loads(output.split("\n", 1)[1].split("\n", 1)[0]) == original
assert github_env == ""


def test_single_target_sync_skips_global_cleanup_before_other_service_mutation() -> None:
import subprocess
import textwrap

workflow = Path(".github/workflows/sync-cloud-run-env.yml").read_text(encoding="utf-8")
assert " configured_service:\n" in workflow
assert "INPUT_CONFIGURED_SERVICE: ${{ inputs.configured_service }}" in workflow
scope_guard = "(env.WORKFLOW_TARGET != 'configured' || env.INPUT_CONFIGURED_SERVICE == '')"
for name in ("Prune old Cloud Run revisions", "Clean up old Cloud Run images"):
step = workflow.split(f" - name: {name}\n", 1)[1].split(" - name:", 1)[0]
assert scope_guard in step.split(" run:", 1)[0]
cleanup = workflow.split(" reconcile_args=(", 1)[1].split(' for update in "${scheduler_updates[@]}";', 1)[0]
assert 'if [ "${WORKFLOW_TARGET:-configured}" != "configured" ] || [ -z "${INPUT_CONFIGURED_SERVICE:-}" ]; then' in cleanup
script = 'python3() { return 83; }\n' + textwrap.dedent(" reconcile_args=(" + cleanup)
for target, selector, expected in (("configured", "service-b", 0), ("configured", "", 83), ("hk-verify", "service-b", 83)):
result = subprocess.run(["/bin/bash", "-c", script], env={"WORKFLOW_TARGET": target, "INPUT_CONFIGURED_SERVICE": selector}, capture_output=True)
assert result.returncode == expected
assert workflow.index("Resolve admissible Cloud Run targets") < workflow.index("Verify deployed runtime target admission before traffic shift")