From 71c19df360c0189c0bb11b86d73cd700cea190ac Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:01:40 +0800 Subject: [PATCH] clarify runtime route roles --- .github/workflows/invoke-cloud-run.yml | 18 ++++++----- main.py | 27 +++++++++++----- tests/test_request_handling.py | 43 +++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/.github/workflows/invoke-cloud-run.yml b/.github/workflows/invoke-cloud-run.yml index 4aed35b..b3d686d 100644 --- a/.github/workflows/invoke-cloud-run.yml +++ b/.github/workflows/invoke-cloud-run.yml @@ -87,8 +87,8 @@ jobs: if [[ "${raw_path}" != /* ]]; then raw_path="/${raw_path}" fi - if [ "${raw_path}" = "/" ] && [ "${{ inputs.allow_live_execution }}" != "true" ]; then - echo "Calling / can trigger the live execution entrypoint. Re-run with allow_live_execution=true if this is intentional." >&2 + if { [ "${raw_path}" = "/" ] || [ "${raw_path}" = "/run" ]; } && [ "${{ inputs.allow_live_execution }}" != "true" ]; then + echo "Calling ${raw_path} can trigger the live execution entrypoint. Re-run with allow_live_execution=true if this is intentional." >&2 exit 1 fi @@ -140,21 +140,25 @@ jobs: invoke_method="direct" scheduler_job="" scheduler_location="" + scheduler_expected_path="" if [ "${service_ingress}" = "internal" ]; then scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}" case "${raw_path}" in - /) + /|/run) scheduler_job="${CLOUD_RUN_SERVICE}-scheduler" + scheduler_expected_path="/" ;; /probe) scheduler_job="${CLOUD_RUN_SERVICE}-probe-scheduler" + scheduler_expected_path="/probe" ;; - /precheck) + /precheck|/dry-run) scheduler_job="${CLOUD_RUN_SERVICE}-precheck-scheduler" + scheduler_expected_path="/precheck" ;; *) echo "Cloud Run service ${CLOUD_RUN_SERVICE} has internal ingress, so GitHub-hosted runners cannot curl ${raw_path} directly." >&2 - echo "Use one of the scheduler-backed paths: /, /probe, /precheck." >&2 + echo "Use one of the scheduler-backed paths: /, /run, /probe, /precheck, /dry-run." >&2 exit 1 ;; esac @@ -179,7 +183,7 @@ jobs: print(normalize(urlparse(os.environ["SCHEDULER_URI"]).path)) PY )" - requested_path="$(RAW_PATH="${raw_path}" python3 - <<'PY' + requested_path="$(RAW_PATH="${scheduler_expected_path}" python3 - <<'PY' import os clean = (os.environ["RAW_PATH"] or "/").rstrip("/") @@ -187,7 +191,7 @@ jobs: PY )" if [ "${scheduler_path}" != "${requested_path}" ]; then - echo "Cloud Scheduler job ${scheduler_job} targets ${scheduler_uri}, not ${raw_path}." >&2 + echo "Cloud Scheduler job ${scheduler_job} targets ${scheduler_uri}, not ${scheduler_expected_path}." >&2 exit 1 fi invoke_method="scheduler" diff --git a/main.py b/main.py index cf4c356..47ed42c 100644 --- a/main.py +++ b/main.py @@ -601,14 +601,6 @@ def run_probe(*, response_body: str = "Probe OK"): composer = build_composer(dry_run_only_override=True) reporting_adapters = composer.build_reporting_adapters() log_context, report = reporting_adapters.start_run() - strategy_plugin_signals, strategy_plugin_error = composer.load_strategy_plugin_signals( - getattr(RUNTIME_SETTINGS, "strategy_plugin_mounts_json", None) - ) - composer.attach_strategy_plugin_report( - report, - signals=strategy_plugin_signals, - error=strategy_plugin_error, - ) reporting_adapters.log_event( log_context, "health_probe_received", @@ -681,6 +673,7 @@ def run_probe(*, response_body: str = "Probe OK"): @app.route("/", methods=["POST", "GET"]) +@app.route("/run", methods=["POST", "GET"]) def handle_trigger(): """Entrypoint for Cloud Run / scheduler: run strategy and return 200.""" return _route_with_runtime_error_fallback( @@ -715,6 +708,19 @@ def handle_precheck(): ) +@app.route("/dry-run", methods=["POST", "GET"]) +def handle_dry_run(): + """Strategy dry-run entrypoint; alias of precheck with clearer operator wording.""" + return _route_with_runtime_error_fallback( + run_strategy, + force_run=True, + validation_only=True, + validation_label="precheck", + success_body="Dry Run OK", + route_label="POST /dry-run", + ) + + @app.route("/probe", methods=["POST", "GET"]) def handle_probe(): """Post-open broker/account health probe; notify only on failure.""" @@ -725,5 +731,10 @@ def handle_probe(): ) +@app.route("/health", methods=["GET"]) +def health(): + return "OK", 200 + + if __name__ == "__main__": app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080))) diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 91f614a..0608653 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -204,6 +204,7 @@ def test_cloud_run_route_contracts_are_registered(self): module = load_module() self.assertIs(module.app._routes[("/", ("POST", "GET"))], module.handle_trigger) + self.assertIs(module.app._routes[("/run", ("POST", "GET"))], module.handle_trigger) self.assertIs( module.app._routes[("/backfill", ("POST", "GET"))], module.handle_backfill, @@ -212,10 +213,24 @@ def test_cloud_run_route_contracts_are_registered(self): module.app._routes[("/precheck", ("POST", "GET"))], module.handle_precheck, ) + self.assertIs( + module.app._routes[("/dry-run", ("POST", "GET"))], + module.handle_dry_run, + ) self.assertIs( module.app._routes[("/probe", ("POST", "GET"))], module.handle_probe, ) + self.assertIs(module.app._routes[("/health", ("GET",))], module.health) + + def test_health_route_returns_ok(self): + module = load_module() + + with module.app.test_request_context("/health", method="GET"): + body, status = module.health() + + self.assertEqual(status, 200) + self.assertEqual(body, "OK") def test_handle_trigger_runs_strategy(self): module = load_module() @@ -356,6 +371,26 @@ def fake_run_strategy(*, force_run=False, validation_only=False, validation_labe self.assertTrue(observed["validation_only"]) self.assertEqual(observed["validation_label"], "precheck") + def test_handle_dry_run_alias_forces_strategy_dry_run(self): + module = load_module() + observed = {"force_run": None, "validation_only": None} + + def fake_run_strategy(*, force_run=False, validation_only=False, validation_label="backfill"): + observed["force_run"] = force_run + observed["validation_only"] = validation_only + observed["validation_label"] = validation_label + + module.run_strategy = fake_run_strategy + + with module.app.test_request_context("/dry-run", method="POST"): + body, status = module.handle_dry_run() + + self.assertEqual(status, 200) + self.assertEqual(body, "Dry Run OK") + self.assertTrue(observed["force_run"]) + self.assertTrue(observed["validation_only"]) + self.assertEqual(observed["validation_label"], "precheck") + def test_handle_probe_checks_account_snapshot_without_success_notification(self): module = load_module() observed = {"override": None, "events": [], "notifications": []} @@ -391,10 +426,10 @@ def build_notification_adapters(self): raise AssertionError("probe success should stay silent") def load_strategy_plugin_signals(self, *_args, **_kwargs): - return (), None + raise AssertionError("health probe should not load strategy plugins") def attach_strategy_plugin_report(self, *_args, **_kwargs): - return None + raise AssertionError("health probe should not attach strategy plugin reports") module.build_composer = lambda *, dry_run_only_override=None: observed.__setitem__("override", dry_run_only_override) or FakeComposer() @@ -442,10 +477,10 @@ def build_notification_adapters(self): return FakeNotifications() def load_strategy_plugin_signals(self, *_args, **_kwargs): - return (), None + raise AssertionError("health probe should not load strategy plugins") def attach_strategy_plugin_report(self, *_args, **_kwargs): - return None + raise AssertionError("health probe should not attach strategy plugin reports") module.build_composer = lambda *, dry_run_only_override=None: FakeComposer()