Repository navigation
241 lines (223 loc) · 11.1 KB
/
Copy pathsimulation-rollback.yml
File metadata and controls
241 lines (223 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
name: Roll back Simulation API
# Undo a release's Modal routing, or route unpinned requests back to a
# policyengine.py release that is already published, optionally move Cloud
# Run entrypoint traffic back to a retained revision, then verify the stable
# service serves the restored release.
#
# Three ways to restore routing (give exactly one):
# - resolve_pending_publication: print and restore the unfinished publication
# from the durable routing journal, even when no artifact was uploaded.
# - deploy_run_id: restore exactly the routing state that deployment replaced,
# from the plans it kept as artifacts (one per run attempt). Each attempt's
# change is undone only while the live state is what that attempt
# published, so it never overwrites a later release; the failed release's
# routes go too.
# - policyengine_version: point latest at that published release. Other
# releases, including the failed one, stay pinnable.
on:
workflow_dispatch:
inputs:
target:
description: "Environment to roll back"
required: true
type: choice
options:
- beta
- prod
deploy_run_id:
description: "Deployment run whose saved routing plans to undo"
required: false
type: string
resolve_pending_publication:
description: "Print and restore the target's unfinished publication from its durable journal"
required: false
default: false
type: boolean
policyengine_version:
description: "Published policyengine.py version to restore as latest"
required: false
type: string
expected_latest:
description: "With policyengine_version: refuse unless latest is currently this version"
required: false
type: string
entrypoint_revision:
description: "Retained Cloud Run revision to route to (blank: leave traffic)"
required: false
type: string
permissions:
contents: read
concurrency:
# Serialize routing writes with both deployment workflows, including restores.
group: simulation-deploy-main
cancel-in-progress: false
jobs:
rollback:
name: Roll back ${{ inputs.target }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
environment: ${{ inputs.target }}
permissions:
actions: read
contents: read
id-token: write
env:
TARGET: ${{ inputs.target }}
DEPLOY_RUN_ID: ${{ inputs.deploy_run_id }}
RESOLVE_PENDING_PUBLICATION: ${{ inputs.resolve_pending_publication }}
POLICYENGINE_VERSION: ${{ inputs.policyengine_version }}
EXPECTED_LATEST: ${{ inputs.expected_latest }}
ENTRYPOINT_REVISION: ${{ inputs.entrypoint_revision }}
MODAL_ENVIRONMENT: ${{ inputs.target == 'prod' && 'main' || 'staging' }}
SERVICE: ${{ inputs.target == 'prod' && 'policyengine-simulation-entry' || 'policyengine-simulation-entry-staging' }}
REGION: ${{ vars.SIMULATION_ENTRYPOINT_GCP_REGION || 'us-central1' }}
PROJECT_ID: ${{ vars.SIMULATION_ENTRYPOINT_GCP_PROJECT_ID }}
steps:
- uses: actions/checkout@v6
- name: Validate the requested rollback
run: .github/scripts/validate-simulation-rollback-inputs.sh
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v8.1.0
with:
version: "0.13.6"
- name: Install routing publisher dependencies
working-directory: projects/policyengine-simulation-executor
run: uv sync --frozen
- name: Download the deployment's routing plans
if: ${{ inputs.deploy_run_id != '' }}
uses: actions/download-artifact@v4
with:
run-id: ${{ inputs.deploy_run_id }}
github-token: ${{ github.token }}
pattern: routing-plan-${{ inputs.target }}-*
path: ${{ runner.temp }}/routing-plans
- name: Restore Modal routing
if: ${{ !inputs.resolve_pending_publication }}
working-directory: projects/policyengine-simulation-executor
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
GH_TOKEN: ${{ github.token }}
run: |
if [[ -n "${DEPLOY_RUN_ID}" ]]; then
# Every attempt whose routing job kept a plan must have one here:
# restoring without an attempt's plan would stop short of what
# the run replaced and still look complete.
attempts="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${DEPLOY_RUN_ID}" --jq .run_attempt)"
for attempt in $(seq 1 "${attempts}"); do
kept="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${DEPLOY_RUN_ID}/attempts/${attempt}/jobs" \
--jq "[.jobs[] | select(.name == \"Complete ${TARGET} deployment / Publish routing after all services pass\") | .steps[] | select(.name == \"Keep the plan for a guarded restore\" and .conclusion == \"success\")] | length" \
| awk '{total += $1} END {print total + 0}')"
if [[ "${kept}" -gt 0 && ! -f "${RUNNER_TEMP}/routing-plans/routing-plan-${TARGET}-${attempt}/plan.json" ]]; then
echo "Attempt ${attempt} of run ${DEPLOY_RUN_ID} kept a ${TARGET} routing plan that cannot be found; if its publication is pending, rerun this workflow with target=${TARGET} and resolve_pending_publication=true, leaving other inputs blank." >&2
exit 1
fi
done
# One plan per run attempt, passed oldest first. The restore undoes
# every attempt's change that is still live, newest first, and skips
# attempts that changed nothing.
plans=()
for attempt in $(
for directory in "${RUNNER_TEMP}"/routing-plans/routing-plan-"${TARGET}"-*/; do
name="${directory%/}"
name="${name##*-}"
if [[ "${name}" =~ ^[0-9]+$ ]]; then echo "${name}"; fi
done | sort -n
); do
plans+=("${RUNNER_TEMP}/routing-plans/routing-plan-${TARGET}-${attempt}/plan.json")
done
if [[ ${#plans[@]} -eq 0 ]]; then
echo "Deployment run ${DEPLOY_RUN_ID} kept no ${TARGET} routing plan." >&2
exit 1
fi
uv run --frozen python -m src.modal.utils.update_version_registry \
--environment "${MODAL_ENVIRONMENT}" \
--restore-plan "${plans[@]}"
else
args=(--environment "${MODAL_ENVIRONMENT}" --restore-latest "${POLICYENGINE_VERSION}")
if [[ -n "${EXPECTED_LATEST}" ]]; then
args+=(--expected-latest "${EXPECTED_LATEST}")
fi
uv run --frozen python -m src.modal.utils.update_version_registry "${args[@]}"
fi
- name: Resolve the unfinished routing publication
if: ${{ inputs.resolve_pending_publication }}
working-directory: projects/policyengine-simulation-executor
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: |
uv run --frozen python -m src.modal.utils.update_version_registry \
--environment "${MODAL_ENVIRONMENT}" --print-pending-plan
uv run --frozen python -m src.modal.utils.update_version_registry \
--environment "${MODAL_ENVIRONMENT}" --finalize-publication \
--release-environment "${TARGET}" --gates-passed false
- name: Read the restored release identity
if: ${{ !inputs.resolve_pending_publication }}
id: identity
working-directory: projects/policyengine-simulation-executor
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
run: |
uv run --frozen python -m src.modal.utils.update_version_registry \
--environment "${MODAL_ENVIRONMENT}" --print-identity \
| grep -E '^(policyengine_version|app_name|uk_model_version|uk_dataset_sha256)=' \
| tee -a "$GITHUB_OUTPUT"
- uses: google-github-actions/auth@v2
if: ${{ !inputs.resolve_pending_publication }}
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
if: ${{ !inputs.resolve_pending_publication }}
- name: Resolve the stable service
if: ${{ !inputs.resolve_pending_publication }}
id: service
run: |
service_json="$(gcloud run services describe "${SERVICE}" \
--project "${PROJECT_ID}" --region "${REGION}" --format=json)"
echo "url=$(jq -er '.status.url' <<<"${service_json}")" >> "$GITHUB_OUTPUT"
echo "revision=$(jq -er '
[.status.traffic[]? | select((.percent // 0) == 100) | .revisionName]
| if length == 1 then .[0] else error("no single 100 percent revision") end
' <<<"${service_json}")" >> "$GITHUB_OUTPUT"
- name: Restore entrypoint traffic
if: ${{ inputs.entrypoint_revision != '' }}
env:
SIMULATION_ENTRYPOINT_GCP_PROJECT_ID: ${{ env.PROJECT_ID }}
SIMULATION_ENTRYPOINT_GCP_REGION: ${{ env.REGION }}
SIMULATION_ENTRYPOINT_SERVICE: ${{ env.SERVICE }}
SIMULATION_ENTRYPOINT_TARGET_REVISION: ${{ inputs.entrypoint_revision }}
SIMULATION_ENTRYPOINT_EXPECTED_CURRENT_REVISION: ${{ steps.service.outputs.revision }}
run: |
if [[ "${SIMULATION_ENTRYPOINT_TARGET_REVISION}" == "${SIMULATION_ENTRYPOINT_EXPECTED_CURRENT_REVISION}" ]]; then
echo "${SERVICE} already serves ${SIMULATION_ENTRYPOINT_TARGET_REVISION}."
else
.github/scripts/set-cloud-run-simulation-entry-revision.sh
fi
- name: Verify the stable service serves the restored release
if: ${{ !inputs.resolve_pending_publication }}
env:
# The whole identity latest now routes to, read from the registry;
# a release from before dataset digests were recorded expects none.
EXPECTED_IDENTITY: required
EXPECTED_POLICYENGINE_VERSION: ${{ steps.identity.outputs.policyengine_version }}
EXPECTED_APP_NAME: ${{ steps.identity.outputs.app_name }}
EXPECTED_UK_MODEL_VERSION: ${{ steps.identity.outputs.uk_model_version }}
EXPECTED_UK_DATASET_SHA256: ${{ steps.identity.outputs.uk_dataset_sha256 || 'unrecorded' }}
EXPECTED_LATEST: "true"
STABLE_URL: ${{ steps.service.outputs.url }}
EXPECTED_REVISION: ${{ inputs.entrypoint_revision }}
PREVIOUS_REVISION: ${{ steps.service.outputs.revision }}
run: |
if [[ -n "${POLICYENGINE_VERSION}" && "${EXPECTED_POLICYENGINE_VERSION}" != "${POLICYENGINE_VERSION}" ]]; then
echo "latest routes to ${EXPECTED_POLICYENGINE_VERSION}, not ${POLICYENGINE_VERSION}" >&2
exit 1
fi
.github/scripts/cloud-run-simulation-entry-smoke.sh \
"${STABLE_URL}" "${EXPECTED_REVISION}" "${PREVIOUS_REVISION}"