Skip to content
Merged
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
121 changes: 92 additions & 29 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}
CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}
CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}
CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT: ${{ vars.CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT }}
TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}
RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}
ACCOUNT_GROUP: ${{ vars.ACCOUNT_GROUP }}
Expand Down Expand Up @@ -168,13 +170,28 @@ jobs:

required_vars=(
CLOUD_RUN_REGION
CLOUD_RUN_SERVICE
ACCOUNT_GROUP
IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME
GLOBAL_TELEGRAM_CHAT_ID
NOTIFY_LANG
)

resolve_cloud_run_services() {
local raw_services="${CLOUD_RUN_SERVICES:-}"
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
raw_services="${CLOUD_RUN_SERVICE:-}"
fi
printf '%s\n' "${raw_services}" \
| tr ',;' '\n' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sed '/^$/d'
}

mapfile -t target_services < <(resolve_cloud_run_services)
if [ "${#target_services[@]}" -eq 0 ]; then
required_vars+=("CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle missing service vars without invalid indirection

Do not append the literal string "CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE" to required_vars here, because the later loop dereferences each entry via ${!var_name:-}. When neither service variable is set, this value is not a valid shell identifier, so bash exits with invalid variable name before producing the intended missing-variable report. This breaks validation behavior in the exact misconfiguration path this check is meant to handle.

Useful? React with 👍 / 👎.

fi

if [ "${REQUIRES_SNAPSHOT_ARTIFACTS:-}" = "true" ]; then
required_vars+=(IBKR_FEATURE_SNAPSHOT_PATH)
fi
Expand Down Expand Up @@ -232,23 +249,50 @@ jobs:
run: |
set -euo pipefail

resolve_cloud_run_services() {
local raw_services="${CLOUD_RUN_SERVICES:-}"
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
raw_services="${CLOUD_RUN_SERVICE:-}"
fi
printf '%s\n' "${raw_services}" \
| tr ',;' '\n' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sed '/^$/d'
}

mapfile -t target_services < <(resolve_cloud_run_services)
if [ "${#target_services[@]}" -eq 0 ]; then
echo "CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE is required." >&2
exit 1
fi

case "${CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT:-true}" in
false|False|FALSE|0|no|No|NO|off|Off|OFF)
echo "Skipping Cloud Run commit wait because CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT is disabled."
exit 0
;;
esac

target_sha="${GITHUB_SHA}"
deadline=$((SECONDS + 1800))

while true; do
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)"
if [ -n "${deployed_sha}" ] && [ "${deployed_sha}" = "${target_sha}" ]; then
echo "Cloud Run service ${CLOUD_RUN_SERVICE} is on commit ${deployed_sha}."
break
fi
for cloud_run_service in "${target_services[@]}"; do
deadline=$((SECONDS + 1800))

if [ "${SECONDS}" -ge "${deadline}" ]; then
echo "Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
exit 1
fi
while true; do
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)"
if [ -n "${deployed_sha}" ] && [ "${deployed_sha}" = "${target_sha}" ]; then
echo "Cloud Run service ${cloud_run_service} is on commit ${deployed_sha}."
break
fi

echo "Waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
sleep 10
if [ "${SECONDS}" -ge "${deadline}" ]; then
echo "Timed out waiting for Cloud Run service ${cloud_run_service} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
exit 1
fi

echo "Waiting for Cloud Run service ${cloud_run_service} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-<none>}" >&2
sleep 10
done
done

- name: Sync Cloud Run environment
Expand All @@ -259,6 +303,23 @@ jobs:
run: |
set -euo pipefail

resolve_cloud_run_services() {
local raw_services="${CLOUD_RUN_SERVICES:-}"
if [ -z "$(printf '%s' "${raw_services}" | tr -d '[:space:],;')" ]; then
raw_services="${CLOUD_RUN_SERVICE:-}"
fi
printf '%s\n' "${raw_services}" \
| tr ',;' '\n' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sed '/^$/d'
}

mapfile -t target_services < <(resolve_cloud_run_services)
if [ "${#target_services[@]}" -eq 0 ]; then
echo "CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE is required." >&2
exit 1
fi

