Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .github/scripts/public-api-commit-on-default-branch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Require the checked-out Public API commit to be on the Public API's default
# branch, so sim's table models only ever describe a migration chain the
# Public API has merged (rule sim.persistence.models-match-public-api).
#
# Usage: public-api-commit-on-default-branch.sh <Public API checkout> <branch>
set -euo pipefail

checkout="${1:?usage: public-api-commit-on-default-branch.sh <checkout> <branch>}"
branch="${2:?usage: public-api-commit-on-default-branch.sh <checkout> <branch>}"
commit="$(git -C "${checkout}" rev-parse HEAD)"
git -C "${checkout}" fetch --quiet origin "${branch}"
if ! git -C "${checkout}" merge-base --is-ancestor "${commit}" FETCH_HEAD; then
printf '%s is not on the Public API'"'"'s %s branch yet; pin a merged commit\n' \
"${commit}" "${branch}" >&2
exit 1
fi
printf '%s is on the Public API'"'"'s %s branch\n' "${commit}" "${branch}"
21 changes: 21 additions & 0 deletions .github/scripts/public-api-schema-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Read the pinned Public API commit whose v2 migration chain sim's table
# models describe (rule sim.persistence.models-match-public-api).
#
# Usage: public-api-schema-commit.sh <pin file>
#
# The pin file holds one full commit hash. The script prints it and, on
# GitHub Actions, passes it to the later steps as the step output `commit`.
set -euo pipefail

pin_file="${1:?usage: public-api-schema-commit.sh <pin file>}"
commit="$(tr -d '[:space:]' < "${pin_file}")"
if ! [[ "${commit}" =~ ^[0-9a-f]{40}$ ]]; then
printf '%s must hold one Public API commit as 40 lowercase hexadecimal characters\n' \
"${pin_file}" >&2
exit 1
fi
printf 'Pinned Public API commit: %s\n' "${commit}"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
printf 'commit=%s\n' "${commit}" >> "${GITHUB_OUTPUT}"
fi
33 changes: 33 additions & 0 deletions .github/scripts/public-api-schema-reference.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Build the Public API's tables with its own v2 migration chain, and grant the
# table privileges its manifest declares, for the comparison with sim's table
# models (rule sim.persistence.models-match-public-api).
#
# Usage: public-api-schema-reference.sh <Public API checkout>
#
# PUBLIC_API_SCHEMA_DATABASE_URL names the empty database to build, 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, and creates the roles its manifest manages
# without a login. The script prints the Public API commit it built 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 build}"

cd "${checkout}"
commit="$(git rev-parse HEAD)"
printf 'Public API commit: %s\n' "${commit}"
if [ -n "${GITHUB_ENV:-}" ]; then
printf 'PUBLIC_API_SCHEMA_COMMIT=%s\n' "${commit}" >> "${GITHUB_ENV}"
fi
# The Public API installs its lock on Python 3.12, as its own CI does.
export UV_PYTHON=3.12
export V2_MIGRATION_DATABASE_URL="postgresql+psycopg://${database_url#*://}"
export V2_ALEMBIC_DISPOSABLE_TEST=1
export DEPLOYMENT_ENVIRONMENT=ci
uv sync --frozen --extra dev
uv run --frozen --extra dev alembic -c alembic-v2.ini upgrade head
uv run --frozen --extra dev python scripts/reconcile_v2_privileges.py apply --create-missing-roles
95 changes: 91 additions & 4 deletions .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, check-changelog, docker-build, test-integration]
needs: [test, simulation-database, public-api-schema, lint, pyright, quality-guards, check-changelog, docker-build, test-integration]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -79,9 +79,10 @@ jobs:

simulation-database:
name: Simulation database
# Runs the persistence tests that need PostgreSQL, which the Test job
# skips, on a fresh server. Full CI runs only on the lowest unmerged pull
# request in a stack.
# Runs the persistence tests marked postgres, which the Test job skips,
# on a fresh server. Tests that need the Public API's tables are marked
# public_api_schema instead and run in the Public API schema job. 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:
Expand Down Expand Up @@ -127,6 +128,92 @@ jobs:
working-directory: libs/policyengine-simulation-persistence
run: uv run --frozen pytest tests/ -v -m postgres

