Post

Replies

Boosts

Views

Activity

Reply to No access to build artifacts in Xcode Cloud
Thought I'd share my solution here for anyone experiencing the same issues and have experience with Python. I made some simple scripts that can be used to manually extract out the download URLs of each artifact. # generate_token.py import jwt import time from datetime import datetime, timedelta # Define your App Store Connect API credentials KEY_ID = '________' ISSUER_ID = '________' PRIVATE_KEY_PATH = '________.p8' with open(PRIVATE_KEY_PATH, 'r') as key_file: private_key = key_file.read() def generate_token(): headers = { "alg": "ES256", "kid": KEY_ID, "typ": "JWT" } payload = { "iss": ISSUER_ID, "exp": int(time.time()) + 20 * 60, # Token expiration in 20 minutes "aud": "appstoreconnect-v1" } token = jwt.encode(payload, private_key, algorithm="ES256", headers=headers) return token # fetch_workflows.py import time import requests from datetime import datetime, timedelta def fetch_products(token): url = f"https://api.appstoreconnect.apple.com/v1/ciProducts" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } response = requests.get(url, headers=headers) response.raise_for_status() return response.json().get("data", []) def fetch_workflows(product_id, token): url = f"https://api.appstoreconnect.apple.com/v1/ciProducts/{product_id}/workflows" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } response = requests.get(url, headers=headers) response.raise_for_status() return response.json().get("data", []) if __name__ == "__main__": from generate_token import generate_token products = fetch_products(generate_token()) print(f"Found {len(products)} products") for product in products: print(f"{product['attributes']['name']} ({product['attributes']['productType']}): {product['id']}") workflows = fetch_workflows(product['id'], generate_token()) print(f"Found {len(workflows)} workflows") for workflow in workflows: print(f"{workflow['attributes']['name']}: {workflow['id']}") # fetch_build_runs.py import time import requests from datetime import datetime, timedelta # Fetch all build runs for the specified workflow def fetch_build_runs(workflow_id, token): url = f"https://api.appstoreconnect.apple.com/v1/ciWorkflows/{workflow_id}/buildRuns" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } params = { "limit": 200 # Adjust the limit as needed; 200 is the maximum } response = requests.get(url, headers=headers, params=params) response.raise_for_status() return response.json().get("data", []) if __name__ == "__main__": import argparse from generate_token import generate_token parser = argparse.ArgumentParser(description="Fetch all build runs for a given workflow ID.") parser.add_argument("workflow_id", help="The workflow ID for which to fetch build runs for.") args = parser.parse_args() build_runs = fetch_build_runs(args.workflow_id, generate_token()) print(f"Found {len(build_runs)} runs for workflow {args.workflow_id}") for build_run in build_runs: print(f"Build {build_run['attributes']['number']}: {build_run['id']}") # fetch_build_actions.py import time import requests from datetime import datetime, timedelta def fetch_build_actions(build_run_id, token): url = f"https://api.appstoreconnect.apple.com/v1/ciBuildRuns/{build_run_id}/actions" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } response = requests.get(url, headers=headers) response.raise_for_status() return response.json().get("data", []) if __name__ == "__main__": import argparse from generate_token import generate_token parser = argparse.ArgumentParser(description="Fetch all build actions for a given build run ID.") parser.add_argument("build_run_id", help="The build run ID for which to fetch build actions from") args = parser.parse_args() build_actions = fetch_build_actions(args.build_run_id, generate_token()) print(f"Found {len(build_actions)} actions for run {args.build_run_id}") for build_action in build_actions: print(f"{build_action['attributes']['name']}: {build_action['id']}") # fetch_artifacts.py import time import requests from datetime import datetime, timedelta def fetch_artifacts(build_action_id, token): url = f"https://api.appstoreconnect.apple.com/v1/ciBuildActions/{build_action_id}/artifacts" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } response = requests.get(url, headers=headers) response.raise_for_status() return response.json().get("data", []) if __name__ == "__main__": import argparse from generate_token import generate_token parser = argparse.ArgumentParser(description="Fetch all artifacts for a given build action ID.") parser.add_argument("build_action_id", help="The build action ID for which to fetch artifacts.") args = parser.parse_args() artifacts = fetch_artifacts(args.build_action_id, generate_token()) print(f"Found {len(artifacts)} artifacts for action {args.build_action_id}") for artifact in artifacts: print(f"{artifact['attributes']['fileName']}: {artifact['attributes']['downloadUrl']}") Note that you will need to pip install cryptography pyjwt requests.
5d
Reply to Drag item from Popover: No path forward
I was also struggling with this issue - draggable views within a popover could not be dropped outside of it. The popover must be dismissed before drop destinations in the presenting view can be detected. As the OP pointed out, it is possible to detect when a drag gesture starts, but a better workaround I found is using DropDelegate. The basic idea is to attach a custom delegate to the popover that detects when the dragged view exits its bounds, allowing us to control when we want to dismiss it. /// A work-around thst allows `.draggable` views presented within a `.popover` modal /// to be dropped outside of the popover. /// ```swift /// NavigationStack { /// Text("Drop Zone") /// .dropDestination(for: String.self) { items, location in /// true /// } isTargeted: { /// print("isTargeted \($0)") /// } /// .toolbar { /// ToolbarItem(placement: .topBarTrailing) { /// Button(“Press Me”) { /// isPresented = true /// } /// .popover(isPresented: $isPresented) { /// NavigationStack { /// ZStack { /// Color.green.ignoresSafeArea() /// Color.red.frame(width: 50, height: 50).draggable("Payload") /// } /// .navigationTitle("Modal") /// } /// .frame(minWidth: 300, minHeight: 500) /// .onDrop(of: [.plainText], delegate: ExitDropDelegate(onDropExit: { isPresented = false })) /// } /// } /// } /// } /// ``` /// In this example, the drop zone in the outer `NavigationStack` does not detect the draggable /// view hovering over it, due to the popover being modally presented. The `ExitDropDelegate` /// dismisses the popover when the view is dragged outside of its bounds, allowing the drop zone to /// be targeted. struct ExitDropDelegate: DropDelegate { var onDropExit: (() -> Void)? = nil func performDrop(info: DropInfo) -> Bool { false } func dropUpdated(info: DropInfo) -> DropProposal? { /// Hides the green plus button .init(operation: .move) } func dropExited(info: DropInfo) { onDropExit?() } } This approach can be extended with more complex logic, for example if we want to dismiss the popover only after the user hovers the view outside of the popover for a certain period of time, then we can add a timer to the DropDelegate that starts when the dropExited is called, and invalidated when dropEntered is called before the timer fires.
Feb ’24