Skip to content

Commit a61fd5f

Browse files
authored
Merge pull request #383 from PolicyEngine/feat/move-sims-to-modal
Migrate simulation API to Modal
2 parents 5603001 + c01cdce commit a61fd5f

39 files changed

+2802
-15
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
# Deploy simulation API to Modal
3+
# Usage: ./modal-deploy-app.sh <modal-environment>
4+
# Required env vars: POLICYENGINE_US_VERSION, POLICYENGINE_UK_VERSION
5+
#
6+
# Deploys two apps:
7+
# 1. policyengine-simulation-gateway - Stable gateway with fixed URL
8+
# 2. policyengine-simulation-us{X}-uk{Y} - Versioned simulation app
9+
10+
set -euo pipefail
11+
12+
MODAL_ENV="${1:?Modal environment required}"
13+
14+
# Generate versioned simulation app name (dots replaced with dashes for URL safety)
15+
US_VERSION_SAFE="${POLICYENGINE_US_VERSION//./-}"
16+
UK_VERSION_SAFE="${POLICYENGINE_UK_VERSION//./-}"
17+
SIMULATION_APP_NAME="policyengine-simulation-us${US_VERSION_SAFE}-uk${UK_VERSION_SAFE}"
18+
19+
echo "========================================"
20+
echo "Deploying to Modal environment: $MODAL_ENV"
21+
echo " US version: ${POLICYENGINE_US_VERSION}"
22+
echo " UK version: ${POLICYENGINE_UK_VERSION}"
23+
echo "========================================"
24+
25+
# 1. Deploy the gateway app (stable URL)
26+
echo ""
27+
echo "Step 1: Deploying gateway app..."
28+
echo " App name: policyengine-simulation-gateway"
29+
uv run modal deploy --env="$MODAL_ENV" src/modal/gateway/app.py
30+
31+
# 2. Deploy the versioned simulation app
32+
echo ""
33+
echo "Step 2: Deploying versioned simulation app..."
34+
echo " App name: ${SIMULATION_APP_NAME}"
35+
export MODAL_APP_NAME="$SIMULATION_APP_NAME"
36+
uv run modal deploy --env="$MODAL_ENV" src/modal/app.py
37+
38+
# 3. Update version registries
39+
echo ""
40+
echo "Step 3: Updating version registries..."
41+
uv run python -m src.modal.utils.update_version_registry \
42+
--app-name "$SIMULATION_APP_NAME" \
43+
--us-version "${POLICYENGINE_US_VERSION}" \
44+
--uk-version "${POLICYENGINE_UK_VERSION}" \
45+
--environment "$MODAL_ENV"
46+
47+
echo ""
48+
echo "========================================"
49+
echo "Deployment complete!"
50+
echo " Gateway app: policyengine-simulation-gateway"
51+
echo " Simulation app: $SIMULATION_APP_NAME"
52+
echo "========================================"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# Generate deployment summary for GitHub Actions
3+
# Usage: ./modal-deployment-summary.sh <beta-result> <beta-url> <prod-result> <prod-url>
4+
5+
set -euo pipefail
6+
7+
BETA_RESULT="${1:-skipped}"
8+
BETA_URL="${2:-}"
9+
PROD_RESULT="${3:-skipped}"
10+
PROD_URL="${4:-}"
11+
12+
{
13+
echo "## Modal Deployment Summary"
14+
echo ""
15+
16+
case "$BETA_RESULT" in
17+
success)
18+
echo "✅ **Beta deployment**: Success"
19+
[ -n "$BETA_URL" ] && echo " - URL: $BETA_URL"
20+
;;
21+
skipped)
22+
echo "⏭️ **Beta deployment**: Skipped"
23+
;;
24+
*)
25+
echo "❌ **Beta deployment**: $BETA_RESULT"
26+
;;
27+
esac
28+
29+
echo ""
30+
31+
case "$PROD_RESULT" in
32+
success)
33+
echo "✅ **Production deployment**: Success"
34+
[ -n "$PROD_URL" ] && echo " - URL: $PROD_URL"
35+
;;
36+
skipped)
37+
echo "⏭️ **Production deployment**: Skipped"
38+
;;
39+
*)
40+
echo "❌ **Production deployment**: $PROD_RESULT"
41+
;;
42+
esac
43+
} >> "$GITHUB_STEP_SUMMARY"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
# Extract policyengine-us and policyengine-uk versions from uv.lock
3+
# Usage: ./modal-extract-versions.sh <project-dir>
4+
# Outputs: Sets us_version and uk_version in GITHUB_OUTPUT
5+
6+
set -euo pipefail
7+
8+
PROJECT_DIR="${1:-.}"
9+
10+
cd "$PROJECT_DIR"
11+
12+
US_VERSION=$(grep -A1 'name = "policyengine-us"' uv.lock | grep version | head -1 | sed 's/.*"\(.*\)".*/\1/')
13+
UK_VERSION=$(grep -A1 'name = "policyengine-uk"' uv.lock | grep version | head -1 | sed 's/.*"\(.*\)".*/\1/')
14+
15+
echo "us_version=$US_VERSION" >> "$GITHUB_OUTPUT"
16+
echo "uk_version=$UK_VERSION" >> "$GITHUB_OUTPUT"
17+
echo "Deploying with policyengine-us=$US_VERSION, policyengine-uk=$UK_VERSION"