join_by_delimiter() {
local delimiter="$1"
shift
Expand Down Expand Up @@ -564,21 +625,23 @@ jobs:
remove_env_vars+=("EXECUTION_REPORT_GCS_URI")
fi

gcloud_args=(
run services update "${CLOUD_RUN_SERVICE}"
--region "${CLOUD_RUN_REGION}"
--concurrency 1
--max-instances 1
--remove-env-vars "$(IFS=,; echo "${remove_env_vars[*]}")"
--update-env-vars "^|^$(join_by_delimiter "|" "${env_pairs[@]}")"
)
for cloud_run_service in "${target_services[@]}"; do
gcloud_args=(
run services update "${cloud_run_service}"
--region "${CLOUD_RUN_REGION}"
--concurrency 1
--max-instances 1
--remove-env-vars "$(IFS=,; echo "${remove_env_vars[*]}")"
--update-env-vars "^|^$(join_by_delimiter "|" "${env_pairs[@]}")"
)

if [ "${#remove_secret_vars[@]}" -gt 0 ]; then
gcloud_args+=(--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")")
fi
if [ "${#remove_secret_vars[@]}" -gt 0 ]; then
gcloud_args+=(--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")")
fi

if [ "${#secret_pairs[@]}" -gt 0 ]; then
gcloud_args+=(--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")")
fi
if [ "${#secret_pairs[@]}" -gt 0 ]; then
gcloud_args+=(--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")")
fi

gcloud "${gcloud_args[@]}"
gcloud "${gcloud_args[@]}"
done
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ Recommended setup:
- **Repository Variables**
- `ENABLE_GITHUB_ENV_SYNC` = `true`
- `CLOUD_RUN_REGION`
- `CLOUD_RUN_SERVICE`
- `CLOUD_RUN_SERVICES` (comma-, semicolon-, or newline-separated Cloud Run service names; preferred for slot deployments)
- `CLOUD_RUN_SERVICE` (single-service fallback when `CLOUD_RUN_SERVICES` is not set)
- 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
- `TELEGRAM_TOKEN_SECRET_NAME` (recommended when Cloud Run already uses Secret Manager for `TELEGRAM_TOKEN`)
- `STRATEGY_PROFILE` (set explicitly to one enabled profile, such as `soxl_soxx_trend_income`)
- `ACCOUNT_GROUP` (recommended: `paper`)
Expand All @@ -243,13 +245,14 @@ Recommended setup:
- `IB_GATEWAY_ZONE`
- `IB_GATEWAY_IP_MODE`

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.
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.

`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`.

Important:

- 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.
- 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.
- 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.
- If `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` is set, the Cloud Run runtime needs Secret Manager access to that secret.
- GitHub now authenticates to Google Cloud with OIDC + Workload Identity Federation, so `GCP_SA_KEY` is no longer required for this workflow.
Expand All @@ -258,7 +261,7 @@ Important:
### Deployment unit and naming

- `QuantPlatformKit` is only a shared dependency; Cloud Run now deploys `InteractiveBrokersPlatform`.
- Recommended Cloud Run service name: `interactive-brokers-quant-service`.
- Recommended single-service Cloud Run name: `interactive-brokers-quant-service`. For slot deployments, keep explicit service names in `CLOUD_RUN_SERVICES`.
- For future multi-account rollout, keep one Cloud Run service per `ACCOUNT_GROUP`, and let each service select its account-group config at runtime.
- 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.
- For the shared deployment model and trigger migration checklist, see [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md).
Expand Down Expand Up @@ -444,7 +447,9 @@ IB_GATEWAY_IP_MODE=internal
- **仓库级 Variables**
- `ENABLE_GITHUB_ENV_SYNC` = `true`
- `CLOUD_RUN_REGION`
- `CLOUD_RUN_SERVICE`
- `CLOUD_RUN_SERVICES`(逗号、分号或换行分隔的 Cloud Run 服务名;slot 部署优先用这个)
- `CLOUD_RUN_SERVICE`(没设置 `CLOUD_RUN_SERVICES` 时的单服务兼容入口)
- 可选:`CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT=false`(当目标服务由另一个部署链路管理、不会在同步前更新 `commit-sha` label 时使用)
- `TELEGRAM_TOKEN_SECRET_NAME`(如果 Cloud Run 上的 `TELEGRAM_TOKEN` 已经改成 Secret Manager,建议配置)
- `STRATEGY_PROFILE`(显式设置为任一已启用 profile,例如 `soxl_soxx_trend_income`)
- `ACCOUNT_GROUP`(建议设为 `paper`)
Expand All @@ -461,13 +466,14 @@ IB_GATEWAY_IP_MODE=internal
- `IB_GATEWAY_ZONE`
- `IB_GATEWAY_IP_MODE`

每次 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 上这两个旧值一起删除,避免双配置源漂移。
每次 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 上这两个旧值一起删除,避免双配置源漂移。

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

注意:

- 只有在 `ENABLE_GITHUB_ENV_SYNC=true` 时,这个 workflow 才会严格校验并执行同步。没打开时会直接跳过。打开后,它会通过 `scripts/print_strategy_profile_status.py --json` 动态解析目标策略需要的 snapshot/config 输入,不再维护硬编码策略名列表。
- slot 部署应配置 `CLOUD_RUN_SERVICES`,不要继续依赖旧的 `CLOUD_RUN_SERVICE`。在 GitHub runtime 变量能准确描述每个 slot 前,不要打开全量同步;否则一份共享的 `RUNTIME_TARGET_JSON` 会覆盖各 slot 自己的运行身份。
- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**。`TELEGRAM_TOKEN` 和 `TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。危机告警如果确实跨平台共用同一套收件人和发送方,可以用 GitHub Organization Variables/Secrets 管理 `CRISIS_ALERT_CHANNELS`、`CRISIS_ALERT_EMAIL_*` 和 `CRISIS_ALERT_PUSH_*`。
- 如果设置了 `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`,Cloud Run 运行时还需要有对应 Secret 的访问权限。
- GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`。
Expand Down
10 changes: 8 additions & 2 deletions tests/test_sync_cloud_run_env_workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ grep -Fq 'strategy_profile=' "$workflow_file"
grep -Fq 'runtime_target_json=' "$workflow_file"
grep -Fq 'Wait for Cloud Run deployment of current commit' "$workflow_file"
grep -Fq 'target_sha="${GITHUB_SHA}"' "$workflow_file"
grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\" --region \"\${CLOUD_RUN_REGION}\" --format='value(spec.template.metadata.labels.commit-sha)'" "$workflow_file"
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"
grep -Fq 'mapfile -t target_services < <(resolve_cloud_run_services)' "$workflow_file"
grep -Fq 'CLOUD_RUN_SERVICES or CLOUD_RUN_SERVICE' "$workflow_file"
grep -Fq 'Skipping Cloud Run commit wait because CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT is disabled.' "$workflow_file"
grep -Fq "gcloud run services describe \"\${cloud_run_service}\" --region \"\${CLOUD_RUN_REGION}\" --format='value(spec.template.metadata.labels.commit-sha)'" "$workflow_file"
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"
grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file"
grep -Fq 'CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }}' "$workflow_file"
grep -Fq 'CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}' "$workflow_file"
grep -Fq 'CLOUD_RUN_SERVICES: ${{ vars.CLOUD_RUN_SERVICES }}' "$workflow_file"
grep -Fq 'CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT: ${{ vars.CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT }}' "$workflow_file"
grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }}' "$workflow_file"
grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file"
grep -Fq 'ACCOUNT_GROUP: ${{ vars.ACCOUNT_GROUP }}' "$workflow_file"
Expand Down Expand Up @@ -193,6 +198,7 @@ grep -Fq '"IB_CLIENT_ID"' "$workflow_file"
grep -Fq '"IB_GATEWAY_INSTANCE_NAME"' "$workflow_file"
grep -Fq '"IB_GATEWAY_MODE"' "$workflow_file"
grep -Fq 'join_by_delimiter()' "$workflow_file"
grep -Fq 'run services update "${cloud_run_service}"' "$workflow_file"
grep -Fq -- '--remove-secrets "$(IFS=,; echo "${remove_secret_vars[*]}")"' "$workflow_file"
grep -Fq -- '--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")"' "$workflow_file"
grep -Fq -- '--concurrency 1' "$workflow_file"
Expand Down