Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
763e66f
Promote each staging deployment's tested entrypoint revision
anth-volk Oct 21, 2026
2b7cde8
Document that a manual staging run from another branch promotes that …
anth-volk Oct 21, 2026
e9e8de9
Document that production waits for the staging revision clean-up
anth-volk Oct 21, 2026
4f34bf0
State in the entrypoint deployment guide that production waits for th…
anth-volk Oct 22, 2026
edaf5f1
Add a result delivery field to the report invocation context
anth-volk Oct 21, 2026
2f6b605
Keep the Stage 12 worker payload unchanged by the delivery field
anth-volk Oct 21, 2026
2ea6342
Add the typed result and error a returned report delivers
anth-volk Oct 21, 2026
38ad880
Let report simulations return their artifact without database rows
anth-volk Oct 21, 2026
aa35c9b
Add the report coordinator's return mode
anth-volk Oct 21, 2026
23703dd
Route the Modal report coordinator by result delivery
anth-volk Oct 21, 2026
73289fd
Cancel a segmented worker's segment calls when Modal stops the worker
anth-volk Oct 22, 2026
2e39d36
Stop a returned report's workers when Modal cancels its coordinator
anth-volk Oct 22, 2026
0ccf58e
Test the Modal boundary of a returned report's error on its own
anth-volk Oct 22, 2026
c8a1b0d
Describe both report coordinators in the coordination module's docstr…
anth-volk Oct 22, 2026
82aa1b7
Leave no started segment call uncancelled when Modal stops the worker
anth-volk Oct 22, 2026
3941606
Say that a segmented worker cancelled through Modal stops its segments
anth-volk Oct 22, 2026
7d11660
Test each way a returned report's aggregate can describe another report
anth-volk Oct 22, 2026
4fdd320
Keep a started worker's call before its dispatch span closes
anth-volk Oct 22, 2026
887baa0
Label a returned report's worker operations in telemetry
anth-volk Oct 22, 2026
9c8504e
Pickle a report execution error's failure as plain JSON values
anth-volk Oct 22, 2026
dc406df
Name a returned simulation's output by its digest, so a restart can s…
anth-volk Oct 26, 2026
52f9be4
Name a returned simulation's input by its digest as well
anth-volk Oct 26, 2026
a915e34
Merge the typed coordinator result into the v2 economy path
anth-volk Oct 27, 2026
41a0770
Define the Modal calls of v2 economy simulation runs and comparisons
anth-volk Oct 27, 2026
6903754
Scope a v2 economy run with its catalog filters, not the region registry
anth-volk Oct 27, 2026
2dd1b7d
Calculate a v2 economy simulation run in its own call and name its files
anth-volk Oct 27, 2026
5705be8
Compare two stored economy simulation outputs in a report run's own call
anth-volk Oct 27, 2026
f74cf36
Declare the v2 economy functions sim starts, each reporting its end
anth-volk Oct 27, 2026
f2ba698
Read the catalog and policy rows a v2 economy run is resolved from
anth-volk Oct 27, 2026
f93dab3
Find runs, comparisons and stored files the economy path starts from
anth-volk Oct 27, 2026
c5f0689
Record finished economy calls: a run's stored files, a comparison's rows
anth-volk Oct 27, 2026
73f0e32
Submit and re-run economy reports: start each run's call, then the co…
anth-volk Oct 27, 2026
4da8c9a
Serve v2 economy reports: submit, read, re-run, list runs, look up
anth-volk Oct 27, 2026
992fca6
Start economy work left waiting from the safety check
anth-volk Oct 27, 2026
7b8df13
Run a small economy report on staging after each staging promotion
anth-volk Oct 27, 2026
aba1e69
Describe how the entrypoint runs v2 economy reports and what it needs
anth-volk Oct 27, 2026
b65541e
Merge staging promotion into the v2 economy path
anth-volk Oct 27, 2026
51805e8
Check that every staging deployment promotes, so the economy check runs
anth-volk Oct 27, 2026
757a945
Refuse a region below the country that has no household filter
anth-volk Oct 27, 2026
ebc4a34
Keep the labour supply columns of a pending run another report reuses
anth-volk Oct 27, 2026
8843a28
Keep the catalog region module free of the main path's types
anth-volk Oct 27, 2026
cc65d02
Move the economy path's requests, errors and store protocols to their…
anth-volk Oct 27, 2026
4daaaf8
Resume each waiting economy run or comparison on its own
anth-volk Oct 27, 2026
1a6442d
Read the safety check's settings from the environment it is given
anth-volk Oct 27, 2026
dba16b3
Answer 404 for a household report on the economy report routes
anth-volk Oct 27, 2026
a006656
Start only the runs a submission created; leave reused pending runs
anth-volk Oct 27, 2026
59ed909
Refuse catalog rows a call cannot carry before writing anything
anth-volk Oct 27, 2026
7d563c7
Read a policy's period dates in UTC, whatever the session's time zone
anth-volk Oct 27, 2026
f73299c
Calculate the staging report once per deployment
anth-volk Oct 27, 2026
852f3f7
Compare the Public API's region types as text, as its enum column needs
anth-volk Oct 27, 2026
b64dfb9
Give a forced run start an attempt of its own when it loses the insert
anth-volk Oct 27, 2026
20e1d6f
Declare the economy routes' 404, 422 and 503 answers in the OpenAPI d…
anth-volk Oct 27, 2026
b8eed8e
Answer a stored result that cannot be rebuilt with a declared 500
anth-volk Oct 27, 2026
67eb9f3
Reuse a report's newest run on a system run, so first opens start one
anth-volk Oct 27, 2026
1d029bd
Describe the staging check's calls and the waits of reused pending runs
anth-volk Oct 27, 2026
01bd24e
Leave a losing first open's unused runs pending, not failed
anth-volk Oct 28, 2026
0e30e2c
Describe forced starts and first report runs, and test the forced-sta…
anth-volk Oct 28, 2026
626c792
Compare the test copies of the Public API's tables with its real schema
anth-volk Oct 28, 2026
9d00535
Run the Public API schema comparison in CI, against its default branch
anth-volk Oct 28, 2026
54eae0a
Report an enum the Public API lacks, and test each kind of difference
anth-volk Oct 28, 2026
50a9efe
Name the Public API commit a comparison used, and tidy the schema check
anth-volk Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/scripts/deploy-cloud-run-simulation-safety-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
# recording pass per execution. It is updated only from a revision that was
# promoted (or restored by a rollback), so a rejected candidate never records
# anything. It copies the revision's image digest, service account, Modal
# token and database URL (at the secret versions the revision pins) and its
# plain telemetry settings; nothing else. A revision deployed before the
# token and database URL (at the secret versions the revision pins), its
# plain telemetry settings, and the Modal environment it starts the economy
# calls left waiting in; nothing else. A revision deployed before the
# safety check existed has no job code, so its schedule is paused instead.
#
# With the argument `pause`, only pause the schedule: a workflow does this
Expand All @@ -33,7 +34,7 @@ pause_schedule() {
# policyengine_simulation_entry.run_recording.
SCHEDULE="*/5 * * * *"
SECRETS=(SIMULATION_DATABASE_URL MODAL_TOKEN_ID MODAL_TOKEN_SECRET)
PLAIN_VARIABLES_FILTER='^(APP_ENVIRONMENT|CLOUD_RUN_REGION|OBSERVABILITY_[A-Z_]+|OTEL_[A-Z_]+|POLICYENGINE_OTEL_GOOGLE_AUDIENCE)$'
PLAIN_VARIABLES_FILTER='^(APP_ENVIRONMENT|CLOUD_RUN_REGION|SIMULATION_CALL_REPORT_MODAL_ENVIRONMENT|OBSERVABILITY_[A-Z_]+|OTEL_[A-Z_]+|POLICYENGINE_OTEL_GOOGLE_AUDIENCE)$'

for variable_name in PROJECT_ID REGION SERVICE; do
[[ -n "${!variable_name:-}" ]] || fail "${variable_name} is required"
Expand Down
30 changes: 30 additions & 0 deletions .github/scripts/public-api-schema-reference.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Build the Public API's tables with its own v2 migration chain, for the
# comparison with the persistence tests' copies of them (rule
# sim.persistence.public-api-copies-match).
#
# Usage: public-api-schema-reference.sh <Public API checkout>
#
# PUBLIC_API_SCHEMA_DATABASE_URL names the empty database to migrate, as
# postgresql://user:password@host:port/database. The Public API's disposable
# migration mode accepts only the local policyengine_v2_alembic_test
# database, with explicit credentials. The script prints the Public API
# commit it migrated and, on GitHub Actions, passes it to the later steps as
# PUBLIC_API_SCHEMA_COMMIT, so a failed comparison names it.
set -euo pipefail

checkout="${1:?usage: public-api-schema-reference.sh <Public API checkout>}"
database_url="${PUBLIC_API_SCHEMA_DATABASE_URL:?PUBLIC_API_SCHEMA_DATABASE_URL must name the database to migrate}"

cd "$checkout"
commit="$(git rev-parse HEAD)"
echo "Public API commit: $commit"
if [ -n "${GITHUB_ENV:-}" ]; then
echo "PUBLIC_API_SCHEMA_COMMIT=$commit" >> "$GITHUB_ENV"
fi
# The Public API installs its lock on Python 3.12, as its own CI does.
export UV_PYTHON=3.12
uv sync --frozen --extra dev
V2_MIGRATION_DATABASE_URL="postgresql+psycopg://${database_url#*://}" \
V2_ALEMBIC_DISPOSABLE_TEST=1 \
uv run --frozen --extra dev alembic -c alembic-v2.ini upgrade head
72 changes: 71 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
# every job passed. See docs/engineering/skills/github-prs.md.
name: CI complete
if: ${{ always() }}
needs: [test, simulation-database, lint, pyright, quality-guards, docker-build, test-integration]
needs: [test, simulation-database, public-api-schema, lint, pyright, quality-guards, docker-build, test-integration]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -124,6 +124,76 @@ jobs:
working-directory: libs/policyengine-simulation-persistence
run: uv run --frozen pytest tests/ -v -m postgres

public-api-schema:
name: Public API schema
# Compares the persistence tests' copies of the Public API's tables with
# the tables the Public API's own v2 migration chain builds, at the Public
# API's default branch, so a change there to a column sim reads fails
# here (rule sim.persistence.public-api-copies-match). The Public API's
# repository is public, so the checkout needs no secret. Full CI runs
# only on the lowest unmerged pull request in a stack.
if: ${{ !github.event.pull_request.stack || github.event.pull_request.stack.base.ref == github.event.pull_request.base.ref }}
runs-on: ubuntu-latest
permissions:
contents: read
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: simulation_ci
# The Public API's disposable migration mode accepts only this
# database; the server's postgres database holds sim's copies.
POSTGRES_DB: policyengine_v2_alembic_test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=5s
--health-timeout=5s
--health-retries=20
env:
SIMULATION_TEST_DATABASE_URL: postgresql://postgres:simulation_ci@127.0.0.1:5432/postgres
PUBLIC_API_SCHEMA_DATABASE_URL: postgresql://postgres:simulation_ci@127.0.0.1:5432/policyengine_v2_alembic_test
# A missing server or Public API database fails the comparison instead
# of skipping it.
SIMULATION_REQUIRE_DATABASE_TESTS: "1"
SIMULATION_REQUIRE_PUBLIC_API_SCHEMA: "1"

steps:
- uses: actions/checkout@v6

- name: Check out the Public API's default branch
uses: actions/checkout@v6
with:
repository: PolicyEngine/policyengine-api
path: public-api
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v6
with:
# 3.12 for the Public API's lock, as its own CI uses; 3.13 for sim's.
python-version: |
3.12
3.13

- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true

- name: Build the Public API's tables from its v2 migration chain
run: .github/scripts/public-api-schema-reference.sh public-api

- name: Install dependencies
working-directory: libs/policyengine-simulation-persistence
run: uv sync --extra test --frozen

- name: Compare the test copies with the Public API's tables
working-directory: libs/policyengine-simulation-persistence
run: uv run --frozen pytest tests/ -v -m public_api_schema

lint:
name: Lint
# Full CI runs only on the lowest unmerged pull request in a stack.
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/simulation-deploy.reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,50 @@ jobs:
if: ${{ failure() && steps.safety_check_restore.outcome == 'failure' }}
run: .github/scripts/deploy-cloud-run-simulation-safety-check.sh pause

economy_staging_check:
name: Run a small v2 economy report on staging
# After the staging promotion, the stable URL serves the revision this
# deployment tested: submit a small report there, run it again with
# fresh runs if the submission reused runs an earlier deployment
# finished, and read it back once its comparison is recorded. The v2
# economy path serves no user yet, so a failure is reported without
# holding back the production deployment.
if: >-
${{
!cancelled() &&
inputs.deployment_environment == 'staging' &&
needs.promote_entrypoint.result == 'success'
}}
needs: [prepare, deploy_entrypoint, promote_entrypoint]
runs-on: ubuntu-latest
continue-on-error: true
timeout-minutes: 30
environment: ${{ inputs.release_environment }}
env:
SIMULATION_ENTRYPOINT_TEST_BASE_URL: ${{ needs.deploy_entrypoint.outputs.stable_url }}
SIMULATION_ENTRYPOINT_TEST_AUTH_ISSUER: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_ISSUER }}
SIMULATION_ENTRYPOINT_TEST_AUTH_AUDIENCE: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_AUDIENCE }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET }}
SIMULATION_STAGING_ECONOMY_POLICYENGINE_VERSION: ${{ needs.prepare.outputs.policyengine_version }}
SIMULATION_STAGING_ECONOMY_CHECK: "1"
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: "3.13"

