Repository navigation
Remove the routes only the legacy site used, check health at /health, and stop serving Canada, Nigeria and Israel - #4017
Draft
anth-volk wants to merge 5 commits into
Conversation
Remove the v1 saved-policy routes (POST, GET and PUT
/{country_id}/user-policy), the user-profile routes (POST, GET and PUT
/{country_id}/user-profile), the policy search (GET /{country_id}/policies),
the recent-simulations list (GET /simulations), the GET / landing page and
GET /specification, in both the Flask application and the native FastAPI
routes. Cloud Run request logs for the 30 days to 2026-10-19 show no
saved-policy or user-profile requests and a few dozen manual or
legacy.policyengine.org requests to the others; GET / is polled by an
external uptime monitor that must move to /health before this merges.
POST /{country_id}/calculate stays: it served about 140,000 requests in the
same period.
Code only these routes called goes too: the home, user-profile and
recent-simulations blueprints, the v1 user-profile service, the policy
search and recent reform impacts service methods, the legacy OpenAPI
document and its loader, and the native route dependency that served it.
`ROUTE_IMPL_SPECIFICATION` is still read because the deployment pipeline
requires and verifies it, but it selects no route. The v1 saved-policy
service and copying modules stay until dual-write is deleted.
The app-v2 contract registry drops the removed requests, the generated
migration contract artifacts are regenerated, the Cloud Run candidate check
asserts the removed paths return 404, the session-ownership rule's route
check covers every Flask route module, and the SPM guide no longer points
at the removed specification document.
Tests (failed first: the removed routes were still registered in the Flask
blueprints and assembled by the app module, and the ASGI app still served
/specification): the route boundary tests, the ASGI factory, request
lifecycle, contract and runtime-cache tests (217 passed); the unit,
contract and to-refactor suites (3,297 passed once the log tests below
were fixed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iness checks
`/health` now answers 503 `{"status": "not_ready"}` until the readiness
probe passes, that is until the startup warmup has finished and while the
runtime configuration is invalid, and 200 `{"status": "healthy"}` after. It
takes over the behaviour of `/readiness-check`, which held traffic off a
booting instance and stopped a deployment whose configuration could not
serve requests.
The Cloud Run startup probe that deploy_cloud_run_candidate.sh sets (the
default in cloud_run_env.sh) and the seven health checks in push.yml (staging
and production candidates, stable URLs, and the revision restores) now poll
`/health`. Every earlier revision also serves `/health` and returns 200 once
up, so restoring a previous revision still passes its health check. This
repository configures no load balancer health check: the public load
balancer's backend is a serverless network endpoint group, which does not
use one.
`/liveness-check` and `/readiness-check` are removed from the Flask
application and the native routes. `ROUTE_IMPL_HEALTH` no longer selects a
route but stays required by the deployment pipeline until the Flask fallback
is removed. The migration baseline probes, the runtime measurement script
and the local boot smoke test (which now starts the ASGI app and checks that
`/health` reports ready) use `/health`, the Cloud Run candidate check
asserts the removed paths return 404, a test requires every push workflow
health check to poll `/health`, and the operations, SPM and runbook
documents name the new path.
Tests (failed first: 26 across the ASGI factory, deploy scripts, push
workflow, boot smoke test, runtime measurement, request lifecycle, route
boundaries and baseline capture, because `/health` ignored readiness, the
probes and workflow still used the removed paths, and the routes were
still registered): then 770 passed in those suites and the routes,
contract and to-refactor API tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v1 routes' country list (`COUNTRIES` in policyengine_api/constants.py),
the package-name lists beside it and the loaded country models
(policyengine_api/country.py) now derive from the one country list in
policyengine_api/supported_countries.py, so they hold the US and the UK.
Canada, Nigeria and Israel lose their economy options and current-law
policy IDs (policyengine_api/country.py and
policyengine_api/utils/get_current_law.py), and `policyengine_canada`,
`policyengine-ng` and `policyengine-il` leave pyproject.toml. uv.lock is
regenerated: it loses the three packages and the thirteen packages only they
needed, and nothing else changes.
Requests for the three countries now get the shared unknown-country 400
response ("Available countries are: uk, us") before any read or write, and
the API process no longer loads their tax-benefit systems. Cloud Run request
logs for the 30 days to 2026-10-19 show about 1,160 requests for them, 784
with a legacy.policyengine.org referer and 49 from app.policyengine.org,
which the app change removing them stops. Existing Cloud SQL rows for them
are kept.
Tests (failed first: 17, because the lists, dependencies and current-law
IDs still named the three countries, their requests reached the services,
and a country added to the one list did not reach the v1 lists): the served
list, dependencies and lockfile, the unknown-country response, the
rejection of their policy and household writes before any write, the
loaded models matching the served list, and, in a fresh interpreter, a
country added to the one list reaching every v1 list. The unit, contract
and to-refactor suites pass (3,304).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups (no bugs found): - `/health` was a GET route, so a HEAD request fell through to the Flask application and got a 404, while the removed Flask liveness check answered HEAD with 200. Uptime monitors and `curl -I` (used by two runbooks) send HEAD, so `/health` now answers it too, with the same status as GET and no body. - The metadata document takes its current-law policy ID from `get_current_law_policy_id` instead of a second copy of the table. - The country list's docstring says that API v1 loads each listed country's package and needs its current-law ID. - Comments say why the route-group tables still name removed paths: so requests that still arrive for them are labelled in the request logs. Tests (failed first: HEAD /health answered 404 whether or not the app was ready): HEAD /health answers 200 when ready and 503 before, with no body; the ASGI factory, request lifecycle, country, metadata, migration-flag, route, contract and served-country tests pass (633). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI found that dropping the Canada package also dropped `coverage` from the environment `uv sync --frozen` installs: `policyengine-canada` was the only default dependency that pulled it in. The cross-database integration workflow runs every test step under `uv run coverage`, so its first test step failed with "Failed to spawn: coverage". The workflow now installs the dev extra, which declares `coverage` and `pytest`. No other workflow runs `coverage` from a `uv sync` environment; the jobs that run `make test` install the dev extra through `make install`. Tests (failed first: the workflow test required `uv sync --frozen --extra dev`): the workflow tests pass, and the quality guards and rules guard pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
Contributor
|
Review at Verdict: the change itself looks correct; it is ready once the four operator steps in the description are done and #4012 has merged. Stacked on #4012, so I reviewed only
Checked and fine:
What I ran: fresh clone, 🤖 Generated with Claude Code |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4016
Do not merge yet. This PR may merge only after legacy.policyengine.org is no longer served, after Cloud Run request logs show no remaining traffic to the routes and countries it removes, and after the external uptime monitor that polls
GET /is moved to/health. It is stacked on #4012.Summary
POST,GET,PUT /{country_id}/user-policy), the user-profile routes (POST,GET,PUT /{country_id}/user-profile), the policy search (GET /{country_id}/policies), the recent-simulations list (GET /simulations), theGET /landing page andGET /specificationwith its hand-written OpenAPI document. Code only they called goes too.POST /{country_id}/calculatestays: it served about 140,000 requests in 30 days, and its callers move first. The v1 saved-policy service and copying modules stay until dual-write is deleted./health./healthanswers 503{"status": "not_ready"}until the readiness probe passes (startup warmup finished, runtime configuration valid) and 200{"status": "healthy"}after, taking over from/readiness-check. The Cloud Run startup probe and the seven health checks inpush.yml(candidates, stable URLs and revision restores) poll it; a previous revision also answers 200 on/healthonce up, so a restore still passes its check./liveness-checkand/readiness-checkare removed. This repository configures no load balancer health check: the public load balancer's backend is a serverless network endpoint group, which does not use one.uv.lockloses them and 13 packages only they needed).COUNTRIES, the package-name lists inconstants.pyand the loaded country models now derive from the one country list inpolicyengine_api/supported_countries.py.Request logs (production, 30 days to 2026-10-19):
user-policyanduser-profile0;GET /{country_id}/policies13;GET /simulations11;GET /about 24,300 (an uptime monitor and link previews); the three countries about 1,160 (784 from legacy.policyengine.org, 49 from app.policyengine.org, which PolicyEngine/policyengine-app-v2#1390 stops).Migration notes
health,specification,policy(saved policies and search),user_profile(v1 profile routes),metadata(country list).POST /{country_id}/calculate./healthanswers 503 while an instance is not ready.ROUTE_IMPL_HEALTHandROUTE_IMPL_SPECIFICATIONno longer select a route but stay required by the deployment pipeline until the Flask fallback is removed.Rules
The session-ownership rule's enforcer now names
test_flask_routes_do_not_manage_sessions(the check covers every Flask route module, instead of only the removed recent-simulations module). This changesrules/registry.toml, so it needs a review from a core developer other than the author.Inline suppressions
None added.
Environment variables
CLOUD_RUN_STARTUP_PROBE) keeps its existing provider: the default in.github/scripts/cloud_run_env.sh, which no workflow overrides; only the probed path changes.deploy_cloud_run_candidate.shpasses it togcloud run deployas before.push.ymlchange only the URL path they poll; their inputs (steps.candidate.outputs.url, the stable URLs and the revisions to restore) come from the same earlier steps as before./healthnow reads the same readiness inputs/readiness-checkread (policyengine_api.readiness.validate_policy_runtime_configuration: the v1 source selectors, the v2 runtime database settings, user authentication and Auth0 management settings, and the SPM selection), all from the existing Cloud Run environment set bydeploy_cloud_run_candidate.sh. Staging and production already pass this check on every deploy through/readiness-check. Not verified beyond that: staging has no Auth0 management settings while production does; the check accepted that configuration on/readiness-check, and/healthuses the same function.uv sync --frozen --extra dev; it reads no new variables (its database URLs are literals for its own service containers).uv sync --frozen --no-dev, which never included the dev tools.What this PR waits for
GET /to/health(an operator step), and any other external monitor or probe that polls/liveness-checkor/readiness-checkmoved too.Testing
Each commit was written test-first:
6426607b0(route removal): 4 tests failed first (the removed routes were still registered in the Flask blueprints and assembled by the app module, and the ASGI app still served/specification); then 217 passed in the route boundary, ASGI factory, request lifecycle, contract and runtime-cache tests.b6f75f19c(health checks): 26 tests failed first across the ASGI factory, deploy scripts, push workflow, boot smoke test, runtime measurement, request lifecycle, route boundaries and baseline capture; then 770 passed in those suites and the routes, contract and to-refactor API tests.df07a5c3d(countries): 17 tests failed first (lists, dependencies and current-law IDs still named the three countries, their writes reached the services, and a country added to the one list did not reach the v1 lists); then the unit, contract and to-refactor suites passed (3,304).8e4b2d6e9(review follow-up): 2 tests failed first (HEAD/healthanswered 404); then 633 passed in the affected suites.379885d41(CI follow-up): droppingpolicyengine-canadaalso droppedcoveragefrom the environmentuv sync --frozeninstalls (it was the only default dependency that pulled it in), so the cross-database integration workflow failed to start its tests. The workflow now installs the dev extra; its workflow test failed first, then passed.tests/integration/test_v2_country_routes.py,test_v2_metadata_routes.py,test_v1_household_dual_write.pyandtest_v1_policy_dual_write.pypass (23).ruff format --check .,ruff check .,uv run --frozen --extra dev mypy,python scripts/run_quality_guards.py,python -m scripts.rules guard --base-ref origin/master;uv lock --checkpasses after the lock change.One review round found no bugs; its follow-ups are the last commit.
🤖 Generated with Claude Code