From 1fab15eda5a045e1279d682fccf8d87bd359c615 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:00:35 +0800 Subject: [PATCH] fix: add explicit single-target image-only no-traffic staging Co-Authored-By: Codex --- .github/workflows/ci.yml | 3 + .../workflows/runtime-target-lifecycle.yml | 1 + .github/workflows/sync-cloud-run-env.yml | 127 +++++++++++++- tests/test_sync_cloud_run_env_workflow.sh | 160 +++++++++++++++++- 4 files changed, 285 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f090ff9..6c763aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,9 @@ jobs: - name: Ensure uv.lock matches pyproject.toml run: uv lock --check + - name: Test bounded deployment workflow + run: bash tests/test_sync_cloud_run_env_workflow.sh + - name: Run unit tests run: | set -euo pipefail diff --git a/.github/workflows/runtime-target-lifecycle.yml b/.github/workflows/runtime-target-lifecycle.yml index 2a8e9cf..c133081 100644 --- a/.github/workflows/runtime-target-lifecycle.yml +++ b/.github/workflows/runtime-target-lifecycle.yml @@ -30,6 +30,7 @@ concurrency: jobs: lifecycle: + if: github.event_name != 'workflow_run' || github.event.workflow_run.display_title != 'Image-only no-traffic staging' name: Publish ${{ matrix.target.label }} target lifecycle runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index a2a1f34..1b69ead 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -1,10 +1,27 @@ name: Deploy Cloud Run +run-name: ${{ inputs.deployment_mode == 'image-only-no-traffic' && 'Image-only no-traffic staging' || 'Deploy Cloud Run' }} on: workflow_dispatch: inputs: + deployment_mode: + description: "Legacy deployment or explicit single-target image-only staging without traffic." + required: true + type: choice + default: legacy + options: + - legacy + - image-only-no-traffic + approved_ref: + description: "Exact workflow branch/tag name approved for image-only staging." + required: false + type: string + source_commit: + description: "Full approved source SHA; must equal the dispatched workflow SHA." + required: false + type: string target: - description: "Deployment target to run. Use hk-verify for an isolated HK dry-run Cloud Run service." + description: "Legacy: configured or isolated verify targets. Image-only: exactly PAPER, HK, or SG." required: true type: choice default: configured @@ -12,6 +29,9 @@ on: - configured - hk-verify - paper-command-verify + - PAPER + - HK + - SG cloud_run_region: description: "Cloud Run region for hk-verify. Leave blank to use the longbridge-hk Environment value." required: false @@ -93,10 +113,106 @@ concurrency: cancel-in-progress: false jobs: + image-only-no-traffic: + name: Stage ${{ inputs.target }} image without traffic + if: github.event_name == 'workflow_dispatch' && inputs.deployment_mode == 'image-only-no-traffic' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write + environment: ${{ inputs.target == 'PAPER' && 'longbridge-paper' || inputs.target == 'HK' && 'longbridge-hk' || inputs.target == 'SG' && 'longbridge-sg' || '' }} + env: + DEPLOYMENT_MODE: ${{ inputs.deployment_mode }} + WORKFLOW_TARGET: ${{ inputs.target }} + APPROVED_REF: ${{ inputs.approved_ref }} + SOURCE_COMMIT: ${{ inputs.source_commit }} + CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} + CLOUD_RUN_SERVICE: ${{ secrets.CLOUD_RUN_SERVICE }} + GCP_ARTIFACT_REGISTRY_HOSTNAME: ${{ vars.GCP_ARTIFACT_REGISTRY_HOSTNAME }} + steps: + - name: Validate image-only dispatch + run: | + set -euo pipefail + if [ "${GITHUB_EVENT_NAME:-}" != "workflow_dispatch" ] || [ "${DEPLOYMENT_MODE:-}" != "image-only-no-traffic" ]; then + echo "Explicit image-only dispatch required." >&2 + exit 1 + fi + case "${WORKFLOW_TARGET:-}" in + PAPER|HK|SG) ;; + *) echo "Image-only staging requires exactly one configured target." >&2; exit 1 ;; + esac + if [ -z "${APPROVED_REF:-}" ] || [ "${APPROVED_REF}" != "${GITHUB_REF_NAME:-}" ] \ + || [[ ! "${SOURCE_COMMIT:-}" =~ ^[0-9a-f]{40}$ ]] || [ "${SOURCE_COMMIT}" != "${GITHUB_SHA:-}" ] \ + || [ "${SOURCE_COMMIT}" != "${GITHUB_WORKFLOW_SHA:-}" ] \ + || [ "${GITHUB_WORKFLOW_REF:-}" != "${GITHUB_REPOSITORY}/.github/workflows/sync-cloud-run-env.yml@${GITHUB_REF}" ]; then + echo "Approved workflow ref and source SHA must match the dispatch." >&2 + exit 1 + fi + if [ -z "${CLOUD_RUN_SERVICE:-}" ] || [ -z "${CLOUD_RUN_REGION:-}" ]; then + echo "Configured service and region are required." >&2 + exit 1 + fi + + - name: Checkout exact image-only source + uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Verify exact image-only source + run: | + set -euo pipefail + if [ "$(git rev-parse HEAD)" != "${SOURCE_COMMIT}" ]; then + echo "Checked-out source does not match the approved SHA." >&2 + exit 1 + fi + + - name: Authenticate selected image-only target + 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 for image-only staging + uses: google-github-actions/setup-gcloud@v3 + with: + project_id: ${{ env.GCP_PROJECT_ID }} + version: ">= 416.0.0" + + - name: Build and stage image without traffic + run: | + set -euo pipefail + # Updating, rather than deploying, requires an existing configured service. + existing_service="$(gcloud run services describe "${CLOUD_RUN_SERVICE}" \ + --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" --format='value(metadata.name)')" + if [ "${existing_service}" != "${CLOUD_RUN_SERVICE}" ]; then + echo "Configured service readback did not match." >&2 + exit 1 + fi + 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}:${SOURCE_COMMIT}-${GITHUB_RUN_ID}" + gcloud auth configure-docker "${artifact_registry_hostname}" --quiet + # Auth writes temporary credentials into the workspace; build tracked source only. + git archive HEAD | docker build --pull -t "${image}" - + docker push "${image}" + digest="$(gcloud artifacts docker images describe "${image}" --format='value(image_summary.digest)')" + if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "Published image did not resolve to an immutable digest." >&2 + exit 1 + fi + # Preserve existing configuration and serving revisions. --no-traffic + # pins an existing LATEST selector to its current revision, without moving traffic. + gcloud run services update "${CLOUD_RUN_SERVICE}" \ + --project="${GCP_PROJECT_ID}" --region="${CLOUD_RUN_REGION}" \ + --image="${image_repo}@${digest}" --no-traffic \ + --update-labels="commit-sha=${SOURCE_COMMIT},github-run-id=${GITHUB_RUN_ID}" --quiet + sync: name: Deploy / Sync ${{ matrix.target.label }} Cloud Run # Production deployment and environment sync require an explicit operator dispatch. - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && inputs.deployment_mode != 'image-only-no-traffic' runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -257,6 +373,11 @@ jobs: printf '%s\n' "$@" >> "$GITHUB_OUTPUT" } + case "${WORKFLOW_TARGET:-configured}" in + configured|hk-verify|paper-command-verify) ;; + *) echo "Explicit configured targets require image-only-no-traffic mode." >&2; exit 1 ;; + esac + deploy_enabled=false env_sync_enabled=false scheduler_sync_enabled=false @@ -1440,7 +1561,7 @@ jobs: cleanup-shared-monitor: name: Retire shared monitor dispatcher needs: sync - if: needs.sync.result == 'success' && inputs.target == 'configured' + if: needs.sync.result == 'success' && inputs.target == 'configured' && inputs.deployment_mode != 'image-only-no-traffic' runs-on: ubuntu-latest timeout-minutes: 10 environment: longbridge-sg diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index cbe437b..94aaf6a 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -33,7 +33,7 @@ grep -Fq 'CLOUD_SCHEDULER_LOCATION: ${{ vars.CLOUD_SCHEDULER_LOCATION }}' "$work grep -Fq 'CLOUD_SCHEDULER_MAIN_TIME: ${{ vars.CLOUD_SCHEDULER_MAIN_TIME }}' "$workflow_file" grep -Fq 'CLOUD_SCHEDULER_PROBE_TIME: ${{ vars.CLOUD_SCHEDULER_PROBE_TIME }}' "$workflow_file" grep -Fq 'CLOUD_SCHEDULER_PRECHECK_TIME: ${{ vars.CLOUD_SCHEDULER_PRECHECK_TIME }}' "$workflow_file" -grep -Fq 'CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON }}' "$workflow_file" +grep -Fq 'CLOUD_RUN_SERVICE_TARGETS_JSON: ${{ vars.CLOUD_RUN_SERVICE_TARGETS_JSON || secrets.CLOUD_RUN_SERVICE_TARGETS_JSON }}' "$workflow_file" grep -Fq 'GCP_SCHEDULER_SERVICE_ACCOUNT: longbridge-platform-scheduler@longbridgequant.iam.gserviceaccount.com' "$workflow_file" grep -Fq 'Skipping Cloud Run commit wait because CLOUD_RUN_ENV_SYNC_WAIT_FOR_COMMIT is disabled.' "$workflow_file" grep -Fq 'permissions:' "$workflow_file" @@ -62,7 +62,7 @@ grep -Fq "gcloud run services describe \"\${CLOUD_RUN_SERVICE}\" --region \"\${C grep -Fq 'Timed out waiting for Cloud Run service ${CLOUD_RUN_SERVICE} to deploy commit ${target_sha}. Last seen commit: ${deployed_sha:-}' "$workflow_file" grep -Fq 'ENABLE_GITHUB_ENV_SYNC: ${{ vars.ENABLE_GITHUB_ENV_SYNC }}' "$workflow_file" grep -Fq 'ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION: ${{ vars.ENABLE_MAIN_PUSH_CLOUD_RUN_AUTOMATION }}' "$workflow_file" -grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" +grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ secrets.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" grep -Fq 'STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.STRATEGY_PLUGIN_ALERT_EMAIL_SENDER_PASSWORD }}' "$workflow_file" grep -Fq 'STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN: ${{ secrets.STRATEGY_PLUGIN_ALERT_SMS_AUTH_TOKEN }}' "$workflow_file" @@ -133,7 +133,7 @@ grep -Fq 'GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_CLOUD_PROJECT || env.GCP_PROJECT grep -Fq 'LONGBRIDGE_DURABLE_EXECUTION_COMMAND_PAPER_ENABLED: ${{ vars.LONGBRIDGE_DURABLE_EXECUTION_COMMAND_PAPER_ENABLED }}' "$workflow_file" grep -Fq 'LONGBRIDGE_DURABLE_EXECUTION_COMMAND_PAPER_CONSUMER_ENABLED: ${{ vars.LONGBRIDGE_DURABLE_EXECUTION_COMMAND_PAPER_CONSUMER_ENABLED }}' "$workflow_file" grep -Fq 'LONGBRIDGE_EXECUTION_COMMAND_CLOUD_URI: ${{ vars.LONGBRIDGE_EXECUTION_COMMAND_CLOUD_URI }}' "$workflow_file" -grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON }}' "$workflow_file" +grep -Fq 'RUNTIME_TARGET_JSON: ${{ vars.RUNTIME_TARGET_JSON || secrets.RUNTIME_TARGET_JSON }}' "$workflow_file" grep -Fq 'ACCOUNT_REGION: ${{ vars.ACCOUNT_REGION || matrix.target.default_account_region }}' "$workflow_file" grep -Fq 'write_github_output "enabled=false"' "$workflow_file" grep -Fq 'Skipping ${DEPLOYMENT_LABEL} Cloud Run automation because ENABLE_GITHUB_CLOUD_RUN_DEPLOY and ENABLE_GITHUB_ENV_SYNC are not true.' "$workflow_file" @@ -322,3 +322,157 @@ if grep -Fq 'LONGPORT_APP_SECRET: ${{ secrets.LONGPORT_APP_SECRET }}' "$workflow echo "unexpected GitHub secret fallback for LONGPORT_APP_SECRET still present" >&2 exit 1 fi + +# Execute the workflow's actual shell blocks with local command stubs only. +python3 - "$workflow_file" <<'PY' +import json +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import textwrap + +workflow = Path(sys.argv[1]).read_text() +lifecycle = Path(sys.argv[1]).with_name("runtime-target-lifecycle.yml").read_text() +run_name = re.search(r"^run-name: \$\{\{ (.+) \}\}$", workflow, re.MULTILINE) +assert run_name, "image-only staging must identify downstream lifecycle exclusion" +lifecycle_job = lifecycle.split(" lifecycle:\n", 1)[1] +lifecycle_if = re.search(r"^ if: (.+)$", lifecycle_job, re.MULTILINE) +assert lifecycle_if, "lifecycle must skip image-only completions before matrix authentication" +# Evaluate the actual two comparison/boolean expressions for dispatch and follow-up events. +for mode in ("legacy", "image-only-no-traffic"): + title = eval(run_name[1].replace("inputs.deployment_mode", "mode").replace("&&", "and").replace("||", "or"), + {"__builtins__": {}}, {"mode": mode}) + for event in ("workflow_run", "workflow_dispatch", "schedule"): + condition = lifecycle_if[1].replace("github.event_name", "event").replace( + "github.event.workflow_run.display_title", "title").replace("||", "or") + allowed = eval(condition, {"__builtins__": {}}, {"event": event, "title": title}) + assert allowed == (event != "workflow_run" or mode == "legacy"), (mode, event) +print("lifecycle event condition cases: 6 passed") +assert " image-only-no-traffic:\n" in workflow, "missing explicit no-traffic job" +job = workflow.split(" image-only-no-traffic:\n", 1)[1].split("\n sync:\n", 1)[0] +assert "matrix" not in job, "image-only mode must select exactly one environment" +assert "inputs.deployment_mode == 'image-only-no-traffic'" in job +assert "inputs.target == 'PAPER'" in job and "inputs.target == 'HK'" in job and "inputs.target == 'SG'" in job +assert "ref: ${{ github.sha }}" in job +legacy = workflow.split("\n sync:\n", 1)[1].split("\n cleanup-shared-monitor:\n", 1)[0] +cleanup = workflow.split("\n cleanup-shared-monitor:\n", 1)[1] +for section in (legacy, cleanup): + assert "inputs.deployment_mode != 'image-only-no-traffic'" in section.split(" steps:", 1)[0] +assert job.index("name: Validate image-only dispatch") < job.index("uses: google-github-actions/auth@") +assert job.index("name: Verify exact image-only source") < job.index("uses: google-github-actions/auth@") +# The isolated job cannot bind broker/runtime credentials or run legacy helpers. +assert re.findall(r"secrets\.([A-Z_]+)", job) == ["CLOUD_RUN_SERVICE"] +for forbidden in ("scripts/", "sync_plan", "scheduler", "cleanup", "retire", "update-traffic"): + assert forbidden not in job.lower(), forbidden + + +def run_block(name): + step = job.split(f" - name: {name}\n", 1)[1].split("\n - ", 1)[0] + return textwrap.dedent(step.split(" run: |\n", 1)[1]) + + +blocks = [run_block(name) for name in ( + "Validate image-only dispatch", "Verify exact image-only source", "Build and stage image without traffic", +)] + +with tempfile.TemporaryDirectory(prefix="lb-no-traffic-test-") as directory: + root = Path(directory) + log = root / "calls.jsonl" + stub = "#!" + sys.executable + "\n" + ''' +import json, os, sys +from pathlib import Path +command = Path(sys.argv[0]).name +args = sys.argv[1:] +with open(os.environ["STUB_LOG"], "a") as stream: + stream.write(json.dumps([command, *args]) + "\\n") +if command == "git" and args == ["rev-parse", "HEAD"]: + print(os.environ["CHECKOUT_SHA"]) +elif command == "git" and args == ["archive", "HEAD"]: + print("synthetic tracked source archive") +elif command == "docker" and args[0] in ("build", "push"): + pass +elif command == "gcloud" and args[:3] == ["run", "services", "describe"]: + if os.environ.get("SERVICE_MISSING") == "1": + sys.exit(1) + print(os.environ["CLOUD_RUN_SERVICE"]) +elif command == "gcloud" and args[:2] == ["auth", "configure-docker"]: + pass +elif command == "gcloud" and args[:4] == ["artifacts", "docker", "images", "describe"]: + print(os.environ["IMAGE_DIGEST"]) +elif command == "gcloud" and args[:3] == ["run", "services", "update"]: + if os.environ.get("UPDATE_FAIL") == "1": + sys.exit(1) +else: + raise SystemExit("unexpected command in image-only workflow") +''' + for command in ("git", "docker", "gcloud"): + path = root / command + path.write_text(stub) + path.chmod(0o700) + base = { + "PATH": f"{root}:/usr/bin:/bin", "HOME": str(root), "STUB_LOG": str(log), + "DEPLOYMENT_MODE": "image-only-no-traffic", "WORKFLOW_TARGET": "PAPER", + "APPROVED_REF": "main", "SOURCE_COMMIT": "a" * 40, + "GITHUB_REF_NAME": "main", "GITHUB_SHA": "a" * 40, "CHECKOUT_SHA": "a" * 40, + "GITHUB_EVENT_NAME": "workflow_dispatch", "GITHUB_RUN_ID": "123", + "GITHUB_REPOSITORY": "synthetic/repository", "GITHUB_REF": "refs/heads/main", + "GITHUB_WORKFLOW_SHA": "a" * 40, + "GITHUB_WORKFLOW_REF": "synthetic/repository/.github/workflows/sync-cloud-run-env.yml@refs/heads/main", + "CLOUD_RUN_SERVICE": "synthetic-paper", "CLOUD_RUN_REGION": "synthetic-region", + "GCP_PROJECT_ID": "synthetic-project", "GCP_ARTIFACT_REGISTRY_HOSTNAME": "registry.invalid", + "GCP_ARTIFACT_REGISTRY_REPOSITORY": "synthetic-images", "IMAGE_DIGEST": "sha256:" + "b" * 64, + } + + def execute(**overrides): + log.write_text("") + result = subprocess.run( + ["bash", "-c", "\n".join(blocks)], env={**base, **overrides}, + text=True, capture_output=True, cwd=root, + ) + return result.returncode, [json.loads(line) for line in log.read_text().splitlines()] + + cases = 0 + for label in ("PAPER", "HK", "SG"): + code, calls = execute(WORKFLOW_TARGET=label, CLOUD_RUN_SERVICE=f"synthetic-{label.lower()}") + assert code == 0, label + updates = [call for call in calls if call[:4] == ["gcloud", "run", "services", "update"]] + assert len(updates) == 1 + service = f"synthetic-{label.lower()}" + image_repo = f"registry.invalid/synthetic-project/synthetic-images/longbridgeplatform/{service}" + assert updates[0] == [ + "gcloud", "run", "services", "update", service, + "--project=synthetic-project", "--region=synthetic-region", + f"--image={image_repo}@{base['IMAGE_DIGEST']}", "--no-traffic", + f"--update-labels=commit-sha={base['SOURCE_COMMIT']},github-run-id=123", "--quiet", + ] + assert sum(call[:2] == ["docker", "push"] for call in calls) == 1 + assert [call for call in calls if call[:2] == ["docker", "build"]] == [ + ["docker", "build", "--pull", "-t", f"{image_repo}:{base['SOURCE_COMMIT']}-123", "-"], + ] + assert ["git", "archive", "HEAD"] in calls + cases += 1 + for overrides in ( + {"WORKFLOW_TARGET": "configured"}, {"WORKFLOW_TARGET": "hk-verify"}, + {"WORKFLOW_TARGET": "paper-command-verify"}, {"WORKFLOW_TARGET": ""}, + {"DEPLOYMENT_MODE": "legacy"}, {"GITHUB_EVENT_NAME": "push"}, + {"APPROVED_REF": "other"}, {"APPROVED_REF": ""}, + {"SOURCE_COMMIT": "main"}, {"SOURCE_COMMIT": "c" * 40}, + {"GITHUB_WORKFLOW_SHA": "c" * 40}, {"GITHUB_WORKFLOW_REF": "other-workflow"}, + {"CLOUD_RUN_SERVICE": ""}, {"CLOUD_RUN_REGION": ""}, + ): + code, calls = execute(**overrides) + assert code != 0 and calls == [], overrides + cases += 1 + for overrides in ({"CHECKOUT_SHA": "c" * 40}, {"SERVICE_MISSING": "1"}, {"IMAGE_DIGEST": "not-a-digest"}): + code, calls = execute(**overrides) + assert code != 0, overrides + assert not any(call[:4] == ["gcloud", "run", "services", "update"] for call in calls) + cases += 1 + code, calls = execute(UPDATE_FAIL="1") + assert code != 0 + assert sum(call[:4] == ["gcloud", "run", "services", "update"] for call in calls) == 1 + cases += 1 + print(f"image-only no-traffic shell cases: {cases} passed") +PY