Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
49d760b
test(red): define ORM session lifecycle
Aug 6, 2026
5fb8441
feat(green): add ORM session management
Aug 6, 2026
7464563
test(red): require canonical Alembic AI guidance
Aug 6, 2026
71bca5c
docs(green): add model-agnostic Alembic skill
Aug 6, 2026
e60600a
test(red): specify the existing v1 ORM schema
Aug 6, 2026
840b1f4
feat(green): map the unchanged v1 schema with ORM
Aug 6, 2026
c0a67f3
test(red): define Alembic baseline safeguards
Aug 6, 2026
af51cd9
feat(green): add autogenerated v1 Alembic baseline
Aug 6, 2026
1b59a36
test(red): define policy household and user DAO behavior
Aug 6, 2026
638f557
feat(green): add policy household and user DAOs
Aug 6, 2026
47f02e2
fix: satisfy Stage 7 changed-file lint
Aug 6, 2026
35c207e
test(red): define economy analysis and tracer DAO behavior
Aug 6, 2026
9073cdd
feat(green): add economy analysis and tracer DAOs
Aug 6, 2026
72953e9
test(red): require DAO boundaries in core services
Aug 6, 2026
0807cfb
feat(green): route core services through DAOs
Aug 6, 2026
f3bcf20
test(red): define simulation and report DAO transactions
Aug 6, 2026
df6ddf3
feat(green): add simulation and report DAOs
Aug 6, 2026
c487893
test(red): require DAO boundaries in local data services
Aug 6, 2026
5bc2c07
feat(green): route local data services through DAOs
Aug 6, 2026
d59622a
test(red): require DAO boundaries in run and spec services
Aug 6, 2026
f5c7ca0
feat(green): route run and spec services through DAOs
Aug 6, 2026
6861622
test(red): require DAO-backed orchestration services
Aug 6, 2026
2af7f94
feat(green): route orchestration through SQLAlchemy DAOs
Aug 6, 2026
914abf4
test(red): prohibit direct SQL outside the data layer
Aug 6, 2026
0192452
feat(green): confine runtime SQL to the DAO layer
Aug 6, 2026
33292c1
test(red): define toy qualification and legacy removal gates
Aug 6, 2026
a900dbb
feat(green): add Stage 7 toy qualification gate
Aug 6, 2026
ae69c5c
style: format Stage 7 migration files
Aug 6, 2026
5e37900
test: patch the Stage 7 policy DAO boundary
Aug 6, 2026
4e89db7
test(red): require read-only schema comparison
Aug 6, 2026
dc74dda
feat(green): add read-only Stage 7 schema comparison
Aug 6, 2026
bada37b
refactor: let SQLAlchemy own Cloud SQL connections
Aug 6, 2026
8efd042
fix: make Alembic baseline match the v1 schema
Aug 6, 2026
dc34676
refactor: make service operations own ORM transactions
Aug 6, 2026
0f38f01
refactor: move v1 domains onto typed repositories
Aug 6, 2026
ecea37f
refactor: make run orchestration transactional
Aug 6, 2026
fbac8b8
fix: own database resources per worker lifecycle
Aug 6, 2026
f9bfd7d
ci: gate Stage 7 on disposable MySQL
Aug 6, 2026
acae6e2
test: align legacy suites with ORM boundaries
Aug 6, 2026
99dbe3c
fix: preserve canonical JSON boundaries
Aug 7, 2026
bdfa214
docs: add Stage 7 changelog fragment
Aug 7, 2026
bb4e448
chore: remove one-time Stage 7 toy database
Aug 7, 2026
1914185
fix: extend App Engine startup budgets
Aug 7, 2026
3c184dc
test: expand Stage 7 ORM coverage
Aug 7, 2026
833936e
refactor: keep database pool settings in code
Aug 7, 2026
6d5624f
refactor: rename V1 DAO container
Aug 7, 2026
9b8da3d
refactor: expose canonical ORM session factories
Aug 7, 2026
c5217d8
test: add ORM-native v1 fixtures
Aug 7, 2026
07dd5b9
refactor: use ORM sessions for policies and households
Aug 7, 2026
8e20f98
refactor: use ORM sessions for users and saved policies
Aug 7, 2026
3077ca9
refactor: use ORM sessions for analysis persistence
Aug 7, 2026
80e0f75
refactor: use ORM sessions for simulations and runs
Aug 7, 2026
4b8ffea
refactor: use ORM sessions for report metadata
Aug 7, 2026
0f40cc7
refactor: orchestrate reports with ORM sessions
Aug 7, 2026
3749642
refactor: remove v1 persistence compatibility layers
Aug 7, 2026
69e67ed
chore: remove unused API dashboard
Aug 8, 2026
6c78041
refactor: bootstrap local database with SQLAlchemy
Aug 8, 2026
82f77f9
refactor: restore service-owned sessions for core resources
Aug 10, 2026
bbcd898
refactor: move simulation transactions into the service
Aug 10, 2026
4578d60
refactor: move report transactions into the service
Aug 10, 2026
8824eb2
refactor: move saved-policy SQL behind services
Aug 10, 2026
3eaea38
refactor: isolate household calculation persistence
Aug 10, 2026
63c3acd
refactor: inject persistence into economy and analysis services
Aug 10, 2026
7138b03
test: enforce service-owned persistence boundaries
Aug 10, 2026
505a457
style: format service persistence tests
Aug 10, 2026
778dfc7
test: restore route modules after contract loading
Aug 10, 2026
9e8a6de
config: require explicit remote database instance
Aug 10, 2026
2491e84
refactor: consolidate Flask handlers into blueprints
Aug 10, 2026
42fc237
refactor: remove the legacy endpoints package
Aug 10, 2026
30da81f
refactor: centralize Flask error responses
Aug 10, 2026
dfbc0f4
test: describe persistence coverage by behavior
Aug 10, 2026
f61394c
fix: preserve ORM JSON in economy submissions
Aug 10, 2026
1e43911
fix: scope saved policy updates by country
Aug 10, 2026
adbb676
fix: preserve v1 reform impact response shapes
Aug 10, 2026
b76a080
test: qualify Alembic against ephemeral MySQL
Aug 10, 2026
e7f844f
ci: qualify Alembic migrations on MySQL
Aug 10, 2026
fe7b944
fix: remove orphaned question table via Alembic
Aug 11, 2026
228c025
fix: align v1 metadata with deployed schema
anth-volk Aug 11, 2026
74addbf
feat: add safe v1 database migration orchestration
anth-volk Aug 11, 2026
bbd4540
ci: gate releases on qualified v1 migrations
anth-volk Aug 11, 2026
42fd630
fix: use installed Python in Alembic CI
anth-volk Aug 11, 2026
ec94729
fix: support connection-driven v1 adoption
anth-volk Aug 11, 2026
208b06e
fix: commit Alembic revision updates
anth-volk Aug 11, 2026
3aea079
chore: remove completed database adoption tooling
anth-volk Aug 11, 2026
446ec19
fix: make production migration job executable
anth-volk Aug 11, 2026
c0edde0
refactor: extract long CI shell blocks
anth-volk Aug 12, 2026
6de7007
docs: clarify disposable Alembic credentials
anth-volk Aug 12, 2026
496451f
ci: run Alembic checks on every PR
anth-volk Aug 12, 2026
3b3daeb
ci: keep v1 migration credentials process-local
anth-volk Aug 12, 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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
# Password for connecting to the PolicyEngine database
POLICYENGINE_DB_PASSWORD=policyengine_db_password

