From f121122efff5a4134626e4f52afdbca5f79a524d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 7 Jul 2026 06:39:03 +0800 Subject: [PATCH] fix: reconcile cloud runtime drift Co-Authored-By: Codex --- .github/workflows/sync-cloud-run-env.yml | 44 +-- scripts/reconcile_cloud_runtime.py | 412 ++++++++++++++++++++++ tests/test_reconcile_cloud_runtime.py | 224 ++++++++++++ tests/test_sync_cloud_run_env_workflow.sh | 8 +- 4 files changed, 658 insertions(+), 30 deletions(-) create mode 100755 scripts/reconcile_cloud_runtime.py create mode 100644 tests/test_reconcile_cloud_runtime.py diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 712640c..082cc51 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -957,6 +957,18 @@ jobs: gcloud "${gcloud_args[@]}" done + - name: Reconcile Cloud Run traffic + if: steps.config.outputs.env_sync_enabled == 'true' + env: + SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_json }} + run: | + set -euo pipefail + python3 scripts/reconcile_cloud_runtime.py \ + --platform=ibkr \ + --project="${GCP_PROJECT_ID}" \ + --region="${CLOUD_RUN_REGION}" \ + --ensure-latest-traffic + - name: Verify strategy plugin mounts if: steps.config.outputs.env_sync_enabled == 'true' env: @@ -1149,32 +1161,12 @@ jobs: --quiet fi - for update in "${scheduler_updates[@]}"; do - IFS=$'\t' read -r cloud_run_service _market_timezone _main_time <<< "${update}" - legacy_candidates=() - if [[ "${cloud_run_service}" == *-service ]]; then - legacy_candidates+=("${cloud_run_service%-service}-probe-scheduler") - legacy_candidates+=("${cloud_run_service%-service}-precheck-scheduler") - fi - legacy_candidates+=("${cloud_run_service}-probe-scheduler") - legacy_candidates+=("${cloud_run_service}-precheck-scheduler") - account_suffix="${cloud_run_service#interactive-brokers-quant-live-}" - account_suffix="${account_suffix%-service}" - if [[ "${account_suffix}" == u* ]]; then - legacy_candidates+=("ibkr-${account_suffix}-backup-execution") - fi - for legacy_job in "${legacy_candidates[@]}"; do - if gcloud scheduler jobs describe "${legacy_job}" \ - --project="${GCP_PROJECT_ID}" \ - --location="${scheduler_location}" >/dev/null 2>&1; then - echo "Deleting legacy Cloud Scheduler job ${legacy_job}; monitor dispatcher now owns probe/precheck." - gcloud scheduler jobs delete "${legacy_job}" \ - --project="${GCP_PROJECT_ID}" \ - --location="${scheduler_location}" \ - --quiet - fi - done - done + python3 scripts/reconcile_cloud_runtime.py \ + --platform=ibkr \ + --project="${GCP_PROJECT_ID}" \ + --region="${CLOUD_RUN_REGION}" \ + --scheduler-location="${scheduler_location}" \ + --delete-legacy-schedulers - name: Prune old Cloud Run revisions if: steps.config.outputs.enabled == 'true' diff --git a/scripts/reconcile_cloud_runtime.py b/scripts/reconcile_cloud_runtime.py new file mode 100755 index 0000000..973c2d8 --- /dev/null +++ b/scripts/reconcile_cloud_runtime.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +"""Reconcile Cloud Run and Cloud Scheduler runtime drift. + +The script intentionally only auto-fixes infrastructure drift that is safe to +repair: Cloud Run traffic pointing at an older revision and known legacy +Scheduler jobs. Runtime code errors, secret gaps, and live/paper policy +conflicts must still fail fast in the owning workflow. +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from typing import Any, Iterable, Mapping, Sequence + + +@dataclass(frozen=True) +class RuntimeTarget: + service_name: str + region: str = "" + account_scope: str = "" + + +class ReconcileError(RuntimeError): + pass + + +def _load_json_object(raw: str, *, field_name: str) -> Mapping[str, Any] | list[Any]: + try: + value = json.loads(raw) + except json.JSONDecodeError as exc: + raise ReconcileError(f"{field_name} must be valid JSON: {exc}") from exc + if not isinstance(value, (Mapping, list)): + raise ReconcileError(f"{field_name} must decode to an object or list") + return value + + +def _runtime_target(entry: Mapping[str, Any]) -> Mapping[str, Any]: + raw = entry.get("runtime_target") or entry.get("runtime_target_json") + if isinstance(raw, Mapping): + return raw + if isinstance(raw, str) and raw.strip(): + loaded = _load_json_object(raw, field_name="runtime_target_json") + if isinstance(loaded, Mapping): + return loaded + return {} + + +def _target_from_entry(entry: Mapping[str, Any]) -> RuntimeTarget | None: + runtime_target = _runtime_target(entry) + service = ( + entry.get("service_name") + or entry.get("service") + or entry.get("cloud_run_service") + or runtime_target.get("service_name") + ) + service_name = str(service or "").strip() + if not service_name: + return None + account_scope = str( + entry.get("ACCOUNT_GROUP") + or entry.get("account_scope") + or runtime_target.get("account_scope") + or "" + ).strip() + region = str(entry.get("region") or entry.get("cloud_run_region") or "").strip() + return RuntimeTarget(service_name=service_name, region=region, account_scope=account_scope) + + +def _dedupe_targets(targets: Iterable[RuntimeTarget]) -> list[RuntimeTarget]: + by_service: dict[str, RuntimeTarget] = {} + for target in targets: + current = by_service.get(target.service_name) + if current is None: + by_service[target.service_name] = target + continue + by_service[target.service_name] = RuntimeTarget( + service_name=target.service_name, + region=current.region or target.region, + account_scope=current.account_scope or target.account_scope, + ) + return list(by_service.values()) + + +def load_targets(*, env: Mapping[str, str]) -> list[RuntimeTarget]: + targets: list[RuntimeTarget] = [] + + raw_plan = str(env.get("SYNC_PLAN_JSON") or "").strip() + if raw_plan: + plan = _load_json_object(raw_plan, field_name="SYNC_PLAN_JSON") + entries = plan.get("targets") if isinstance(plan, Mapping) else plan + if not isinstance(entries, list): + raise ReconcileError("SYNC_PLAN_JSON.targets must be a list") + for entry in entries: + if isinstance(entry, Mapping): + target = _target_from_entry(entry) + if target: + targets.append(target) + + raw_targets = str(env.get("CLOUD_RUN_SERVICE_TARGETS_JSON") or "").strip() + if raw_targets: + payload = _load_json_object(raw_targets, field_name="CLOUD_RUN_SERVICE_TARGETS_JSON") + entries = payload.get("targets") if isinstance(payload, Mapping) else payload + if not isinstance(entries, list): + raise ReconcileError("CLOUD_RUN_SERVICE_TARGETS_JSON.targets must be a list") + for entry in entries: + if isinstance(entry, Mapping): + target = _target_from_entry(entry) + if target: + targets.append(target) + + raw_services = str(env.get("CLOUD_RUN_SERVICES") or env.get("CLOUD_RUN_SERVICE") or "").strip() + for chunk in raw_services.replace(";", ",").replace("\n", ",").split(","): + service_name = chunk.strip() + if service_name: + targets.append(RuntimeTarget(service_name=service_name)) + + return _dedupe_targets(targets) + + +def _run(args: Sequence[str], *, json_output: bool = False, dry_run: bool = False) -> Any: + printable = " ".join(args) + if dry_run: + print(f"DRY-RUN {printable}") + return {} if json_output else "" + completed = subprocess.run( + list(args), + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if completed.returncode != 0: + detail = (completed.stderr or completed.stdout or "").strip() + raise ReconcileError(f"Command failed ({completed.returncode}): {printable}\n{detail}") + if json_output: + try: + return json.loads(completed.stdout or "{}") + except json.JSONDecodeError as exc: + raise ReconcileError(f"Command did not return JSON: {printable}") from exc + return completed.stdout + + +def _run_optional(args: Sequence[str], *, dry_run: bool = False) -> bool: + printable = " ".join(args) + if dry_run: + print(f"DRY-RUN {printable}") + return True + completed = subprocess.run( + list(args), + check=False, + text=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return completed.returncode == 0 + + +def _traffic_on_latest(service: Mapping[str, Any], latest_revision: str) -> bool: + traffic = service.get("status", {}).get("traffic") or [] + if not isinstance(traffic, list): + return False + for item in traffic: + if not isinstance(item, Mapping): + continue + try: + percent = int(item.get("percent") or 0) + except (TypeError, ValueError): + percent = 0 + if percent == 100 and (item.get("latestRevision") is True or item.get("revisionName") == latest_revision): + return True + return False + + +def _revision_commit(*, project: str, region: str, revision: str, dry_run: bool) -> str: + payload = _run( + [ + "gcloud", + "run", + "revisions", + "describe", + revision, + f"--project={project}", + f"--region={region}", + "--format=json", + ], + json_output=True, + dry_run=dry_run, + ) + labels = payload.get("metadata", {}).get("labels") or {} + return str(labels.get("commit-sha") or "").strip() + + +def ensure_latest_traffic( + *, + project: str, + region: str, + targets: Sequence[RuntimeTarget], + expected_commit: str, + dry_run: bool, +) -> None: + for target in targets: + target_region = target.region or region + if not target_region: + raise ReconcileError(f"Region is required for {target.service_name}") + service = _run( + [ + "gcloud", + "run", + "services", + "describe", + target.service_name, + f"--project={project}", + f"--region={target_region}", + "--format=json", + ], + json_output=True, + dry_run=dry_run, + ) + status = service.get("status", {}) if isinstance(service, Mapping) else {} + latest = str(status.get("latestReadyRevisionName") or "").strip() + if not latest: + raise ReconcileError(f"Unable to resolve latest ready revision for {target.service_name}") + if expected_commit: + actual_commit = _revision_commit( + project=project, + region=target_region, + revision=latest, + dry_run=dry_run, + ) + if actual_commit != expected_commit: + raise ReconcileError( + f"{target.service_name} latest revision {latest} commit {actual_commit!r} " + f"does not match expected {expected_commit!r}" + ) + if not _traffic_on_latest(service, latest): + print(f"Updating {target.service_name} traffic to latest revision {latest}.") + _run( + [ + "gcloud", + "run", + "services", + "update-traffic", + target.service_name, + f"--project={project}", + f"--region={target_region}", + "--to-latest", + "--quiet", + ], + dry_run=dry_run, + ) + verified = _run( + [ + "gcloud", + "run", + "services", + "describe", + target.service_name, + f"--project={project}", + f"--region={target_region}", + "--format=json", + ], + json_output=True, + dry_run=dry_run, + ) + verified_latest = str(verified.get("status", {}).get("latestReadyRevisionName") or "").strip() + if not verified_latest: + raise ReconcileError(f"Unable to resolve latest ready revision for {target.service_name}") + if not _traffic_on_latest(verified, verified_latest): + raise ReconcileError(f"{target.service_name} traffic is not 100% on latest revision") + print(f"Cloud Run traffic OK for {target.service_name}: {verified_latest}") + + +def _legacy_jobs_for_target(platform: str, target: RuntimeTarget) -> list[str]: + service = target.service_name + jobs = [f"{service}-probe-scheduler", f"{service}-precheck-scheduler"] + if service.endswith("-service"): + base = service[: -len("-service")] + jobs.extend([f"{base}-probe-scheduler", f"{base}-precheck-scheduler"]) + + if platform == "longbridge": + scope = service + if scope.startswith("longbridge-quant-"): + scope = scope[len("longbridge-quant-") :] + if scope.endswith("-service"): + scope = scope[: -len("-service")] + if scope and scope != service: + jobs.append(f"lb-{scope}-backup-execution") + elif platform == "ibkr": + prefix = "interactive-brokers-quant-live-" + suffix = service + if suffix.startswith(prefix): + suffix = suffix[len(prefix) :] + if suffix.endswith("-service"): + suffix = suffix[: -len("-service")] + if suffix.startswith("u"): + jobs.append(f"ibkr-{suffix}-backup-execution") + + return list(dict.fromkeys(jobs)) + + +def _scheduler_locations(*, region: str, scheduler_location: str, targets: Sequence[RuntimeTarget], env: Mapping[str, str]) -> list[str]: + locations = [scheduler_location or region] + raw_extra = str(env.get("CLOUD_SCHEDULER_LEGACY_LOCATIONS") or "").strip() + for chunk in raw_extra.replace(";", ",").split(","): + if chunk.strip(): + locations.append(chunk.strip()) + for target in targets: + if target.region: + locations.append(target.region) + return [item for item in dict.fromkeys(locations) if item] + + +def delete_legacy_schedulers( + *, + platform: str, + project: str, + region: str, + scheduler_location: str, + targets: Sequence[RuntimeTarget], + env: Mapping[str, str], + dry_run: bool, +) -> None: + locations = _scheduler_locations( + region=region, + scheduler_location=scheduler_location, + targets=targets, + env=env, + ) + for target in targets: + for job in _legacy_jobs_for_target(platform, target): + for location in locations: + if not _run_optional( + [ + "gcloud", + "scheduler", + "jobs", + "describe", + job, + f"--project={project}", + f"--location={location}", + ], + dry_run=dry_run, + ): + continue + print(f"Deleting legacy Cloud Scheduler job {job} in {location}.") + _run( + [ + "gcloud", + "scheduler", + "jobs", + "delete", + job, + f"--project={project}", + f"--location={location}", + "--quiet", + ], + dry_run=dry_run, + ) + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--platform", choices=("ibkr", "longbridge"), required=True) + parser.add_argument("--project", default=os.environ.get("GCP_PROJECT_ID", "")) + parser.add_argument("--region", default=os.environ.get("CLOUD_RUN_REGION", "")) + parser.add_argument("--scheduler-location", default=os.environ.get("CLOUD_SCHEDULER_LOCATION", "")) + parser.add_argument("--expected-commit", default=os.environ.get("GITHUB_SHA", "")) + parser.add_argument("--ensure-latest-traffic", action="store_true") + parser.add_argument("--delete-legacy-schedulers", action="store_true") + parser.add_argument("--dry-run", action="store_true") + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv or sys.argv[1:]) + if not args.project: + raise ReconcileError("--project or GCP_PROJECT_ID is required") + targets = load_targets(env=os.environ) + if not targets: + raise ReconcileError("No Cloud Run targets resolved from SYNC_PLAN_JSON, CLOUD_RUN_SERVICE_TARGETS_JSON, or CLOUD_RUN_SERVICE") + if args.ensure_latest_traffic: + ensure_latest_traffic( + project=args.project, + region=args.region, + targets=targets, + expected_commit=args.expected_commit, + dry_run=args.dry_run, + ) + if args.delete_legacy_schedulers: + delete_legacy_schedulers( + platform=args.platform, + project=args.project, + region=args.region, + scheduler_location=args.scheduler_location, + targets=targets, + env=os.environ, + dry_run=args.dry_run, + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except ReconcileError as exc: + print(f"error: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/tests/test_reconcile_cloud_runtime.py b/tests/test_reconcile_cloud_runtime.py new file mode 100644 index 0000000..8a92475 --- /dev/null +++ b/tests/test_reconcile_cloud_runtime.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +import json +import unittest +from unittest import mock + +from scripts import reconcile_cloud_runtime as rcr + + +class ReconcileCloudRuntimeTests(unittest.TestCase): + def test_load_targets_reads_all_supported_env_shapes(self) -> None: + env = { + "SYNC_PLAN_JSON": json.dumps( + { + "targets": [ + { + "runtime_target_json": json.dumps( + { + "service_name": "interactive-brokers-quant-live-u1234-service", + "account_scope": "u1234", + } + ), + "region": "us-central1", + } + ] + } + ), + "CLOUD_RUN_SERVICE_TARGETS_JSON": json.dumps( + { + "targets": [ + {"service_name": "interactive-brokers-quant-live-u1234-service"}, + {"service_name": "interactive-brokers-quant-live-u5678-service", "region": "asia-east1"}, + ] + } + ), + "CLOUD_RUN_SERVICES": "interactive-brokers-quant-live-u9999-service, extra-service;interactive-brokers-quant-live-u1234-service", + } + + targets = rcr.load_targets(env=env) + + self.assertEqual( + [target.service_name for target in targets], + [ + "interactive-brokers-quant-live-u1234-service", + "interactive-brokers-quant-live-u5678-service", + "interactive-brokers-quant-live-u9999-service", + "extra-service", + ], + ) + self.assertEqual(targets[0].region, "us-central1") + self.assertEqual(targets[0].account_scope, "u1234") + self.assertEqual(targets[1].region, "asia-east1") + + def test_legacy_jobs_for_ibkr_service_include_only_explicit_candidates(self) -> None: + target = rcr.RuntimeTarget(service_name="interactive-brokers-quant-live-u1234-service") + + self.assertEqual( + set(rcr._legacy_jobs_for_target("ibkr", target)), + { + "interactive-brokers-quant-live-u1234-service-probe-scheduler", + "interactive-brokers-quant-live-u1234-service-precheck-scheduler", + "interactive-brokers-quant-live-u1234-probe-scheduler", + "interactive-brokers-quant-live-u1234-precheck-scheduler", + "ibkr-u1234-backup-execution", + }, + ) + + def test_ensure_latest_traffic_updates_then_verifies_latest_commit(self) -> None: + target = rcr.RuntimeTarget( + service_name="interactive-brokers-quant-live-u1234-service", + region="us-central1", + ) + service_before = { + "status": { + "latestReadyRevisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + "traffic": [ + {"percent": 100, "revisionName": "interactive-brokers-quant-live-u1234-service-00000-old"} + ], + } + } + service_after = { + "status": { + "latestReadyRevisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + "traffic": [ + { + "percent": 100, + "latestRevision": True, + "revisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + } + ], + } + } + revision = {"metadata": {"labels": {"commit-sha": "abc123"}}} + calls: list[list[str]] = [] + service_describe_count = 0 + + def fake_run(args, *, json_output=False, dry_run=False): + nonlocal service_describe_count + calls.append(list(args)) + self.assertFalse(dry_run) + if args[:4] == ["gcloud", "run", "services", "describe"]: + service_describe_count += 1 + return service_before if service_describe_count == 1 else service_after + if args[:4] == ["gcloud", "run", "revisions", "describe"]: + return revision + if args[:4] == ["gcloud", "run", "services", "update-traffic"]: + return "" + self.fail(f"unexpected command: {args}") + + with mock.patch.object(rcr, "_run", side_effect=fake_run): + rcr.ensure_latest_traffic( + project="interactivebrokersquant", + region="us-central1", + targets=[target], + expected_commit="abc123", + dry_run=False, + ) + + self.assertEqual(service_describe_count, 2) + self.assertTrue(any(cmd[:4] == ["gcloud", "run", "services", "update-traffic"] for cmd in calls)) + self.assertTrue(any(cmd[:4] == ["gcloud", "run", "revisions", "describe"] for cmd in calls)) + + def test_ensure_latest_traffic_requires_latest_ready_revision(self) -> None: + target = rcr.RuntimeTarget(service_name="interactive-brokers-quant-live-u1234-service", region="us-central1") + + def fake_run(args, *, json_output=False, dry_run=False): + if args[:4] == ["gcloud", "run", "services", "describe"]: + return { + "status": { + "latestCreatedRevisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + "traffic": [], + } + } + self.fail(f"unexpected command: {args}") + + with mock.patch.object(rcr, "_run", side_effect=fake_run): + with self.assertRaises(rcr.ReconcileError): + rcr.ensure_latest_traffic( + project="interactivebrokersquant", + region="us-central1", + targets=[target], + expected_commit="abc123", + dry_run=False, + ) + + def test_ensure_latest_traffic_rejects_commit_mismatch(self) -> None: + target = rcr.RuntimeTarget(service_name="interactive-brokers-quant-live-u1234-service", region="us-central1") + + def fake_run(args, *, json_output=False, dry_run=False): + if args[:4] == ["gcloud", "run", "services", "describe"]: + return { + "status": { + "latestReadyRevisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + "traffic": [ + { + "percent": 100, + "latestRevision": True, + "revisionName": "interactive-brokers-quant-live-u1234-service-00001-abc", + } + ], + } + } + if args[:4] == ["gcloud", "run", "revisions", "describe"]: + return {"metadata": {"labels": {"commit-sha": "wrong-sha"}}} + self.fail(f"unexpected command: {args}") + + with mock.patch.object(rcr, "_run", side_effect=fake_run): + with self.assertRaises(rcr.ReconcileError): + rcr.ensure_latest_traffic( + project="interactivebrokersquant", + region="us-central1", + targets=[target], + expected_commit="abc123", + dry_run=False, + ) + + def test_delete_legacy_schedulers_deletes_only_known_jobs(self) -> None: + target = rcr.RuntimeTarget(service_name="interactive-brokers-quant-live-u1234-service") + expected_jobs = { + "interactive-brokers-quant-live-u1234-service-probe-scheduler", + "interactive-brokers-quant-live-u1234-service-precheck-scheduler", + "interactive-brokers-quant-live-u1234-probe-scheduler", + "interactive-brokers-quant-live-u1234-precheck-scheduler", + "ibkr-u1234-backup-execution", + } + describe_calls: list[list[str]] = [] + delete_calls: list[list[str]] = [] + + def fake_run_optional(args, *, dry_run=False): + self.assertFalse(dry_run) + describe_calls.append(list(args)) + self.assertEqual(args[:4], ["gcloud", "scheduler", "jobs", "describe"]) + self.assertIn(args[4], expected_jobs) + self.assertIn("--location=us-central1", args) + return True + + def fake_run(args, *, json_output=False, dry_run=False): + self.assertFalse(json_output) + self.assertFalse(dry_run) + delete_calls.append(list(args)) + self.assertEqual(args[:4], ["gcloud", "scheduler", "jobs", "delete"]) + self.assertIn(args[4], expected_jobs) + self.assertIn("--location=us-central1", args) + return "" + + with mock.patch.object(rcr, "_run_optional", side_effect=fake_run_optional), mock.patch.object( + rcr, "_run", side_effect=fake_run + ): + rcr.delete_legacy_schedulers( + platform="ibkr", + project="interactivebrokersquant", + region="us-central1", + scheduler_location="us-central1", + targets=[target], + env={}, + dry_run=False, + ) + + self.assertEqual({call[4] for call in describe_calls}, expected_jobs) + self.assertEqual({call[4] for call in delete_calls}, expected_jobs) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 90d499f..005a71f 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -85,6 +85,10 @@ grep -Fq 'Timed out waiting for Cloud Run service ${cloud_run_service} to deploy grep -Fq -- '--network=default' "$workflow_file" grep -Fq -- '--subnet=default' "$workflow_file" grep -Fq -- '--vpc-egress=private-ranges-only' "$workflow_file" +grep -Fq 'python3 scripts/reconcile_cloud_runtime.py' "$workflow_file" +grep -Fq -- '--platform=ibkr' "$workflow_file" +grep -Fq -- '--ensure-latest-traffic' "$workflow_file" +grep -Fq -- '--delete-legacy-schedulers' "$workflow_file" grep -Fq 'emit_target_env_pairs()' "$workflow_file" grep -Fq 'emit_target_remove_env_vars()' "$workflow_file" @@ -149,10 +153,6 @@ grep -Fq -- '--oidc-token-audience="${service_url}"' "$workflow_file" grep -Fq 'monitor_job_name="interactive-brokers-monitor-dispatcher-scheduler"' "$workflow_file" grep -Fq 'monitor_uri="${monitor_dispatch_base_url}/monitor-dispatch"' "$workflow_file" grep -Fq -- '--schedule="*/5 * * * *"' "$workflow_file" -grep -Fq 'legacy_candidates+=("${cloud_run_service%-service}-probe-scheduler")' "$workflow_file" -grep -Fq 'legacy_candidates+=("${cloud_run_service%-service}-precheck-scheduler")' "$workflow_file" -grep -Fq 'legacy_candidates+=("ibkr-${account_suffix}-backup-execution")' "$workflow_file" -grep -Fq 'gcloud scheduler jobs delete "${legacy_job}"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_TO"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD"' "$workflow_file"