- uses: astral-sh/setup-uv@v8.1.0

- name: Install staging check dependencies
working-directory: projects/policyengine-simulation-entry
run: uv sync --extra test --frozen

- name: Run a small economy report on staging and read it back
working-directory: projects/policyengine-simulation-entry
run: uv run --frozen pytest authenticated_tests/test_staging_economy_report.py -v

finalize_routing:
name: Finalize the Modal routing publication
# Finish the durable journal after every gate, including failed uploads
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/simulation-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ jobs:
entrypoint_revision_prefix: s-
entrypoint_min_instances: 0
entrypoint_max_instances: 2
promote_entrypoint: false
# Staging's stable URL must serve the revision this deployment tested,
# because staging callers such as the Public API's staging service use
# that URL. Rule: sim.deploy.staging-promotes-tested-revision.
promote_entrypoint: true
run_full_integration: true
deploy_existing_stack: true
deploy_stage12_v2: true
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/stage12-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ jobs:
entrypoint_revision_prefix: s-
entrypoint_min_instances: 0
entrypoint_max_instances: 2
promote_entrypoint: false
# Like the main deployment, promote the tested staging revision.
# Rule: sim.deploy.staging-promotes-tested-revision.
promote_entrypoint: true
# This workflow deploys and validates only the independent Stage 12 path.
# The main API deployment continues to run the existing-stack suite.
run_full_integration: false
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.

