Skip to content

Commit d729cc0

Browse files
authored
Sync Cloud Scheduler from runtime target
1 parent 1078ec5 commit d729cc0

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

.github/workflows/sync-cloud-run-env.yml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ jobs:
3232
ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION: ${{ vars.ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION }}
3333
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
3434
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
35+
CLOUD_SCHEDULER_LOCATION: ${{ vars.CLOUD_SCHEDULER_LOCATION }}
36+
CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }}
37+
CLOUD_SCHEDULER_PROBE_TIME: ${{ vars.CLOUD_SCHEDULER_PROBE_TIME }}
38+
CLOUD_SCHEDULER_PRECHECK_TIME: ${{ vars.CLOUD_SCHEDULER_PRECHECK_TIME }}
3539
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
3640
FIRSTRADE_USERNAME_SECRET_NAME: ${{ vars.FIRSTRADE_USERNAME_SECRET_NAME }}
3741
FIRSTRADE_PASSWORD_SECRET_NAME: ${{ vars.FIRSTRADE_PASSWORD_SECRET_NAME }}
@@ -600,6 +604,125 @@ jobs:
600604
601605
gcloud "${gcloud_args[@]}"
602606
607+
- name: Sync Cloud Scheduler schedule
608+
if: steps.env_sync_config.outputs.enabled == 'true'
609+
env:
610+
RUNTIME_TARGET_JSON: ${{ steps.strategy_requirements.outputs.runtime_target_json }}
611+
run: |
612+
set -euo pipefail
613+
614+
scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}"
615+
if [ -z "${scheduler_location}" ]; then
616+
echo "Cloud Scheduler schedule sync requires CLOUD_RUN_REGION or CLOUD_SCHEDULER_LOCATION." >&2
617+
exit 1
618+
fi
619+
620+
mapfile -t scheduler_config < <(python - <<'PY'
621+
import json
622+
import os
623+
624+
raw_runtime_target = os.environ.get("RUNTIME_TARGET_JSON", "").strip()
625+
runtime_scheduler = {}
626+
if raw_runtime_target:
627+
try:
628+
runtime_target = json.loads(raw_runtime_target)
629+
except json.JSONDecodeError:
630+
runtime_target = {}
631+
scheduler = runtime_target.get("scheduler") if isinstance(runtime_target, dict) else {}
632+
if isinstance(scheduler, dict):
633+
runtime_scheduler = scheduler
634+
635+
def configured_time(key: str, name: str, default: str) -> str:
636+
return str(runtime_scheduler.get(key) or os.environ.get(name, "").strip() or default)
637+
638+
print(str(runtime_scheduler.get("timezone") or "America/New_York").strip())
639+
print(configured_time("main_time", "CLOUD_SCHEDULER_MAIN_TIME", "45 15"))
640+
print(configured_time("probe_time", "CLOUD_SCHEDULER_PROBE_TIME", "35 9,15"))
641+
print(configured_time("precheck_time", "CLOUD_SCHEDULER_PRECHECK_TIME", "45 9"))
642+
PY
643+
)
644+
market_timezone="${scheduler_config[0]}"
645+
main_time="${scheduler_config[1]}"
646+
probe_time="${scheduler_config[2]}"
647+
precheck_time="${scheduler_config[3]}"
648+
649+
service_url="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" \
650+
--project="${GCP_PROJECT_ID}" \
651+
--region="${CLOUD_RUN_REGION}" \
652+
--format='value(status.url)' 2>/dev/null || true)"
653+
if [ -z "${service_url}" ]; then
654+
echo "Unable to resolve Cloud Run service URL for ${CLOUD_RUN_SERVICE}; cannot sync scheduler URI." >&2
655+
exit 1
656+
fi
657+
658+
for suffix in scheduler probe-scheduler precheck-scheduler; do
659+
case "${suffix}" in
660+
scheduler)
661+
schedule_time="${main_time}"
662+
scheduler_path="/run"
663+
;;
664+
probe-scheduler)
665+
schedule_time="${probe_time}"
666+
scheduler_path="/probe"
667+
;;
668+
precheck-scheduler)
669+
schedule_time="${precheck_time}"
670+
scheduler_path="/dry-run"
671+
;;
672+
esac
673+
674+
scheduler_job_candidates=("${CLOUD_RUN_SERVICE}-${suffix}")
675+
if [[ "${CLOUD_RUN_SERVICE}" == *-service ]]; then
676+
scheduler_job_candidates+=("${CLOUD_RUN_SERVICE%-service}-${suffix}")
677+
fi
678+
679+
job_name=""
680+
current_schedule=""
681+
for candidate_job in "${scheduler_job_candidates[@]}"; do
682+
current_schedule="$(gcloud scheduler jobs describe "${candidate_job}" \
683+
--project="${GCP_PROJECT_ID}" \
684+
--location="${scheduler_location}" \
685+
--format='value(schedule)' 2>/dev/null || true)"
686+
if [ -n "${current_schedule}" ]; then
687+
job_name="${candidate_job}"
688+
break
689+
fi
690+
done
691+
692+
if [ -z "${current_schedule}" ]; then
693+
echo "Cloud Scheduler job for ${CLOUD_RUN_SERVICE} ${suffix} was not found in ${scheduler_location}; skipping schedule sync."
694+
continue
695+
fi
696+
697+
desired_schedule="$(CURRENT_SCHEDULE="${current_schedule}" SCHEDULE_TIME="${schedule_time}" python - <<'PY'
698+
import os
699+
700+
current_fields = os.environ["CURRENT_SCHEDULE"].split()
701+
time_fields = os.environ["SCHEDULE_TIME"].split()
702+
if len(current_fields) != 5:
703+
raise SystemExit(f"Cloud Scheduler schedule must have 5 fields: {os.environ['CURRENT_SCHEDULE']!r}")
704+
if len(time_fields) == 5:
705+
print(" ".join(time_fields))
706+
elif len(time_fields) == 2:
707+
print(" ".join([*time_fields, *current_fields[2:]]))
708+
else:
709+
raise SystemExit(
710+
f"Cloud Scheduler override must have 2 time fields or 5 cron fields: {os.environ['SCHEDULE_TIME']!r}"
711+
)
712+
PY
713+
)"
714+
715+
scheduler_uri="${service_url}${scheduler_path}"
716+
echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule}, timezone to ${market_timezone}, and URI to ${scheduler_uri}."
717+
gcloud scheduler jobs update http "${job_name}" \
718+
--project="${GCP_PROJECT_ID}" \
719+
--location="${scheduler_location}" \
720+
--uri="${scheduler_uri}" \
721+
--schedule="${desired_schedule}" \
722+
--time-zone="${market_timezone}" \
723+
--quiet
724+
done
725+
603726
- name: Clean up old Cloud Run revisions and images
604727
if: steps.deploy_config.outputs.enabled == 'true'
605728
run: |

