Skip to content

Commit 5b76c4b

Browse files
authored
Support IBKR slot env sync targets (#89)
1 parent 7f6e867 commit 5b76c4b

3 files changed

Lines changed: 111 additions & 36 deletions

File tree

.github/workflows/sync-cloud-run-env.yml

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}
2121
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
2222
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
23+
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
24+
CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT: ${{ vars.CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT }}
2325
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
2426
RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}
2527
ACCOUNT_GROUP: ${{ vars.ACCOUNT_GROUP }}
@@ -168,13 +170,28 @@ jobs:
168170
169171
required_vars=(
170172
CLOUD_RUN_REGION
171-
CLOUD_RUN_SERVICE
172173
ACCOUNT_GROUP
173174
IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME
174175
GLOBAL_TELEGRAM_CHAT_ID
175176
NOTIFY_LANG
176177
)
177178
179+
resolve_cloud_run_services() {
180+
local raw_services="${CLOUD_RUN_SERVICES:-}"
181+
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
182+
raw_services="${CLOUD_RUN_SERVICE:-}"
183+
fi
184+
printf '%s\n' "${raw_services}" \
185+
| tr ',;' '\n' \
186+
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
187+
| sed '/^$/d'
188+
}
189+
190+
mapfile -t target_services < <(resolve_cloud_run_services)
191+
if [ "${#target_services[@]}" -eq 0 ]; then
192+
required_vars+=("CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE")
193+
fi
194+
178195
if [ "${REQUIRES_SNAPSHOT_ARTIFACTS:-}" = "true" ]; then
179196
required_vars+=(IBKR_FEATURE_SNAPSHOT_PATH)
180197
fi
@@ -232,23 +249,50 @@ jobs:
232249
run: |
233250
set -euo pipefail
234251
252+
resolve_cloud_run_services() {
253+
local raw_services="${CLOUD_RUN_SERVICES:-}"
254+
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
255+
raw_services="${CLOUD_RUN_SERVICE:-}"
256+
fi
257+
printf '%s\n' "${raw_services}" \
258+
| tr ',;' '\n' \
259+
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
260+
| sed '/^$/d'
261+
}
262+
263+
mapfile -t target_services < <(resolve_cloud_run_services)
264+
if [ "${#target_services[@]}" -eq 0 ]; then
265+
echo "CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE is required." >&2
266+
exit 1
267+
fi
268+
269+
case "${CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT:-true}" in
270+
false|False|FALSE|0|no|No|NO|off|Off|OFF)
271+
echo "Skipping Cloud Run commit wait because CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT is disabled."
272+
exit 0
273+
;;
274+
esac
275+
235276
target_sha="${GITHUB_SHA}"
236-
deadline=$((SECONDS + 1800))
237277
238-
while true; do
239-
deployed_sha="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" --region "${CLOUD_RUN_REGION}" --format='value(spec.template.metadata.labels.commit-sha)' 2>/dev/null || true)"
240-
if [ -n "${deployed_sha}" ] && [ "${deployed_sha}" = "${target_sha}" ]; then
241-
echo "Cloud Run service ${CLOUD_RUN_SERVICE} is on commit ${deployed_sha}."
242-
break
243-
fi
278+
for cloud_run_service in "${target_services[@]}"; do
279+
deadline=$((SECONDS + 1800))
244280
245-
if [ "${SECONDS}" -ge "${deadline}" ]; then
246-
echo "Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
247-
exit 1
248-
fi
281+
while true; do
282+
deployed_sha="$(gcloud run services describe "${cloud_run_service}" --region "${CLOUD_RUN_REGION}" --format='value(spec.template.metadata.labels.commit-sha)' 2>/dev/null || true)"
283+
if [ -n "${deployed_sha}" ] && [ "${deployed_sha}" = "${target_sha}" ]; then
284+
echo "Cloud Run service ${cloud_run_service} is on commit ${deployed_sha}."
285+
break
286+
fi
249287
250-
echo "Waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
251-
sleep 10
288+
if [ "${SECONDS}" -ge "${deadline}" ]; then
289+
echo "Timed out waiting for Cloud Run service ${cloud_run_service} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
290+
exit 1
291+
fi
292+
293+
echo "Waiting for Cloud Run service ${cloud_run_service} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
294+
sleep 10
295+
done
252296
done
253297
254298
- name: Sync Cloud Run environment
@@ -259,6 +303,23 @@ jobs:
259303
run: |
260304
set -euo pipefail
261305
306+
resolve_cloud_run_services() {
307+
local raw_services="${CLOUD_RUN_SERVICES:-}"
308+
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
309+
raw_services="${CLOUD_RUN_SERVICE:-}"
310+
fi
311+
printf '%s\n' "${raw_services}" \
312+
| tr ',;' '\n' \
313+
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
314+
| sed '/^$/d'
315+
}
316+
317+
mapfile -t target_services < <(resolve_cloud_run_services)
318+
if [ "${#target_services[@]}" -eq 0 ]; then
319+
echo "CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE is required." >&2
320+
exit 1
321+
fi
322+
262323
join_by_delimiter() {
263324
local delimiter="$1"
264325
shift
@@ -564,21 +625,23 @@ jobs:
564625
remove_env_vars+=("EXECUTION_REPORT_GCS_URI")
565626
fi
566627
567-
gcloud_args=(
568-
run services update "${CLOUD_RUN_SERVICE}"
569-
--region "${CLOUD_RUN_REGION}"
570-
--concurrency 1
571-
--max-instances 1
572-
--remove-env-vars "$(IFS=,; echo "${remove_env_vars[*]}")"
573-
--update-env-vars "^|^$(join_by_delimiter "|" "${env_pairs[@]}")"
574-
)
628+
for cloud_run_service in "${target_services[@]}"; do
629+
gcloud_args=(
630+
run services update "${cloud_run_service}"
631+
--region "${CLOUD_RUN_REGION}"
632+
--concurrency 1
633+
--max-instances 1
634+
--remove-env-vars "$(IFS=,; echo "${remove_env_vars[*]}")"
635+
--update-env-vars "^|^$(join_by_delimiter "|" "${env_pairs[@]}")"
636+
)
575637
576-
if [ "${#remove_secret_vars[@]}" -gt 0 ]; then
577-
gcloud_args+=(--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")")
578-
fi
638+
if [ "${#remove_secret_vars[@]}" -gt 0 ]; then
639+
gcloud_args+=(--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")")
640+
fi
579641
580-
if [ "${#secret_pairs[@]}" -gt 0 ]; then
581-
gcloud_args+=(--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")")
582-
fi
642+
if [ "${#secret_pairs[@]}" -gt 0 ]; then
643+
gcloud_args+=(--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")")
644+
fi
583645
584-
gcloud "${gcloud_args[@]}"
646+
gcloud "${gcloud_args[@]}"
647+
done

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@ Recommended setup:
226226
- **Repository Variables**
227227
- `ENABLE_GITHUB_ENV_SYNC` = `true`
228228
- `CLOUD_RUN_REGION`
229-
- `CLOUD_RUN_SERVICE`
229+
- `CLOUD_RUN_SERVICES` (comma-, semicolon-, or newline-separated Cloud Run service names; preferred for slot deployments)
230+
- `CLOUD_RUN_SERVICE` (single-service fallback when `CLOUD_RUN_SERVICES` is not set)
231+
- Optional: `CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT=false` if the target services are managed by a deployment flow that does not update the `commit-sha` label before this sync runs
230232
- `TELEGRAM_TOKEN_SECRET_NAME` (recommended when Cloud Run already uses Secret Manager for `TELEGRAM_TOKEN`)
231233
- `STRATEGY_PROFILE` (set explicitly to one enabled profile, such as `soxl_soxx_trend_income`)
232234
- `ACCOUNT_GROUP` (recommended: `paper`)
@@ -243,13 +245,14 @@ Recommended setup:
243245
- `IB_GATEWAY_ZONE`
244246
- `IB_GATEWAY_IP_MODE`
245247

246-
On every push to `main`, the workflow updates the existing Cloud Run service with the values above and removes legacy env vars that should now live in the account-group config (`IB_CLIENT_ID`, `IB_GATEWAY_INSTANCE_NAME`, `IB_GATEWAY_MODE`) plus the older transport vars (`IB_GATEWAY_HOST`, `IB_GATEWAY_PORT`, `TELEGRAM_CHAT_ID`). If `IB_GATEWAY_ZONE` or `IB_GATEWAY_IP_MODE` are blank in GitHub, the workflow also removes them from Cloud Run to avoid drift.
248+
On every push to `main`, the workflow updates the configured Cloud Run service or services with the values above and removes legacy env vars that should now live in the account-group config (`IB_CLIENT_ID`, `IB_GATEWAY_INSTANCE_NAME`, `IB_GATEWAY_MODE`) plus the older transport vars (`IB_GATEWAY_HOST`, `IB_GATEWAY_PORT`, `TELEGRAM_CHAT_ID`). If `IB_GATEWAY_ZONE` or `IB_GATEWAY_IP_MODE` are blank in GitHub, the workflow also removes them from Cloud Run to avoid drift.
247249

248250
`STRATEGY_PROFILE` is resolved from a platform capability matrix plus a rollout allowlist derived from `runtime_enabled` strategy metadata. The current strategy domain is `us_equity`: `eligible` means the platform can run the strategy in theory, while `enabled` means the current rollout really allows it. `ACCOUNT_GROUP` selects one account-group config entry, and the service fails fast if that runtime identity is incomplete. `RUNTIME_TARGET_JSON` carries the structured runtime identity; strategy defaults continue to come from `UsEquityStrategies`.
249251

250252
Important:
251253

252254
- The workflow only becomes strict when `ENABLE_GITHUB_ENV_SYNC=true`. If this variable is unset, the sync job is skipped. When enabled, it resolves the selected profile's snapshot/config requirements from `scripts/print_strategy_profile_status.py --json` instead of a hard-coded strategy-name list.
255+
- For slot deployments, configure `CLOUD_RUN_SERVICES` instead of the legacy `CLOUD_RUN_SERVICE`. Do not enable the workflow until the GitHub runtime variables describe every selected slot correctly; otherwise one shared `RUNTIME_TARGET_JSON` can overwrite per-slot runtime identity.
253256
- Here "shared config" still only means the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). `TELEGRAM_TOKEN` and `TELEGRAM_TOKEN_SECRET_NAME` remain repository-specific. If crisis alert recipients and sender policy are shared across platforms, manage `CRISIS_ALERT_CHANNELS`, `CRISIS_ALERT_EMAIL_*`, and `CRISIS_ALERT_PUSH_*` with GitHub Organization Variables/Secrets or GCP Secret Manager.
254257
- If `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` is set, the Cloud Run runtime needs Secret Manager access to that secret.
255258
- GitHub now authenticates to Google Cloud with OIDC + Workload Identity Federation, so `GCP_SA_KEY` is no longer required for this workflow.
@@ -258,7 +261,7 @@ Important:
258261
### Deployment unit and naming
259262

