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/cloud-run-simulation-entry-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

set -euo pipefail

base_url="${1:?Simulation Entrypoint base URL is required}"
base_url="${base_url%/}"

curl --fail --silent --show-error "${base_url}/health" |
jq -e '.status == "healthy"' >/dev/null
curl --fail --silent --show-error "${base_url}/ready" |
jq -e '.status == "ready"' >/dev/null
curl --fail --silent --show-error "${base_url}/versions" >/dev/null
curl --fail --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--data '{"value": 1}' \
"${base_url}/ping" |
jq -e '.incremented == 2' >/dev/null
49 changes: 49 additions & 0 deletions .github/scripts/configure-cloud-run-simulation-entry-domains.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash

set -euo pipefail

gcloud_bin="${GCLOUD_BIN:-gcloud}"
project_id="${SIMULATION_ENTRYPOINT_GCP_PROJECT_ID:?SIMULATION_ENTRYPOINT_GCP_PROJECT_ID is required}"
region="${SIMULATION_ENTRYPOINT_GCP_REGION:-us-central1}"
production_domain="${SIMULATION_ENTRYPOINT_PRODUCTION_DOMAIN:-simulation.api.policyengine.org}"
staging_domain="${SIMULATION_ENTRYPOINT_STAGING_DOMAIN:-staging.simulation.api.policyengine.org}"
dry_run="${SIMULATION_ENTRYPOINT_DOMAINS_DRY_RUN:-0}"

run() {
if [ "${dry_run}" = "1" ]; then
printf '+'
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}

exists() {
if [ "${dry_run}" = "1" ]; then
return 1
fi
"$@" >/dev/null 2>&1
}

ensure_mapping() {
local service="${1:?service is required}"
local domain="${2:?domain is required}"

if ! exists "${gcloud_bin}" beta run domain-mappings describe \
--project "${project_id}" \
--region "${region}" \
--domain "${domain}"; then
run "${gcloud_bin}" beta run domain-mappings create \
--project "${project_id}" \
--region "${region}" \
--service "${service}" \
--domain "${domain}"
fi
}

ensure_mapping policyengine-simulation-entry-staging "${staging_domain}"
ensure_mapping policyengine-simulation-entry "${production_domain}"

printf '\nInspect the mappings for the DNS records that must be published:\n'
printf ' %s\n' "${staging_domain}" "${production_domain}"
62 changes: 62 additions & 0 deletions .github/scripts/set-cloud-run-simulation-entry-revision.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash

# Operator-run production/staging command. GitHub Actions intentionally never
# invokes this script or changes Cloud Run traffic.

set -euo pipefail

gcloud_bin="${GCLOUD_BIN:-gcloud}"
project_id="${SIMULATION_ENTRYPOINT_GCP_PROJECT_ID:?SIMULATION_ENTRYPOINT_GCP_PROJECT_ID is required}"
region="${SIMULATION_ENTRYPOINT_GCP_REGION:-us-central1}"
environment="${SIMULATION_ENTRYPOINT_DEPLOYMENT_ENVIRONMENT:?SIMULATION_ENTRYPOINT_DEPLOYMENT_ENVIRONMENT is required}"
revision="${SIMULATION_ENTRYPOINT_TARGET_REVISION:?SIMULATION_ENTRYPOINT_TARGET_REVISION is required}"
dry_run="${SIMULATION_ENTRYPOINT_TRAFFIC_DRY_RUN:-0}"

case "${environment}" in
staging)
service="policyengine-simulation-entry-staging"
;;
production)
service="policyengine-simulation-entry"
;;
*)
printf 'SIMULATION_ENTRYPOINT_DEPLOYMENT_ENVIRONMENT must be staging or production\n' >&2
exit 2
;;
esac

if [ "${dry_run}" = "1" ]; then
printf '+ %q' "${gcloud_bin}"
printf ' %q' run services update-traffic "${service}" \
--project "${project_id}" \
--region "${region}" \
--to-revisions "${revision}=100"
printf '\n'
exit 0
fi

revision_json="$("${gcloud_bin}" run revisions describe "${revision}" \
--project "${project_id}" \
--region "${region}" \
--format=json)"
revision_service="$(jq -r \
'.metadata.labels["serving.knative.dev/service"] // empty' \
<<<"${revision_json}")"

if [ "${revision_service}" != "${service}" ]; then
printf 'Revision %s belongs to %s, not %s\n' \
"${revision}" "${revision_service:-an unknown service}" "${service}" >&2
exit 2
fi

if ! jq -e \
'.status.conditions[]? | select(.type == "Ready" and .status == "True")' \
>/dev/null <<<"${revision_json}"; then
printf 'Revision %s is not Ready\n' "${revision}" >&2
exit 2
fi