public-api-schema:
name: Public API schema
# Builds the Public API's tables with its own v2 migration chain, at the
# commit pinned in libs/policyengine-simulation-persistence/public-api-commit,
# grants the privileges its manifest declares, and runs the persistence
# tests marked public_api_schema against them: sim's table models must
# match those tables, and the simulation service's role must write them
# (rule sim.persistence.models-match-public-api). The pinned commit must
# also be on the Public API's default branch. The Public API's repository
# is public, so the job reads 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 the models' tables.
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 tests instead of
# skipping them.
SIMULATION_REQUIRE_DATABASE_TESTS: "1"
SIMULATION_REQUIRE_PUBLIC_API_SCHEMA: "1"

steps:
# Neither checkout keeps the job's token, since the job runs the Public
# API's code from a pinned commit.
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Read the pinned Public API commit
id: public-api
run: .github/scripts/public-api-schema-commit.sh libs/policyengine-simulation-persistence/public-api-commit

- name: Check out the pinned Public API commit
uses: actions/checkout@v6
with:
repository: PolicyEngine/policyengine-api
ref: ${{ steps.public-api.outputs.commit }}
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:
version: "0.13.6"
enable-cache: true

- name: Build the Public API's tables at the pinned commit
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 sim's table models with the Public API's tables
working-directory: libs/policyengine-simulation-persistence
run: uv run --frozen pytest tests/ -v -m public_api_schema

- name: Require the pinned commit on the Public API's default branch
run: .github/scripts/public-api-commit-on-default-branch.sh public-api master

lint:
name: Lint
# Full CI runs only on the lowest unmerged pull request in a stack.
Expand Down
87 changes: 87 additions & 0 deletions .github/workflows/public-api-schema-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Public API schema drift

# Compares sim's table models with the tables the Public API's v2 migration
# chain builds at the head of its default branch, every Monday and on demand.
# Pull requests compare them at the pinned commit instead (the PR workflow's
# public-api-schema job). A failure here means the Public API has merged a
# change to a table the simulation service writes, or to its role's grants,
# that the models and the pin need to follow, or that its tables can't be
# built at that head; the log says which (rule
# sim.persistence.models-match-public-api). The Public API's repository is
# public, so the workflow reads no secret.

on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
public-api-schema-drift:
name: Compare the models with the Public API's default branch
runs-on: ubuntu-latest
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 the models' tables.
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 tests instead of
# skipping them.
SIMULATION_REQUIRE_DATABASE_TESTS: "1"
SIMULATION_REQUIRE_PUBLIC_API_SCHEMA: "1"

steps:
# Neither checkout keeps the job's token, since the job runs the Public
# API's code.
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Check out the Public API's default branch
uses: actions/checkout@v6
with:
repository: PolicyEngine/policyengine-api
ref: master
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:
version: "0.13.6"
enable-cache: true

- name: Build the Public API's tables at its default branch
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 sim's table models with the Public API's tables
working-directory: libs/policyengine-simulation-persistence
run: uv run --frozen pytest tests/ -v -m public_api_schema
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ same-repository draft PR.
- The simulation service lives in `projects/policyengine-simulation-executor`.
- API integration tests live in `projects/policyengine-apis-integ`.
- PR CI runs simulation unit tests, the persistence tests that need
PostgreSQL, Ruff format and lint checks, Pyright for every package, quality
PostgreSQL, the comparison of the persistence models with the Public API's
tables at a pinned commit, Ruff format and lint checks, Pyright for every
package, quality
guards (including the rules registry), the changelog fragment check, Docker
build, and local integration tests. A separate
`PR description` job checks that every open PR's description starts with
Expand Down Expand Up @@ -133,7 +135,7 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.

### Simulation service database

- Checked: `sim.persistence.missing-privileges-reported`
- Checked: `sim.persistence.missing-privileges-reported`, `sim.persistence.models-match-public-api`

### Deployment pipeline

Expand Down
1 change: 1 addition & 0 deletions changelog.d/b2-public-api-schema-check.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CI checks sim's models of the tables the simulation service writes against the tables the Public API's migration chain builds at a pinned Public API commit, and that the service's role can write them; a weekly run compares them with the Public API's default branch.
8 changes: 5 additions & 3 deletions docs/engineering/skills/database-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ but still connect through SQLAlchemy.

