diff --git a/.github/workflows/ios-xcode-cloud.yml b/.github/workflows/ios-xcode-cloud.yml index 2f77734d..75edea76 100644 --- a/.github/workflows/ios-xcode-cloud.yml +++ b/.github/workflows/ios-xcode-cloud.yml @@ -11,6 +11,7 @@ on: - inspect - start - monitor + - describe - store-status default: inspect workflow_id: @@ -68,6 +69,14 @@ jobs: fi python3 scripts/xcode-cloud-control.py "${args[@]}" ;; + describe) + if [[ -z "$XCODE_CLOUD_BUILD_RUN_ID" ]]; then + echo "::error::build_run_id is required in describe mode" + exit 1 + fi + python3 scripts/xcode-cloud-control.py describe \ + --build-run-id "$XCODE_CLOUD_BUILD_RUN_ID" + ;; monitor) if [[ -z "$XCODE_CLOUD_BUILD_RUN_ID" ]]; then echo "::error::build_run_id is required in monitor mode" diff --git a/scripts/xcode-cloud-control.py b/scripts/xcode-cloud-control.py index b4ad8b08..d464665f 100644 --- a/scripts/xcode-cloud-control.py +++ b/scripts/xcode-cloud-control.py @@ -325,10 +325,26 @@ def describe_failed_run(client: AppStoreConnect, build_run_id: str) -> None: print( f"- action={action_attributes.get('name')} " f"type={action_attributes.get('actionType')} " + f"progress={action_attributes.get('executionProgress')} " f"completion={action_attributes.get('completionStatus')} " f"issues={action_attributes.get('issueCounts')}", flush=True, ) + action_id = action.get("id") + if not action_id: + continue + issue_payload = client.request( + "GET", + f"/v1/ciBuildActions/{action_id}/issues?limit=50", + ) + for issue in issue_payload.get("data") or []: + issue_attributes = issue.get("attributes") or {} + print( + f" issue type={issue_attributes.get('issueType')} " + f"category={issue_attributes.get('category')} " + f"message={issue_attributes.get('message')}", + flush=True, + ) def wait_for_app_store_build( @@ -441,6 +457,35 @@ def command_start(args: argparse.Namespace, client: AppStoreConnect) -> None: print_run_outputs(run, store_build) +def command_describe(args: argparse.Namespace, client: AppStoreConnect) -> None: + run = read_build_run(client, args.build_run_id) + attributes = run.get("attributes") or {} + print( + f"Xcode Cloud build id={run.get('id')} number={attributes.get('number')} " + f"progress={attributes.get('executionProgress')} " + f"completion={attributes.get('completionStatus')} " + f"sha={source_sha(run) or 'pending'}", + flush=True, + ) + describe_failed_run(client, args.build_run_id) + builds = client.request( + "GET", + f"/v1/ciBuildRuns/{args.build_run_id}/builds?limit=50&" + "fields[builds]=version,processingState,uploadedDate", + ).get("data") or [] + if not builds: + print("No App Store Connect builds are attached to this Cloud run.", flush=True) + return + for build in builds: + build_attributes = build.get("attributes") or {} + print( + f"App Store build id={build.get('id')} version={build_attributes.get('version')} " + f"processing={build_attributes.get('processingState')} " + f"uploaded={build_attributes.get('uploadedDate')}", + flush=True, + ) + + def command_wait(args: argparse.Namespace, client: AppStoreConnect) -> None: run = wait_for_cloud_build( client, @@ -504,6 +549,9 @@ def build_parser() -> argparse.ArgumentParser: start.add_argument("--timeout-seconds", type=int, default=85 * 60) start.add_argument("--valid-timeout-seconds", type=int, default=25 * 60) + describe = sub.add_parser("describe", help="Dump a Cloud run's actions, issues, and ASC builds") + describe.add_argument("--build-run-id", required=True) + wait = sub.add_parser("wait", help="Monitor an existing Xcode Cloud build") wait.add_argument("--build-run-id", required=True) wait.add_argument("--require-sha", default="") @@ -526,6 +574,9 @@ def main(argv: list[str] | None = None) -> None: if args.command == "start": command_start(args, client) return + if args.command == "describe": + command_describe(args, client) + return if args.command == "wait": command_wait(args, client) return diff --git a/scripts/xcode-cloud-control.test.mjs b/scripts/xcode-cloud-control.test.mjs index 0c959834..74a960e9 100644 --- a/scripts/xcode-cloud-control.test.mjs +++ b/scripts/xcode-cloud-control.test.mjs @@ -18,6 +18,8 @@ describe("Xcode Cloud control", () => { expect(script).toContain('sub.add_parser("start"'); expect(script).toContain('sub.add_parser("wait"'); + expect(script).toContain('sub.add_parser("describe"'); + expect(script).toContain("/issues?limit=50"); expect(script).toContain("--wait-valid"); expect(script).toContain("--require-sha"); expect(script).toContain("sourceBranchOrTag"); @@ -30,6 +32,7 @@ describe("Xcode Cloud control", () => { expect(workflow).toContain("git diff --name-only"); expect(cloudWorkflow).toContain("scripts/xcode-cloud-control.py"); + expect(cloudWorkflow).toContain("describe"); expect(cloudWorkflow).not.toContain("submit-review"); expect(cloudWorkflow).not.toContain('APP_STORE_BUILD_NUMBER: "49"'); });