# Cloud SQL instance targeted by remote database connections
POLICYENGINE_DB_INSTANCE_CONNECTION_NAME=policyengine-api:us-central1:policyengine-api-data

# Github Microdata Token
POLICYENGINE_GITHUB_MICRODATA_AUTH_TOKEN=policyengine_github_token

Expand Down
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ or migration guard changes, read
For tests, read `docs/engineering/skills/testing.md` before adding, moving, or
reviewing test files.

For SQLAlchemy model or Alembic migration work, read
`docs/engineering/skills/alembic-migrations.md`.

For pull requests, read `docs/engineering/skills/github-prs.md` before opening,
replacing, or sharing a PR.
2 changes: 0 additions & 2 deletions .github/scripts/cloud_run_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ cloud_run_set_defaults() {
# image built by the staging track, so it must not embed the service name.
CLOUD_RUN_IMAGE_NAME="${CLOUD_RUN_IMAGE_NAME:-policyengine-api}"
CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT="${CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT:-policyengine-api-cr-runtime@policyengine-api.iam.gserviceaccount.com}"
CLOUD_RUN_CLOUD_SQL_INSTANCE="${CLOUD_RUN_CLOUD_SQL_INSTANCE:-policyengine-api:us-central1:policyengine-api-data}"
CLOUD_RUN_CPU="${CLOUD_RUN_CPU:-4}"
CLOUD_RUN_MEMORY="${CLOUD_RUN_MEMORY:-16Gi}"
CLOUD_RUN_TIMEOUT="${CLOUD_RUN_TIMEOUT:-300}"
Expand Down Expand Up @@ -57,7 +56,6 @@ cloud_run_set_defaults() {
export CLOUD_RUN_ARTIFACT_REPOSITORY
export CLOUD_RUN_IMAGE_NAME
export CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT
export CLOUD_RUN_CLOUD_SQL_INSTANCE
export CLOUD_RUN_CPU
export CLOUD_RUN_MEMORY
export CLOUD_RUN_TIMEOUT
Expand Down
33 changes: 33 additions & 0 deletions .github/scripts/create_cloud_sql_backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash

set -euo pipefail

: "${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME:?POLICYENGINE_DB_INSTANCE_CONNECTION_NAME is required}"

instance_id="${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME##*:}"
description="policyengine-api-v1-alembic-${GITHUB_SHA:-manual}-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
gcloud sql backups create \
--project policyengine-api \
--instance "${instance_id}" \
--description "${description}" \
--quiet >&2

# `gcloud sql backups create` waits for completion but does not consistently
# emit the created resource with value-format output. Recover the ID from the
# unique workflow description and require the service-reported successful state.
backup_id="$(
gcloud sql backups list \
--project policyengine-api \
--instance "${instance_id}" \
--filter="description=${description} AND status=SUCCESSFUL" \
--sort-by='~startTime' \
--limit=1 \
--format='value(id)'
)"

if [[ -z "${backup_id}" ]]; then
echo "Cloud SQL did not return a completed backup ID." >&2
exit 1
fi

printf '%s\n' "${backup_id}"
4 changes: 2 additions & 2 deletions .github/scripts/deploy_cloud_run_candidate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ cloud_run_set_defaults
bash .github/scripts/validate_cloud_run_deploy_env.sh

env_vars=(
"POLICYENGINE_DB_INSTANCE_CONNECTION_NAME=${CLOUD_RUN_CLOUD_SQL_INSTANCE}"
"POLICYENGINE_DB_INSTANCE_CONNECTION_NAME=${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME}"
"POLICYENGINE_DB_USER=${POLICYENGINE_DB_USER:-policyengine}"
"POLICYENGINE_DB_NAME=${POLICYENGINE_DB_NAME:-policyengine}"
"GATEWAY_AUTH_REQUIRED=1"
Expand Down Expand Up @@ -54,7 +54,7 @@ cloud_run_run gcloud run deploy "${CLOUD_RUN_SERVICE}" \
--allow-unauthenticated \
--execution-environment gen2 \
--service-account "${CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT}" \
--add-cloudsql-instances "${CLOUD_RUN_CLOUD_SQL_INSTANCE}" \
--add-cloudsql-instances "${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME}" \
--port "${CLOUD_RUN_PORT}" \
--cpu "${CLOUD_RUN_CPU}" \
--cpu-boost \
Expand Down
53 changes: 53 additions & 0 deletions .github/scripts/migrate_v1_cloud_sql.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash

set -euo pipefail

: "${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME:?POLICYENGINE_DB_INSTANCE_CONNECTION_NAME is required}"

readonly_password="$(
gcloud secrets versions access latest \
--secret policyengine-api-prod-db-readonly-password \
--project policyengine-api
)"
migration_password="$(
gcloud secrets versions access latest \
--secret policyengine-api-prod-db-migration-password \
--project policyengine-api
)"

