diff --git a/.github/workflows/invoke-cloud-run.yml b/.github/workflows/invoke-cloud-run.yml index 8f9de5f..ac88ea7 100644 --- a/.github/workflows/invoke-cloud-run.yml +++ b/.github/workflows/invoke-cloud-run.yml @@ -10,6 +10,7 @@ on: type: choice options: - longbridge-hk + - longbridge-paper - longbridge-sg path: description: "HTTP path to call" @@ -39,7 +40,7 @@ jobs: set -euo pipefail case "${{ inputs.environment }}" in - longbridge-hk|longbridge-sg) ;; + longbridge-hk|longbridge-paper|longbridge-sg) ;; *) echo "Unsupported environment: ${{ inputs.environment }}" >&2 exit 1 diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 0c3a5a2..55d809b 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -10,16 +10,31 @@ env: GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com jobs: - sync-paper: - name: Sync PAPER Cloud Run Env + sync: + name: Sync ${{ matrix.target.label }} Cloud Run Env runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: + - label: PAPER + environment: longbridge-paper + default_account_region: PAPER + - label: HK + environment: longbridge-hk + default_account_region: HK + - label: SG + environment: longbridge-sg + default_account_region: SG permissions: contents: read id-token: write - environment: longbridge-paper + environment: ${{ matrix.target.environment }} env: + DEPLOYMENT_LABEL: ${{ matrix.target.label }} + GITHUB_ENVIRONMENT_NAME: ${{ matrix.target.environment }} ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }} - # Set CLOUD_RUN_REGION per Environment so paper/SG can target different regions. + # Set CLOUD_RUN_REGION per Environment so paper/HK/SG can target different regions. CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }} ACCOUNT_PREFIX: ${{ vars.ACCOUNT_PREFIX }} @@ -27,7 +42,7 @@ jobs: LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }} LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }} RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }} - ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || 'PAPER' }} + ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || matrix.target.default_account_region }} LONGPORT_SECRET_NAME: ${{ vars.LONGPORT_SECRET_NAME }} LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }} LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }} @@ -50,7 +65,7 @@ jobs: if [ "${ENABLE_GITHUB_ENV_SYNC:-}" != "true" ]; then echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "Skipping PAPER Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2 + echo "Skipping ${DEPLOYMENT_LABEL} Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2 exit 0 fi @@ -93,342 +108,15 @@ jobs: if not profile: raise SystemExit("RUNTIME_TARGET_JSON.strategy_profile is required") canonical_profile = resolve_canonical_profile(profile) - - raw_status = subprocess.check_output( - [sys.executable, "scripts/print_strategy_profile_status.py", "--json"], - text=True, - ) - rows = json.loads(raw_status) - selected = next((row for row in rows if row["canonical_profile"] == canonical_profile), None) - if selected is None: - supported = ", ".join(sorted(row["canonical_profile"] for row in rows)) - raise SystemExit(f"Unsupported STRATEGY_PROFILE={profile!r}; supported: {supported}") - if not selected.get("eligible") or not selected.get("enabled"): - raise SystemExit(f"STRATEGY_PROFILE={profile!r} is not eligible/enabled: {selected}") - - output_path = os.environ["GITHUB_OUTPUT"] - with open(output_path, "a", encoding="utf-8") as output: - output.write( - f"requires_snapshot_artifacts={str(bool(selected.get('requires_snapshot_artifacts'))).lower()}\n" + runtime_target["strategy_profile"] = canonical_profile + + expected_service = os.environ.get("CLOUD_RUN_SERVICE", "").strip() + configured_service = str(runtime_target.get("service_name") or "").strip() + if configured_service and expected_service and configured_service != expected_service: + raise SystemExit( + "RUNTIME_TARGET_JSON.service_name does not match CLOUD_RUN_SERVICE: " + f"{configured_service!r} != {expected_service!r}" ) - output.write( - f"requires_snapshot_manifest_path={str(bool(selected.get('requires_snapshot_manifest_path'))).lower()}\n" - ) - output.write( - f"requires_strategy_config_path={str(bool(selected.get('requires_strategy_config_path'))).lower()}\n" - ) - output.write( - f"config_source_policy={str(selected.get('config_source_policy') or 'none')}\n" - ) - output.write(f"runtime_target_json={json.dumps(runtime_target, sort_keys=True)}\n") - PY - - - name: Validate PAPER env sync inputs - if: steps.config.outputs.enabled == 'true' - env: - REQUIRES_SNAPSHOT_ARTIFACTS: ${{ steps.strategy_requirements.outputs.requires_snapshot_artifacts }} - REQUIRES_SNAPSHOT_MANIFEST_PATH: ${{ steps.strategy_requirements.outputs.requires_snapshot_manifest_path }} - REQUIRES_STRATEGY_CONFIG_PATH: ${{ steps.strategy_requirements.outputs.requires_strategy_config_path }} - CONFIG_SOURCE_POLICY: ${{ steps.strategy_requirements.outputs.config_source_policy }} - RUNTIME_TARGET_JSON: ${{ steps.strategy_requirements.outputs.runtime_target_json }} - run: | - set -euo pipefail - - required_vars=( - CLOUD_RUN_REGION - CLOUD_RUN_SERVICE - ACCOUNT_PREFIX - LONGPORT_SECRET_NAME - NOTIFY_LANG - GLOBAL_TELEGRAM_CHAT_ID - ) - - missing_vars=() - for var_name in "${required_vars[@]}"; do - if [ -z "${!var_name:-}" ]; then - missing_vars+=("${var_name}") - fi - done - - if [ -z "${TELEGRAM_TOKEN_SECRET_NAME:-}" ] && [ -z "${TELEGRAM_TOKEN:-}" ]; then - missing_vars+=("TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN") - fi - - if [ -z "${LONGPORT_APP_KEY_SECRET_NAME:-}" ]; then - missing_vars+=("LONGPORT_APP_KEY_SECRET_NAME") - fi - - if [ -z "${LONGPORT_APP_SECRET_SECRET_NAME:-}" ]; then - missing_vars+=("LONGPORT_APP_SECRET_SECRET_NAME") - fi - - if [ "${REQUIRES_SNAPSHOT_ARTIFACTS:-}" = "true" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then - missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH") - fi - - if [ "${REQUIRES_SNAPSHOT_MANIFEST_PATH:-}" = "true" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH:-}" ]; then - missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH") - fi - - if [ "${REQUIRES_STRATEGY_CONFIG_PATH:-}" = "true" ] \ - && [ "${CONFIG_SOURCE_POLICY:-}" = "env_only" ] \ - && [ -z "${LONGBRIDGE_STRATEGY_CONFIG_PATH:-}" ]; then - missing_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH") - fi - - if [ "${#missing_vars[@]}" -gt 0 ]; then - echo "PAPER Cloud Run env sync is enabled, but these values are missing:" >&2 - echo " - If paper and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-paper Environment." >&2 - echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-paper Environment so paper does not fall back to shared repository defaults." >&2 - printf ' - %s\n' "${missing_vars[@]}" >&2 - exit 1 - fi - - - name: Authenticate to Google Cloud - id: auth - if: steps.config.outputs.enabled == 'true' - uses: google-github-actions/auth@v3 - with: - workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} - - - name: Set up gcloud - if: steps.config.outputs.enabled == 'true' - uses: google-github-actions/setup-gcloud@v3 - with: - project_id: ${{ env.GCP_PROJECT_ID }} - version: ">= 416.0.0" - - - name: Wait for Cloud Run deployment of current commit - if: steps.config.outputs.enabled == 'true' - run: | - set -euo pipefail - - 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 - - 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:-}" >&2 - exit 1 - fi - - echo "Waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-}" >&2 - sleep 10 - done - - - name: Sync Cloud Run environment - if: steps.config.outputs.enabled == 'true' - env: - RUNTIME_TARGET_JSON: ${{ steps.strategy_requirements.outputs.runtime_target_json }} - run: | - set -euo pipefail - - join_by_delimiter() { - local delimiter="$1" - shift - local output="" - local item - for item in "$@"; do - if [ -z "${output}" ]; then - output="${item}" - else - output="${output}${delimiter}${item}" - fi - done - printf '%s' "${output}" - } - - env_pairs=( - "GLOBAL_TELEGRAM_CHAT_ID=${GLOBAL_TELEGRAM_CHAT_ID}" - "NOTIFY_LANG=${NOTIFY_LANG}" - "LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}" - "ACCOUNT_PREFIX=${ACCOUNT_PREFIX}" - "ACCOUNT_REGION=${ACCOUNT_REGION}" - "RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}" - ) - secret_pairs=() - remove_env_vars=( - "TELEGRAM_CHAT_ID" - "SERVICE_NAME" - "LONGBRIDGE_FRACTIONAL_SHARES_ENABLED" - "LONGBRIDGE_ORDER_QUANTITY_STEP" - "LONGBRIDGE_MIN_ORDER_NOTIONAL_USD" - ) - remove_secret_vars=() - - if [ -n "${TELEGRAM_TOKEN_SECRET_NAME:-}" ]; then - secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest") - remove_env_vars+=("TELEGRAM_TOKEN") - else - env_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN}") - remove_secret_vars+=("TELEGRAM_TOKEN") - fi - - secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest") - remove_env_vars+=("LONGPORT_APP_KEY") - - secret_pairs+=("LONGPORT_APP_SECRET=${LONGPORT_APP_SECRET_SECRET_NAME}:latest") - remove_env_vars+=("LONGPORT_APP_SECRET") - - if [ -n "${EXECUTION_REPORT_GCS_URI:-}" ]; then - env_pairs+=("EXECUTION_REPORT_GCS_URI=${EXECUTION_REPORT_GCS_URI}") - else - remove_env_vars+=("EXECUTION_REPORT_GCS_URI") - fi - - if [ -n "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then - env_pairs+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH=${LONGBRIDGE_FEATURE_SNAPSHOT_PATH}") - else - remove_env_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH") - fi - - if [ -n "${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH:-}" ]; then - env_pairs+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH=${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH}") - else - remove_env_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH") - fi - - if [ -n "${LONGBRIDGE_STRATEGY_CONFIG_PATH:-}" ]; then - env_pairs+=("LONGBRIDGE_STRATEGY_CONFIG_PATH=${LONGBRIDGE_STRATEGY_CONFIG_PATH}") - else - remove_env_vars+=("LONGBRIDGE_STRATEGY_CONFIG_PATH") - fi - - if [ -n "${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON:-}" ]; then - env_pairs+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON=${LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON}") - else - remove_env_vars+=("LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON") - fi - - if [ -n "${LONGBRIDGE_DRY_RUN_ONLY:-}" ]; then - env_pairs+=("LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}") - else - remove_env_vars+=("LONGBRIDGE_DRY_RUN_ONLY") - fi - if [ -n "${LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET:-}" ]; then - env_pairs+=("LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=${LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET}") - else - remove_env_vars+=("LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET") - fi - - if [ -n "${INCOME_THRESHOLD_USD:-}" ]; then - env_pairs+=("INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}") - else - remove_env_vars+=("INCOME_THRESHOLD_USD") - fi - - if [ -n "${QQQI_INCOME_RATIO:-}" ]; then - env_pairs+=("QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}") - else - remove_env_vars+=("QQQI_INCOME_RATIO") - fi - - gcloud_args=( - run services update "${CLOUD_RUN_SERVICE}" - --region "${CLOUD_RUN_REGION}" - --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 [ "${#secret_pairs[@]}" -gt 0 ]; then - gcloud_args+=(--update-secrets "$(IFS=,; echo "${secret_pairs[*]}")") - fi - - gcloud "${gcloud_args[@]}" - - sync-sg: - name: Sync SG Cloud Run Env - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - environment: longbridge-sg - env: - ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }} - # Set CLOUD_RUN_REGION per Environment so paper/SG can target different regions. - CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} - CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }} - ACCOUNT_PREFIX: ${{ vars.ACCOUNT_PREFIX }} - TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }} - LONGPORT_APP_KEY_SECRET_NAME: ${{ vars.LONGPORT_APP_KEY_SECRET_NAME }} - LONGPORT_APP_SECRET_SECRET_NAME: ${{ vars.LONGPORT_APP_SECRET_SECRET_NAME }} - RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }} - ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || 'SG' }} - LONGPORT_SECRET_NAME: ${{ vars.LONGPORT_SECRET_NAME }} - LONGBRIDGE_FEATURE_SNAPSHOT_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_PATH }} - LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH }} - LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }} - LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }} - INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }} - QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }} - NOTIFY_LANG: ${{ vars.NOTIFY_LANG }} - EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }} - LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }} - LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET: ${{ vars.LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET }} - GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }} - TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} - steps: - - name: Check whether env sync is enabled - id: config - run: | - set -euo pipefail - - if [ "${ENABLE_GITHUB_ENV_SYNC:-}" != "true" ]; then - echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "Skipping SG Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2 - exit 0 - fi - - echo "enabled=true" >> "$GITHUB_OUTPUT" - - - name: Checkout repository - if: steps.config.outputs.enabled == 'true' - uses: actions/checkout@v6 - - - name: Set up Python for strategy requirement resolution - if: steps.config.outputs.enabled == 'true' - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install strategy status dependencies - if: steps.config.outputs.enabled == 'true' - run: | - set -euo pipefail - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - - - name: Resolve selected strategy runtime requirements - id: strategy_requirements - if: steps.config.outputs.enabled == 'true' - run: | - set -euo pipefail - python - <<'PY' - import json - import os - import subprocess - import sys - from us_equity_strategies import resolve_canonical_profile - - raw_runtime_target = os.environ.get("RUNTIME_TARGET_JSON", "").strip() - if not raw_runtime_target: - raise SystemExit("RUNTIME_TARGET_JSON is required") - runtime_target = json.loads(raw_runtime_target) - profile = str(runtime_target.get("strategy_profile") or "").strip().lower() - if not profile: - raise SystemExit("RUNTIME_TARGET_JSON.strategy_profile is required") - canonical_profile = resolve_canonical_profile(profile) raw_status = subprocess.check_output( [sys.executable, "scripts/print_strategy_profile_status.py", "--json"], @@ -444,6 +132,7 @@ jobs: output_path = os.environ["GITHUB_OUTPUT"] with open(output_path, "a", encoding="utf-8") as output: + output.write(f"canonical_profile={canonical_profile}\n") output.write( f"requires_snapshot_artifacts={str(bool(selected.get('requires_snapshot_artifacts'))).lower()}\n" ) @@ -456,11 +145,10 @@ jobs: output.write( f"config_source_policy={str(selected.get('config_source_policy') or 'none')}\n" ) - normalized_region = os.environ.get("ACCOUNT_REGION", "").strip().upper() output.write(f"runtime_target_json={json.dumps(runtime_target, sort_keys=True)}\n") PY - - name: Validate SG env sync inputs + - name: Validate env sync inputs if: steps.config.outputs.enabled == 'true' env: REQUIRES_SNAPSHOT_ARTIFACTS: ${{ steps.strategy_requirements.outputs.requires_snapshot_artifacts }} @@ -518,9 +206,9 @@ jobs: fi if [ "${#missing_vars[@]}" -gt 0 ]; then - echo "SG Cloud Run env sync is enabled, but these values are missing:" >&2 - echo " - If paper and SG run in different regions, set CLOUD_RUN_REGION on the longbridge-sg Environment." >&2 - echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-sg Environment so SG does not fall back to shared repository defaults." >&2 + echo "${DEPLOYMENT_LABEL} Cloud Run env sync is enabled, but these values are missing:" >&2 + echo " - Set CLOUD_RUN_REGION on the ${GITHUB_ENVIRONMENT_NAME} Environment so each service can target its own region." >&2 + echo " - Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the ${GITHUB_ENVIRONMENT_NAME} Environment so credentials do not fall back to shared defaults." >&2 printf ' - %s\n' "${missing_vars[@]}" >&2 exit 1 fi @@ -567,6 +255,7 @@ jobs: - name: Sync Cloud Run environment if: steps.config.outputs.enabled == 'true' env: + STRATEGY_PROFILE: ${{ steps.strategy_requirements.outputs.canonical_profile }} RUNTIME_TARGET_JSON: ${{ steps.strategy_requirements.outputs.runtime_target_json }} run: | set -euo pipefail @@ -591,6 +280,7 @@ jobs: "NOTIFY_LANG=${NOTIFY_LANG}" "LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}" "ACCOUNT_PREFIX=${ACCOUNT_PREFIX}" + "STRATEGY_PROFILE=${STRATEGY_PROFILE}" "ACCOUNT_REGION=${ACCOUNT_REGION}" "RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}" ) diff --git a/README.md b/README.md index b365db5..0307c63 100644 --- a/README.md +++ b/README.md @@ -119,20 +119,20 @@ Recommended setup: - **Repository Secrets (shared):** - Optional fallback only: `TELEGRAM_TOKEN` - **GitHub Environment: `longbridge-paper`** - - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` + - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) - Current live example: `STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced` - Recommended secret-name values: `longport-app-key-paper`, `longport-app-secret-paper` - **GitHub Environment: `longbridge-sg`** - - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` + - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) - Current live example: `STRATEGY_PROFILE=soxl_soxx_trend_income` - Recommended secret-name values: `longport-app-key-sg`, `longport-app-secret-sg` -- **GitHub Environment: `longbridge-hk`** (reserved for later) - - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` +- **GitHub Environment: `longbridge-hk`** + - Variables: `CLOUD_RUN_REGION`, `CLOUD_RUN_SERVICE`, `ACCOUNT_PREFIX`, `ACCOUNT_REGION`, `RUNTIME_TARGET_JSON`, `STRATEGY_PROFILE`, `LONGPORT_SECRET_NAME`, `LONGPORT_APP_KEY_SECRET_NAME`, `LONGPORT_APP_SECRET_SECRET_NAME` - Optional variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`, `LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`, `LONGBRIDGE_STRATEGY_CONFIG_PATH`, `LONGBRIDGE_DRY_RUN_ONLY`, `LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`, `INCOME_THRESHOLD_USD`, `QQQI_INCOME_RATIO` (strategy overrides only; leave unset to inherit `UsEquityStrategies`) - - Current target example: `STRATEGY_PROFILE=` - - Recommended secret-name values: `longport-app-key-paper`, `longport-app-secret-paper` + - Current live example: `STRATEGY_PROFILE=tech_communication_pullback_enhancement` + - Recommended secret-name values: `longport-app-key-hk`, `longport-app-secret-hk` On every push to `main`, the workflow updates the configured Cloud Run services with the shared and per-environment values above, and removes `TELEGRAM_CHAT_ID` from each Cloud Run service. @@ -151,8 +151,8 @@ Important: - `QuantPlatformKit` is only a shared dependency; Cloud Run still deploys `LongBridgePlatform` itself. - Recommended Cloud Run service names: `longbridge-quant-paper-service`, `longbridge-quant-hk-service`, and `longbridge-quant-sg-service`. -- Keep using two triggers and two GitHub Environments today; add `HK` with the same pattern later. The split key is still `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`, and the runtime identity is now explicit through `RUNTIME_TARGET_JSON` with `STRATEGY_PROFILE + ACCOUNT_REGION` kept for compatibility. -- If you later rename or move this repository, rebuild the GitHub source binding in Google Cloud for both triggers instead of assuming the existing source binding will follow the rename. +- Keep using three triggers and three GitHub Environments today: `longbridge-paper`, `longbridge-hk`, and `longbridge-sg`. The split key is still `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`, and the runtime identity is now explicit through `RUNTIME_TARGET_JSON` with `STRATEGY_PROFILE + ACCOUNT_REGION` kept for compatibility. +- If you later rename or move this repository, rebuild the GitHub source binding in Google Cloud for all Cloud Build triggers instead of assuming the existing source binding will follow the rename. - For the shared deployment model and trigger migration checklist, see [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md). ### Quick deploy @@ -279,19 +279,19 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo - **仓库级 Secrets(共享):** - 仅保留为 fallback:`TELEGRAM_TOKEN` - **GitHub Environment: `longbridge-paper`** - - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` + - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) - 当前线上示例:`STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced` - 建议的 secret-name 值:`longport-app-key-paper`、`longport-app-secret-paper` - **GitHub Environment: `longbridge-sg`** - - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` + - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) - 当前线上示例:`STRATEGY_PROFILE=soxl_soxx_trend_income` - 建议的 secret-name 值:`longport-app-key-sg`、`longport-app-secret-sg` -- **GitHub Environment: `longbridge-hk`**(保留给后续补齐) - - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` +- **GitHub Environment: `longbridge-hk`** + - Variables: `CLOUD_RUN_REGION`、`CLOUD_RUN_SERVICE`、`ACCOUNT_PREFIX`、`ACCOUNT_REGION`、`RUNTIME_TARGET_JSON`、`STRATEGY_PROFILE`、`LONGPORT_SECRET_NAME`、`LONGPORT_APP_KEY_SECRET_NAME`、`LONGPORT_APP_SECRET_SECRET_NAME` - 可选 Variables: `LONGBRIDGE_FEATURE_SNAPSHOT_PATH`、`LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH`、`LONGBRIDGE_STRATEGY_CONFIG_PATH`、`LONGBRIDGE_DRY_RUN_ONLY`、`LONGBRIDGE_DEBUG_POSITION_SNAPSHOT`、`INCOME_THRESHOLD_USD`、`QQQI_INCOME_RATIO`(仅策略 override;不填则继承 `UsEquityStrategies`) - - 当前目标示例:`STRATEGY_PROFILE=` + - 当前线上示例:`STRATEGY_PROFILE=tech_communication_pullback_enhancement` - 建议的 secret-name 值:`longport-app-key-hk`、`longport-app-secret-hk` 每次 push 到 `main` 时,这个 workflow 会更新配置的 Cloud Run 服务,把共享和各自隔离的变量同步进去,并删除旧的 `TELEGRAM_CHAT_ID`。 @@ -310,8 +310,8 @@ Secret Manager 中需存在 `LONGPORT_SECRET_NAME` 指定的密钥(默认: `lo - `QuantPlatformKit` 只是共享依赖,不单独部署;Cloud Run 继续只部署 `LongBridgePlatform`。 - 推荐 Cloud Run 服务名:`longbridge-quant-paper-service`、`longbridge-quant-hk-service` 和 `longbridge-quant-sg-service`。 -- 继续保留两个 trigger 和两个 GitHub Environment today;HK 补齐后再按同样模式扩展第三个环境。区分键始终是 `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`,运行身份再通过 `STRATEGY_PROFILE + ACCOUNT_REGION` 明确下来。 -- 如果后面改 GitHub 仓库名或再次迁组织,Google Cloud 里的两个 trigger 都要重新选择 GitHub 来源,不要假设旧绑定会自动跟过去。 +- 现在使用三个 trigger 和三个 GitHub Environment:`longbridge-paper`、`longbridge-hk`、`longbridge-sg`。区分键始终是 `CLOUD_RUN_SERVICE + CLOUD_RUN_REGION`,运行身份通过 `RUNTIME_TARGET_JSON` 明确,`STRATEGY_PROFILE + ACCOUNT_REGION` 保留为兼容输入。 +- 如果后面改 GitHub 仓库名或再次迁组织,Google Cloud 里的所有 Cloud Build trigger 都要重新选择 GitHub 来源,不要假设旧绑定会自动跟过去。 - 统一部署模型和触发器迁移清单见 [`QuantPlatformKit/docs/deployment_model.md`](../QuantPlatformKit/docs/deployment_model.md)。 ### 快速部署 diff --git a/tests/test_invoke_cloud_run_workflow.sh b/tests/test_invoke_cloud_run_workflow.sh index 531c710..26c6e3d 100644 --- a/tests/test_invoke_cloud_run_workflow.sh +++ b/tests/test_invoke_cloud_run_workflow.sh @@ -12,7 +12,7 @@ grep -Fq "google-github-actions/auth@v3" "$workflow_file" grep -Fq "google-github-actions/setup-gcloud@v3" "$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 "longbridge-hk|longbridge-sg" "$workflow_file" +grep -Fq "longbridge-hk|longbridge-paper|longbridge-sg" "$workflow_file" grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\"" "$workflow_file" grep -Fq "token_format: id_token" "$workflow_file" grep -Fq "id_token_audience: \${{ steps.service.outputs.url }}" "$workflow_file" diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 8c30532..796bbc6 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -6,6 +6,12 @@ workflow_file="$repo_dir/.github/workflows/sync-cloud-run-env.yml" grep -Fq 'GCP_WORKLOAD_IDENTITY_PROVIDER: projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main' "$workflow_file" grep -Fq 'GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com' "$workflow_file" +grep -Fq 'name: Sync ${{ matrix.target.label }} Cloud Run Env' "$workflow_file" +grep -Fq 'fail-fast: false' "$workflow_file" +grep -Fq 'environment: longbridge-paper' "$workflow_file" +grep -Fq 'environment: longbridge-hk' "$workflow_file" +grep -Fq 'environment: longbridge-sg' "$workflow_file" +grep -Fq 'environment: ${{ matrix.target.environment }}' "$workflow_file" grep -Fq 'permissions:' "$workflow_file" grep -Fq 'id-token: write' "$workflow_file" grep -Fq 'workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}' "$workflow_file" @@ -21,13 +27,12 @@ grep -Fq 'requires_snapshot_artifacts=' "$workflow_file" grep -Fq 'requires_snapshot_manifest_path=' "$workflow_file" grep -Fq 'requires_strategy_config_path=' "$workflow_file" grep -Fq 'config_source_policy=' "$workflow_file" +grep -Fq 'canonical_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:-}' "$workflow_file" -grep -Fq "environment: longbridge-paper" "$workflow_file" -grep -Fq "environment: longbridge-sg" "$workflow_file" grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file" grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" @@ -44,17 +49,12 @@ grep -Fq 'QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }}' "$workflow_file" grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET: ${{ vars.LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET }}' "$workflow_file" grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file" -grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'PAPER' }}" "$workflow_file" -grep -Fq "ACCOUNT_REGION: \${{ vars.ACCOUNT_REGION || 'SG' }}" "$workflow_file" +grep -Fq 'ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || matrix.target.default_account_region }}' "$workflow_file" grep -Fq 'echo "enabled=false" >> "$GITHUB_OUTPUT"' "$workflow_file" -grep -Fq "Skipping PAPER Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" -grep -Fq "Skipping SG Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" -grep -Fq "PAPER Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" -grep -Fq "SG Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" -grep -Fq "set CLOUD_RUN_REGION on the longbridge-paper Environment" "$workflow_file" -grep -Fq "set CLOUD_RUN_REGION on the longbridge-sg Environment" "$workflow_file" -grep -Fq "Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-paper Environment" "$workflow_file" -grep -Fq "Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the longbridge-sg Environment" "$workflow_file" +grep -Fq 'Skipping ${DEPLOYMENT_LABEL} Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true.' "$workflow_file" +grep -Fq '${DEPLOYMENT_LABEL} Cloud Run env sync is enabled, but these values are missing:' "$workflow_file" +grep -Fq 'Set CLOUD_RUN_REGION on the ${GITHUB_ENVIRONMENT_NAME} Environment so each service can target its own region.' "$workflow_file" +grep -Fq 'Set LONGPORT_APP_KEY_SECRET_NAME and LONGPORT_APP_SECRET_SECRET_NAME on the ${GITHUB_ENVIRONMENT_NAME} Environment so credentials do not fall back to shared defaults.' "$workflow_file" grep -Fq "if: steps.config.outputs.enabled == 'true'" "$workflow_file" grep -Fq 'missing_vars+=("TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN")' "$workflow_file" grep -Fq 'missing_vars+=("LONGPORT_APP_KEY_SECRET_NAME")' "$workflow_file" @@ -66,6 +66,7 @@ grep -Fq 'REQUIRES_SNAPSHOT_ARTIFACTS: ${{ steps.strategy_requirements.outputs.r grep -Fq 'REQUIRES_SNAPSHOT_MANIFEST_PATH: ${{ steps.strategy_requirements.outputs.requires_snapshot_manifest_path }}' "$workflow_file" grep -Fq 'REQUIRES_STRATEGY_CONFIG_PATH: ${{ steps.strategy_requirements.outputs.requires_strategy_config_path }}' "$workflow_file" grep -Fq 'CONFIG_SOURCE_POLICY: ${{ steps.strategy_requirements.outputs.config_source_policy }}' "$workflow_file" +grep -Fq 'STRATEGY_PROFILE: ${{ steps.strategy_requirements.outputs.canonical_profile }}' "$workflow_file" grep -Fq 'RUNTIME_TARGET_JSON: ${{ steps.strategy_requirements.outputs.runtime_target_json }}' "$workflow_file" grep -Fq 'if [ "${REQUIRES_SNAPSHOT_ARTIFACTS:-}" = "true" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then' "$workflow_file" grep -Fq 'if [ "${REQUIRES_SNAPSHOT_MANIFEST_PATH:-}" = "true" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH:-}" ]; then' "$workflow_file" @@ -83,6 +84,7 @@ grep -Fq 'LONGBRIDGE_DRY_RUN_ONLY=${LONGBRIDGE_DRY_RUN_ONLY}' "$workflow_file" grep -Fq 'LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET=${LONGBRIDGE_FRACTIONAL_LIMIT_BUY_FALLBACK_TO_MARKET}' "$workflow_file" grep -Fq 'INCOME_THRESHOLD_USD=${INCOME_THRESHOLD_USD}' "$workflow_file" grep -Fq 'QQQI_INCOME_RATIO=${QQQI_INCOME_RATIO}' "$workflow_file" +grep -Fq 'STRATEGY_PROFILE=${STRATEGY_PROFILE}' "$workflow_file" grep -Fq 'ACCOUNT_REGION=${ACCOUNT_REGION}' "$workflow_file" grep -Fq 'RUNTIME_TARGET_JSON=${RUNTIME_TARGET_JSON}' "$workflow_file" grep -Fq '"LONGBRIDGE_FRACTIONAL_SHARES_ENABLED"' "$workflow_file"