Skip to content
Merged
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
50 changes: 3 additions & 47 deletions .github/check-policyengine-bundle-supported.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,12 @@

set -euo pipefail

CHECK_ONLY_IF_CHANGED=0
BASE_REF=""
VERSION_GUARD_SCRIPT="${SIMULATION_VERSION_GUARD_SCRIPT:-.github/request-simulation-model-versions.sh}"

usage() {
echo "Usage: $0 [--if-changed-from-base <base_ref>]"
if [ $# -ne 0 ]; then
echo "Usage: $0"
exit 1
}

while [ $# -gt 0 ]; do
case "$1" in
--if-changed-from-base)
CHECK_ONLY_IF_CHANGED=1
BASE_REF="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Error: Unknown option $1"
usage
;;
esac
done
fi

extract_policyengine_version() {
sed -n 's/.*policyengine\[models\]==\([0-9.][0-9.]*\).*/\1/p' | head -n 1
Expand All @@ -38,29 +19,4 @@ if [ -z "$current_version" ]; then
exit 1
fi

if [ "$CHECK_ONLY_IF_CHANGED" = "1" ]; then
if [ -z "$BASE_REF" ]; then
echo "ERROR: --if-changed-from-base requires a base ref"
exit 1
fi

git fetch --no-tags --depth=1 origin "$BASE_REF"
base_version="$(
git show "origin/${BASE_REF}:pyproject.toml" \
| extract_policyengine_version \
|| true
)"

if [ "$current_version" = "$base_version" ] \
&& git diff --quiet "origin/${BASE_REF}" -- \
policyengine_api/spm.py policyengine_api/worker_spm.py \
policyengine_api/worker_spm_release.py policyengine_api/constants.py \
policyengine_api/country.py \
.github/check-policyengine-bundle-supported.sh \
.github/request-simulation-model-versions.sh; then
echo "Bundle pin and SPM integration are unchanged; skipping simulation API support check."
exit 0
fi
fi

bash "$VERSION_GUARD_SCRIPT" -py "$current_version" --check-installed-spm
69 changes: 18 additions & 51 deletions .github/request-simulation-model-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
set -euo pipefail

# Simulation gateway version check script.
# Verifies that the PolicyEngine .py bundle used by API v1 is deployed in the
# selected simulation gateway before allowing API v1 deployment to proceed.
# US/UK versions are optional compatibility checks that should resolve to the
# same gateway app as the .py bundle route.
# Verifies that the PolicyEngine .py bundle the Public API installs is the latest
# one the selected simulation gateway has published, and that the gateway routes
# it, before a pull request merges or a deployment proceeds. The Public API's
# requests name their .py version, so the gateway's US and UK routes are not
# compared.

source .github/scripts/simulation_entrypoint_env.sh

Expand All @@ -15,21 +16,17 @@ GATEWAY_URL="$(
)"

usage() {
echo "Usage: $0 -py <policyengine_version> [-us <us_version>] [-uk <uk_version>]"
echo "Usage: $0 -py <policyengine_version> [--check-installed-spm]"
echo ""
echo "Required flags:"
echo " -py, --policyengine policyengine_version PolicyEngine .py bundle version"
echo ""
echo "Optional compatibility checks:"
echo " -us us_version Expected bundled policyengine-us version"
echo " -uk uk_version Expected bundled policyengine-uk version"
echo "Optional checks:"
echo " --check-installed-spm Compare the installed bundle with worker capability"
exit 1
}

POLICYENGINE_VERSION=""
US_VERSION=""
UK_VERSION=""
CHECK_INSTALLED_SPM=0

while [ $# -gt 0 ]; do
Expand All @@ -38,14 +35,6 @@ while [ $# -gt 0 ]; do
POLICYENGINE_VERSION="$2"
shift 2
;;
-us)
US_VERSION="$2"
shift 2
;;
-uk)
UK_VERSION="$2"
shift 2
;;
--check-installed-spm)
CHECK_INSTALLED_SPM=1
shift
Expand All @@ -68,12 +57,6 @@ fi
echo "Checking selected simulation API versions..."
echo " Gateway: $GATEWAY_URL"
echo " Expected PolicyEngine .py bundle: $POLICYENGINE_VERSION"
if [ -n "$US_VERSION" ]; then
echo " Expected policyengine-us: $US_VERSION"
fi
if [ -n "$UK_VERSION" ]; then
echo " Expected policyengine-uk: $UK_VERSION"
fi
echo ""

VERSIONS_RESPONSE=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 60 "${GATEWAY_URL}/versions")
Expand All @@ -92,33 +75,17 @@ if [ -z "$BUNDLE_APP" ]; then
fi
echo "PolicyEngine .py bundle $POLICYENGINE_VERSION is deployed (app: $BUNDLE_APP)"

