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
43 changes: 43 additions & 0 deletions .github/scripts/cloud-run-simulation-entry-candidate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

# Resolve the URL, revision and image digest of the no-traffic candidate
# revision that carries the given Cloud Run tag.
#
# Required env vars: ENTRYPOINT_SERVICE, TAG, PROJECT_ID, REGION, GITHUB_OUTPUT.
# Outputs: url, revision and image in GITHUB_OUTPUT. Fails when the tag has no
# URL or revision, or the revision's image is not pinned by digest.

set -euo pipefail

service_json="$(gcloud run services describe "${ENTRYPOINT_SERVICE}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
url="$(jq -er --arg tag "${TAG}" '
.status.traffic[]
| select(.tag == $tag)
| .url
| select(type == "string" and length > 0)
' <<<"${service_json}")"
revision="$(jq -er --arg tag "${TAG}" '
.status.traffic[]
| select(.tag == $tag)
| .revisionName
| select(type == "string" and length > 0)
' <<<"${service_json}")"
revision_json="$(gcloud run revisions describe "${revision}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
image="$(jq -er '
(.status.imageDigest // .spec.containers[0].image)
| select(
type == "string"
and contains("@sha256:")
)
' <<<"${revision_json}")"
{
echo "url=${url}"
echo "revision=${revision}"
echo "image=${image}"
} >> "$GITHUB_OUTPUT"
31 changes: 31 additions & 0 deletions .github/scripts/cloud-run-simulation-entry-stable-state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash

# Record the stable Cloud Run service URL and the one revision serving 100
# percent of its traffic, before a candidate is deployed.
#
# Required env vars: ENTRYPOINT_SERVICE, PROJECT_ID, REGION, GITHUB_OUTPUT.
# Outputs: stable_url and revision in GITHUB_OUTPUT. Fails when the service has
# no URL or not exactly one revision at 100 percent.

set -euo pipefail

service_json="$(gcloud run services describe "${ENTRYPOINT_SERVICE}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
stable_url="$(jq -er '.status.url | select(type == "string" and length > 0)' <<<"${service_json}")"
revision="$(jq -er '
[
.status.traffic[]?
| select((.percent // 0) == 100)
| .revisionName
| select(type == "string" and length > 0)
]
| if length == 1 then .[0]
else error("service must have exactly one revision at 100 percent")
end
' <<<"${service_json}")"
{
echo "stable_url=${stable_url}"
echo "revision=${revision}"
} >> "$GITHUB_OUTPUT"
23 changes: 23 additions & 0 deletions .github/scripts/modal-publish-routing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
# Publish the active routing state for the deployed versioned executor app.
# Usage: modal-publish-routing.sh <modal-environment>
# Run from projects/policyengine-simulation-executor.
# Required env vars: APP_NAME, POLICYENGINE_VERSION, POLICYENGINE_US_VERSION,
# POLICYENGINE_UK_VERSION and FORCE_LATEST; FORCE_LATEST=true forces the latest
# routing pointers for an intentional rollback.

set -euo pipefail

MODAL_ENV="${1:?Modal environment required}"

args=(
--app-name "${APP_NAME}"
--policyengine-version "${POLICYENGINE_VERSION}"
--us-version "${POLICYENGINE_US_VERSION}"
--uk-version "${POLICYENGINE_UK_VERSION}"
--environment "${MODAL_ENV}"
)
if [[ "${FORCE_LATEST}" == "true" ]]; then
args+=(--force-latest)
fi
uv run python -m src.modal.utils.update_version_registry "${args[@]}"
17 changes: 17 additions & 0 deletions .github/scripts/simulation-deployment-ready.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
# Require every deployment phase to have succeeded, and the promotion to have
# run exactly when it was requested, so a skipped phase cannot report a
# successful partial deployment.
# Required env vars: PROMOTION_REQUESTED, ENTRYPOINT_RESULT, INTEGRATION_RESULT,
# AUTHENTICATED_TEST_RESULT, PROMOTION_RESULT (GitHub job results).

set -euo pipefail

test "${ENTRYPOINT_RESULT}" = "success"
test "${INTEGRATION_RESULT}" = "success"
test "${AUTHENTICATED_TEST_RESULT}" = "success"
if [[ "${PROMOTION_REQUESTED}" = "true" ]]; then
test "${PROMOTION_RESULT}" = "success"
else
test "${PROMOTION_RESULT}" = "skipped"
fi
17 changes: 17 additions & 0 deletions .github/scripts/stage12-deployment-ready.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Require the Stage 12 v2 jobs to have run exactly when they were requested:
# all skipped when not requested, all successful when requested.
# Required env vars: REQUESTED, CONFIGURATION_RESULT, DEPLOYMENT_RESULT,
# PUBLICATION_RESULT (GitHub job results).

set -euo pipefail

if [[ "${REQUESTED}" != "true" ]]; then
test "${CONFIGURATION_RESULT}" = "skipped"
test "${DEPLOYMENT_RESULT}" = "skipped"
test "${PUBLICATION_RESULT}" = "skipped"
exit 0
fi
test "${CONFIGURATION_RESULT}" = "success"
test "${DEPLOYMENT_RESULT}" = "success"
test "${PUBLICATION_RESULT}" = "success"
10 changes: 10 additions & 0 deletions .github/scripts/stage12-v2-application-name.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Select the versioned v2 Modal application name from the packaged
# PolicyEngine.py bundle.
# Run from projects/policyengine-simulation-executor.
# Outputs: name in GITHUB_OUTPUT.

set -euo pipefail

name="$(uv run python -c 'from policyengine_simulation_contract.stage12_manifest import v2_application_name; from policyengine_simulation_executor.stage12_bundle import load_stage12_bundle; print(v2_application_name(load_stage12_bundle().bundle.policyengine_version))')"
echo "name=${name}" >> "$GITHUB_OUTPUT"
17 changes: 17 additions & 0 deletions .github/scripts/stage12-validate-v2-workers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Validate the installed bundles, secrets, datasets and calculations of the
# deployed v2 workers, and keep the result for manifest publication.
# Usage: stage12-validate-v2-workers.sh <modal-environment>
# Run from projects/policyengine-simulation-executor.
# Required env vars: RUNNER_TEMP, GITHUB_OUTPUT.
# Outputs: file (the validation result path) in GITHUB_OUTPUT.

set -euo pipefail

MODAL_ENV="${1:?Modal environment required}"

output="${RUNNER_TEMP}/stage12-worker-validation.json"
uv run python -m src.modal.utils.validate_v2_worker_release \
--environment "${MODAL_ENV}" \
--output "${output}"
echo "file=${output}" >> "$GITHUB_OUTPUT"
139 changes: 19 additions & 120 deletions .github/workflows/simulation-deploy.reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,7 @@ jobs:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
PE_UK_PRIVATE_HF_READ_TOKEN: ${{ secrets.PE_UK_PRIVATE_HF_READ_TOKEN }}
run: |
uv run modal secret create pe-uk-private-hf-read-token \
"HUGGING_FACE_TOKEN=${PE_UK_PRIVATE_HF_READ_TOKEN}" \
--env="${{ inputs.modal_environment }}" \
--force
run: uv run modal secret create pe-uk-private-hf-read-token "HUGGING_FACE_TOKEN=${PE_UK_PRIVATE_HF_READ_TOKEN}" --env="${{ inputs.modal_environment }}" --force

- name: Synchronize Modal runtime secrets
if: ${{ inputs.deploy_existing_stack }}
Expand Down Expand Up @@ -199,39 +195,19 @@ jobs:

- name: Capture stable service state before deployment
id: previous
run: |
service_json="$(gcloud run services describe "${{ inputs.entrypoint_service }}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
stable_url="$(jq -er '.status.url | select(type == "string" and length > 0)' <<<"${service_json}")"
revision="$(jq -er '
[
.status.traffic[]?
| select((.percent // 0) == 100)
| .revisionName
| select(type == "string" and length > 0)
]
| if length == 1 then .[0]
else error("service must have exactly one revision at 100 percent")
end
' <<<"${service_json}")"
{
echo "stable_url=${stable_url}"
echo "revision=${revision}"
} >> "$GITHUB_OUTPUT"
env:
ENTRYPOINT_SERVICE: ${{ inputs.entrypoint_service }}
run: .github/scripts/cloud-run-simulation-entry-stable-state.sh

- name: Select immutable image
id: image
run: echo "uri=${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"

- name: Build and push entrypoint image
run: |
docker build \
--file "${PROJECT_DIR}/Dockerfile" \
--tag "${{ steps.image.outputs.uri }}" \
.
docker push "${{ steps.image.outputs.uri }}"
- name: Build entrypoint image
run: docker build --file "${PROJECT_DIR}/Dockerfile" --tag "${{ steps.image.outputs.uri }}" .

- name: Push entrypoint image
run: docker push "${{ steps.image.outputs.uri }}"

- name: Select no-traffic revision tag
id: metadata
Expand Down Expand Up @@ -269,40 +245,9 @@ jobs:
- name: Resolve exact candidate metadata
id: candidate
env:
ENTRYPOINT_SERVICE: ${{ inputs.entrypoint_service }}
TAG: ${{ steps.metadata.outputs.tag }}
run: |
service_json="$(gcloud run services describe "${{ inputs.entrypoint_service }}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
url="$(jq -er --arg tag "${TAG}" '
.status.traffic[]
| select(.tag == $tag)
| .url
| select(type == "string" and length > 0)
' <<<"${service_json}")"
revision="$(jq -er --arg tag "${TAG}" '
.status.traffic[]
| select(.tag == $tag)
| .revisionName
| select(type == "string" and length > 0)
' <<<"${service_json}")"
revision_json="$(gcloud run revisions describe "${revision}" \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)"
image="$(jq -er '
(.status.imageDigest // .spec.containers[0].image)
| select(
type == "string"
and contains("@sha256:")
)
' <<<"${revision_json}")"
{
echo "url=${url}"
echo "revision=${revision}"
echo "image=${image}"
} >> "$GITHUB_OUTPUT"
run: .github/scripts/cloud-run-simulation-entry-candidate.sh

- name: Install entrypoint unit-test dependencies
working-directory: ${{ env.PROJECT_DIR }}
Expand Down Expand Up @@ -401,9 +346,7 @@ jobs:

- name: Select versioned executor app
id: app
run: |
version_safe="${POLICYENGINE_VERSION//./-}"
echo "name=policyengine-simulation-py${version_safe}" >> "$GITHUB_OUTPUT"
run: echo "name=policyengine-simulation-py${POLICYENGINE_VERSION//./-}" >> "$GITHUB_OUTPUT"

- name: Deploy versioned Modal executor
working-directory: projects/policyengine-simulation-executor
Expand Down Expand Up @@ -457,18 +400,7 @@ jobs:
POLICYENGINE_US_VERSION: ${{ needs.prepare.outputs.us_version }}
POLICYENGINE_UK_VERSION: ${{ needs.prepare.outputs.uk_version }}
FORCE_LATEST: ${{ inputs.force_latest }}
run: |
args=(
--app-name "${APP_NAME}"
--policyengine-version "${POLICYENGINE_VERSION}"
--us-version "${POLICYENGINE_US_VERSION}"
--uk-version "${POLICYENGINE_UK_VERSION}"
--environment "${{ inputs.modal_environment }}"
)
if [[ "${FORCE_LATEST}" == "true" ]]; then
args+=(--force-latest)
fi
uv run python -m src.modal.utils.update_version_registry "${args[@]}"
run: ../../.github/scripts/modal-publish-routing.sh "${{ inputs.modal_environment }}"

configure_stage12_v2:
name: Validate pre-provisioned Stage 12 runtime access
Expand Down Expand Up @@ -547,19 +479,12 @@ jobs:

- name: Validate the packaged PolicyEngine.py bundle
working-directory: projects/policyengine-simulation-executor
run: |
uv run python -m src.modal.utils.extract_stage12_bundle \
--expect "policyengine_version=${{ needs.prepare.outputs.policyengine_version }}" \
--expect "countries.us.country_package_version=${{ needs.prepare.outputs.us_version }}" \
--expect "countries.uk.country_package_version=${{ needs.prepare.outputs.uk_version }}" \
--output stage12-bundle.json
run: uv run python -m src.modal.utils.extract_stage12_bundle --expect "policyengine_version=${{ needs.prepare.outputs.policyengine_version }}" --expect "countries.us.country_package_version=${{ needs.prepare.outputs.us_version }}" --expect "countries.uk.country_package_version=${{ needs.prepare.outputs.uk_version }}" --output stage12-bundle.json

- name: Select the bundle-derived v2 application name
id: application
working-directory: projects/policyengine-simulation-executor
run: |
name="$(uv run python -c 'from policyengine_simulation_contract.stage12_manifest import v2_application_name; from policyengine_simulation_executor.stage12_bundle import load_stage12_bundle; print(v2_application_name(load_stage12_bundle().bundle.policyengine_version))')"
echo "name=${name}" >> "$GITHUB_OUTPUT"
run: ../../.github/scripts/stage12-v2-application-name.sh

- name: Synchronize the separate Stage 12 runtime secret
working-directory: projects/policyengine-simulation-executor
Expand Down Expand Up @@ -598,12 +523,7 @@ jobs:
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: |
output="${RUNNER_TEMP}/stage12-worker-validation.json"
uv run python -m src.modal.utils.validate_v2_worker_release \
--environment "${{ inputs.modal_environment }}" \
--output "${output}"
echo "file=${output}" >> "$GITHUB_OUTPUT"
run: ../../.github/scripts/stage12-validate-v2-workers.sh "${{ inputs.modal_environment }}"

- name: Retain worker validation for manifest publication
uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -642,11 +562,7 @@ jobs:
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: |
uv run python -m src.modal.utils.update_v2_version_manifest \
--environment "${{ inputs.modal_environment }}" \
--manifest-name simulation-api-v2-version-manifest \
--validation-file "${RUNNER_TEMP}/stage12-worker-validation/stage12-worker-validation.json"
run: uv run python -m src.modal.utils.update_v2_version_manifest --environment "${{ inputs.modal_environment }}" --manifest-name simulation-api-v2-version-manifest --validation-file "${RUNNER_TEMP}/stage12-worker-validation/stage12-worker-validation.json"

stage12_v2_ready:
name: Require requested Stage 12 v2 deployment to succeed
Expand All @@ -661,16 +577,7 @@ jobs:
CONFIGURATION_RESULT: ${{ needs.configure_stage12_v2.result }}
DEPLOYMENT_RESULT: ${{ needs.deploy_stage12_v2.result }}
PUBLICATION_RESULT: ${{ needs.publish_stage12_v2_manifest.result }}
run: |
if [[ "${REQUESTED}" != "true" ]]; then
test "${CONFIGURATION_RESULT}" = "skipped"
test "${DEPLOYMENT_RESULT}" = "skipped"
test "${PUBLICATION_RESULT}" = "skipped"
exit 0
fi
test "${CONFIGURATION_RESULT}" = "success"
test "${DEPLOYMENT_RESULT}" = "success"
test "${PUBLICATION_RESULT}" = "success"
run: .github/scripts/stage12-deployment-ready.sh

integration:
name: Verify deployed three-service stack
Expand Down Expand Up @@ -870,15 +777,7 @@ jobs:
INTEGRATION_RESULT: ${{ needs.integration.result }}
AUTHENTICATED_TEST_RESULT: ${{ needs.authenticated_test.result }}
PROMOTION_RESULT: ${{ needs.promote_entrypoint.result }}
run: |
test "${ENTRYPOINT_RESULT}" = "success"
test "${INTEGRATION_RESULT}" = "success"
test "${AUTHENTICATED_TEST_RESULT}" = "success"
if [[ "${PROMOTION_REQUESTED}" = "true" ]]; then
test "${PROMOTION_RESULT}" = "success"
else
test "${PROMOTION_RESULT}" = "skipped"
fi
run: .github/scripts/simulation-deployment-ready.sh

cleanup_entrypoint_revisions:
name: Remove superseded Cloud Run revisions
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/simulation-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,4 @@ jobs:
- uses: actions/checkout@v6

- name: Report promotion status
run: |
.github/scripts/simulation-deployment-summary.sh \
"${{ needs.beta.result }}" \
"${{ needs.beta.outputs.simulation_api_url }}" \
"${{ needs.prod.result }}" \
"${{ needs.prod.outputs.stable_url }}"
run: .github/scripts/simulation-deployment-summary.sh "${{ needs.beta.result }}" "${{ needs.beta.outputs.simulation_api_url }}" "${{ needs.prod.result }}" "${{ needs.prod.outputs.stable_url }}"
Loading
Loading