.github/scripts/modal-get-url.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
# Get the deployed Modal gateway URL
3+
# Usage: ./modal-get-url.sh <modal-environment>
4+
# Outputs: Sets simulation_api_url in GITHUB_OUTPUT
5+
#
6+
# Returns the stable gateway URL (policyengine-simulation-gateway)
7+
8+
set -euo pipefail
9+
10+
MODAL_ENV="${1:?Modal environment required}"
11+
GATEWAY_APP_NAME="policyengine-simulation-gateway"
12+
FUNCTION_NAME="web-app"
13+
14+
# Construct URL based on environment
15+
# URL format:
16+
# main: https://policyengine--<app-name>-<function-name>.modal.run
17+
# other: https://policyengine-<env>--<app-name>-<function-name>.modal.run
18+
if [ "$MODAL_ENV" = "main" ]; then
19+
SIMULATION_URL="https://policyengine--${GATEWAY_APP_NAME}-${FUNCTION_NAME}.modal.run"
20+
else
21+
SIMULATION_URL="https://policyengine-${MODAL_ENV}--${GATEWAY_APP_NAME}-${FUNCTION_NAME}.modal.run"
22+
fi
23+
24+
echo "simulation_api_url=$SIMULATION_URL" >> "$GITHUB_OUTPUT"
25+
echo "Gateway URL: $SIMULATION_URL"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
# Verify Modal deployment health with retries
3+
# Usage: ./modal-health-check.sh <base-url> [max-attempts] [sleep-seconds]
4+
5+
set -euo pipefail
6+
7+
BASE_URL="${1:?Base URL required}"
8+
MAX_ATTEMPTS="${2:-5}"
9+
SLEEP_SECONDS="${3:-10}"
10+
11+
HEALTH_URL="${BASE_URL}/health"
12+
echo "Checking health at: $HEALTH_URL"
13+
14+
for i in $(seq 1 "$MAX_ATTEMPTS"); do
15+
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
16+
echo "Health check passed!"
17+
curl -s "$HEALTH_URL" | jq .
18+
exit 0
19+
fi
20+
echo "Attempt $i/$MAX_ATTEMPTS: Waiting for deployment to be ready..."
21+
sleep "$SLEEP_SECONDS"
22+
done
23+
24+
echo "Health check failed after $MAX_ATTEMPTS attempts"
25+
exit 1
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
# Run simulation integration tests
3+
# Usage: ./modal-run-integ-tests.sh <environment> <base-url>
4+
# Environment: beta runs all tests, prod excludes beta_only tests
5+
6+
set -euo pipefail
7+
8+
ENVIRONMENT="${1:?Environment required (beta or prod)}"
9+
BASE_URL="${2:?Base URL required}"
10+
11+
cd projects/policyengine-apis-integ
12+
uv sync --extra test
13+
14+
export simulation_integ_test_base_url="$BASE_URL"
15+
16+
if [ "$ENVIRONMENT" = "beta" ]; then
17+
echo "Running all simulation integration tests (including beta_only)"
18+
uv run pytest tests/simulation/ -v
19+
else
20+
echo "Running simulation integration tests (excluding beta_only)"
21+
uv run pytest tests/simulation/ -v -m "not beta_only"
22+
fi
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
# Ensure Modal environments exist
3+
# Usage: ./modal-setup-environments.sh
4+
5+
set -euo pipefail
6+
7+
# Create staging environment if it doesn't exist
8+
uv run modal environment create staging 2>/dev/null || echo "staging environment already exists"
9+
10+
# main environment exists by default
11+
echo "Modal environments ready"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
# Sync secrets from GitHub to Modal environment
3+
# Usage: ./modal-sync-secrets.sh <modal-environment> <gh-environment>
4+
# Required env vars: LOGFIRE_TOKEN, GCP_CREDENTIALS_JSON (optional)
5+
6+
set -euo pipefail
7+
8+
MODAL_ENV="${1:?Modal environment required}"
9+
GH_ENV="${2:?GitHub environment required}"
10+
11+
echo "Syncing secrets to Modal environment: $MODAL_ENV"
12+
13+
# Sync Logfire secret
14+
uv run modal secret create policyengine-logfire \
15+
"LOGFIRE_TOKEN=${LOGFIRE_TOKEN:-}" \
16+
"LOGFIRE_ENVIRONMENT=$GH_ENV" \
17+
--env="$MODAL_ENV" \
18+
--force || true
19+
20+
# Sync GCP credentials if provided
21+
if [ -n "${GCP_CREDENTIALS_JSON:-}" ]; then
22+
uv run modal secret create gcp-credentials \
23+
"GOOGLE_APPLICATION_CREDENTIALS_JSON=$GCP_CREDENTIALS_JSON" \
24+
--env="$MODAL_ENV" \
25+
--force || true
26+
fi
27+
28+
echo "Modal secrets synced"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Reusable Modal deploy workflow
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
environment:
7+
required: true
8+
type: string
9+
description: 'The environment to deploy to (e.g., beta, prod)'
10+
modal_environment:
11+
required: true
12+
type: string
13+
description: 'The Modal environment name (e.g., staging, main)'
14+
outputs:
15+
simulation_api_url:
16+
description: 'The deployed simulation API URL'
17+
value: ${{ jobs.deploy.outputs.simulation_api_url }}
18+
19+
jobs:
20+
deploy:
21+
name: Deploy to Modal
22+
runs-on: ubuntu-latest
23+
environment: ${{ inputs.environment }}
24+
outputs:
25+
simulation_api_url: ${{ steps.get-url.outputs.simulation_api_url }}
26+
27+
steps:
28+
- name: Checkout repo
29+
uses: actions/checkout@v4
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.13"
35+
36+
- name: Install uv
37+
uses: astral-sh/setup-uv@v3
38+
39+
- name: Make scripts executable
40+
run: chmod +x .github/scripts/*.sh
41+
42+
- name: Install dependencies
43+
working-directory: projects/policyengine-api-simulation
44+
run: uv sync
45+
46+
- name: Extract package versions
47+
id: versions
48+
run: .github/scripts/modal-extract-versions.sh projects/policyengine-api-simulation
49+
50+
- name: Sync Modal secrets from GitHub
51+
working-directory: projects/policyengine-api-simulation
52+
env:
53+
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
54+
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
55+
LOGFIRE_TOKEN: ${{ secrets.LOGFIRE_TOKEN }}
56+
GCP_CREDENTIALS_JSON: ${{ secrets.GCP_CREDENTIALS_JSON }}
57+
run: ../../.github/scripts/modal-sync-secrets.sh "${{ inputs.modal_environment }}" "${{ inputs.environment }}"
58+
59+
- name: Deploy simulation API to Modal
60+
working-directory: projects/policyengine-api-simulation
61+
env:
62+
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
63+
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
64+
POLICYENGINE_US_VERSION: ${{ steps.versions.outputs.us_version }}
65+
POLICYENGINE_UK_VERSION: ${{ steps.versions.outputs.uk_version }}
66+
run: ../../.github/scripts/modal-deploy-app.sh "${{ inputs.modal_environment }}" src/modal/app.py
67+
68+
- name: Get deployed URL
69+
id: get-url
70+
working-directory: projects/policyengine-api-simulation
71+
env:
72+
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
73+
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
74+
run: ../../.github/scripts/modal-get-url.sh "${{ inputs.modal_environment }}"
75+
76+
- name: Verify deployment health
77+
run: .github/scripts/modal-health-check.sh "${{ steps.get-url.outputs.simulation_api_url }}"
78+
79+
integ_test:
80+
name: Run integration tests
81+
needs: [deploy]
82+
runs-on: ubuntu-latest
83+
environment: ${{ inputs.environment }}
84+
85+
steps:
86+
- name: Checkout repo
87+
uses: actions/checkout@v4
88+
89+
- name: Set up Python
90+
uses: actions/setup-python@v5
91+
with:
92+
python-version: "3.13"
93+
94+
- name: Install uv
95+
uses: astral-sh/setup-uv@v3
96+
97+
- name: Make scripts executable
98+
run: chmod +x .github/scripts/*.sh
99+
100+
- name: Generate API clients
101+
run: ./scripts/generate-clients.sh
102+
103+
- name: Run simulation integration tests
104+
run: .github/scripts/modal-run-integ-tests.sh "${{ inputs.environment }}" "${{ needs.deploy.outputs.simulation_api_url }}"

0 commit comments

Comments
 (0)