check_country_route() {
local country="$1"
local version="$2"
local app

if [ -z "$version" ]; then
return
fi

app=$(echo "$VERSIONS_RESPONSE" | jq -r --arg country "$country" --arg v "$version" '.[$country][$v] // empty')
if [ -z "$app" ]; then
echo "ERROR: ${country} version ${version} is NOT deployed in the simulation API"
echo "Available ${country} versions:"
echo "$VERSIONS_RESPONSE" | jq -r --arg country "$country" '.[$country] | keys[]'
exit 1
fi

if [ "$app" != "$BUNDLE_APP" ]; then
echo "ERROR: ${country} version ${version} resolves to ${app}, not bundle app ${BUNDLE_APP}"
exit 1
fi

echo "${country} version ${version} resolves to the same app"
}

check_country_route "us" "$US_VERSION"
check_country_route "uk" "$UK_VERSION"
LATEST_POLICYENGINE_VERSION=$(echo "$VERSIONS_RESPONSE" | jq -r '.policyengine.latest // empty')
if [ -z "$LATEST_POLICYENGINE_VERSION" ]; then
echo "ERROR: The simulation API reports no latest PolicyEngine .py bundle"
exit 1
fi
if [ "$LATEST_POLICYENGINE_VERSION" != "$POLICYENGINE_VERSION" ]; then
echo "ERROR: The Public API is on PolicyEngine .py $POLICYENGINE_VERSION, but the simulation API's latest is $LATEST_POLICYENGINE_VERSION"
echo "Update the Public API's PolicyEngine bundle to $LATEST_POLICYENGINE_VERSION before deploying"
exit 1
fi
echo "PolicyEngine .py bundle $POLICYENGINE_VERSION is the simulation API's latest"

if [ "$CHECK_INSTALLED_SPM" = "1" ]; then
printf '%s' "$VERSIONS_RESPONSE" \
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: bash .github/scripts/check_ci_complete.sh

ensure-policyengine-bundle-supported-by-simulation-api:
name: Ensure PolicyEngine bundle is supported by simulation API
name: Ensure PolicyEngine bundle is the simulation API's latest
# 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
Expand All @@ -63,14 +63,12 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install jq
run: sudo apt-get install -y jq
- name: Set up uv for installed bundle validation
uses: astral-sh/setup-uv@v6
- name: Check simulation API supports updated PolicyEngine bundle
run: bash .github/check-policyengine-bundle-supported.sh --if-changed-from-base "${{ github.base_ref }}"
- name: Check the PolicyEngine bundle is the simulation API's latest
run: bash .github/check-policyengine-bundle-supported.sh

lint:
name: Lint
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ jobs:
run: sudo apt-get install -y jq
- name: Set up uv for installed bundle validation
uses: astral-sh/setup-uv@v6
- name: Check simulation API supports PolicyEngine bundle
- name: Check the PolicyEngine bundle is the simulation API's latest
run: bash .github/check-policyengine-bundle-supported.sh

versioning:
Expand Down Expand Up @@ -647,7 +647,7 @@ jobs:
run: sudo apt-get install -y jq
- name: Set up uv for installed bundle validation
uses: astral-sh/setup-uv@v6
- name: Check simulation API supports PolicyEngine bundle
- name: Check the PolicyEngine bundle is the simulation API's latest
run: bash .github/check-policyengine-bundle-supported.sh

migrate-v1-production-cloud-sql:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/4157.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pull requests and deploys now fail unless the installed PolicyEngine.py version is the latest one the simulation API has published. The check no longer compares the simulation API's US and UK routes, and runs on every pull request.
18 changes: 13 additions & 5 deletions docs/canonical-spm.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,19 @@ years, and geography it used. This is the concrete behavior referred to elsewher
as SPM selection and provenance.

