Support income layer start amount (#177) #282
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: | |
| push: | |
| 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_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 | |
| 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: | |
| 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 }} | |
| 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_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 }} | |
| 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_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 }} | |
| 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 }} | |
| 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 | |
| deploy_enabled=false | |
| env_sync_enabled=false | |
| if [ "${GITHUB_EVENT_NAME:-}" = "push" ] && [ "${ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION:-}" != "true" ]; then | |
| echo "deploy_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "env_sync_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| 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 | |
| echo "deploy_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "env_sync_enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| 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 | |
| echo "deploy_enabled=${deploy_enabled}" >> "$GITHUB_OUTPUT" | |
| echo "env_sync_enabled=${env_sync_enabled}" >> "$GITHUB_OUTPUT" | |
| if [ "${deploy_enabled}" != "true" ] && [ "${env_sync_enabled}" != "true" ]; then | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| 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 | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| - name: Checkout repository | |
| if: steps.config.outputs.enabled == 'true' | |
| uses: actions/checkout@v6 | |
| - 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 selected strategy runtime requirements | |
| id: strategy_requirements | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| from strategy_registry import LONGBRIDGE_PLATFORM, resolve_strategy_definition | |
| 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_strategy_definition( | |
| profile, | |
| platform_id=LONGBRIDGE_PLATFORM, | |
| ).profile | |
| 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}" | |
| ) | |
| 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"canonical_profile={canonical_profile}\n") | |
| output.write( | |
| f"requires_snapshot_artifacts={str(bool(selected.get('requires_snapshot_artifacts'))).lower()}\n" | |
| ) | |
| 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 env sync inputs | |
| if: steps.config.outputs.env_sync_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 "${RUNTIME_TARGET_JSON:-}" ]; then | |
| missing_vars+=("RUNTIME_TARGET_JSON") | |
| 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 "${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: 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: | |
| STRATEGY_PROFILE: ${{ steps.strategy_requirements.outputs.canonical_profile }} | |
| 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}" | |
| "STRATEGY_PROFILE=${STRATEGY_PROFILE}" | |
| "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" | |
| "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 "${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_MARKET:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_MARKET=${LONGBRIDGE_MARKET}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_MARKET") | |
| fi | |
| if [ -n "${LONGBRIDGE_MARKET_CALENDAR:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_MARKET_CALENDAR=${LONGBRIDGE_MARKET_CALENDAR}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_MARKET_CALENDAR") | |
| fi | |
| if [ -n "${LONGBRIDGE_MARKET_TIMEZONE:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_MARKET_TIMEZONE=${LONGBRIDGE_MARKET_TIMEZONE}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_MARKET_TIMEZONE") | |
| fi | |
| if [ -n "${LONGBRIDGE_SYMBOL_SUFFIX:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_SYMBOL_SUFFIX=${LONGBRIDGE_SYMBOL_SUFFIX}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_SYMBOL_SUFFIX") | |
| fi | |
| if [ -n "${LONGBRIDGE_TRADING_CURRENCY:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_TRADING_CURRENCY=${LONGBRIDGE_TRADING_CURRENCY}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_TRADING_CURRENCY") | |
| fi | |
| if [ -n "${LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD") | |
| fi | |
| if [ -n "${LONGBRIDGE_MIN_RESERVED_CASH_USD:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_MIN_RESERVED_CASH_USD=${LONGBRIDGE_MIN_RESERVED_CASH_USD}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_MIN_RESERVED_CASH_USD") | |
| fi | |
| if [ -n "${LONGBRIDGE_RESERVED_CASH_RATIO:-}" ]; then | |
| env_pairs+=("LONGBRIDGE_RESERVED_CASH_RATIO=${LONGBRIDGE_RESERVED_CASH_RATIO}") | |
| else | |
| remove_env_vars+=("LONGBRIDGE_RESERVED_CASH_RATIO") | |
| 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 | |
| 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 | |
| if [ -n "${INCOME_LAYER_ENABLED:-}" ]; then | |
| env_pairs+=("INCOME_LAYER_ENABLED=${INCOME_LAYER_ENABLED}") | |
| else | |
| remove_env_vars+=("INCOME_LAYER_ENABLED") | |
| fi | |
| if [ -n "${INCOME_LAYER_START_USD:-}" ]; then | |
| env_pairs+=("INCOME_LAYER_START_USD=${INCOME_LAYER_START_USD}") | |
| else | |
| remove_env_vars+=("INCOME_LAYER_START_USD") | |
| fi | |
| if [ -n "${INCOME_LAYER_MAX_RATIO:-}" ]; then | |
| env_pairs+=("INCOME_LAYER_MAX_RATIO=${INCOME_LAYER_MAX_RATIO}") | |
| else | |
| remove_env_vars+=("INCOME_LAYER_MAX_RATIO") | |
| fi | |
| if [ -n "${RUNTIME_TARGET_ENABLED:-}" ]; then | |
| env_pairs+=("RUNTIME_TARGET_ENABLED=${RUNTIME_TARGET_ENABLED}") | |
| else | |
| remove_env_vars+=("RUNTIME_TARGET_ENABLED") | |
| 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[@]}" | |
| - name: Verify strategy plugin mounts | |
| if: steps.config.outputs.env_sync_enabled == 'true' | |
| env: | |
| 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' | |
| 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_market_config < <(python - <<'PY' | |
| import os | |
| market = os.environ.get("LONGBRIDGE_MARKET", "").strip().upper() | |
| timezone = os.environ.get("LONGBRIDGE_MARKET_TIMEZONE", "").strip() | |
| if not timezone: | |
| timezone = "Asia/Hong_Kong" if market == "HK" else "America/New_York" | |
| def configured_time(name: str, default: str) -> str: | |
| return os.environ.get(name, "").strip() or default | |
| print(timezone) | |
| print(configured_time("CLOUD_SCHEDULER_MAIN_TIME", "45 15")) | |
| print(configured_time("CLOUD_SCHEDULER_PROBE_TIME", "35 9,15")) | |
| print(configured_time("CLOUD_SCHEDULER_PRECHECK_TIME", "45 9")) | |
| PY | |
| ) | |
| market_timezone="${scheduler_market_config[0]}" | |
| main_time="${scheduler_market_config[1]}" | |
| probe_time="${scheduler_market_config[2]}" | |
| precheck_time="${scheduler_market_config[3]}" | |
| 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 | |
| for suffix in scheduler probe-scheduler precheck-scheduler; do | |
| job_name="${CLOUD_RUN_SERVICE}-${suffix}" | |
| case "${suffix}" in | |
| scheduler) | |
| schedule_time="${main_time}" | |
| scheduler_path="/run" | |
| ;; | |
| probe-scheduler) | |
| schedule_time="${probe_time}" | |
| scheduler_path="/probe" | |
| ;; | |
| precheck-scheduler) | |
| schedule_time="${precheck_time}" | |
| scheduler_path="/dry-run" | |
| ;; | |
| esac | |
| current_schedule="$(gcloud scheduler jobs describe "${job_name}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --location="${scheduler_location}" \ | |
| --format='value(schedule)' 2>/dev/null || true)" | |
| if [ -z "${current_schedule}" ]; then | |
| echo "Cloud Scheduler job ${job_name} was not found in ${scheduler_location}; skipping schedule sync." | |
| continue | |
| fi | |
| desired_schedule="$(CURRENT_SCHEDULE="${current_schedule}" SCHEDULE_TIME="${schedule_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) != 2: | |
| raise SystemExit(f"Cloud Scheduler time override must have 2 cron fields: {os.environ['SCHEDULE_TIME']!r}") | |
| print(" ".join([*time_fields, *current_fields[2:]])) | |
| PY | |
| )" | |
| scheduler_uri="${service_url}${scheduler_path}" | |
| 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 | |
| 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 | |
| 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}" |