From 366a6e021f911df5c59311d5f4da4a83f0d7691c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:32:18 +0800 Subject: [PATCH 1/2] ops: add FT AFS align workflow for sole broker account rotation Discover the sole Firstrade broker account from GCS funds snapshots, patch RUNTIME_TARGET_JSON.account_selector, and emit a one-day rotation artifact for QRS Firstrade source-binding rotation. Masks account ids. --- .../workflows/ft-afs-align-broker-account.yml | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .github/workflows/ft-afs-align-broker-account.yml diff --git a/.github/workflows/ft-afs-align-broker-account.yml b/.github/workflows/ft-afs-align-broker-account.yml new file mode 100644 index 0000000..abf8fe9 --- /dev/null +++ b/.github/workflows/ft-afs-align-broker-account.yml @@ -0,0 +1,183 @@ +name: FT AFS align broker account + +# Discover the sole live Firstrade broker account from GCS funds snapshots, +# patch RUNTIME_TARGET_JSON.account_selector, and emit QRS rotation JSON +# (artifact) for LongBridge-style binding rotation. Masks account ids in logs. +# No strategy / trading-mode changes. + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + GCP_PROJECT_ID: firstradequant + GCP_WORKLOAD_IDENTITY_PROVIDER: projects/1088907247379/locations/global/workloadIdentityPools/github-actions/providers/github-main + GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: firstrade-platform-deploy@firstradequant.iam.gserviceaccount.com + +concurrency: + group: ft-afs-align-broker-account + cancel-in-progress: false + +jobs: + align: + name: Discover sole account and prepare rotation + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + secrets: write + env: + CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} + CLOUD_RUN_SERVICE: ${{ secrets.CLOUD_RUN_SERVICE }} + FIRSTRADE_GCS_STATE_BUCKET: ${{ vars.FIRSTRADE_GCS_STATE_BUCKET }} + FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX || 'firstrade-platform' }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v3 + with: + project_id: ${{ env.GCP_PROJECT_ID }} + - name: Discover sole account, patch selector, emit rotation artifact + run: | + set -euo pipefail + umask 077 + svc="${CLOUD_RUN_SERVICE}" + region="${CLOUD_RUN_REGION}" + gcloud run services describe "${svc}" --region="${region}" --format=json > /tmp/service.json + rev="$(python3 - <<'PY' + import json + s=json.load(open("/tmp/service.json")) + print((s.get("status") or {}).get("latestReadyRevisionName") or "") + PY + )" + test -n "${rev}" + gcloud run revisions describe "${rev}" --region="${region}" --format=json > /tmp/rev.json + mkdir -p /tmp/ft-afs-align + python3 - <<'PY' + import hashlib, json, os, re, subprocess, sys + + def mask(value: str) -> str: + value = str(value or "") + if len(value) <= 4: + return "*" * len(value) + return ("*" * (len(value) - 4)) + value[-4:] + + rev = json.load(open("/tmp/rev.json")) + envs = (((rev.get("spec") or {}).get("containers") or [{}])[0].get("env") or []) + by_name = {row.get("name"): row for row in envs if isinstance(row, dict)} + + def env_value(name: str) -> str: + row = by_name.get(name) or {} + if "value" in row and row["value"] is not None: + return str(row["value"]) + return "" + + runtime_raw = env_value("RUNTIME_TARGET_JSON") + if not runtime_raw: + print("status=blocked reason=runtime_target_missing") + sys.exit(1) + try: + runtime = json.loads(runtime_raw) + except Exception: + print("status=blocked reason=runtime_target_invalid") + sys.exit(1) + if not isinstance(runtime, dict) or runtime.get("platform_id") != "firstrade": + print("status=blocked reason=runtime_target_invalid") + sys.exit(1) + selectors = runtime.get("account_selector") + if isinstance(selectors, str): + selectors = [selectors] + if not isinstance(selectors, list) or len(selectors) != 1 or not isinstance(selectors[0], str): + print("status=blocked reason=runtime_selector_invalid") + sys.exit(1) + previous_selector = selectors[0] + previous_binding = env_value("FIRSTRADE_ACCOUNT_FACTS_SOURCE_BINDING_ID") + target_id = env_value("FIRSTRADE_ACCOUNT_FACTS_TARGET_ID") or "firstrade-homepage" + if not re.fullmatch(r"[a-f0-9]{64}", previous_binding or ""): + print("status=blocked reason=previous_binding_missing") + sys.exit(1) + + bucket = os.environ["FIRSTRADE_GCS_STATE_BUCKET"] + prefix = os.environ["FIRSTRADE_STATE_PREFIX"].rstrip("/") + listed = subprocess.check_output( + ["gcloud", "storage", "ls", f"gs://{bucket}/{prefix}/accounts/*/funds/latest.json"], + text=True, + ).strip().splitlines() + accounts = [] + for uri in listed: + if not uri.strip(): + continue + raw = subprocess.check_output(["gcloud", "storage", "cat", uri.strip()], text=True) + try: + payload = json.loads(raw) + except Exception: + continue + account = payload.get("account") + if isinstance(account, str) and account and account not in accounts: + accounts.append(account) + if len(accounts) != 1: + print(f"status=blocked reason=sole_account_not_unique count={len(accounts)}") + sys.exit(1) + sole = accounts[0] + if not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", sole): + print("status=blocked reason=sole_account_invalid") + sys.exit(1) + if not sole.endswith("5979"): + print(f"status=blocked reason=sole_account_suffix_mismatch masked={mask(sole)}") + sys.exit(1) + print(f"selector_was={mask(previous_selector)} sole={mask(sole)} target_id={target_id}") + print(f"previous_source_binding_id={previous_binding}") + + service_name = str(runtime.get("service_name") or "") + deployment_selector = str(runtime.get("deployment_selector") or "") + account_scope = str(runtime.get("account_scope") or "") + if not service_name or not deployment_selector or not account_scope: + print("status=blocked reason=runtime_identity_incomplete") + sys.exit(1) + # Match Node JSON.stringify key insertion order from prepare_firstrade_account_facts_binding.mjs + next_id = hashlib.sha256(json.dumps({ + "platform": "firstrade", + "service_name": service_name, + "deployment_selector": deployment_selector, + "account_scope": account_scope, + "broker_account_id": sole, + }, separators=(",", ":")).encode()).hexdigest() + if next_id == previous_binding: + print("status=blocked reason=binding_id_unchanged") + sys.exit(1) + print(f"next_source_binding_id={next_id}") + + runtime["account_selector"] = [sole] + patched = json.dumps(runtime, separators=(",", ":")) + subprocess.run( + ["gh", "secret", "set", "RUNTIME_TARGET_JSON", "--repo", "QuantStrategyLab/FirstradePlatform"], + input=patched, text=True, check=True, + ) + print("runtime_target_secret=updated") + + rotation = { + "target_id": target_id, + "previous_source_binding_id": previous_binding, + "next_source_binding_id": next_id, + "next_broker_account_id": sole, + } + out = "/tmp/ft-afs-align/firstrade-afs-rotation.json" + with open(out, "w", encoding="utf-8") as handle: + json.dump(rotation, handle, separators=(",", ":")) + os.chmod(out, 0o600) + print("rotation_artifact=ready") + print("status=prepared") + PY + - name: Upload rotation artifact + uses: actions/upload-artifact@v4 + with: + name: firstrade-afs-rotation + path: /tmp/ft-afs-align/firstrade-afs-rotation.json + if-no-files-found: error + retention-days: 1 From 64e063446bb68c17636d45f43cffa381eabd91e8 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:33:07 +0800 Subject: [PATCH 2/2] ops: emit FT AFS rotation artifacts without GITHUB_TOKEN secret writes GITHUB_TOKEN cannot update repository secrets. Discover sole broker account and upload rotation + patched RUNTIME_TARGET for operator apply. --- .../workflows/ft-afs-align-broker-account.yml | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ft-afs-align-broker-account.yml b/.github/workflows/ft-afs-align-broker-account.yml index abf8fe9..0707f71 100644 --- a/.github/workflows/ft-afs-align-broker-account.yml +++ b/.github/workflows/ft-afs-align-broker-account.yml @@ -1,9 +1,9 @@ name: FT AFS align broker account -# Discover the sole live Firstrade broker account from GCS funds snapshots, -# patch RUNTIME_TARGET_JSON.account_selector, and emit QRS rotation JSON -# (artifact) for LongBridge-style binding rotation. Masks account ids in logs. -# No strategy / trading-mode changes. +# Discover the sole live Firstrade broker account from GCS funds snapshots and +# emit rotation + patched RUNTIME_TARGET artifacts for QRS/FT secret updates. +# Masks account ids in logs. No strategy / trading-mode changes. +# Does not write GitHub secrets (GITHUB_TOKEN cannot); operator applies artifacts. on: workflow_dispatch: @@ -28,13 +28,11 @@ jobs: permissions: contents: read id-token: write - secrets: write env: CLOUD_RUN_REGION: ${{ vars.CLOUD_RUN_REGION }} CLOUD_RUN_SERVICE: ${{ secrets.CLOUD_RUN_SERVICE }} FIRSTRADE_GCS_STATE_BUCKET: ${{ vars.FIRSTRADE_GCS_STATE_BUCKET }} FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX || 'firstrade-platform' }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: google-github-actions/auth@v3 with: @@ -43,7 +41,7 @@ jobs: - uses: google-github-actions/setup-gcloud@v3 with: project_id: ${{ env.GCP_PROJECT_ID }} - - name: Discover sole account, patch selector, emit rotation artifact + - name: Discover sole account and emit rotation artifacts run: | set -euo pipefail umask 077 @@ -140,7 +138,6 @@ jobs: if not service_name or not deployment_selector or not account_scope: print("status=blocked reason=runtime_identity_incomplete") sys.exit(1) - # Match Node JSON.stringify key insertion order from prepare_firstrade_account_facts_binding.mjs next_id = hashlib.sha256(json.dumps({ "platform": "firstrade", "service_name": service_name, @@ -154,30 +151,28 @@ jobs: print(f"next_source_binding_id={next_id}") runtime["account_selector"] = [sole] - patched = json.dumps(runtime, separators=(",", ":")) - subprocess.run( - ["gh", "secret", "set", "RUNTIME_TARGET_JSON", "--repo", "QuantStrategyLab/FirstradePlatform"], - input=patched, text=True, check=True, - ) - print("runtime_target_secret=updated") - + out_dir = "/tmp/ft-afs-align" + with open(f"{out_dir}/runtime-target.patched.json", "w", encoding="utf-8") as handle: + json.dump(runtime, handle, separators=(",", ":")) rotation = { "target_id": target_id, "previous_source_binding_id": previous_binding, "next_source_binding_id": next_id, "next_broker_account_id": sole, } - out = "/tmp/ft-afs-align/firstrade-afs-rotation.json" - with open(out, "w", encoding="utf-8") as handle: + with open(f"{out_dir}/firstrade-afs-rotation.json", "w", encoding="utf-8") as handle: json.dump(rotation, handle, separators=(",", ":")) - os.chmod(out, 0o600) + for name in ("runtime-target.patched.json", "firstrade-afs-rotation.json"): + os.chmod(f"{out_dir}/{name}", 0o600) print("rotation_artifact=ready") print("status=prepared") PY - - name: Upload rotation artifact + - name: Upload rotation artifacts uses: actions/upload-artifact@v4 with: name: firstrade-afs-rotation - path: /tmp/ft-afs-align/firstrade-afs-rotation.json + path: | + /tmp/ft-afs-align/firstrade-afs-rotation.json + /tmp/ft-afs-align/runtime-target.patched.json if-no-files-found: error retention-days: 1