The API and simulation worker can be reviewed independently. Economy calculations
using this API version require a worker whose `/versions` response maps the
installed PolicyEngine.py, US, and UK versions to the same worker application and
reports the matching `canonical-spm-v1` settings. Until such a worker is deployed,
the API returns `SPM_CONFIGURATION_UNAVAILABLE` instead of submitting a calculation
to an incompatible worker. [Simulation API PR 703](https://github.com/PolicyEngine/policyengine-sim-api/pull/703)
using this API version require a worker whose `/versions` response routes the
installed PolicyEngine.py version and reports the matching `canonical-spm-v1`
settings for it. Until such a worker is deployed, the API returns
`SPM_CONFIGURATION_UNAVAILABLE` instead of submitting a calculation to an
incompatible worker.

Every pull request, and every staging and production deploy, also runs
`.github/check-policyengine-bundle-supported.sh`. It fails unless the installed
PolicyEngine.py version is the `policyengine.latest` version that environment's
`/versions` response reports, so the API stays on the latest bundle the
simulation API has published. The API's economy requests name their
PolicyEngine.py version, and the simulation API routes them by it, so the check
doesn't compare the US and UK routes. [Simulation API PR 703](https://github.com/PolicyEngine/policyengine-sim-api/pull/703)
implements the corresponding worker package update.

The automated dependency updater changes the single `policyengine[models]`
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ flowchart LR
| `api.migration.alembic-autogenerated-only` | Alembic revisions are autogenerated against an explicitly selected v1 or v2 configuration; never hand-written DDL or data operations. | mechanical | doc-only | no | nothing | — | — |
| `api.migration.alembic-v1-qualified` | v1 Alembic revisions qualify against a disposable MySQL database. | mechanical | enforced | yes | CI job `alembic-v1-check` | `pr.yml:alembic-v1-check` | — |
| `api.migration.alembic-v2-qualified` | v2 Alembic revisions qualify against a disposable Postgres database, and alembic check finds no schema drift. | mechanical | enforced | yes | CI job `alembic-v2-check` | `pr.yml:alembic-v2-check` | — |
| `api.migration.bundle-supported-by-simulation-api` | A PolicyEngine bundle version change is supported by the simulation API before it merges. | mechanical | enforced | yes | CI job `ensure-policyengine-bundle-supported-by-simulation-api` | `pr.yml:ensure-policyengine-bundle-supported-by-simulation-api` | — |
| `api.migration.bundle-supported-by-simulation-api` | The installed PolicyEngine bundle is the latest one the simulation API has published, checked on every pull request and before each staging and production deploy. | mechanical | enforced | yes | CI job `ensure-policyengine-bundle-supported-by-simulation-api` | `pr.yml:ensure-policyengine-bundle-supported-by-simulation-api` | — |
| `api.migration.contracts-consistent` | Migration route groups and workflow contracts are internally consistent, and their generated docs are current. | mechanical | enforced | yes | quality guard `migration-contracts` | `pr.yml:quality-guards` | **unproven** |
| `api.migration.cross-database-integration` | Cross-database integration tests pass against real MySQL and Postgres. | mechanical | enforced | yes | CI job `v2-integration-check` | `pr.yml:v2-integration-check` | — |
| `api.v2.deployed-privileges-match` | Every v2 deploy grants the managed roles each privilege policyengine_api/data/v2/privileges.py declares right after the schema upgrade, reports privileges it doesn't declare without revoking them, and stops before the catalog is seeded if a declared privilege is still missing. | mechanical | enforced | yes | CI job `seed`, `catalog-runtime-integration` | `seed-v2-database.yml:seed`, `v2-integration-check.yml:catalog-runtime-integration` | yes |
Expand Down
35 changes: 7 additions & 28 deletions policyengine_api/worker_spm_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,14 @@ def _bundle_package_versions() -> dict[str, str]:
return versions


def _registered_application(
registry: dict,
route_kind: str,
route_version: str,
) -> str:
routes = registry.get(route_kind) if isinstance(registry, dict) else None
app = routes.get(route_version) if isinstance(routes, dict) else None
if not isinstance(app, str) or not app:
msg = f"The worker registry has no {route_kind} {route_version} application"
raise ValueError(msg)
return app


def validate_installed_worker(registry: dict, expected_bundle: str) -> dict | None:
"""Validate local package versions, worker routes, and SPM capability."""
"""Validate local package versions and the worker's SPM capability.

``.github/request-simulation-model-versions.sh`` checks the registry's
PolicyEngine.py routes before this runs. The gateway routes the Public
API's requests by PolicyEngine.py version, so the US and UK routes are not
compared.
"""
versions = _bundle_package_versions()
if versions["policyengine"] != expected_bundle:
msg = "The bundle manifest differs from the release requirement"
Expand All @@ -94,20 +87,6 @@ def validate_installed_worker(registry: dict, expected_bundle: str) -> dict | No
msg = f"Installed {package_name} does not match the bundle manifest"
raise ValueError(msg)

bundle_app = _registered_application(registry, "policyengine", expected_bundle)
for country_id, package_name in (
("us", "policyengine-us"),
("uk", "policyengine-uk"),
):
country_app = _registered_application(
registry,
country_id,
versions[package_name],
)
if country_app != bundle_app:
msg = f"The {country_id} route resolves to a different worker application"
raise ValueError(msg)

return validate_worker_spm(
"us",
gateway=RegistryCapability(registry),
Expand Down
4 changes: 2 additions & 2 deletions rules/registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,8 @@ kind = "none"
[[rule]]
id = "api.migration.bundle-supported-by-simulation-api"
category = "migration"
statement = "A PolicyEngine bundle version change is supported by the simulation API before it merges."
rationale = "docs/engineering/skills/github-prs.md"
statement = "The installed PolicyEngine bundle is the latest one the simulation API has published, checked on every pull request and before each staging and production deploy."
rationale = "docs/canonical-spm.md#policyengine-6-compatibility"
scope = ["pyproject.toml"]
tier = "mechanical"
status = "enforced"
Expand Down
Loading
Loading