### Simulation persistence

- Checked: `sim.persistence.executor-independent`, `sim.persistence.migrate-before-entrypoint`, `sim.persistence.migration-cycle`, `sim.persistence.no-modal-database-credentials`, `sim.persistence.one-migration-chain`
- Checked: `sim.persistence.executor-independent`, `sim.persistence.migrate-before-entrypoint`, `sim.persistence.migration-cycle`, `sim.persistence.no-modal-database-credentials`, `sim.persistence.one-migration-chain`, `sim.persistence.public-api-copies-match`

### Deployment pipeline

- Checked: `sim.deploy.container-context-minimal`, `sim.deploy.docker-images-build`, `sim.deploy.gcloud-without-terraform`, `sim.deploy.modal-image-smoke`, `sim.deploy.numerical-sentinel-gate`, `sim.deploy.permission-enforcement-wired`, `sim.deploy.promotion-order-explicit`, `sim.deploy.release-identity-smoke`, `sim.deploy.revision-tags-valid`, `sim.deploy.routing-publication-journaled`, `sim.deploy.shell-scripts-valid`, `sim.deploy.smoke-tests-strict`, `sim.deploy.traffic-changes-exact`, `sim.deploy.workflow-shell-safety`
- Checked: `sim.deploy.container-context-minimal`, `sim.deploy.docker-images-build`, `sim.deploy.gcloud-without-terraform`, `sim.deploy.modal-image-smoke`, `sim.deploy.numerical-sentinel-gate`, `sim.deploy.permission-enforcement-wired`, `sim.deploy.promotion-order-explicit`, `sim.deploy.release-identity-smoke`, `sim.deploy.revision-tags-valid`, `sim.deploy.routing-publication-journaled`, `sim.deploy.shell-scripts-valid`, `sim.deploy.smoke-tests-strict`, `sim.deploy.staging-promotes-tested-revision`, `sim.deploy.traffic-changes-exact`, `sim.deploy.workflow-shell-safety`
- `sim.deploy.image-smoke-covers-every-image-lib`: The image smoke path filter includes every library mounted into a Modal image, so a change to any of them runs the smoke. (doc-only, not checked: follow it yourself)