260263
- `QuantPlatformKit` is only a shared dependency; Cloud Run now deploys `InteractiveBrokersPlatform`.
261-
- Recommended Cloud Run service name: `interactive-brokers-quant-service`.
264+
- Recommended single-service Cloud Run name: `interactive-brokers-quant-service`. For slot deployments, keep explicit service names in `CLOUD_RUN_SERVICES`.
262265
- For future multi-account rollout, keep one Cloud Run service per `ACCOUNT_GROUP`, and let each service select its account-group config at runtime.
263266
- If you later rename or move this repository, reselect the GitHub source in Cloud Build / Cloud Run trigger instead of assuming the existing source binding will update itself.
264267
- For the shared deployment model and trigger migration checklist, see [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md).
@@ -444,7 +447,9 @@ IB_GATEWAY_IP_MODE=internal
444447
- **仓库级 Variables**
445448
- `ENABLE_GITHUB_ENV_SYNC` = `true`
446449
- `CLOUD_RUN_REGION`
447-
- `CLOUD_RUN_SERVICE`
450+
- `CLOUD_RUN_SERVICES`(逗号、分号或换行分隔的 Cloud Run 服务名;slot 部署优先用这个)
451+
- `CLOUD_RUN_SERVICE`(没设置 `CLOUD_RUN_SERVICES` 时的单服务兼容入口)
452+
- 可选:`CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT=false`(当目标服务由另一个部署链路管理、不会在同步前更新 `commit-sha` label 时使用)
448453
- `TELEGRAM_TOKEN_SECRET_NAME`(如果 Cloud Run 上的 `TELEGRAM_TOKEN` 已经改成 Secret Manager,建议配置)
449454
- `STRATEGY_PROFILE`(显式设置为任一已启用 profile,例如 `soxl_soxx_trend_income`
450455
- `ACCOUNT_GROUP`(建议设为 `paper`
@@ -461,13 +466,14 @@ IB_GATEWAY_IP_MODE=internal
461466
- `IB_GATEWAY_ZONE`
462467
- `IB_GATEWAY_IP_MODE`
463468

464-
每次 push 到 `main` 时,这个 workflow 会把上面这些值同步到现有 Cloud Run 服务里,并清掉已经转移到账号组配置里的旧 env(`IB_CLIENT_ID``IB_GATEWAY_INSTANCE_NAME``IB_GATEWAY_MODE`)以及更早的传输层 env(`IB_GATEWAY_HOST``IB_GATEWAY_PORT``TELEGRAM_CHAT_ID`)。如果 GitHub 里没有配置 `IB_GATEWAY_ZONE``IB_GATEWAY_IP_MODE`,workflow 也会把 Cloud Run 上这两个旧值一起删除,避免双配置源漂移。
469+
每次 push 到 `main` 时,这个 workflow 会把上面这些值同步到配置的一个或多个 Cloud Run 服务里,并清掉已经转移到账号组配置里的旧 env(`IB_CLIENT_ID``IB_GATEWAY_INSTANCE_NAME``IB_GATEWAY_MODE`)以及更早的传输层 env(`IB_GATEWAY_HOST``IB_GATEWAY_PORT``TELEGRAM_CHAT_ID`)。如果 GitHub 里没有配置 `IB_GATEWAY_ZONE``IB_GATEWAY_IP_MODE`,workflow 也会把 Cloud Run 上这两个旧值一起删除,避免双配置源漂移。
465470

466471
`STRATEGY_PROFILE` 由平台能力矩阵和从 `runtime_enabled` 策略元数据派生的 rollout allowlist 一起决定。当前策略域仍是 `us_equity``eligible` 表示平台理论上能跑,`enabled` 表示当前 rollout 真正放开。`ACCOUNT_GROUP` 是严格必填项,并会选中一份账号组配置。运行身份不完整时,服务会直接失败,不再静默回退。
467472

468473
注意:
469474

470475
- 只有在 `ENABLE_GITHUB_ENV_SYNC=true` 时,这个 workflow 才会严格校验并执行同步。没打开时会直接跳过。打开后,它会通过 `scripts/print_strategy_profile_status.py --json` 动态解析目标策略需要的 snapshot/config 输入,不再维护硬编码策略名列表。
476+
- slot 部署应配置 `CLOUD_RUN_SERVICES`,不要继续依赖旧的 `CLOUD_RUN_SERVICE`。在 GitHub runtime 变量能准确描述每个 slot 前,不要打开全量同步;否则一份共享的 `RUNTIME_TARGET_JSON` 会覆盖各 slot 自己的运行身份。
471477
- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**`TELEGRAM_TOKEN``TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。危机告警如果确实跨平台共用同一套收件人和发送方,可以用 GitHub Organization Variables/Secrets 管理 `CRISIS_ALERT_CHANNELS``CRISIS_ALERT_EMAIL_*``CRISIS_ALERT_PUSH_*`
472478
- 如果设置了 `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`,Cloud Run 运行时还需要有对应 Secret 的访问权限。
473479
- GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`

tests/test_sync_cloud_run_env_workflow.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ grep -Fq 'strategy_profile=' "$workflow_file"
2626
grep -Fq 'runtime_target_json=' "$workflow_file"
2727
grep -Fq 'Wait for Cloud Run deployment of current commit' "$workflow_file"
2828
grep -Fq 'target_sha="${GITHUB_SHA}"' "$workflow_file"
29-
grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\" --region \"\${CLOUD_RUN_REGION}\" --format='value(spec.template.metadata.labels.commit-sha)'" "$workflow_file"
30-
grep -Fq 'Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}' "$workflow_file"
29+
grep -Fq 'mapfile -t target_services < <(resolve_cloud_run_services)' "$workflow_file"
30+
grep -Fq 'CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE' "$workflow_file"
31+
grep -Fq 'Skipping Cloud Run commit wait because CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT is disabled.' "$workflow_file"
32+
grep -Fq "gcloud run services describe \"\${cloud_run_service}\" --region \"\${CLOUD_RUN_REGION}\" --format='value(spec.template.metadata.labels.commit-sha)'" "$workflow_file"
33+
grep -Fq 'Timed out waiting for Cloud Run service ${cloud_run_service} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}' "$workflow_file"
3134
grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file"
3235
grep -Fq 'CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}' "$workflow_file"
3336
grep -Fq 'CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}' "$workflow_file"
37+
grep -Fq 'CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}' "$workflow_file"
38+
grep -Fq 'CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT: ${{ vars.CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT }}' "$workflow_file"
3439
grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}' "$workflow_file"
3540
grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file"
3641
grep -Fq 'ACCOUNT_GROUP: ${{ vars.ACCOUNT_GROUP }}' "$workflow_file"
@@ -193,6 +198,7 @@ grep -Fq '"IB_CLIENT_ID"' "$workflow_file"
193198
grep -Fq '"IB_GATEWAY_INSTANCE_NAME"' "$workflow_file"
194199
grep -Fq '"IB_GATEWAY_MODE"' "$workflow_file"
195200
grep -Fq 'join_by_delimiter()' "$workflow_file"
201+
grep -Fq 'run services update "${cloud_run_service}"' "$workflow_file"
196202
grep -Fq -- '--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")"' "$workflow_file"
197203
grep -Fq -- '--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")"' "$workflow_file"
198204
grep -Fq -- '--concurrency 1' "$workflow_file"

0 commit comments

Comments
 (0)