Repository navigation
Conversation
Replace the sim.stage12.no-migration-chain rule, which forbade any Alembic chain in this repository, with ownership rules for the Simulation Entrypoint's own Postgres schema: - one Alembic chain at migrations/, configured by migrations/alembic.ini, that keeps its version table at simulation.alembic_version; - the Modal executor and gateway never depend on the persistence library; - no Modal secret or deployment receives a simulation database URL; - each deployment migrates in its own job before the entrypoint deploys. Add libs/policyengine-simulation-persistence with the schema names, the runtime engine and a revision reader that needs no Alembic. The initial revision grants the simulation_runtime role schema usage, revision reads and default DML privileges on tables the migrator creates. A new CI job runs the chain's upgrade, alembic check, downgrade and upgrade on PostgreSQL 17 with a non-superuser migrator, and checks the runtime role cannot change the schema. The deploy workflow gains a migrate_simulation_database job, the only one that receives SIMULATION_MIGRATION_DATABASE_URL; the entrypoint deploy now needs it and mounts SIMULATION_DATABASE_URL from the Secret Manager secret named by SIMULATION_DATABASE_URL_SECRET_NAME. GET /ready reports the database's revision next to the image's expected one without changing the readiness status, and the candidate smoke requires them to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added this pull request to stack #850
October 19, 2026 22:12
This was referenced Oct 19, 2026
The executor's script tests require every .github/scripts/simulation-*.sh script to start with #!/bin/bash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostgreSQL checks CREATE on the database before it checks whether a schema exists, even for CREATE SCHEMA IF NOT EXISTS. The migration environment ran that statement on every upgrade, so the operator alternative described for the migrator role (create the simulation schema in advance, owned by the migrator, instead of granting it CREATE on the database) failed with "permission denied for database". The environment now inspects the database and creates the schema only when it does not exist. The new test pre-creates the schema in a database the migrator does not own and upgrades to the head as the migrator; it failed at the schema statement before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation-only naming change: the README now names the service that owns the Stage 12 comparison tables "the Public API", as the repository's public text does elsewhere. No test can fail first for a wording change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 20, 2026
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 #844
What this changes
Step 1 of the API v2 migration, item 1A-2. This is the first of three stacked PRs: #847 (this one), then #848 (job store), then #849 (report tables).
Ownership rules. The rule
sim.stage12.no-migration-chain, which asserted that this repository has no Alembic chain, is removed. A new "Simulation persistence" category inrules/registry.tomlreplaces it:sim.persistence.one-migration-chain: thesimulationschema changes only through the Alembic chain inmigrations/, configured bymigrations/alembic.ini, with its version table atsimulation.alembic_version. No other Alembic configuration, environment or revision directory may exist, and runtime source code contains no schema DDL. Proof fixtures included.sim.persistence.migration-cycle: CI runs upgrade,alembic check, downgrade and upgrade on PostgreSQL 17 with a non-superuser migrator, and checks that the runtime role cannot change the schema.sim.persistence.executor-independent: the Modal executor and gateway never declare, lock or import the persistence library. Proof fixtures included.sim.persistence.no-modal-database-credentials: no step that creates a Modal secret or deploys a Modal application receivesSIMULATION_DATABASE_URLorSIMULATION_MIGRATION_DATABASE_URL. The Stage 12 comparison's own secret stays the only database credential in Modal. Proof fixtures included.sim.persistence.migrate-before-entrypoint: the deploy ordering and secret scoping described below.Persistence library.
libs/policyengine-simulation-persistenceholds the schema names, the runtime engine and a revision reader that needs no Alembic. The engine uses psycopg 3 with a small pool and no server-side prepared statements, so it works through either Supabase pooler mode. Alembic is an optionalmigrationsextra; the Simulation Entrypoint image does not install it.Initial revision (0001).
migrations/env.pycreates thesimulationschema if it is missing (owned by the migrator) and keeps the version table there. Revision 0001 grantssimulation_runtimeUSAGEon the schema,SELECTon the version table, and defaultSELECT, INSERT, UPDATEprivileges on tables (andUSAGE, SELECTon sequences) that the migrator creates later.CI. A new
Simulation databasejob inpr.ymlruns the persistence tests markedpostgresagainst apostgres:17service container.SIMULATION_REQUIRE_DATABASE_TESTS=1makes a missing server fail the job instead of skipping the tests. The job is inCI complete'sneedsand has the stack condition. The library is also added to thetestmatrix and tomake test.Deploy.
simulation-deploy.reusable.ymlgains amigrate_simulation_databasejob (needsprepare; GitHub environmentbetaorprod). It runs.github/scripts/simulation-database-migrate.sh, which appliesalembic upgrade headand then requires the head withalembic current --check-heads.deploy_entrypointnow needs this job and runs only if it succeeded. The deploy script mountsSIMULATION_DATABASE_URLfrom Secret Manager, pinned to the newest enabled version like the existing secrets.Readiness. When a database is configured,
GET /readyaddssimulation_database: {reachable, revision, expected_revision}. It does not changestatus: no old-gateway or Stage 12 route uses this database, and the rollback workflow's smoke check must keep passing when an older image runs against a newer schema. The candidate smoke check in theintegrationjob setsREQUIRE_CURRENT_SIMULATION_DATABASE_REVISION=true, so a deploy fails unless the candidate revision reaches its database as the runtime role and finds the revision its image expects. Without a configured database, the response is unchanged.Unchanged: the old-gateway pass-through routes, the Stage 12 comparison and the OpenAPI route table. The only OpenAPI change is the
/readyresponse model (two added schemas).Before merging: operator steps (1A-1) in both staging and production
Merging to
maindeploys staging and then production, and the deploy fails without these. None of them has been performed.simulation_migratorandsimulation_runtimelogin roles in the staging and production v2 databases.simulation_migratorneedsCREATEon the database, or thesimulationschema must be created in advance and owned by it.simulation_runtimeneeds onlyCONNECT; the migrations grant the rest.postgresql://...) as theSIMULATION_MIGRATION_DATABASE_URLsecret in thebetaandprodGitHub environments.postgresql://...). Grantroles/secretmanager.secretAccessoronly tosim-entry-beta-runtimeorsim-entry-prod-runtime, and let the deploy service account list its versions (the deploy script runsgcloud secrets versions list). Set the secret's name as theSIMULATION_DATABASE_URL_SECRET_NAMEvariable inbetaandprod.Environment variables and secrets
SIMULATION_MIGRATION_DATABASE_URLmigrations/env.py, in the migration jobbeta,prod)simulation-deploy.ymlandstage12-deploy.ymlcall the reusable workflow withsecrets: inherit→ jobmigrate_simulation_databasewithenvironment: ${{ inputs.release_environment }}→env:of the step "Apply simulation schema migrations" only. A test asserts no other step or job carries it.SIMULATION_DATABASE_URL_SECRET_NAMEdeploy-cloud-run-simulation-entry.shbeta,prod)env:of the step "Deploy Cloud Run entrypoint candidate" → the script resolves<name>:<newest enabled version>→gcloud run deploy --set-secrets SIMULATION_DATABASE_URL=...SIMULATION_DATABASE_URLconfig.py)No Modal secret changes.
Checks run
libs/policyengine-simulation-persistence:uv run pytest tests/gives 19 passed and 4 skipped without a database; against a local PostgreSQL 17 withSIMULATION_REQUIRE_DATABASE_TESTS=1, 23 passed.projects/policyengine-simulation-entry:uv run pytest tests/, 568 passed.projects/policyengine-simulation-executor:uv run pytest tests/, 1292 passed and 22 skipped (run on the top of the stack). The first CI run failedtest_modal_scripts.py, which requiressimulation-*.shscripts to start with#!/bin/bash; the second commit fixes the new script.ruff check .andruff format --check projects/*/src libs/*/src(ruff 0.15.22),python -m scripts.rules guard --base-ref origin/main,python scripts/run_quality_guards.py(with shellcheck) andpython -m unittest discover -s scripts/rules/tests -t ..simulation_runtimeagainst a migrated local database,/readyreported revision0001..github/scripts/simulation-database-migrate.shtwice against a local PostgreSQL 17 database owned by a non-superuser migrator. The second run changed nothing, andalembic checkfound no differences.test-integration) and the deploy workflow; they need CI or the deployed environments.Notes for review
rules/need approval from a core developer other than the author (sim.process.rule-machinery-code-owned).app.py,config.pyand tests that this PR also edits). Whichever merges second rebases.🤖 Generated with Claude Code