"${gcloud_bin}" run services update-traffic "${service}" \
--project "${project_id}" \
--region "${region}" \
--to-revisions "${revision}=100"
211 changes: 211 additions & 0 deletions .github/workflows/cloud-run-simulation-entry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
name: Deploy Simulation Entrypoint to Cloud Run

on:
push:
branches: [main]
paths:
- "projects/policyengine-simulation-entry/**"
- "libs/policyengine-fastapi/**"
- "libs/policyengine-simulation-contract/**"
- "libs/policyengine-simulation-observability/**"
- ".github/scripts/cloud-run-simulation-entry-smoke.sh"
- ".github/workflows/cloud-run-simulation-entry.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: cloud-run-simulation-entry
cancel-in-progress: false

env:
PROJECT_DIR: projects/policyengine-simulation-entry
REGION: ${{ vars.SIMULATION_ENTRYPOINT_GCP_REGION || 'us-central1' }}
PROJECT_ID: ${{ vars.SIMULATION_ENTRYPOINT_GCP_PROJECT_ID }}
REPOSITORY: ${{ vars.SIMULATION_ENTRYPOINT_ARTIFACT_REPOSITORY || 'policyengine-simulation-entry' }}
IMAGE_NAME: policyengine-simulation-entry

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v8.1.0
- run: uv sync --extra test --frozen
working-directory: ${{ env.PROJECT_DIR }}
- run: uv run pytest tests/ -v
working-directory: ${{ env.PROJECT_DIR }}

build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
outputs:
image: ${{ steps.image.outputs.uri }}
steps:
- uses: actions/checkout@v6
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
- id: image
run: echo "uri=${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- run: docker build --file "${PROJECT_DIR}/Dockerfile" --tag "${{ steps.image.outputs.uri }}" .
- run: docker push "${{ steps.image.outputs.uri }}"

deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
permissions:
contents: read
id-token: write
outputs:
tag: ${{ steps.metadata.outputs.tag }}
url: ${{ steps.url.outputs.url }}
steps:
- uses: actions/checkout@v6
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- id: metadata
run: echo "tag=s${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Deploy tagged staging candidate
env:
IMAGE: ${{ needs.build.outputs.image }}
TAG: ${{ steps.metadata.outputs.tag }}
run: |
gcloud run deploy policyengine-simulation-entry-staging \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--image "${IMAGE}" \
--tag "${TAG}" \
--no-traffic \
--allow-unauthenticated \
--service-account "${{ vars.SIMULATION_ENTRYPOINT_RUNTIME_SERVICE_ACCOUNT }}" \
--set-env-vars "^@^APP_ENVIRONMENT=staging@SIMULATION_ENTRYPOINT_PUBLIC_URL=https://staging.simulation.api.policyengine.org@SIMULATION_ENTRYPOINT_AUTH_REQUIRED=1@SIMULATION_ENTRYPOINT_AUTH_ISSUER=${{ secrets.SIMULATION_ENTRYPOINT_AUTH_ISSUER }}@SIMULATION_ENTRYPOINT_AUTH_AUDIENCE=${{ secrets.SIMULATION_ENTRYPOINT_AUTH_AUDIENCE }}@OLD_GATEWAY_URL=${{ secrets.OLD_GATEWAY_URL }}@OLD_GATEWAY_AUTH_ISSUER=${{ secrets.OLD_GATEWAY_AUTH_ISSUER }}@OLD_GATEWAY_AUTH_AUDIENCE=${{ secrets.OLD_GATEWAY_AUTH_AUDIENCE }}@OLD_GATEWAY_AUTH_CLIENT_ID=${{ secrets.OLD_GATEWAY_AUTH_CLIENT_ID }}" \
--set-secrets "OLD_GATEWAY_AUTH_CLIENT_SECRET=${{ vars.OLD_GATEWAY_AUTH_CLIENT_SECRET_SECRET_NAME }}:latest" \
--cpu 1 \
--memory 512Mi \
--concurrency 80 \
--timeout 60 \
--min-instances 0 \
--max-instances 2
- id: url
env:
TAG: ${{ steps.metadata.outputs.tag }}
run: |
url="$(gcloud run services describe policyengine-simulation-entry-staging \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json | jq -r --arg tag "${TAG}" '.status.traffic[] | select(.tag == $tag) | .url')"
test -n "${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
- run: bash .github/scripts/cloud-run-simulation-entry-smoke.sh "${{ steps.url.outputs.url }}"