if [[ -z "${readonly_password}" || -z "${migration_password}" ]]; then
echo "Cloud SQL database credentials must not be empty." >&2
exit 1
fi

printf '::add-mask::%s\n' "${readonly_password}"
printf '::add-mask::%s\n' "${migration_password}"
export POLICYENGINE_DB_READONLY_PASSWORD="${readonly_password}"
export POLICYENGINE_DB_MIGRATION_PASSWORD="${migration_password}"

# Never allow a job-level URL to bypass the credentials fetched for this release.
unset STAGE7_EXISTING_DATABASE_URL ALEMBIC_DATABASE_URL

database_state="$(python scripts/v1_database_migration.py --mode state)"
echo "Detected v1 database state: ${database_state}"

case "${database_state}" in
head)
;;
pending)
backup_id="$(bash .github/scripts/create_cloud_sql_backup.sh)"
python scripts/v1_database_migration.py \
--mode upgrade \
--backup-id "${backup_id}"
;;
unversioned | invalid)
echo "database is unversioned or has invalid Alembic state; automatic baseline stamping is disabled and manual recovery is required" >&2
exit 1
;;
*)
echo "Unrecognized v1 database state: ${database_state}" >&2
exit 1
;;
esac

python scripts/v1_database_migration.py --mode verify-head
41 changes: 41 additions & 0 deletions .github/scripts/start_cloud_sql_proxy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash

set -euo pipefail

: "${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME:?POLICYENGINE_DB_INSTANCE_CONNECTION_NAME is required}"