### API contracts and integration
Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,20 @@ time:
three parallel service jobs;
3. publish routing only after all three service jobs pass;
4. run generated-client and live authentication tests through the complete
three-service request path; and
5. begin the `prod` deployment only after every `beta` deployment and
qualification job succeeds;
6. assign stable production traffic to the exact tested entrypoint revision
after the complete production suite passes; and
7. verify the stable endpoint, restoring its previous revision if immediate
post-promotion checks fail.
three-service request path;
5. assign the environment's stable traffic to the exact tested entrypoint
revision after that environment's tests pass, in `beta` as in `prod`;
6. verify the stable endpoint, restoring its previous revision if immediate
post-promotion checks fail; and
7. begin the `prod` deployment only after every `beta` deployment,
qualification, promotion and revision clean-up job succeeds, so a failed
staging clean-up also stops the production deployment.

The staging service's stable URL therefore serves the revision that passed the
staging tests, so staging callers of that URL, such as the Public API's staging
service, reach the build the deployment tested. After each successful
promotion, the deployment deletes the service's older revisions, keeping only
the promoted revision and the one it replaced, for rollback.

A successful push-triggered full-stack deployment publishes the API client from
the exact deployed commit.
Expand Down
1 change: 1 addition & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- Split the routing publisher (src/modal/utils/update_version_registry.py, 1,584 lines) into a src/modal/routing_publisher package of modules for the bundle manifest, the routing state, routing plans and their publication record, the check that only serialized workflows write routing, Modal Dict access, plan publication, the legacy seed and the command-line options, and its 2,728-line test file into a tests/version_registry directory of nine test files with shared fixtures; update_version_registry.py keeps the command line, still runs with python -m src.modal.utils.update_version_registry and re-exports every public name; behaviour and output are unchanged, and the private helpers the new modules share are now public
- Removed the UK constituency and local-authority weight-matrix region fallback (_build_uk_weight_replacement_region, _require_certified_uk_weight_matrix and its Stage 12 call); each deployment rebuilds only the app for the current policyengine.py version, now 6.10.0, which routes those regions to the local-area dataset, and apps deployed for older versions are not rebuilt and keep their own code; on a bundle that does not route them, an area region is now an unsupported region
- Made UK-wide and nation-level runs on a policyengine.py bundle that routes UK area regions to a local-area release dataset (6.10.0) load only the national dataset and return null for constituency_impact and local_authority_impact, whose results come from a breakdown=local_areas run instead; on a bundle that does not route them, those runs still compute both from the national dataset, and Stage 12 leaves the same breakdowns out of the same reports
- Promoted each staging deployment's tested Simulation Entrypoint revision to the staging service's stable traffic, as production deployments already do, in both the main deployment's beta job and the Stage 12 comparison path's staging deployment; the staging stable URL now serves the build that passed the staging tests, promotion runs the same stable-URL checks as production's and restores the previous revision when they fail, and the existing clean-up then deletes the staging service's older revisions except the promoted one and the one it replaced; the new rule sim.deploy.staging-promotes-tested-revision requires it
- Enabled every ruff rule (select ALL), ignoring only COM812 and ISC001, which conflict with ruff format, and the D203 and D213 halves of the mutually exclusive docstring pairs; complex functions, long parameter lists and magic numbers were split into smaller helpers and grouped parameters, each behind new characterization tests, across the contract and persistence validation, entry settings, app factory and Stage 12 adapter, gateway route resolution, Stage 12 coordination, workers, parity and qualification, the precompute and national reduce, the routing publisher and its command line, the release sentinel, the package update helper and the shared rules tool; Modal function, FastAPI route, Pydantic validator and command-line signatures and every OpenAPI document are unchanged; the contract library's batch-state builder and SPM selection resolver take their route values as one grouped argument, the simulation observability initializer drops a service_version parameter no caller passed, and no PLR0913 suppression is needed
- Enabled ruff's argument, idiom and pytest-style rules (ARG, ASYNC, B, C4, FBT, PD, PERF, PGH, PLW, PT, PTH, RUF, SIM); unused arguments are removed, underscored or explained, FastAPI auth dependencies use Annotated, re-raised errors keep their cause, and boolean options of in-repo helpers are keyword-only; OpenAPI documents are unchanged, and the shared rules tool's unittest tests are exempt from pytest-style rules; tests take the unused-argument rules (ARG001, ARG002, ARG005) as file-level exceptions, since their fakes and stubs mirror real signatures; best-effort telemetry and cleanup calls that swallow any error use contextlib.suppress, with the reason as a comment
- Enabled ruff's structure and import rules (INP, PLC, SLF, N, A, FAST, E); long lines are wrapped, deferred imports that Modal images, test seams, import cycles or heavy dependencies need carry a reason, script directories are declared to ruff as namespace packages, four internal exceptions gained an Error suffix, and published field names, OpenAPI documents and logged error types are unchanged; tests are also exempt from SLF001, INP001 and PLC0415; heavy or optional packages that load only inside functions are listed in ruff's banned-module-level-imports, which keeps them out of module-level imports (TID253) and lets their deferred imports go without a suppression, and the Modal utility programs take INP001 file-wide
Expand Down
Loading
Loading