Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 147 additions & 11 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Sync Cloud Run Env
name: Deploy Cloud Run

on:
push:
Expand All @@ -8,10 +8,12 @@ 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

jobs:
sync:
name: Sync ${{ matrix.target.label }} Cloud Run Env
name: Deploy / Sync ${{ matrix.target.label }} Cloud Run
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -33,7 +35,9 @@ jobs:
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 }}
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 }}
Expand Down Expand Up @@ -95,14 +99,28 @@ jobs:
CRISIS_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_ACCESS_TOKEN }}
CRISIS_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.CRISIS_ALERT_TELEGRAM_BOT_TOKEN }}
steps:
- name: Check whether env sync is enabled
- name: Check whether Cloud Run automation is enabled
id: config
run: |
set -euo pipefail

if [ "${ENABLE_GITHUB_ENV_SYNC:-}" != "true" ]; then
deploy_enabled=false
env_sync_enabled=false

if [ "${ENABLE_GITHUB_CLOUD_RUN_DEPLOY:-}" = "true" ]; then
deploy_enabled=true
fi

if [ "${ENABLE_GITHUB_ENV_SYNC:-}" = "true" ]; then
env_sync_enabled=true
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 env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." >&2
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

Expand All @@ -113,21 +131,21 @@ jobs:
uses: actions/checkout@v6

- name: Set up Python for strategy requirement resolution
if: steps.config.outputs.enabled == 'true'
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.enabled == 'true'
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.enabled == 'true'
if: steps.config.outputs.env_sync_enabled == 'true'
run: |
set -euo pipefail
python - <<'PY'
Expand Down Expand Up @@ -186,7 +204,7 @@ jobs:
PY

- name: Validate env sync inputs
if: steps.config.outputs.enabled == 'true'
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 }}
Expand Down Expand Up @@ -250,6 +268,24 @@ jobs:
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'
Expand All @@ -265,8 +301,36 @@ jobs:
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}" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Put the commit label where the wait step reads it

When ENABLE_GITHUB_CLOUD_RUN_DEPLOY=true and ENABLE_GITHUB_ENV_SYNC=true, this deploy writes commit-sha with gcloud run deploy --labels, but the next step waits on spec.template.metadata.labels.commit-sha. Cloud Run documents --labels as service metadata labels, while revision labels live under spec.template.metadata.labels (see https://cloud.google.com/run/docs/configuring/services/labels), so the wait loop never sees the SHA from this deploy and times out before syncing env vars. Either query the service metadata label here or set the label on the revision template before the wait.

Useful? React with 👍 / 👎.

--quiet

- name: Wait for Cloud Run deployment of current commit
if: steps.config.outputs.enabled == 'true'
if: steps.config.outputs.env_sync_enabled == 'true'
run: |
set -euo pipefail

Expand All @@ -290,7 +354,7 @@ jobs:
done

- name: Sync Cloud Run environment
if: steps.config.outputs.enabled == 'true'
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 }}
Expand Down Expand Up @@ -658,3 +722,75 @@ jobs:
fi

gcloud "${gcloud_args[@]}"

- 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 \
--quiet
done <<< "${old_digests}"
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PORT=8080

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
&& python -m pip install -r requirements.txt

COPY . .

CMD ["gunicorn", "--bind", ":8080", "--workers", "1", "--threads", "1", "--timeout", "300", "main:app"]
Loading