proxy_version="2.25.0"
proxy_sha256="091a9a12eddab6c028b6c563a4f2dacd067e8f7689c25a3fb4afce397e1f0c60"
proxy_path="${RUNNER_TEMP:-/tmp}/cloud-sql-proxy"
pid_path="${RUNNER_TEMP:-/tmp}/cloud-sql-proxy.pid"
log_path="${RUNNER_TEMP:-/tmp}/cloud-sql-proxy.log"

curl -fsSL \
"https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v${proxy_version}/cloud-sql-proxy.linux.amd64" \
--output "${proxy_path}"
printf '%s %s\n' "${proxy_sha256}" "${proxy_path}" | sha256sum --check --status
chmod +x "${proxy_path}"

"${proxy_path}" \
--quota-project policyengine-api \
--address 127.0.0.1 \
--port 3307 \
"${POLICYENGINE_DB_INSTANCE_CONNECTION_NAME}" \
>"${log_path}" 2>&1 &
proxy_pid="$!"
printf '%s\n' "${proxy_pid}" >"${pid_path}"

for _ in $(seq 1 30); do
if ! kill -0 "${proxy_pid}" 2>/dev/null; then
echo "Cloud SQL Auth Proxy exited before becoming ready." >&2
sed -n '1,120p' "${log_path}" >&2
exit 1
fi
if python -c 'import socket; socket.create_connection(("127.0.0.1", 3307), 1).close()' 2>/dev/null; then
exit 0
fi
sleep 1
done

echo "Cloud SQL Auth Proxy did not become ready." >&2
exit 1
14 changes: 14 additions & 0 deletions .github/scripts/stop_cloud_sql_proxy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

set -euo pipefail

pid_path="${RUNNER_TEMP:-/tmp}/cloud-sql-proxy.pid"
if [[ ! -f "${pid_path}" ]]; then
exit 0
fi

proxy_pid="$(cat "${pid_path}")"
if kill -0 "${proxy_pid}" 2>/dev/null; then
kill "${proxy_pid}"
wait "${proxy_pid}" 2>/dev/null || true
fi
1 change: 1 addition & 0 deletions .github/scripts/validate_app_engine_deploy_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ selected_url_env="$(
)"

required=(
POLICYENGINE_DB_INSTANCE_CONNECTION_NAME
SIM_ENTRYPOINT
"${selected_url_env}"
GATEWAY_AUTH_ISSUER
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/validate_cloud_run_deploy_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cloud_run_require_env \
CLOUD_RUN_IMAGE_URI \
CLOUD_RUN_TAG \
CLOUD_RUN_RUNTIME_SERVICE_ACCOUNT \
CLOUD_RUN_CLOUD_SQL_INSTANCE \
POLICYENGINE_DB_INSTANCE_CONNECTION_NAME \
CLOUD_RUN_POLICYENGINE_DB_PASSWORD_SECRET \
CLOUD_RUN_GITHUB_MICRODATA_TOKEN_SECRET \
CLOUD_RUN_ANTHROPIC_API_KEY_SECRET \
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/alembic-v1-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Alembic v1 checks

on:
workflow_call:
workflow_dispatch:

jobs:
mysql-lifecycle:
name: Alembic MySQL lifecycle
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.4
# This database exists only for this job, so these test credentials are
# intentionally non-secret and safe to expose in the workflow.
env:
MYSQL_ROOT_PASSWORD: policyengine_test
MYSQL_DATABASE: policyengine_alembic_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping --silent"
--health-interval=5s
--health-timeout=5s
--health-retries=20
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: make install
- name: Test Alembic configuration and MySQL lifecycle
run: >-
python -m pytest tests/unit/data/test_alembic_baseline.py
tests/unit/data/test_v1_database_migration.py
tests/integration/test_alembic_mysql_lifecycle.py -q
env:
ALEMBIC_DATABASE_URL: mysql+pymysql://root:policyengine_test@127.0.0.1:3306/policyengine_alembic_test
- name: Require database at all v1 heads
run: python -m alembic -c alembic-v1.ini current --check-heads
env:
ALEMBIC_DATABASE_URL: mysql+pymysql://root:policyengine_test@127.0.0.1:3306/policyengine_alembic_test
- name: Require no ungenerated v1 operations
run: python -m alembic -c alembic-v1.ini check
env:
ALEMBIC_DATABASE_URL: mysql+pymysql://root:policyengine_test@127.0.0.1:3306/policyengine_alembic_test
4 changes: 4 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ jobs:
python-version: "3.12"
- name: Run quality guards
run: python scripts/run_quality_guards.py

alembic-v1-check:
name: Alembic v1 qualification
uses: ./.github/workflows/alembic-v1-check.yml
check-changelog:
name: Check changelog fragment
runs-on: ubuntu-latest
Expand Down
Loading
Loading