From 67353cdce65f70f6c9ee979d9ba617d058784a7c Mon Sep 17 00:00:00 2001 From: Pigbibi Date: Fri, 9 Oct 2026 15:31:40 +0800 Subject: [PATCH] Fix invoke workflow YAML by indenting nested Python heredocs. Unindented heredoc bodies broke GitHub's workflow_dispatch registration for Invoke Cloud Run. --- .github/workflows/invoke-cloud-run.yml | 184 ++++++++++++------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/.github/workflows/invoke-cloud-run.yml b/.github/workflows/invoke-cloud-run.yml index e0357d1..a617627 100644 --- a/.github/workflows/invoke-cloud-run.yml +++ b/.github/workflows/invoke-cloud-run.yml @@ -193,40 +193,40 @@ jobs: fi echo "sync_token_len=${#sync_token}" SYNC_TOKEN_SECRET_NAME="${secret_name}" python3 - <<'PY' -import json, os, subprocess, sys -svc = os.environ["CLOUD_RUN_SERVICE"] -region = os.environ["CLOUD_RUN_REGION"] -project = os.environ["GCP_PROJECT_ID"] -want = os.environ["SYNC_TOKEN_SECRET_NAME"] -svc_j = json.loads(subprocess.check_output([ - "gcloud", "run", "services", "describe", svc, - "--project", project, "--region", region, "--format=json", -], text=True)) -rev = (svc_j.get("status") or {}).get("latestReadyRevisionName") or "" -print(f"serving_revision={rev}") -data = json.loads(subprocess.check_output([ - "gcloud", "run", "revisions", "describe", rev, - "--project", project, "--region", region, "--format=json", -], text=True)) -envs = (((data.get("spec") or {}).get("containers") or [{}])[0].get("env") or []) -row = next((e for e in envs if e.get("name") == "FIRSTRADE_ACCOUNT_FACTS_SYNC_TOKEN"), None) -if row is None: - print("sync_token_mount=MISSING", file=sys.stderr) - sys.exit(1) -vf = row.get("valueFrom") or {} -vs = row.get("valueSource") or {} -skr = vf.get("secretKeyRef") or vs.get("secretKeyRef") or {} -ref = skr.get("name") or skr.get("secret") or "" -print(f"sync_token_env_shape={sorted(row.keys())}") -print(f"sync_token_secret_ref={ref or ''}") -print(f"sync_token_literal_set={bool(row.get('value'))}") -if ref and ref != want: - print(f"secret_ref_mismatch want={want} got={ref}", file=sys.stderr) - sys.exit(1) -if not ref and not row.get("value"): - print("serving revision has neither secret ref nor literal SYNC_TOKEN", file=sys.stderr) - sys.exit(1) -PY + import json, os, subprocess, sys + svc = os.environ["CLOUD_RUN_SERVICE"] + region = os.environ["CLOUD_RUN_REGION"] + project = os.environ["GCP_PROJECT_ID"] + want = os.environ["SYNC_TOKEN_SECRET_NAME"] + svc_j = json.loads(subprocess.check_output([ + "gcloud", "run", "services", "describe", svc, + "--project", project, "--region", region, "--format=json", + ], text=True)) + rev = (svc_j.get("status") or {}).get("latestReadyRevisionName") or "" + print(f"serving_revision={rev}") + data = json.loads(subprocess.check_output([ + "gcloud", "run", "revisions", "describe", rev, + "--project", project, "--region", region, "--format=json", + ], text=True)) + envs = (((data.get("spec") or {}).get("containers") or [{}])[0].get("env") or []) + row = next((e for e in envs if e.get("name") == "FIRSTRADE_ACCOUNT_FACTS_SYNC_TOKEN"), None) + if row is None: + print("sync_token_mount=MISSING", file=sys.stderr) + sys.exit(1) + vf = row.get("valueFrom") or {} + vs = row.get("valueSource") or {} + skr = vf.get("secretKeyRef") or vs.get("secretKeyRef") or {} + ref = skr.get("name") or skr.get("secret") or "" + print(f"sync_token_env_shape={sorted(row.keys())}") + print(f"sync_token_secret_ref={ref or ''}") + print(f"sync_token_literal_set={bool(row.get('value'))}") + if ref and ref != want: + print(f"secret_ref_mismatch want={want} got={ref}", file=sys.stderr) + sys.exit(1) + if not ref and not row.get("value"): + print("serving revision has neither secret ref nor literal SYNC_TOKEN", file=sys.stderr) + sys.exit(1) + PY if gcloud scheduler jobs describe "${afs_job}" \ --project="${GCP_PROJECT_ID}" \ @@ -243,29 +243,29 @@ PY --quiet AFS_JOB_NAME="${afs_job}" AFS_LOCATION="${afs_location}" AFS_SYNC_TOKEN="${sync_token}" python3 - <<'PY' -import json, os, subprocess, urllib.request -project = os.environ["GCP_PROJECT_ID"] -location = os.environ["AFS_LOCATION"] -job = os.environ["AFS_JOB_NAME"] -token = os.environ["AFS_SYNC_TOKEN"] -name = f"projects/{project}/locations/{location}/jobs/{job}" -access = subprocess.check_output(["gcloud", "auth", "print-access-token"], text=True).strip() -url = f"https://cloudscheduler.googleapis.com/v1/{name}" -payload = json.dumps({"httpTarget": {"headers": {"Authorization": f"Bearer {token}"}}}).encode() -patch = urllib.request.Request( - url + "?updateMask=httpTarget.headers", - data=payload, - method="PATCH", - headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"}, -) -with urllib.request.urlopen(patch, timeout=60) as resp: - updated = json.load(resp) -auth = ((updated.get("httpTarget") or {}).get("headers") or {}).get("Authorization") or "" -print(f"scheduler_authorization_len={len(auth)}") -print(f"scheduler_authorization_bearer_prefix={str(auth.startswith('Bearer ')).lower()}") -if not auth.startswith("Bearer ") or len(auth) != len("Bearer ") + len(token): - raise SystemExit("scheduler Authorization header length/prefix mismatch after PATCH") -PY + import json, os, subprocess, urllib.request + project = os.environ["GCP_PROJECT_ID"] + location = os.environ["AFS_LOCATION"] + job = os.environ["AFS_JOB_NAME"] + token = os.environ["AFS_SYNC_TOKEN"] + name = f"projects/{project}/locations/{location}/jobs/{job}" + access = subprocess.check_output(["gcloud", "auth", "print-access-token"], text=True).strip() + url = f"https://cloudscheduler.googleapis.com/v1/{name}" + payload = json.dumps({"httpTarget": {"headers": {"Authorization": f"Bearer {token}"}}}).encode() + patch = urllib.request.Request( + url + "?updateMask=httpTarget.headers", + data=payload, + method="PATCH", + headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(patch, timeout=60) as resp: + updated = json.load(resp) + auth = ((updated.get("httpTarget") or {}).get("headers") or {}).get("Authorization") or "" + print(f"scheduler_authorization_len={len(auth)}") + print(f"scheduler_authorization_bearer_prefix={str(auth.startswith('Bearer ')).lower()}") + if not auth.startswith("Bearer ") or len(auth) != len("Bearer ") + len(token): + raise SystemExit("scheduler Authorization header length/prefix mismatch after PATCH") + PY else echo "Creating account-facts-sync scheduler ${afs_job} (OIDC + app Authorization header)." gcloud scheduler jobs create http "${afs_job}" \ @@ -281,29 +281,29 @@ PY --quiet AFS_JOB_NAME="${afs_job}" AFS_LOCATION="${afs_location}" AFS_SYNC_TOKEN="${sync_token}" python3 - <<'PY' -import json, os, subprocess, urllib.request -project = os.environ["GCP_PROJECT_ID"] -location = os.environ["AFS_LOCATION"] -job = os.environ["AFS_JOB_NAME"] -token = os.environ["AFS_SYNC_TOKEN"] -name = f"projects/{project}/locations/{location}/jobs/{job}" -access = subprocess.check_output(["gcloud", "auth", "print-access-token"], text=True).strip() -url = f"https://cloudscheduler.googleapis.com/v1/{name}" -payload = json.dumps({"httpTarget": {"headers": {"Authorization": f"Bearer {token}"}}}).encode() -patch = urllib.request.Request( - url + "?updateMask=httpTarget.headers", - data=payload, - method="PATCH", - headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"}, -) -with urllib.request.urlopen(patch, timeout=60) as resp: - updated = json.load(resp) -auth = ((updated.get("httpTarget") or {}).get("headers") or {}).get("Authorization") or "" -print(f"scheduler_authorization_len={len(auth)}") -print(f"scheduler_authorization_bearer_prefix={str(auth.startswith('Bearer ')).lower()}") -if not auth.startswith("Bearer ") or len(auth) != len("Bearer ") + len(token): - raise SystemExit("scheduler Authorization header length/prefix mismatch after PATCH") -PY + import json, os, subprocess, urllib.request + project = os.environ["GCP_PROJECT_ID"] + location = os.environ["AFS_LOCATION"] + job = os.environ["AFS_JOB_NAME"] + token = os.environ["AFS_SYNC_TOKEN"] + name = f"projects/{project}/locations/{location}/jobs/{job}" + access = subprocess.check_output(["gcloud", "auth", "print-access-token"], text=True).strip() + url = f"https://cloudscheduler.googleapis.com/v1/{name}" + payload = json.dumps({"httpTarget": {"headers": {"Authorization": f"Bearer {token}"}}}).encode() + patch = urllib.request.Request( + url + "?updateMask=httpTarget.headers", + data=payload, + method="PATCH", + headers={"Authorization": f"Bearer {access}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(patch, timeout=60) as resp: + updated = json.load(resp) + auth = ((updated.get("httpTarget") or {}).get("headers") or {}).get("Authorization") or "" + print(f"scheduler_authorization_len={len(auth)}") + print(f"scheduler_authorization_bearer_prefix={str(auth.startswith('Bearer ')).lower()}") + if not auth.startswith("Bearer ") or len(auth) != len("Bearer ") + len(token): + raise SystemExit("scheduler Authorization header length/prefix mismatch after PATCH") + PY fi unset sync_token # Confirm Authorization header is present without printing its value. @@ -409,20 +409,20 @@ PY echo "restore_probe_uri=${restore_probe_uri}" } >> "$GITHUB_OUTPUT" - - name: Authenticate for service invocation - if: steps.service.outputs.invoke_method == 'direct' - id: invoke-auth - uses: google-github-actions/auth@v3 - with: + - name: Authenticate for service invocation + if: steps.service.outputs.invoke_method == 'direct' + id: invoke-auth + uses: google-github-actions/auth@v3 + with: workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} token_format: id_token id_token_audience: ${{ steps.service.outputs.url }} id_token_include_email: true - - name: Invoke service - if: steps.service.outputs.invoke_method == 'direct' - run: | + - name: Invoke service + if: steps.service.outputs.invoke_method == 'direct' + run: | set -euo pipefail curl --fail-with-body --show-error --silent \ @@ -430,12 +430,12 @@ PY --header "Authorization: Bearer ${{ steps.invoke-auth.outputs.id_token }}" \ "${{ steps.service.outputs.url }}${{ steps.service.outputs.path }}" - - name: Invoke internal service through Cloud Scheduler - if: steps.service.outputs.invoke_method == 'scheduler' - env: + - name: Invoke internal service through Cloud Scheduler + if: steps.service.outputs.invoke_method == 'scheduler' + env: RESTORE_PROBE_URI: ${{ steps.service.outputs.restore_probe_uri }} SERVICE_URL: ${{ steps.service.outputs.url }} - run: | + run: | set -euo pipefail started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"