Skip to content

Commit 38d881e

Browse files
authored
clarify runtime route roles (#174)
1 parent 07862bb commit 38d881e

3 files changed

Lines changed: 69 additions & 19 deletions

File tree

.github/workflows/invoke-cloud-run.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ jobs:
8787
if [[ "${raw_path}" != /* ]]; then
8888
raw_path="/${raw_path}"
8989
fi
90-
if [ "${raw_path}" = "/" ] && [ "${{ inputs.allow_live_execution }}" != "true" ]; then
91-
echo "Calling / can trigger the live execution entrypoint. Re-run with allow_live_execution=true if this is intentional." >&2
90+
if { [ "${raw_path}" = "/" ] || [ "${raw_path}" = "/run" ]; } && [ "${{ inputs.allow_live_execution }}" != "true" ]; then
91+
echo "Calling ${raw_path} can trigger the live execution entrypoint. Re-run with allow_live_execution=true if this is intentional." >&2
9292
exit 1
9393
fi
9494
@@ -140,21 +140,25 @@ jobs:
140140
invoke_method="direct"
141141
scheduler_job=""
142142
scheduler_location=""
143+
scheduler_expected_path=""
143144
if [ "${service_ingress}" = "internal" ]; then
144145
scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}"
145146
case "${raw_path}" in
146-
/)
147+
/|/run)
147148
scheduler_job="${CLOUD_RUN_SERVICE}-scheduler"
149+
scheduler_expected_path="/"
148150
;;
149151
/probe)
150152
scheduler_job="${CLOUD_RUN_SERVICE}-probe-scheduler"
153+
scheduler_expected_path="/probe"
151154
;;
152-
/precheck)
155+
/precheck|/dry-run)
153156
scheduler_job="${CLOUD_RUN_SERVICE}-precheck-scheduler"
157+
scheduler_expected_path="/precheck"
154158
;;
155159
*)
156160
echo "Cloud Run service ${CLOUD_RUN_SERVICE} has internal ingress, so GitHub-hosted runners cannot curl ${raw_path} directly." >&2
157-
echo "Use one of the scheduler-backed paths: /, /probe, /precheck." >&2
161+
echo "Use one of the scheduler-backed paths: /, /run, /probe, /precheck, /dry-run." >&2
158162
exit 1
159163
;;
160164
esac
@@ -179,15 +183,15 @@ jobs:
179183
print(normalize(urlparse(os.environ["SCHEDULER_URI"]).path))
180184
PY
181185
)"
182-
requested_path="$(RAW_PATH="${raw_path}" python3 - <<'PY'
186+
requested_path="$(RAW_PATH="${scheduler_expected_path}" python3 - <<'PY'
183187
import os
184188
185189
clean = (os.environ["RAW_PATH"] or "/").rstrip("/")
186190
print(clean or "/")
187191
PY
188192
)"
189193
if [ "${scheduler_path}" != "${requested_path}" ]; then
190-
echo "Cloud Scheduler job ${scheduler_job} targets ${scheduler_uri}, not ${raw_path}." >&2
194+
echo "Cloud Scheduler job ${scheduler_job} targets ${scheduler_uri}, not ${scheduler_expected_path}." >&2
191195
exit 1
192196
fi
193197
invoke_method="scheduler"

main.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,6 @@ def run_probe(*, response_body: str = "Probe OK"):
601601
composer = build_composer(dry_run_only_override=True)
602602
reporting_adapters = composer.build_reporting_adapters()
603603
log_context, report = reporting_adapters.start_run()
604-
strategy_plugin_signals, strategy_plugin_error = composer.load_strategy_plugin_signals(
605-
getattr(RUNTIME_SETTINGS, "strategy_plugin_mounts_json", None)
606-
)
607-
composer.attach_strategy_plugin_report(
608-
report,
609-
signals=strategy_plugin_signals,
610-
error=strategy_plugin_error,
611-
)
612604
reporting_adapters.log_event(
613605
log_context,
614606
"health_probe_received",
@@ -681,6 +673,7 @@ def run_probe(*, response_body: str = "Probe OK"):
681673

682674

683675
@app.route("/", methods=["POST", "GET"])
676+
@app.route("/run", methods=["POST", "GET"])
684677
def handle_trigger():
685678
"""Entrypoint for Cloud Run / scheduler: run strategy and return 200."""
686679
return _route_with_runtime_error_fallback(
@@ -715,6 +708,19 @@ def handle_precheck():
715708
)
716709

717710

711+
@app.route("/dry-run", methods=["POST", "GET"])
712+
def handle_dry_run():
713+
"""Strategy dry-run entrypoint; alias of precheck with clearer operator wording."""
714+
return _route_with_runtime_error_fallback(
715+
run_strategy,
716+
force_run=True,
717+
validation_only=True,
718+
validation_label="precheck",
719+
success_body="Dry Run OK",
720+
route_label="POST /dry-run",
721+
)
722+
723+
718724
@app.route("/probe", methods=["POST", "GET"])
719725
def handle_probe():
720726
"""Post-open broker/account health probe; notify only on failure."""
@@ -725,5 +731,10 @@ def handle_probe():
725731
)
726732

727733

734+
@app.route("/health", methods=["GET"])
735+
def health():
736+
return "OK", 200
737+
738+
728739
if __name__ == "__main__":
729740
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

tests/test_request_handling.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def test_cloud_run_route_contracts_are_registered(self):
204204
module = load_module()
205205

206206
self.assertIs(module.app._routes[("/", ("POST", "GET"))], module.handle_trigger)
207+
self.assertIs(module.app._routes[("/run", ("POST", "GET"))], module.handle_trigger)
207208
self.assertIs(
208209
module.app._routes[("/backfill", ("POST", "GET"))],
209210
module.handle_backfill,
@@ -212,10 +213,24 @@ def test_cloud_run_route_contracts_are_registered(self):
212213
module.app._routes[("/precheck", ("POST", "GET"))],
213214
module.handle_precheck,
214215
)
216+
self.assertIs(
217+
module.app._routes[("/dry-run", ("POST", "GET"))],
218+
module.handle_dry_run,
219+
)
215220
self.assertIs(
216221
module.app._routes[("/probe", ("POST", "GET"))],
217222
module.handle_probe,
218223
)
224+
self.assertIs(module.app._routes[("/health", ("GET",))], module.health)
225+
226+
def test_health_route_returns_ok(self):
227+
module = load_module()
228+
229+
with module.app.test_request_context("/health", method="GET"):
230+
body, status = module.health()
231+
232+
self.assertEqual(status, 200)
233+
self.assertEqual(body, "OK")
219234

220235
def test_handle_trigger_runs_strategy(self):
221236
module = load_module()
@@ -356,6 +371,26 @@ def fake_run_strategy(*, force_run=False, validation_only=False, validation_labe
356371
self.assertTrue(observed["validation_only"])
357372
self.assertEqual(observed["validation_label"], "precheck")
358373

374+
def test_handle_dry_run_alias_forces_strategy_dry_run(self):
375+
module = load_module()
376+
observed = {"force_run": None, "validation_only": None}
377+
378+
def fake_run_strategy(*, force_run=False, validation_only=False, validation_label="backfill"):
379+
observed["force_run"] = force_run
380+
observed["validation_only"] = validation_only
381+
observed["validation_label"] = validation_label
382+
383+
module.run_strategy = fake_run_strategy
384+
385+
with module.app.test_request_context("/dry-run", method="POST"):
386+
body, status = module.handle_dry_run()
387+
388+
self.assertEqual(status, 200)
389+
self.assertEqual(body, "Dry Run OK")
390+
self.assertTrue(observed["force_run"])
391+
self.assertTrue(observed["validation_only"])
392+
self.assertEqual(observed["validation_label"], "precheck")
393+
359394
def test_handle_probe_checks_account_snapshot_without_success_notification(self):
360395
module = load_module()
361396
observed = {"override": None, "events": [], "notifications": []}
@@ -391,10 +426,10 @@ def build_notification_adapters(self):
391426
raise AssertionError("probe success should stay silent")
392427

393428
def load_strategy_plugin_signals(self, *_args, **_kwargs):
394-
return (), None
429+
raise AssertionError("health probe should not load strategy plugins")
395430

396431
def attach_strategy_plugin_report(self, *_args, **_kwargs):
397-
return None
432+
raise AssertionError("health probe should not attach strategy plugin reports")
398433

399434
module.build_composer = lambda *, dry_run_only_override=None: observed.__setitem__("override", dry_run_only_override) or FakeComposer()
400435

@@ -442,10 +477,10 @@ def build_notification_adapters(self):
442477
return FakeNotifications()
443478

444479
def load_strategy_plugin_signals(self, *_args, **_kwargs):
445-
return (), None
480+
raise AssertionError("health probe should not load strategy plugins")
446481

447482
def attach_strategy_plugin_report(self, *_args, **_kwargs):
448-
return None
483+
raise AssertionError("health probe should not attach strategy plugin reports")
449484

450485
module.build_composer = lambda *, dry_run_only_override=None: FakeComposer()
451486

0 commit comments

Comments
 (0)