From 398fcffed93f94e3d44ecd3777927bfdc00a2a8c Mon Sep 17 00:00:00 2001 From: riddim-developer-bot Date: Mon, 25 May 2026 15:43:13 -0400 Subject: [PATCH] [EPAC-2080]: read ASC credentials from env vars before falling back to AWS Secrets Manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three release scripts (resolve_build_id.py, wait_for_build_processed.py, attach_build_to_group.py) unconditionally called AWS Secrets Manager for App Store Connect credentials. The production TestFlight workflow provides these via GitHub secrets, not AWS, so the Secrets Manager call fails with exit 255. Add get_asc_credentials() to each script — checks ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY / ASC_KEY_PATH env vars first, matching the pattern already used by submit_beta_app_review.py. Export ASC_KEY_PATH from both the workflow and fetch_asc_secret.sh so downstream steps can locate the .p8 file. --- .github/workflows/testflight-build.yml | 1 + scripts/release/attach_build_to_group.py | 29 ++++++++++++----- scripts/release/resolve_build_id.py | 35 ++++++++++++++++----- scripts/release/wait_for_build_processed.py | 22 +++++++++++-- scripts/runner/fetch_asc_secret.sh | 1 + 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/.github/workflows/testflight-build.yml b/.github/workflows/testflight-build.yml index ac80e0b0..49741144 100644 --- a/.github/workflows/testflight-build.yml +++ b/.github/workflows/testflight-build.yml @@ -119,6 +119,7 @@ jobs: { echo "ASC_KEY_ID=$ASC_KEY_ID" echo "ASC_ISSUER_ID=$ASC_ISSUER_ID" + echo "ASC_KEY_PATH=$p8_path" } >> "$GITHUB_ENV" - name: Install release helper dependencies diff --git a/scripts/release/attach_build_to_group.py b/scripts/release/attach_build_to_group.py index fcbdb30d..b34f1bf9 100644 --- a/scripts/release/attach_build_to_group.py +++ b/scripts/release/attach_build_to_group.py @@ -9,6 +9,7 @@ import subprocess import sys import time +from pathlib import Path import jwt import requests @@ -26,7 +27,6 @@ class RetriesExhausted(AttachBuildError): """Raised after retry budget is exhausted.""" - def get_asc_secret() -> dict[str, str]: """Fetch ASC API credentials from AWS Secrets Manager.""" result = subprocess.run( @@ -50,6 +50,24 @@ def get_asc_secret() -> dict[str, str]: return json.loads(result.stdout) +def get_asc_credentials() -> tuple[str, str, str]: + """Read ASC credentials from environment first, otherwise from Secrets Manager.""" + key_id = os.getenv("ASC_KEY_ID") + issuer_id = os.getenv("ASC_ISSUER_ID") + private_key = os.getenv("ASC_PRIVATE_KEY") + + if key_id and issuer_id and private_key: + return key_id, issuer_id, private_key + + key_path = os.getenv("ASC_KEY_PATH") + if key_id and issuer_id and key_path: + return key_id, issuer_id, Path(os.path.expanduser(key_path)).read_text(encoding="utf-8") + + secret = get_asc_secret() + private_key = secret.get("private_key") or secret.get("key") + return secret["key_id"], secret["issuer_id"], private_key + + def get_asc_token(key_id: str, issuer_id: str, private_key: str) -> str: """Generate a signed JWT for ASC API authentication.""" now = int(time.time()) @@ -173,13 +191,8 @@ def main(argv: list[str] | None = None) -> int: args = parse_args(argv) bundle_id = args.bundle_id.strip() or None - secret = get_asc_secret() - private_key = secret["private_key"] if "private_key" in secret else secret["key"] - token = get_asc_token( - secret["key_id"], - secret["issuer_id"], - private_key, - ) + key_id, issuer_id, private_key = get_asc_credentials() + token = get_asc_token(key_id, issuer_id, private_key) try: group_id = find_group_id(args.group_name, token, bundle_id=bundle_id) diff --git a/scripts/release/resolve_build_id.py b/scripts/release/resolve_build_id.py index c839bd7a..fe5bf101 100755 --- a/scripts/release/resolve_build_id.py +++ b/scripts/release/resolve_build_id.py @@ -5,29 +5,47 @@ import subprocess import sys import time +from pathlib import Path import jwt import requests + def get_asc_secret(): env = os.environ.copy() env["AWS_PROFILE"] = os.environ.get("AWS_PROFILE", "riddim-agent") res = subprocess.run( [ - "aws", "secretsmanager", "get-secret-value", - "--secret-id", "appstore/connect-api", - "--region", "us-east-1", - "--query", "SecretString", + "aws", "secretsmanager", "get-secret-value", + "--secret-id", "appstore/connect-api", + "--region", "us-east-1", + "--query", "SecretString", "--output", "text" - ], - check=True, - capture_output=True, + ], + check=True, + capture_output=True, text=True, env=env ) secret = json.loads(res.stdout) return secret["key_id"], secret["issuer_id"], secret.get("key") or secret.get("private_key") + +def get_asc_credentials(): + key_id = os.getenv("ASC_KEY_ID") + issuer_id = os.getenv("ASC_ISSUER_ID") + private_key = os.getenv("ASC_PRIVATE_KEY") + + if key_id and issuer_id and private_key: + return key_id, issuer_id, private_key + + key_path = os.getenv("ASC_KEY_PATH") + if key_id and issuer_id and key_path: + return key_id, issuer_id, Path(os.path.expanduser(key_path)).read_text(encoding="utf-8") + + return get_asc_secret() + + def get_token(key_id, issuer_id, key): now = int(time.time()) payload = { @@ -38,6 +56,7 @@ def get_token(key_id, issuer_id, key): } return jwt.encode(payload, key, algorithm="ES256", headers={"kid": key_id}) + def main(): parser = argparse.ArgumentParser() parser.add_argument("--app-id", required=True) @@ -45,7 +64,7 @@ def main(): parser.add_argument("--build-number", required=True) args = parser.parse_args() - key_id, issuer_id, key = get_asc_secret() + key_id, issuer_id, key = get_asc_credentials() token = get_token(key_id, issuer_id, key) headers = {"Authorization": f"Bearer {token}"} diff --git a/scripts/release/wait_for_build_processed.py b/scripts/release/wait_for_build_processed.py index 7169a577..02c10e6f 100644 --- a/scripts/release/wait_for_build_processed.py +++ b/scripts/release/wait_for_build_processed.py @@ -12,6 +12,7 @@ import os import subprocess import sys +from pathlib import Path from typing import Any import jwt @@ -59,6 +60,23 @@ def get_asc_secret() -> dict[str, str]: } +def get_asc_credentials() -> tuple[str, str, str]: + """Read ASC credentials from environment first, otherwise from Secrets Manager.""" + key_id = os.getenv("ASC_KEY_ID") + issuer_id = os.getenv("ASC_ISSUER_ID") + private_key = os.getenv("ASC_PRIVATE_KEY") + + if key_id and issuer_id and private_key: + return key_id, issuer_id, private_key + + key_path = os.getenv("ASC_KEY_PATH") + if key_id and issuer_id and key_path: + return key_id, issuer_id, Path(os.path.expanduser(key_path)).read_text(encoding="utf-8") + + secret = get_asc_secret() + return secret["key_id"], secret["issuer_id"], secret["private_key"] + + def get_asc_token(key_id: str, issuer_id: str, private_key: str) -> str: """Create an App Store Connect API JWT.""" now = int(__import__("time").time()) @@ -198,8 +216,8 @@ def main() -> None: sys.exit(1) try: - secret = get_asc_secret() - token = get_asc_token(secret["key_id"], secret["issuer_id"], secret["private_key"]) + key_id, issuer_id, private_key = get_asc_credentials() + token = get_asc_token(key_id, issuer_id, private_key) status, payload = wait_for_build_processed( args.build_id, token, diff --git a/scripts/runner/fetch_asc_secret.sh b/scripts/runner/fetch_asc_secret.sh index 8b019e74..b0cff7fc 100755 --- a/scripts/runner/fetch_asc_secret.sh +++ b/scripts/runner/fetch_asc_secret.sh @@ -45,6 +45,7 @@ chmod 600 /tmp/asc_api_key.json if [[ -n "${GITHUB_ENV:-}" ]]; then echo "ASC_KEY_ID=${KEY_ID}" >> "$GITHUB_ENV" echo "ASC_ISSUER_ID=${ISSUER_ID}" >> "$GITHUB_ENV" + echo "ASC_KEY_PATH=${P8_PATH}" >> "$GITHUB_ENV" fi echo "fetch_asc_secret: wrote ${P8_PATH} and /tmp/asc_api_key.json"