tests/test_sync_cloud_run_env_workflow.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ def test_sync_cloud_run_env_workflow_syncs_strategy_plugin_alert_settings():
77
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
88
workflow = workflow_path.read_text(encoding="utf-8")
99

10+
for name in (
11+
"CLOUD_SCHEDULER_LOCATION",
12+
"CLOUD_SCHEDULER_MAIN_TIME",
13+
"CLOUD_SCHEDULER_PROBE_TIME",
14+
"CLOUD_SCHEDULER_PRECHECK_TIME",
15+
):
16+
assert f"{name}: ${{{{ vars.{name} }}}}" in workflow
17+
1018
for name in (
1119
"STRATEGY_PLUGIN_ALERT_CHANNELS",
1220
"STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS",
@@ -106,3 +114,25 @@ def test_sync_cloud_run_env_workflow_syncs_strategy_plugin_alert_settings():
106114
assert '"CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT"' in workflow
107115
assert '"CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY"' in workflow
108116
assert '"CRISIS_ALERT_SMTP_HOST"' in workflow
117+
118+
119+
def test_sync_cloud_run_env_workflow_syncs_scheduler_from_runtime_target():
120+
workflow_path = Path(__file__).resolve().parents[1] / ".github/workflows/sync-cloud-run-env.yml"
121+
workflow = workflow_path.read_text(encoding="utf-8")
122+
123+
assert "Sync Cloud Scheduler schedule" in workflow
124+
assert 'scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}"' in workflow
125+
assert 'raw_runtime_target = os.environ.get("RUNTIME_TARGET_JSON", "").strip()' in workflow
126+
assert 'scheduler = runtime_target.get("scheduler") if isinstance(runtime_target, dict) else {}' in workflow
127+
assert 'print(str(runtime_scheduler.get("timezone") or "America/New_York").strip())' in workflow
128+
assert 'configured_time("main_time", "CLOUD_SCHEDULER_MAIN_TIME", "45 15")' in workflow
129+
assert 'configured_time("probe_time", "CLOUD_SCHEDULER_PROBE_TIME", "35 9,15")' in workflow
130+
assert 'configured_time("precheck_time", "CLOUD_SCHEDULER_PRECHECK_TIME", "45 9")' in workflow
131+
assert 'scheduler_job_candidates=("${CLOUD_RUN_SERVICE}-${suffix}")' in workflow
132+
assert 'scheduler_job_candidates+=("${CLOUD_RUN_SERVICE%-service}-${suffix}")' in workflow
133+
assert 'if len(time_fields) == 5:' in workflow
134+
assert 'print(" ".join(time_fields))' in workflow
135+
assert 'print(" ".join([*time_fields, *current_fields[2:]]))' in workflow
136+
assert 'gcloud scheduler jobs update http "${job_name}"' in workflow
137+
assert '--schedule="${desired_schedule}"' in workflow
138+
assert '--time-zone="${market_timezone}"' in workflow

0 commit comments

Comments
 (0)