Deploy Cloud Run #509
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Cloud Run | |
| on: | |
| workflow_run: | |
| workflows: [CI] | |
| types: [completed] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: "Deployment target to run. Use hk-verify for an isolated HK dry-run Cloud Run service." | |
| required: true | |
| type: choice | |
| default: configured | |
| options: | |
| - configured | |
| - hk-verify | |
| cloud_run_region: | |
| description: "Cloud Run region for hk-verify. Leave blank to use the longbridge-hk Environment value." | |
| required: false | |
| type: string | |
| cloud_run_service: | |
| description: "Cloud Run service for hk-verify." | |
| required: false | |
| type: string | |
| default: longbridge-quant-hk-verify-service | |
| longport_secret_name: | |
| description: "LongPort token Secret Manager secret name for hk-verify." | |
| required: false | |
| type: string | |
| default: longport_token_hk | |
| longport_app_key_secret_name: | |
| description: "LongPort app-key Secret Manager secret name for hk-verify." | |
| required: false | |
| type: string | |
| default: longport-app-key-hk | |
| longport_app_secret_secret_name: | |
| description: "LongPort app-secret Secret Manager secret name for hk-verify." | |
| required: false | |
| type: string | |
| default: longport-app-secret-hk | |
| deploy_image: | |
| description: "Build and deploy the container image for hk-verify." | |
| required: true | |
| type: boolean | |
| default: true | |
| sync_env: | |
| description: "Sync hk-verify Cloud Run environment variables." | |
| required: true | |
| type: boolean | |
| default: true | |
| env: | |
| GCP_PROJECT_ID: longbridgequant | |
| GCP_WORKLOAD_IDENTITY_PROVIDER: projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main | |
| GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: longbridge-platform-deploy@longbridgequant.iam.gserviceaccount.com | |
| GCP_RUNTIME_SERVICE_ACCOUNT: longbridge-platform-runtime@longbridgequant.iam.gserviceaccount.com | |
| GCP_SCHEDULER_SERVICE_ACCOUNT: longbridge-platform-scheduler@longbridgequant.iam.gserviceaccount.com | |
| GCP_ARTIFACT_REGISTRY_REPOSITORY: cloud-run-source-deploy | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| sync: | |
| name: Deploy / Sync ${{ matrix.target.label }} Cloud Run | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| ( | |
| github.event_name == 'workflow_run' && | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push' | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| 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: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| environment: ${{ matrix.target.environment }} | |
| env: | |
| DEPLOYMENT_LABEL: ${{ matrix.target.label }} | |
| GITHUB_ENVIRONMENT_NAME: ${{ matrix.target.environment }} | |
| ENABLE_GITHUB_CLOUD_RUN_DEPLOY: ${{ vars.ENABLE_GITHUB_CLOUD_RUN_DEPLOY }} | |
| ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }} | |
| ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION: ${{ vars.ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION }} | |
| QSL_ENABLE_CLOUD_RUN_AUTOMATION: ${{ vars.QSL_ENABLE_CLOUD_RUN_AUTOMATION }} | |
| WORKFLOW_TARGET: ${{ inputs.target || 'configured' }} | |
| INPUT_CLOUD_RUN_REGION: ${{ inputs.cloud_run_region }} | |
| INPUT_CLOUD_RUN_SERVICE: ${{ inputs.cloud_run_service }} | |
| INPUT_LONGPORT_SECRET_NAME: ${{ inputs.longport_secret_name }} | |
| INPUT_LONGPORT_APP_KEY_SECRET_NAME: ${{ inputs.longport_app_key_secret_name }} | |
| INPUT_LONGPORT_APP_SECRET_SECRET_NAME: ${{ inputs.longport_app_secret_secret_name }} | |
| INPUT_DEPLOY_IMAGE: ${{ inputs.deploy_image }} | |
| INPUT_SYNC_ENV: ${{ inputs.sync_env }} | |
| GCP_ARTIFACT_REGISTRY_HOSTNAME: ${{ vars.GCP_ARTIFACT_REGISTRY_HOSTNAME }} | |
| # 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 }} | |
| CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }} | |
| CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT: ${{ vars.CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT }} | |
| CLOUD_SCHEDULER_LOCATION: ${{ vars.CLOUD_SCHEDULER_LOCATION }} | |
| CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }} | |
| CLOUD_SCHEDULER_PROBE_TIME: ${{ vars.CLOUD_SCHEDULER_PROBE_TIME }} | |
| CLOUD_SCHEDULER_PRECHECK_TIME: ${{ vars.CLOUD_SCHEDULER_PRECHECK_TIME }} | |
| MONITOR_DISPATCHER_OWNER_LABEL: ${{ vars.MONITOR_DISPATCHER_OWNER_LABEL || 'SG' }} | |
| 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 || 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 }} | |
| LONGBRIDGE_FEATURE_SNAPSHOT_FALLBACK_MODE: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_FALLBACK_MODE }} | |
| LONGBRIDGE_FEATURE_SNAPSHOT_FALLBACK_CACHE_DIR: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_FALLBACK_CACHE_DIR }} | |
| LONGBRIDGE_FEATURE_SNAPSHOT_MAX_STALE_DAYS: ${{ vars.LONGBRIDGE_FEATURE_SNAPSHOT_MAX_STALE_DAYS }} | |
| LONGBRIDGE_STRATEGY_CONFIG_PATH: ${{ vars.LONGBRIDGE_STRATEGY_CONFIG_PATH }} | |
| LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON }} | |
| LONGBRIDGE_MARKET: ${{ vars.LONGBRIDGE_MARKET }} | |
| LONGBRIDGE_MARKET_CALENDAR: ${{ vars.LONGBRIDGE_MARKET_CALENDAR }} | |
| LONGBRIDGE_MARKET_TIMEZONE: ${{ vars.LONGBRIDGE_MARKET_TIMEZONE }} | |
| LONGBRIDGE_SYMBOL_SUFFIX: ${{ vars.LONGBRIDGE_SYMBOL_SUFFIX }} | |
| LONGBRIDGE_TRADING_CURRENCY: ${{ vars.LONGBRIDGE_TRADING_CURRENCY }} | |
| LONGBRIDGE_MIN_RESERVED_CASH_USD: ${{ vars.LONGBRIDGE_MIN_RESERVED_CASH_USD }} | |
| LONGBRIDGE_RESERVED_CASH_RATIO: ${{ vars.LONGBRIDGE_RESERVED_CASH_RATIO }} | |
| LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }} | |
| LONGBRIDGE_MARKET_SIGNAL_HANDOFF_INDEX_URI: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_HANDOFF_INDEX_URI }} | |
| LONGBRIDGE_MARKET_SIGNAL_HANDOFF_MANIFEST_URI: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_HANDOFF_MANIFEST_URI }} | |
| LONGBRIDGE_MARKET_SIGNAL_CONSUMPTION_AUDIT_URI: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_CONSUMPTION_AUDIT_URI }} | |
| LONGBRIDGE_MARKET_SIGNAL_CACHE_DIR: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_CACHE_DIR }} | |
| LONGBRIDGE_MARKET_SIGNAL_REQUIRED: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_REQUIRED }} | |
| LONGBRIDGE_MARKET_SIGNAL_FALLBACK_MODE: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_FALLBACK_MODE }} | |
| LONGBRIDGE_MARKET_SIGNAL_MAX_STALE_DAYS: ${{ vars.LONGBRIDGE_MARKET_SIGNAL_MAX_STALE_DAYS }} | |
| STRATEGY_PLUGIN_ALERT_CHANNELS: ${{ vars.STRATEGY_PLUGIN_ALERT_CHANNELS }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY: ${{ vars.STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY }} | |
| STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS }} | |
| STRATEGY_PLUGIN_ALERT_SMS_PROVIDER: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_PROVIDER }} | |
| STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID }} | |
| STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN_SECRET_NAME }} | |
| STRATEGY_PLUGIN_ALERT_SMS_SENDER: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_SENDER }} | |
| STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID }} | |
| STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL }} | |
| STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN_SECRET_NAME }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_DEVICE: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_DEVICE }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_TAGS: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_TAGS }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS: ${{ vars.STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS: ${{ vars.STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS }} | |
| # Optional strategy overrides; leave unset to inherit the UsEquityStrategies profile defaults. | |
| INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }} | |
| QQQI_INCOME_RATIO: ${{ vars.QQQI_INCOME_RATIO }} | |
| INCOME_LAYER_ENABLED: ${{ vars.INCOME_LAYER_ENABLED }} | |
| INCOME_LAYER_START_USD: ${{ vars.INCOME_LAYER_START_USD }} | |
| INCOME_LAYER_MAX_RATIO: ${{ vars.INCOME_LAYER_MAX_RATIO }} | |
| LONGBRIDGE_CASH_ONLY_EXECUTION: ${{ vars.LONGBRIDGE_CASH_ONLY_EXECUTION }} | |
| CASH_ONLY_EXECUTION: ${{ vars.CASH_ONLY_EXECUTION }} | |
| DCA_MODE: ${{ vars.DCA_MODE }} | |
| DCA_BASE_INVESTMENT_USD: ${{ vars.DCA_BASE_INVESTMENT_USD }} | |
| IBIT_ZSCORE_EXIT_ENABLED: ${{ vars.IBIT_ZSCORE_EXIT_ENABLED }} | |
| IBIT_ZSCORE_EXIT_MODE: ${{ vars.IBIT_ZSCORE_EXIT_MODE }} | |
| IBIT_ZSCORE_EXIT_PARKING_SYMBOL: ${{ vars.IBIT_ZSCORE_EXIT_PARKING_SYMBOL }} | |
| IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE: ${{ vars.IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE }} | |
| IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE: ${{ vars.IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE }} | |
| IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW: ${{ vars.IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW }} | |
| RUNTIME_TARGET_ENABLED: ${{ vars.RUNTIME_TARGET_ENABLED }} | |
| NOTIFY_LANG: ${{ vars.NOTIFY_LANG }} | |
| EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }} | |
| LONGBRIDGE_DRY_RUN_ONLY: ${{ vars.LONGBRIDGE_DRY_RUN_ONLY }} | |
| GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }} | |
| TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} | |
| STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD }} | |
| STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN }} | |
| STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN }} | |
| STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN }} | |
| steps: | |
| - name: Check whether Cloud Run automation is enabled | |
| id: config | |
| run: | | |
| set -euo pipefail | |
| write_github_output() { | |
| printf '%s\n' "$@" >> "$GITHUB_OUTPUT" | |
| } | |
| deploy_enabled=false | |
| env_sync_enabled=false | |
| # QSL_ENABLE_CLOUD_RUN_AUTOMATION overrides ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION | |
| ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION="${QSL_ENABLE_CLOUD_RUN_AUTOMATION:-$ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION}" | |
| if [ "${GITHUB_EVENT_NAME:-}" = "push" ] && [ "${ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION:-}" != "true" ]; then | |
| write_github_output \ | |
| "deploy_enabled=false" \ | |
| "env_sync_enabled=false" \ | |
| "enabled=false" | |
| echo "Skipping ${DEPLOYMENT_LABEL} Cloud Run automation on push because ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION is not true." >&2 | |
| exit 0 | |
| fi | |
| if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ] \ | |
| && [ "${WORKFLOW_TARGET:-configured}" = "hk-verify" ] \ | |
| && [ "${DEPLOYMENT_LABEL:-}" != "HK" ]; then | |
| write_github_output \ | |
| "deploy_enabled=false" \ | |
| "env_sync_enabled=false" \ | |
| "enabled=false" | |
| echo "Skipping ${DEPLOYMENT_LABEL} Cloud Run automation because hk-verify targets only the HK deployment." >&2 | |
| exit 0 | |
| fi | |
| if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ] && [ "${WORKFLOW_TARGET:-configured}" = "hk-verify" ]; then | |
| if [ "${INPUT_DEPLOY_IMAGE:-true}" = "true" ]; then | |
| deploy_enabled=true | |
| fi | |
| if [ "${INPUT_SYNC_ENV:-true}" = "true" ]; then | |
| env_sync_enabled=true | |
| fi | |
| else | |
| if [ "${ENABLE_GITHUB_CLOUD_RUN_DEPLOY:-}" = "true" ]; then | |
| deploy_enabled=true | |
| fi | |
| if [ "${ENABLE_GITHUB_ENV_SYNC:-}" = "true" ]; then | |
| env_sync_enabled=true | |
| fi | |
| fi | |
| write_github_output \ | |
| "deploy_enabled=${deploy_enabled}" \ | |
| "env_sync_enabled=${env_sync_enabled}" | |
| if [ "${deploy_enabled}" != "true" ] && [ "${env_sync_enabled}" != "true" ]; then | |
| write_github_output "enabled=false" | |
| echo "Skipping ${DEPLOYMENT_LABEL} Cloud Run automation because ENABLE_GITHUB_CLOUD_RUN_DEPLOY and ENABLE_GITHUB_ENV_SYNC are not true." >&2 | |
| exit 0 | |
| fi | |
| write_github_output "enabled=true" | |
| - name: Checkout repository | |
| if: steps.config.outputs.enabled == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} | |
| - name: Apply HK verify-only dispatch defaults | |
| if: steps.config.outputs.enabled == 'true' && github.event_name == 'workflow_dispatch' && inputs.target == 'hk-verify' | |
| run: | | |
| set -euo pipefail | |
| service="${INPUT_CLOUD_RUN_SERVICE:-longbridge-quant-hk-verify-service}" | |
| region="${INPUT_CLOUD_RUN_REGION:-${CLOUD_RUN_REGION:-}}" | |
| runtime_target="$(SERVICE="${service}" python3 - <<'PY' | |
| import json | |
| import os | |
| print( | |
| json.dumps( | |
| { | |
| "platform_id": "longbridge", | |
| "strategy_profile": "hk_global_etf_tactical_rotation", | |
| "deployment_selector": "hk-verify", | |
| "account_scope": "hk-verify", | |
| "execution_mode": "paper", | |
| "dry_run_only": True, | |
| "service_name": os.environ["SERVICE"], | |
| }, | |
| separators=(",", ":"), | |
| ) | |
| ) | |
| PY | |
| )" | |
| { | |
| echo "CLOUD_RUN_SERVICE=${service}" | |
| echo "ACCOUNT_REGION=HK" | |
| echo "ACCOUNT_PREFIX=HK" | |
| echo "RUNTIME_TARGET_JSON=${runtime_target}" | |
| echo "LONGBRIDGE_DRY_RUN_ONLY=true" | |
| echo "LONGBRIDGE_MARKET=HK" | |
| echo "LONGBRIDGE_MARKET_CALENDAR=XHKG" | |
| echo "LONGBRIDGE_MARKET_TIMEZONE=Asia/Hong_Kong" | |
| echo "LONGBRIDGE_SYMBOL_SUFFIX=.HK" | |
| echo "LONGBRIDGE_TRADING_CURRENCY=HKD" | |
| } >> "$GITHUB_ENV" | |
| if [ -n "${region}" ]; then | |
| echo "CLOUD_RUN_REGION=${region}" >> "$GITHUB_ENV" | |
| fi | |
| if [ -n "${INPUT_LONGPORT_SECRET_NAME:-}" ]; then | |
| echo "LONGPORT_SECRET_NAME=${INPUT_LONGPORT_SECRET_NAME}" >> "$GITHUB_ENV" | |
| fi | |
| if [ -n "${INPUT_LONGPORT_APP_KEY_SECRET_NAME:-}" ]; then | |
| echo "LONGPORT_APP_KEY_SECRET_NAME=${INPUT_LONGPORT_APP_KEY_SECRET_NAME}" >> "$GITHUB_ENV" | |
| fi | |
| if [ -n "${INPUT_LONGPORT_APP_SECRET_SECRET_NAME:-}" ]; then | |
| echo "LONGPORT_APP_SECRET_SECRET_NAME=${INPUT_LONGPORT_APP_SECRET_SECRET_NAME}" >> "$GITHUB_ENV" | |
| fi | |
| if [ "${INPUT_DEPLOY_IMAGE:-true}" != "true" ]; then | |
| echo "CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT=false" >> "$GITHUB_ENV" | |
| fi | |
| - name: Set up Python for strategy requirement resolution | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install strategy status dependencies | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install -r requirements.txt | |
| - name: Resolve Cloud Run sync targets | |
| id: strategy_requirements | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| sync_plan_json="$(python scripts/build_cloud_run_env_sync_plan.py --json)" | |
| { | |
| echo "sync_plan_json<<__SYNC_PLAN_JSON__" | |
| printf '%s\n' "${sync_plan_json}" | |
| echo "__SYNC_PLAN_JSON__" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Validate env sync inputs | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| env: | |
| SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_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 | |
| python - <<'PY' | |
| import json | |
| import os | |
| plan = json.loads(os.environ["SYNC_PLAN_JSON"]) | |
| targets = plan.get("targets") or [] | |
| if not targets: | |
| raise SystemExit("Cloud Run env sync did not resolve any targets") | |
| for target in targets: | |
| service_name = str(target.get("service_name") or "").strip() | |
| if not service_name: | |
| raise SystemExit("Cloud Run sync target is missing service_name") | |
| if not isinstance(target.get("env"), dict): | |
| raise SystemExit(f"Cloud Run sync target {service_name} is missing env") | |
| PY | |
| if [ "${#missing_vars[@]}" -gt 0 ]; then | |
| 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 | |
| - name: Validate deploy inputs | |
| if: steps.config.outputs.deploy_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| missing_vars=() | |
| for var_name in CLOUD_RUN_REGION CLOUD_RUN_SERVICE; do | |
| if [ -z "${!var_name:-}" ]; then | |
| missing_vars+=("${var_name}") | |
| fi | |
| done | |
| if [ "${#missing_vars[@]}" -gt 0 ]; then | |
| echo "${DEPLOYMENT_LABEL} Cloud Run deploy is enabled, but these values are missing:" >&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: Download constraints | |
| if: steps.config.outputs.deploy_enabled == 'true' | |
| run: curl -sL https://raw.githubusercontent.com/QuantStrategyLab/QuantPlatformKit/main/constraints.txt -o constraints.txt | |
| - name: Build, push, and deploy Cloud Run image | |
| if: steps.config.outputs.deploy_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| artifact_registry_hostname="${GCP_ARTIFACT_REGISTRY_HOSTNAME:-${CLOUD_RUN_REGION}-docker.pkg.dev}" | |
| image_repo="${artifact_registry_hostname}/${GCP_PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_REPOSITORY}/longbridgeplatform/${CLOUD_RUN_SERVICE}" | |
| image="${image_repo}:${GITHUB_SHA}" | |
| deployment_label="$(printf '%s' "${DEPLOYMENT_LABEL}" | tr '[:upper:]' '[:lower:]')" | |
| gcloud auth configure-docker "${artifact_registry_hostname}" --quiet | |
| docker build --pull -t "${image}" . | |
| docker push "${image}" | |
| gcloud run deploy "${CLOUD_RUN_SERVICE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --platform=managed \ | |
| --image="${image}" \ | |
| --service-account="${GCP_RUNTIME_SERVICE_ACCOUNT}" \ | |
| --ingress=internal \ | |
| --max-instances=1 \ | |
| --memory=512Mi \ | |
| --cpu=1 \ | |
| --timeout=300s \ | |
| --labels="managed-by=github-actions,commit-sha=${GITHUB_SHA},github-run-id=${GITHUB_RUN_ID},deployment-label=${deployment_label}" \ | |
| --quiet | |
| - name: Wait for Cloud Run deployment of current commit | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| 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 | |
| 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 | |
| - name: Sync Cloud Run environment | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| env: | |
| SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_json }} | |
| run: | | |
| set -euo pipefail | |
| mapfile -t target_env_pairs < <(python - <<'PY' | |
| import json | |
| import os | |
| plan = json.loads(os.environ["SYNC_PLAN_JSON"]) | |
| service = os.environ.get("CLOUD_RUN_SERVICE", "").strip() | |
| targets = plan.get("targets") or [] | |
| target = next( | |
| (item for item in targets if str(item.get("service_name") or "").strip() == service), | |
| targets[0], | |
| ) | |
| for key, value in sorted(target["env"].items()): | |
| print(f"{key}={value}") | |
| PY | |
| ) | |
| mapfile -t target_remove_env_vars < <(python - <<'PY' | |
| import json | |
| import os | |
| plan = json.loads(os.environ["SYNC_PLAN_JSON"]) | |
| service = os.environ.get("CLOUD_RUN_SERVICE", "").strip() | |
| targets = plan.get("targets") or [] | |
| target = next( | |
| (item for item in targets if str(item.get("service_name") or "").strip() == service), | |
| targets[0], | |
| ) | |
| for key in sorted(target.get("remove_env_vars") or []): | |
| print(key) | |
| PY | |
| ) | |
| 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=("${target_env_pairs[@]}") | |
| secret_pairs=() | |
| remove_env_vars=("${target_remove_env_vars[@]}") | |
| remove_env_vars+=( | |
| "TELEGRAM_CHAT_ID" | |
| "SERVICE_NAME" | |
| "LONGBRIDGE_FRACTIONAL_SHARES_ENABLED" | |
| "LONGBRIDGE_ORDER_QUANTITY_STEP" | |
| "LONGBRIDGE_MIN_ORDER_NOTIONAL_USD" | |
| "CRISIS_ALERT_GOOGLE_VOICE_TO" | |
| "CRISIS_ALERT_GOOGLE_VOICE_GATEWAY" | |
| "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER" | |
| "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" | |
| "CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY" | |
| "CRISIS_ALERT_SMTP_FROM" | |
| "CRISIS_ALERT_SMTP_HOST" | |
| "CRISIS_ALERT_SMTP_PORT" | |
| "CRISIS_ALERT_SMTP_USERNAME" | |
| "CRISIS_ALERT_SMTP_PASSWORD" | |
| "CRISIS_ALERT_SMTP_STARTTLS" | |
| "CRISIS_ALERT_SMTP_SSL" | |
| "CRISIS_ALERT_CHANNELS" | |
| "CRISIS_ALERT_EMAIL_RECIPIENTS" | |
| "CRISIS_ALERT_EMAIL_SENDER_EMAIL" | |
| "CRISIS_ALERT_EMAIL_SENDER_PASSWORD" | |
| "CRISIS_ALERT_EMAIL_SMTP_HOST" | |
| "CRISIS_ALERT_EMAIL_SMTP_PORT" | |
| "CRISIS_ALERT_EMAIL_SMTP_SECURITY" | |
| "CRISIS_ALERT_SMS_RECIPIENTS" | |
| "CRISIS_ALERT_SMS_PROVIDER" | |
| "CRISIS_ALERT_SMS_ACCOUNT_ID" | |
| "CRISIS_ALERT_SMS_AUTH_TOKEN" | |
| "CRISIS_ALERT_SMS_SENDER" | |
| "CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID" | |
| "CRISIS_ALERT_SMS_API_BASE_URL" | |
| "CRISIS_ALERT_SMS_BODY_MAX_CHARS" | |
| "CRISIS_ALERT_PUSH_RECIPIENTS" | |
| "CRISIS_ALERT_PUSH_PROVIDER" | |
| "CRISIS_ALERT_PUSH_APP_TOKEN" | |
| "CRISIS_ALERT_PUSH_ACCESS_TOKEN" | |
| "CRISIS_ALERT_PUSH_API_BASE_URL" | |
| "CRISIS_ALERT_PUSH_DEVICE" | |
| "CRISIS_ALERT_PUSH_PRIORITY" | |
| "CRISIS_ALERT_PUSH_TAGS" | |
| "CRISIS_ALERT_PUSH_BODY_MAX_CHARS" | |
| "CRISIS_ALERT_TELEGRAM_CHAT_IDS" | |
| "CRISIS_ALERT_TELEGRAM_BOT_TOKEN" | |
| "CRISIS_ALERT_TELEGRAM_API_BASE_URL" | |
| "CRISIS_ALERT_TELEGRAM_PARSE_MODE" | |
| "CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW" | |
| "CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS" | |
| ) | |
| remove_secret_vars=( | |
| "CRISIS_ALERT_SMTP_PASSWORD" | |
| "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" | |
| "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD" | |
| "CRISIS_ALERT_EMAIL_SENDER_PASSWORD" | |
| "CRISIS_ALERT_SMS_AUTH_TOKEN" | |
| "CRISIS_ALERT_PUSH_APP_TOKEN" | |
| "CRISIS_ALERT_PUSH_ACCESS_TOKEN" | |
| "CRISIS_ALERT_TELEGRAM_BOT_TOKEN" | |
| ) | |
| 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 | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME:-}" ]; then | |
| secret_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD=${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME}:latest") | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD") | |
| elif [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD=${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD}") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN_SECRET_NAME:-}" ]; then | |
| secret_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN=${STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN_SECRET_NAME}:latest") | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN") | |
| elif [ -n "${STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN=${STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN}") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN_SECRET_NAME:-}" ]; then | |
| secret_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN=${STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN_SECRET_NAME}:latest") | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN") | |
| elif [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN=${STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN}") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_APP_TOKEN") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME:-}" ]; then | |
| secret_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN=${STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME}:latest") | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN") | |
| elif [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN=${STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN}") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_ACCESS_TOKEN") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME:-}" ]; then | |
| secret_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN=${STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME}:latest") | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN") | |
| elif [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN=${STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN}") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_TOKEN") | |
| remove_secret_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BOT_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 "${LONGPORT_SECRET_NAME:-}" ]; then | |
| env_pairs+=("LONGPORT_SECRET_NAME=${LONGPORT_SECRET_NAME}") | |
| else | |
| remove_env_vars+=("LONGPORT_SECRET_NAME") | |
| fi | |
| if [ -n "${ACCOUNT_REGION:-}" ]; then | |
| env_pairs+=("ACCOUNT_REGION=${ACCOUNT_REGION}") | |
| else | |
| remove_env_vars+=("ACCOUNT_REGION") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_CHANNELS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_CHANNELS=${STRATEGY_PLUGIN_ALERT_CHANNELS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_CHANNELS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS=${STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL=${STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_EMAIL") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST=${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_HOST") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT=${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_PORT") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY=${STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_EMAIL_SMTP_SECURITY") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS=${STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_RECIPIENTS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_PROVIDER:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_PROVIDER=${STRATEGY_PLUGIN_ALERT_SMS_PROVIDER}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_PROVIDER") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID=${STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_ACCOUNT_ID") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_SENDER:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_SENDER=${STRATEGY_PLUGIN_ALERT_SMS_SENDER}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_SENDER") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID=${STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_MESSAGING_SERVICE_ID") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL=${STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_API_BASE_URL") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS=${STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_SMS_BODY_MAX_CHARS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS=${STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_RECIPIENTS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER=${STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_PROVIDER") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL=${STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_API_BASE_URL") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_DEVICE:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_DEVICE=${STRATEGY_PLUGIN_ALERT_PUSH_DEVICE}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_DEVICE") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY=${STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_PRIORITY") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_TAGS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_TAGS=${STRATEGY_PLUGIN_ALERT_PUSH_TAGS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_TAGS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS=${STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_PUSH_BODY_MAX_CHARS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS=${STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_CHAT_IDS") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL=${STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_API_BASE_URL") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE=${STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_PARSE_MODE") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW=${STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW") | |
| fi | |
| if [ -n "${STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS:-}" ]; then | |
| env_pairs+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS=${STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS}") | |
| else | |
| remove_env_vars+=("STRATEGY_PLUGIN_ALERT_TELEGRAM_BODY_MAX_CHARS") | |
| fi | |
| monitor_targets_json="$( | |
| python - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| plan = json.loads(os.environ["SYNC_PLAN_JSON"]) | |
| project = os.environ.get("GCP_PROJECT_ID", "").strip() | |
| default_region = os.environ.get("CLOUD_RUN_REGION", "").strip() | |
| configured_targets = json.loads( | |
| os.environ.get("CLOUD_RUN_SERVICE_TARGETS_JSON") or "{}" | |
| ).get("targets") or [] | |
| region_by_service: dict[str, str] = {} | |
| service_url_by_service: dict[str, str] = {} | |
| for item in configured_targets: | |
| if not isinstance(item, dict): | |
| continue | |
| service_name = str(item.get("service_name") or item.get("service") or "").strip() | |
| if not service_name: | |
| continue | |
| region = str(item.get("region") or "").strip() | |
| if region: | |
| region_by_service[service_name] = region | |
| service_url = str(item.get("service_url") or "").strip() | |
| if service_url: | |
| service_url_by_service[service_name] = service_url | |
| output = [] | |
| for target in plan.get("targets") or []: | |
| if not isinstance(target, dict): | |
| continue | |
| env = target.get("env") or {} | |
| runtime_target = json.loads(env.get("RUNTIME_TARGET_JSON") or "{}") | |
| service_name = str(target.get("service_name") or "").strip() | |
| if not service_name: | |
| continue | |
| service_url = service_url_by_service.get(service_name) | |
| if not service_url: | |
| region = region_by_service.get(service_name) or default_region | |
| if not region: | |
| continue | |
| service_url = subprocess.check_output( | |
| [ | |
| "gcloud", | |
| "run", | |
| "services", | |
| "describe", | |
| service_name, | |
| f"--project={project}", | |
| f"--region={region}", | |
| "--format=value(status.url)", | |
| ], | |
| text=True, | |
| ).strip() | |
| if not service_url: | |
| continue | |
| output.append( | |
| { | |
| "service_name": service_name, | |
| "service_url": service_url, | |
| "strategy_profile": target.get("strategy_profile") or env.get("STRATEGY_PROFILE"), | |
| "account_scope": runtime_target.get("account_scope"), | |
| "runtime_target_enabled": env.get("RUNTIME_TARGET_ENABLED", "true"), | |
| "scheduler": target.get("scheduler") or {}, | |
| } | |
| ) | |
| print(json.dumps({"targets": output}, separators=(",", ":"))) | |
| PY | |
| )" | |
| env_pairs+=("MONITOR_DISPATCH_TARGETS_JSON=${monitor_targets_json}") | |
| 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[@]}" | |
| - name: Verify strategy plugin mounts | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| env: | |
| SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_json }} | |
| STRATEGY_PLUGIN_MOUNT_ENV_NAMES: LONGBRIDGE_STRATEGY_PLUGIN_MOUNTS_JSON | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/verify_cloud_run_strategy_plugin_mounts.py | |
| - name: Sync Cloud Scheduler schedule | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| env: | |
| SYNC_PLAN_JSON: ${{ steps.strategy_requirements.outputs.sync_plan_json }} | |
| run: | | |
| set -euo pipefail | |
| scheduler_location="${CLOUD_SCHEDULER_LOCATION:-${CLOUD_RUN_REGION}}" | |
| if [ -z "${scheduler_location}" ]; then | |
| echo "Cloud Scheduler schedule sync requires CLOUD_RUN_REGION or CLOUD_SCHEDULER_LOCATION." >&2 | |
| exit 1 | |
| fi | |
| mapfile -t scheduler_config < <(python - <<'PY' | |
| import json | |
| import os | |
| plan = json.loads(os.environ["SYNC_PLAN_JSON"]) | |
| service = os.environ.get("CLOUD_RUN_SERVICE", "").strip() | |
| targets = plan.get("targets") or [] | |
| target = next( | |
| (item for item in targets if str(item.get("service_name") or "").strip() == service), | |
| targets[0], | |
| ) | |
| scheduler = target.get("scheduler") or {} | |
| print(str(scheduler.get("timezone") or os.environ.get("LONGBRIDGE_MARKET_TIMEZONE", "")).strip() or "America/New_York") | |
| print(str(scheduler.get("main_time") or os.environ.get("CLOUD_SCHEDULER_MAIN_TIME", "").strip() or "45 15")) | |
| print(str(scheduler.get("probe_time") or os.environ.get("CLOUD_SCHEDULER_PROBE_TIME", "").strip() or "35 9,15")) | |
| print(str(scheduler.get("precheck_time") or os.environ.get("CLOUD_SCHEDULER_PRECHECK_TIME", "").strip() or "45 9")) | |
| PY | |
| ) | |
| market_timezone="${scheduler_config[0]}" | |
| main_time="${scheduler_config[1]}" | |
| probe_time="${scheduler_config[2]}" | |
| precheck_time="${scheduler_config[3]}" | |
| # Keep probe/precheck parsed for runtime-target schema parity; this step only syncs the main scheduler. | |
| : "${probe_time}" "${precheck_time}" | |
| service_url="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --format='value(status.url)' 2>/dev/null || true)" | |
| if [ -z "${service_url}" ]; then | |
| echo "Unable to resolve Cloud Run service URL for ${CLOUD_RUN_SERVICE}; cannot sync scheduler URI." >&2 | |
| exit 1 | |
| fi | |
| gcloud run services add-iam-policy-binding "${CLOUD_RUN_SERVICE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --member="serviceAccount:${GCP_SCHEDULER_SERVICE_ACCOUNT}" \ | |
| --role="roles/run.invoker" \ | |
| --quiet | |
| gcloud run services add-iam-policy-binding "${CLOUD_RUN_SERVICE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --member="serviceAccount:${GCP_RUNTIME_SERVICE_ACCOUNT}" \ | |
| --role="roles/run.invoker" \ | |
| --quiet | |
| scheduler_job_candidates=("${CLOUD_RUN_SERVICE}-scheduler") | |
| if [[ "${CLOUD_RUN_SERVICE}" == *-service ]]; then | |
| scheduler_job_candidates+=("${CLOUD_RUN_SERVICE%-service}-scheduler") | |
| fi | |
| job_name="" | |
| current_schedule="" | |
| for candidate_job in "${scheduler_job_candidates[@]}"; do | |
| current_schedule="$(gcloud scheduler jobs describe "${candidate_job}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --format='value(schedule)' 2>/dev/null || true)" | |
| if [ -n "${current_schedule}" ]; then | |
| job_name="${candidate_job}" | |
| break | |
| fi | |
| done | |
| if [ -z "${job_name}" ]; then | |
| job_name="${scheduler_job_candidates[0]}" | |
| fi | |
| if [ -n "${current_schedule}" ]; then | |
| desired_schedule="$(CURRENT_SCHEDULE="${current_schedule}" SCHEDULE_TIME="${main_time}" python - <<'PY' | |
| import os | |
| current_fields = os.environ["CURRENT_SCHEDULE"].split() | |
| time_fields = os.environ["SCHEDULE_TIME"].split() | |
| if len(current_fields) != 5: | |
| raise SystemExit(f"Cloud Scheduler schedule must have 5 fields: {os.environ['CURRENT_SCHEDULE']!r}") | |
| if len(time_fields) == 5: | |
| print(" ".join(time_fields)) | |
| elif len(time_fields) == 2: | |
| print(" ".join([*time_fields, *current_fields[2:]])) | |
| else: | |
| raise SystemExit( | |
| f"Cloud Scheduler override must have 2 time fields or 5 cron fields: {os.environ['SCHEDULE_TIME']!r}" | |
| ) | |
| PY | |
| )" | |
| else | |
| desired_schedule="$(SCHEDULE_TIME="${main_time}" python - <<'PY' | |
| import os | |
| fields = os.environ["SCHEDULE_TIME"].split() | |
| if len(fields) == 5: | |
| print(" ".join(fields)) | |
| elif len(fields) == 2: | |
| print(" ".join([*fields, "*", "*", "*"])) | |
| else: | |
| raise SystemExit( | |
| f"Cloud Scheduler override must have 2 time fields or 5 cron fields: {os.environ['SCHEDULE_TIME']!r}" | |
| ) | |
| PY | |
| )" | |
| fi | |
| scheduler_uri="${service_url}/run" | |
| if [ -n "${current_schedule}" ]; then | |
| echo "Updating Cloud Scheduler job ${job_name} schedule to ${desired_schedule}, timezone to ${market_timezone}, and URI to ${scheduler_uri}." | |
| gcloud scheduler jobs update http "${job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --uri="${scheduler_uri}" \ | |
| --schedule="${desired_schedule}" \ | |
| --time-zone="${market_timezone}" \ | |
| --quiet | |
| else | |
| echo "Creating Cloud Scheduler job ${job_name} schedule ${desired_schedule}, timezone ${market_timezone}, and URI ${scheduler_uri}." | |
| gcloud scheduler jobs create http "${job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --uri="${scheduler_uri}" \ | |
| --http-method=POST \ | |
| --oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}" \ | |
| --oidc-token-audience="${service_url}" \ | |
| --schedule="${desired_schedule}" \ | |
| --time-zone="${market_timezone}" \ | |
| --attempt-deadline=600s \ | |
| --quiet | |
| fi | |
| if [ "${DEPLOYMENT_LABEL:-}" = "${MONITOR_DISPATCHER_OWNER_LABEL:-SG}" ]; then | |
| monitor_job_name="longbridge-monitor-dispatcher-scheduler" | |
| monitor_uri="${service_url}/monitor-dispatch" | |
| if gcloud scheduler jobs describe "${monitor_job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" >/dev/null 2>&1; then | |
| echo "Updating Cloud Scheduler job ${monitor_job_name} to ${monitor_uri}." | |
| gcloud scheduler jobs update http "${monitor_job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --uri="${monitor_uri}" \ | |
| --http-method=POST \ | |
| --oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}" \ | |
| --oidc-token-audience="${service_url}" \ | |
| --schedule="*/5 * * * *" \ | |
| --time-zone="UTC" \ | |
| --attempt-deadline=180s \ | |
| --quiet | |
| else | |
| echo "Creating Cloud Scheduler job ${monitor_job_name} at ${monitor_uri}." | |
| gcloud scheduler jobs create http "${monitor_job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --uri="${monitor_uri}" \ | |
| --http-method=POST \ | |
| --oidc-service-account-email="${GCP_SCHEDULER_SERVICE_ACCOUNT}" \ | |
| --oidc-token-audience="${service_url}" \ | |
| --schedule="*/5 * * * *" \ | |
| --time-zone="UTC" \ | |
| --attempt-deadline=180s \ | |
| --quiet | |
| fi | |
| else | |
| echo "Skipping shared LongBridge monitor dispatcher scheduler from ${DEPLOYMENT_LABEL}; owner is ${MONITOR_DISPATCHER_OWNER_LABEL:-SG}." | |
| fi | |
| legacy_jobs=( | |
| "${CLOUD_RUN_SERVICE}-probe-scheduler" | |
| "${CLOUD_RUN_SERVICE}-precheck-scheduler" | |
| ) | |
| if [[ "${CLOUD_RUN_SERVICE}" == *-service ]]; then | |
| legacy_jobs+=( | |
| "${CLOUD_RUN_SERVICE%-service}-probe-scheduler" | |
| "${CLOUD_RUN_SERVICE%-service}-precheck-scheduler" | |
| ) | |
| fi | |
| legacy_scope="${CLOUD_RUN_SERVICE#longbridge-quant-}" | |
| legacy_scope="${legacy_scope%-service}" | |
| if [ -n "${legacy_scope}" ] && [ "${legacy_scope}" != "${CLOUD_RUN_SERVICE}" ]; then | |
| legacy_jobs+=("lb-${legacy_scope}-backup-execution") | |
| fi | |
| for legacy_job in "${legacy_jobs[@]}"; do | |
| if gcloud scheduler jobs describe "${legacy_job}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" >/dev/null 2>&1; then | |
| echo "Deleting legacy Cloud Scheduler job ${legacy_job}; monitor dispatcher now owns probe/precheck." | |
| gcloud scheduler jobs delete "${legacy_job}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --quiet | |
| fi | |
| done | |
| - name: Prune old Cloud Run revisions | |
| if: steps.config.outputs.enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| keep_revisions="$( | |
| gcloud run services describe "${CLOUD_RUN_SERVICE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --format=json \ | |
| | python -c ' | |
| import json | |
| import sys | |
| service = json.load(sys.stdin) | |
| revisions = { | |
| item.get("revisionName") | |
| for item in service.get("status", {}).get("traffic", []) | |
| if item.get("revisionName") and int(item.get("percent") or 0) > 0 | |
| } | |
| latest_ready = service.get("status", {}).get("latestReadyRevisionName") | |
| if latest_ready: | |
| revisions.add(latest_ready) | |
| for revision in sorted(revisions): | |
| print(revision) | |
| ' | |
| )" | |
| if [ -z "${keep_revisions}" ]; then | |
| echo "No active Cloud Run revision found for ${CLOUD_RUN_SERVICE}; skipping revision prune." >&2 | |
| exit 0 | |
| fi | |
| mapfile -t revisions_to_delete < <( | |
| gcloud run revisions list \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --service "${CLOUD_RUN_SERVICE}" \ | |
| --region "${CLOUD_RUN_REGION}" \ | |
| --format='value(metadata.name)' \ | |
| | grep -Fvx -f <(printf '%s\n' "${keep_revisions}") || true | |
| ) | |
| for revision in "${revisions_to_delete[@]}"; do | |
| echo "Deleting old Cloud Run revision ${revision} for ${CLOUD_RUN_SERVICE}." | |
| gcloud run revisions delete "${revision}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --region="${CLOUD_RUN_REGION}" \ | |
| --quiet 2>&1 || true | |
| done | |
| - name: Clean up old Cloud Run images | |
| if: steps.config.outputs.deploy_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| artifact_registry_hostname="${GCP_ARTIFACT_REGISTRY_HOSTNAME:-${CLOUD_RUN_REGION}-docker.pkg.dev}" | |
| image_repo="${artifact_registry_hostname}/${GCP_PROJECT_ID}/${GCP_ARTIFACT_REGISTRY_REPOSITORY}/longbridgeplatform/${CLOUD_RUN_SERVICE}" | |
| old_digests="$(gcloud artifacts docker images list "${image_repo}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --include-tags \ | |
| --format=json \ | |
| | python -c 'import json,os,sys; keep=os.environ["GITHUB_SHA"]; rows=json.load(sys.stdin); print("\n".join(row["version"] for row in rows if keep not in set(row.get("tags") or [])))')" | |
| while IFS= read -r digest; do | |
| if [ -z "${digest}" ]; then | |
| continue | |
| fi | |
| gcloud artifacts docker images delete "${image_repo}@${digest}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --delete-tags \ | |
| --async \ | |
| --quiet | |
| done <<< "${old_digests}" |