authenticated-test-staging:
name: Authenticated deployment test (staging)
needs: deploy-staging
runs-on: ubuntu-latest
environment: staging
env:
SIMULATION_ENTRYPOINT_TEST_BASE_URL: ${{ needs.deploy-staging.outputs.url }}
SIMULATION_ENTRYPOINT_TEST_AUTH_ISSUER: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_ISSUER }}
SIMULATION_ENTRYPOINT_TEST_AUTH_AUDIENCE: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_AUDIENCE }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v8.1.0
- run: uv sync --extra test --frozen
working-directory: ${{ env.PROJECT_DIR }}
- run: uv run pytest authenticated_tests/ -v
working-directory: ${{ env.PROJECT_DIR }}

deploy-production-candidate:
needs: [build, authenticated-test-staging]
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
outputs:
tag: ${{ steps.metadata.outputs.tag }}
url: ${{ steps.url.outputs.url }}
steps:
- uses: actions/checkout@v6
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.SIMULATION_ENTRYPOINT_GCP_DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- id: metadata
run: echo "tag=p${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Deploy no-traffic production candidate
env:
IMAGE: ${{ needs.build.outputs.image }}
TAG: ${{ steps.metadata.outputs.tag }}
run: |
gcloud run deploy policyengine-simulation-entry \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--image "${IMAGE}" \
--tag "${TAG}" \
--no-traffic \
--allow-unauthenticated \
--service-account "${{ vars.SIMULATION_ENTRYPOINT_RUNTIME_SERVICE_ACCOUNT }}" \
--set-env-vars "^@^APP_ENVIRONMENT=production@SIMULATION_ENTRYPOINT_PUBLIC_URL=https://simulation.api.policyengine.org@SIMULATION_ENTRYPOINT_AUTH_REQUIRED=1@SIMULATION_ENTRYPOINT_AUTH_ISSUER=${{ secrets.SIMULATION_ENTRYPOINT_AUTH_ISSUER }}@SIMULATION_ENTRYPOINT_AUTH_AUDIENCE=${{ secrets.SIMULATION_ENTRYPOINT_AUTH_AUDIENCE }}@OLD_GATEWAY_URL=${{ secrets.OLD_GATEWAY_URL }}@OLD_GATEWAY_AUTH_ISSUER=${{ secrets.OLD_GATEWAY_AUTH_ISSUER }}@OLD_GATEWAY_AUTH_AUDIENCE=${{ secrets.OLD_GATEWAY_AUTH_AUDIENCE }}@OLD_GATEWAY_AUTH_CLIENT_ID=${{ secrets.OLD_GATEWAY_AUTH_CLIENT_ID }}" \
--set-secrets "OLD_GATEWAY_AUTH_CLIENT_SECRET=${{ vars.OLD_GATEWAY_AUTH_CLIENT_SECRET_SECRET_NAME }}:latest" \
--cpu 1 \
--memory 512Mi \
--concurrency 80 \
--timeout 60 \
--min-instances 1 \
--max-instances 20
- id: url
name: Resolve candidate URL
env:
TAG: ${{ steps.metadata.outputs.tag }}
run: |
url="$(gcloud run services describe policyengine-simulation-entry \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json | jq -r --arg tag "${TAG}" '.status.traffic[] | select(.tag == $tag) | .url')"
test -n "${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
- run: bash .github/scripts/cloud-run-simulation-entry-smoke.sh "${{ steps.url.outputs.url }}"

authenticated-test-production:
name: Authenticated deployment test (production)
needs: deploy-production-candidate
runs-on: ubuntu-latest
environment: production
env:
SIMULATION_ENTRYPOINT_TEST_BASE_URL: ${{ needs.deploy-production-candidate.outputs.url }}
SIMULATION_ENTRYPOINT_TEST_AUTH_ISSUER: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_ISSUER }}
SIMULATION_ENTRYPOINT_TEST_AUTH_AUDIENCE: ${{ secrets.SIMULATION_ENTRYPOINT_AUTH_AUDIENCE }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID }}
SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET: ${{ secrets.SIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v8.1.0
- run: uv sync --extra test --frozen
working-directory: ${{ env.PROJECT_DIR }}
- run: uv run pytest authenticated_tests/ -v
working-directory: ${{ env.PROJECT_DIR }}
3 changes: 2 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
# Modal image installs with uv_sync(frozen=True).
project:
- projects/policyengine-simulation-executor
- projects/policyengine-simulation-entry
- projects/policyengine-simulation-gateway
- libs/policyengine-simulation-contract
- libs/policyengine-simulation-observability
Expand Down Expand Up @@ -75,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
service: [simulation-executor]
service: [simulation-entry, simulation-executor]

steps:
- uses: actions/checkout@v6
Expand Down
Loading