Repository navigation
Conversation
MaxGhenis
added a commit
that referenced
this pull request
Oct 11, 2026
Review findings on #743: - H1: /versions route maps hold versions as latest and app names under each version key. The smoke now checks the release's own route and UK bundle identity, that its UK model routes to the same app, its dataset digest, and, with EXPECTED_LATEST=true, that latest routes to that version and model. Its test fixture is now the gateway's real /versions output, kept current by a gateway differential test. - M1: one historical bundle with an unresolvable dataset is reported without a dataset instead of failing /versions. - M2: tests pin the EXPECTED_* wiring, prepare's dataset output and the updater's RELEASE_PAYLOAD. - M3: verify-dispatch fails closed when the installed bundle cannot report a field the dispatch names (except the certificate digest), and reports dispatch_verified only when something was compared. - policyengine.py sends data_release_version as the data package version; compare it with a new uk_data_package_version output (the record's build-style version never matched). A differential test derives a real dispatch from the locked bundle and verifies it. - Qualification fields are validated; a pair marked not promotable, or with a verdict that is never promoted, is refused. Digests compare case-insensitively and malformed bundle digests are refused. - Docs: /versions reports the registry's identity for each bundle; the numerical sentinel (#744) checks what the worker computes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
w4/sentinel-rollback
branch
from
October 11, 2026 22:02
87ade7a to
adc244f
Compare
- release_sentinel computes UK sentinel values in-process with the exact bundle being deployed (run_simulation_impl, the worker's own implementation, on the payload the gateway forwards) and checks that the candidate and then the stable service reproduce them, with the deployed policyengine version, UK model and dataset SHA-256. - update_routing records latest before and after publishing; revert_routing restores the previous latest and withdraws the failed release when any later gate fails or the run is cancelled, conditional on latest still being that release. - update_version_registry gains --print-latest and --restore-latest with --expected-latest and --withdraw; publish then rollback returns routing to its prior state (property-tested). - Revision cleanup keeps the previous serving revision as a rollback target. - simulation-rollback.yml restores a published release (and optionally a retained Cloud Run revision) on demand and verifies the served identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update_routing records whether this release should now be latest (newer than the previous latest, or forced) and fails if publishing should have advanced latest but did not. The deployed-stack and stable-service smokes require latest to route to the release only then, so an intentional older-version deploy still verifies its own route and identity. The rollback workflow requires latest to route to the restored release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- update_routing writes a routing plan (the state before and after publishing, with digests) and keeps it as an artifact before it changes anything, then applies it only onto its own starting state. - revert_routing restores the plan's starting state when routing failed after planning, any later gate failed, or the run was cancelled. It acts only while the live state is still what this release published, and a plan that was never applied restores nothing. First publications, older releases that do not move latest, and every route the release touched are undone exactly; nothing is inferred from remaining manifests. - --withdraw and predecessor guessing are gone. The manual rollback restores a deployment's kept plan by run id, or points latest at a published version, then verifies the whole identity latest routes to. - The deploy smoke accepts EXPECTED_UK_DATASET_SHA256=unrecorded for releases published before digests were recorded, requiring none served. - The sentinel fails a complete job whose result is not an object. - Tests evaluate the revert condition and EXPECTED_LATEST as GitHub expressions, parse the workflows, and kill the surviving mutants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
w4/sentinel-rollback
branch
from
October 12, 2026 03:18
adc244f to
97a0412
Compare
…ithfully - Every writer of the routing state (apply, restore, restore latest, publish, legacy seed) holds a write lock taken with Modal's atomic put(skip_if_exists=True), so a check is never stale by the time its write lands. A lock left by a writer that died expires after 15 minutes and is broken by the next writer. - The manual rollback passes every run attempt's plan, oldest first, and the restore undoes each attempt's change that is still live, newest first, skipping plans that changed nothing, so a failed retry cannot hide the publication it followed. - The workflow tests evaluate conditions with GitHub's semantics (an implicit success() when no status function is named, type coercion in ==), cover cancellation, run both sentinel steps against a failing stub, and pin Stage 12-only promotion and a lagging latest wrapper. - Sentinel values that are booleans are refused and tested; the gateway skips blank or non-string model versions; bundle-versions passes the receipt certificate digest through unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Lock acquisition retries the atomic insert when the lock is absent and never deletes a lock it did not read as expired. An expired lock is reclaimed by a single waiter (an atomic reclaim token per stale owner), and a lock that changed hands is put straight back; release never removes someone else's lock. The lease is now 300 seconds. - Each routing plan names its publication (run id, attempt and environment) in the published generation, so a later publication of the same release is a different state and an old plan cannot withdraw it. - The automatic restore and the manual rollback wait 420 seconds for the lock, past its expiry, so a publisher that died holding it never blocks them. - The manual rollback requires the plan of every attempt whose routing job kept one, and refuses an incomplete set instead of stopping short. - Tests: the lock race, single-winner reclaim, release safety, a healthy republication, a held lock on apply, the restore step failing, the plan inventory, and the sentinel's tolerance boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four review rounds reproduced races in the routing write lock. modal.Dict offers insert-if-absent but no compare-and-delete, so no lock on it is both safe and live: an expired lock cannot be reclaimed safely, and an unreclaimable one blocks restores after a writer dies. Every workflow that writes routing state already runs in the repository-wide concurrency group simulation-deploy-main with cancel-in-progress false, which runs one at a time across workflows. So: - The lock, reclaim tokens, TTL and wait constants and --lock-wait-seconds are gone. - The CLI refuses any routing write unless it runs in GitHub Actions in PolicyEngine/policyengine-sim-api from simulation-deploy.yml, stage12-deploy.yml or simulation-rollback.yml (GITHUB_WORKFLOW_REF names the caller of a reusable workflow). Reads, plans and dry runs stay local. - Invariant tests: every writer, direct or through the reusable workflow, declares that exact concurrency group; the guard's list equals the writers and the group's members; writers that share a Modal environment are ordered (stage12's parallel jobs write staging and main). - Round-4 Lows: real jq selectors, sentinel default dependencies and both read-backs are tested. 14 of 14 mutants are killed. Co-Authored-By: GPT-6.1 Sol via Subfleet <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been 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.
Draft, stacked on #743 (base
w4/release-identity-sentinel). Part of W4 in the UK Autumn Budget 2026 CI plan: production readiness means the expected/versionsidentity plus a numerical HTTP sentinel, with a tested rollback.What changes
Numerical release sentinel (
src/modal/utils/release_sentinel.py)sentinel_expectedjob, which runs in parallel with the service deploys. It runs the sentinel request in-process throughrun_simulation_impl, the worker's own implementation. It uses the exact bundle this deployment installs and the payload the gateway forwards (worker_payloadis the gateway'smodel_dump). It then checks that the result names the deployed policyengine version, UK model and dataset SHA-256.integrationjob checks that the candidate URL reproduces those numbers;promote_entrypointchecks the stable URL. In both, the request is pinned to the deployed policyengine version. They compare the served bundle identity andbudget.{budgetary_impact, tax_revenue_impact, benefit_spending_impact, baseline_net_income}, at relative 1e-9 / absolute £0.01.PE_UK_PRIVATE_HF_READ_TOKENandGCP_CREDENTIALS_JSON(the constituency lookup is read from GCS). The environments already hold both.Routing revert on failure
update_routingrecordslatest.policyenginebefore and after publishing, and whether this release should now be latest (newer than the previous latest, or forced). It fails if publishing should have advanced latest but did not.latestto route to the release only when it should advance it, so an intentional older-version deploy still verifies its own route and identity.revert_routingjob: if integration, the authenticated tests or the requested promotion fail (or the run is cancelled), it restores the previous latest and withdraws the failed release. The restore applies only if latest is still that release, so it never overwrites a newer one.update_version_registrygains--print-latestand--restore-latestwith--expected-latestand--withdraw. Withdrawn country routes go back to the newest remaining bundle with that model version, or are removed.Rollback target and workflow
SIMULATION_ENTRYPOINT_ROLLBACK_REVISION), so there is something to roll back to.simulation-rollback.yml(workflow_dispatch). Its inputs are passed through the environment and validated before anything changes. It restores a published release, optionally withdraws one and moves Cloud Run traffic to a retained revision, then runs the identity smoke against the stable URL. It shares the deployments' concurrency group.Invariants (tested)
Tests
pr.yml.hypothesisis added to the executor's test extra (lock: one package added).Review
Independent review (GPT-6.1 Sol) is running at adc244f.
Still to do
sentinel_expectedand the candidate check. Then a manualsimulation-rollback.ymlrun on beta restores the previous release, checks the served identity, and re-promotes. I'll record the run links here.budget.householdssumshousehold_weightthrough the weighted-sum helper, giving about 8.3e11 for the UK;🤖 Generated with Claude Code