The guard reads source, not types. It misses imports through `importlib` or
`__import__`, a table reached through some other local variable, and SQL text
assembled from separate literals. It doesn't read shell scripts or SQL files;
today no shell script calls a database client, and the repository has no SQL
files.
assembled from separate literals. It doesn't read shell scripts or SQL files.
The one shell script that reaches a database,
`.github/scripts/public-api-schema-reference.sh`, runs the Public API's own
migration and privilege commands against a disposable CI database for
`sim.persistence.models-match-public-api`; the repository has no SQL files.

## Out of scope

Expand Down
12 changes: 10 additions & 2 deletions docs/generated/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Every rule that governs `policyengine-sim-api` is listed here with how it is enf

| Tier | enforced | ratcheted | advisory | doc-only | Total |
| --- | ---: | ---: | ---: | ---: | ---: |
| mechanical | 58 | 2 | 0 | 14 | 74 |
| mechanical | 59 | 2 | 0 | 14 | 75 |
| metric | 1 | 0 | 0 | 0 | 1 |
| judgment | 0 | 0 | 0 | 0 | 0 |

**Blocking:** 61 of 75 rules.
**Blocking:** 62 of 76 rules.

## Gaps

Expand Down Expand Up @@ -141,14 +141,22 @@ flowchart LR
| Rule | Statement | Tier | Status | Blocking | Enforced by | CI | Proof |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `sim.persistence.missing-privileges-reported` | The simulation service's privilege check, which its readiness check reports, names every table privilege its role needs on the Public API's tables and lacks, as PostgreSQL reports it, counts a table missing from the public schema as lacking all of them, and names a missing USAGE privilege on that schema. | mechanical | enforced | yes | pytest `libs/policyengine-simulation-persistence/tests/test_access.py` | `pr.yml:simulation-database` | yes |
| `sim.persistence.models-match-public-api` | Sim's models of the Public API's tables match the tables the Public API's v2 migration chain builds at the pinned Public API commit, which is on the Public API's default branch: every column's type, nullability and default, every constraint and index by name and definition, and every enum's values; and the simulation service's role, with the privileges the Public API's manifest declares, writes every modelled table. | mechanical | enforced | yes | pytest `libs/policyengine-simulation-persistence/tests/test_public_api_schema.py`, `libs/policyengine-simulation-persistence/tests/test_service_writes.py` | `pr.yml:public-api-schema`, `public-api-schema-drift.yml:public-api-schema-drift` | yes |

```mermaid
flowchart LR
r_sim_persistence_missing_privileges_reported("sim.persistence.missing-privileges-reported (enforced)")
e_pytest_libs_policyengine_simulation_persistence_tests_test_access_py["pytest libs/policyengine-simulation-persistence/tests/test_access.py"]
j_CI_pr_yml_simulation_database["CI pr.yml:simulation-database"]
r_sim_persistence_models_match_public_api("sim.persistence.models-match-public-api (enforced)")
e_pytest_libs_policyengine_simulation_persistence_tests_test_public_api_schema_py__libs_policyengine_simulation_persistence_tests_test_service_writes_py["pytest libs/policyengine-simulation-persistence/tests/test_public_api_schema.py, libs/policyengine-simulation-persistence/tests/test_service_writes.py"]
j_CI_pr_yml_public_api_schema["CI pr.yml:public-api-schema"]
j_CI_public_api_schema_drift_yml_public_api_schema_drift["CI public-api-schema-drift.yml:public-api-schema-drift"]
r_sim_persistence_missing_privileges_reported --> e_pytest_libs_policyengine_simulation_persistence_tests_test_access_py
e_pytest_libs_policyengine_simulation_persistence_tests_test_access_py --> j_CI_pr_yml_simulation_database
r_sim_persistence_models_match_public_api --> e_pytest_libs_policyengine_simulation_persistence_tests_test_public_api_schema_py__libs_policyengine_simulation_persistence_tests_test_service_writes_py
e_pytest_libs_policyengine_simulation_persistence_tests_test_public_api_schema_py__libs_policyengine_simulation_persistence_tests_test_service_writes_py --> j_CI_pr_yml_public_api_schema
e_pytest_libs_policyengine_simulation_persistence_tests_test_public_api_schema_py__libs_policyengine_simulation_persistence_tests_test_service_writes_py --> j_CI_public_api_schema_drift_yml_public_api_schema_drift
```

## Deployment pipeline
Expand Down
Loading
Loading