> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noqa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# CI/CD

> Integrate noqa into your CI/CD pipeline

## Overview

This guide walks you through automating mobile app testing with the noqa API in your CI/CD pipeline. You'll learn how to:

1. Get your applications list
2. Upload your iOS or Android build
3. Retrieve test cases
4. Get available devices
5. Create and run tests
6. Monitor test results

All API endpoints require authentication using an API key passed in the `x-api-key` header.

<Note>
  For complete API documentation with all available endpoints and parameters, see the [API Reference](/api-reference).
</Note>

## Step 1: Get Your Apps

First, retrieve the list of your applications to get the `app_id` needed for subsequent steps.

<CodeGroup>
  ```python Python theme={null}
  import requests

  API_KEY = "your-api-key"
  BASE_URL = "https://api.noqa.ai"

  # Get list of apps
  response = requests.get(
      f"{BASE_URL}/v1/apps/",
      headers={"x-api-key": API_KEY}
  )
  apps = response.json()

  # Select first app or find by name
  app = apps[0]
  APP_ID = app["id"]

  print(f"Using app: {app['name']} ({APP_ID})")
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = "your-api-key";
  const BASE_URL = "https://api.noqa.ai";

  // Get list of apps
  const appsResponse = await fetch(`${BASE_URL}/v1/apps/`, {
    headers: {
      "x-api-key": API_KEY,
    },
  });

  const apps = await appsResponse.json();

  // Select first app or find by name
  const app = apps[0];
  const APP_ID = app.id;

  console.log(`Using app: ${app.name} (${APP_ID})`);
  ```

  ```bash cURL theme={null}
  curl -X GET "https://api.noqa.ai/v1/apps/" \
    -H "x-api-key: your-api-key"
  ```
</CodeGroup>

**Response:**

```json theme={null}
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "My Mobile App",
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z",
    "app_context": null
  }
]
```

## Step 2: Get Presigned Upload URL

Request a presigned URL to upload your `.ipa` (iOS) or `.apk` (Android) build file.

<CodeGroup>
  ```python Python theme={null}
  import requests

  API_KEY = "your-api-key"
  APP_ID = "550e8400-e29b-41d4-a716-446655440000"
  BASE_URL = "https://api.noqa.ai"
  FILENAME = "app.ipa"  # or "app.apk" for Android

  # Get presigned URL
  response = requests.post(
      f"{BASE_URL}/v1/builds/presigned-url",
      headers={"x-api-key": API_KEY},
      json={
          "app_id": APP_ID,
          "filename": FILENAME
      }
  )
  presigned_data = response.json()

  upload_url = presigned_data["upload_url"]
  object_key = presigned_data["key"]
  expires_in = presigned_data["expires_in"]

  print(f"Upload URL expires in {expires_in} seconds")
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = "your-api-key";
  const APP_ID = "550e8400-e29b-41d4-a716-446655440000";
  const BASE_URL = "https://api.noqa.ai";
  const FILENAME = "app.ipa"; // or "app.apk" for Android

  // Get presigned URL
  const presignedResponse = await fetch(
    `${BASE_URL}/v1/builds/presigned-url`,
    {
      method: "POST",
      headers: {
        "x-api-key": API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ 
        app_id: APP_ID,
        filename: FILENAME
      }),
    }
  );

  const presignedData = await presignedResponse.json();
  const uploadUrl = presignedData.upload_url;
  const objectKey = presignedData.key;

  console.log(`Upload URL expires in ${presignedData.expires_in} seconds`);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.noqa.ai/v1/builds/presigned-url" \
    -H "x-api-key: your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "app_id": "550e8400-e29b-41d4-a716-446655440000",
      "filename": "app.ipa"
    }'
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "upload_url": "https://storage.example.com/...",
  "key": "builds/d773ade3-7053-4a91-9dd8-d2e59ba58d0a/550e8400-e29b-41d4-a716-446655440000.ipa",
  "expires_in": 3600,
  "method": "PUT"
}
```

## Step 2: Upload Build File

Upload your `.ipa` (iOS) or `.apk` (Android) file to the presigned URL using PUT request.

<CodeGroup>
  ```python Python theme={null}
  # Upload the build file (app.ipa for iOS or app.apk for Android)
  with open("path/to/your/app.ipa", "rb") as f:
      upload_response = requests.put(
          upload_url,
          data=f,
          headers={"Content-Type": "application/octet-stream"}
      )

  if upload_response.status_code == 200:
      print("Build uploaded successfully")
  else:
      print(f"Upload failed: {upload_response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  // Upload the build file (app.ipa for iOS or app.apk for Android)
  const fileBuffer = await fs.readFile("path/to/your/app.ipa");

  const uploadResponse = await fetch(uploadUrl, {
    method: "PUT",
    headers: {
      "Content-Type": "application/octet-stream",
    },
    body: fileBuffer,
  });

  if (uploadResponse.ok) {
    console.log("Build uploaded successfully");
  } else {
    console.error(`Upload failed: ${uploadResponse.status}`);
  }
  ```

  ```bash cURL theme={null}
  curl -X PUT "https://storage.example.com/..." \
    -H "Content-Type: application/octet-stream" \
    --data-binary "@path/to/your/app.ipa"
  ```
</CodeGroup>

## Step 4: Create Build Record

After uploading the file, create a build record in noqa.

<CodeGroup>
  ```python Python theme={null}
  # Create build record
  build_response = requests.post(
      f"{BASE_URL}/v1/builds/",
      headers={"x-api-key": API_KEY},
      json={
          "app_id": APP_ID,
          "key": object_key,
          "name": "CI Build #123"
      }
  )
  build_data = build_response.json()
  build_id = build_data["id"]

  print(f"Build created: {build_id}")
  print(f"Version: {build_data['version']}")
  ```

  ```javascript JavaScript theme={null}
  // Create build record
  const buildResponse = await fetch(`${BASE_URL}/v1/builds/`, {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      app_id: APP_ID,
      key: objectKey,
      name: "CI Build #123",
    }),
  });

  const buildData = await buildResponse.json();
  const buildId = buildData.id;

  console.log(`Build created: ${buildId}`);
  console.log(`Version: ${buildData.version}`);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.noqa.ai/v1/builds/" \
    -H "x-api-key: your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "app_id": "550e8400-e29b-41d4-a716-446655440000",
      "key": "builds/d773ade3-7053-4a91-9dd8-d2e59ba58d0a/550e8400-e29b-41d4-a716-446655440000.ipa",
      "name": "CI Build #123"
    }'
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-12-31T10:30:00Z",
  "name": "CI Build #123",
  "version": "1.0.0 (5)",
  "key": "builds/.../550e8400-e29b-41d4-a716-446655440000.ipa",
  "file_size": 10485760,
  "bundle_id": "com.example.app"
}
```

## Step 5: Get Test Cases

Retrieve the list of test cases for your app.

<CodeGroup>
  ```python Python theme={null}
  # Get test cases
  cases_response = requests.get(
      f"{BASE_URL}/v1/cases/",
      headers={"x-api-key": API_KEY},
      params={"app_id": APP_ID}
  )
  cases = cases_response.json()

  # Get first test case ID
  test_case_id = cases[0]["id"]
  print(f"Found {len(cases)} test cases")
  print(f"Using test case: {cases[0]['title']} ({test_case_id})")
  ```

  ```javascript JavaScript theme={null}
  // Get test cases
  const casesResponse = await fetch(
    `${BASE_URL}/v1/cases/?app_id=${APP_ID}`,
    {
      headers: {
        "x-api-key": API_KEY,
      },
    }
  );

  const cases = await casesResponse.json();
  const testCaseId = cases[0].id;

  console.log(`Found ${cases.length} test cases`);
  console.log(`Using test case: ${cases[0].title} (${testCaseId})`);
  ```

  ```bash cURL theme={null}
  curl -X GET "https://api.noqa.ai/v1/cases/?app_id=550e8400-e29b-41d4-a716-446655440000" \
    -H "x-api-key: your-api-key"
  ```
</CodeGroup>

**Response:**

```json theme={null}
[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "title": "Login with valid credentials",
    "instructions": "Open app, tap login button, enter valid credentials...",
    "tags": ["smoke", "authentication"]
  }
]
```

## Step 6: Get Available Devices

Fetch the list of available devices for testing. You can filter by platform (iOS or Android).

<CodeGroup>
  ```python Python theme={null}
  # Get available devices (optionally filter by platform)
  devices_response = requests.get(
      f"{BASE_URL}/v1/devices/",
      headers={"x-api-key": API_KEY},
      params={"platform": "ios"}  # or "android", omit to get all
  )
  devices = devices_response.json()

  # Select first available device
  device = devices[0]
  print(f"Using device: {device['model']} ({device['platform'].upper()} {device['os_version']})")
  ```

  ```javascript JavaScript theme={null}
  // Get available devices (optionally filter by platform)
  const devicesResponse = await fetch(
    `${BASE_URL}/v1/devices/?platform=ios`, // or "android", omit to get all
    {
      headers: {
        "x-api-key": API_KEY,
      },
    }
  );

  const devices = await devicesResponse.json();
  const device = devices[0];

  console.log(`Using device: ${device.model} (${device.platform.toUpperCase()} ${device.os_version})`);
  ```

  ```bash cURL theme={null}
  # Get iOS devices
  curl -X GET "https://api.noqa.ai/v1/devices/?platform=ios" \
    -H "x-api-key: your-api-key"

  # Get Android devices
  curl -X GET "https://api.noqa.ai/v1/devices/?platform=android" \
    -H "x-api-key: your-api-key"

  # Get all devices
  curl -X GET "https://api.noqa.ai/v1/devices/" \
    -H "x-api-key: your-api-key"
  ```
</CodeGroup>

**Response:**

```json theme={null}
[
  {
    "model": "iPhone 15 Pro",
    "os_version": "17.6.1",
    "platform": "ios"
  },
  {
    "model": "iPhone 14",
    "os_version": "17.5.0",
    "platform": "ios"
  },
  {
    "model": "Google Pixel 8",
    "os_version": "14",
    "platform": "android"
  }
]
```

## Step 7: Create Test Run

Start a new test run with your build, test cases, and selected device.

<CodeGroup>
  ```python Python theme={null}
  # Create test run
  run_response = requests.post(
      f"{BASE_URL}/v1/runs/",
      headers={"x-api-key": API_KEY},
      json={
          "app_id": APP_ID,
          "build_id": build_id,
          "cases": {
              "ids": [test_case_id]
          },
          "device": {
              "model": device["model"],
              "os_version": device["os_version"],
              "platform": device["platform"]
          }
      }
  )
  run_data = run_response.json()
  run_id = run_data["id"]

  print(f"Test run started: {run_id}")
  print(f"Status: {run_data['status']}")
  ```

  ```javascript JavaScript theme={null}
  // Create test run
  const runResponse = await fetch(`${BASE_URL}/v1/runs/`, {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      app_id: APP_ID,
      build_id: buildId,
      cases: {
        ids: [testCaseId],
      },
      device: {
        model: device.model,
        os_version: device.os_version,
        platform: device.platform,
      },
    }),
  });

  const runData = await runResponse.json();
  const runId = runData.id;

  console.log(`Test run started: ${runId}`);
  console.log(`Status: ${runData.status}`);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.noqa.ai/v1/runs/" \
    -H "x-api-key: your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "app_id": "550e8400-e29b-41d4-a716-446655440000",
      "build_id": "550e8400-e29b-41d4-a716-446655440000",
      "cases": {
        "ids": ["123e4567-e89b-12d3-a456-426614174000"]
      },
      "device": {
        "model": "iPhone 15 Pro",
        "os_version": "17.6.1",
        "platform": "ios"
      }
    }'
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "id": "789e0123-e89b-12d3-a456-426614174999",
  "created_at": "2024-12-31T10:35:00Z",
  "status": "queued",
  "test_count": 1,
  "statuses": {
    "queued": 1
  }
}
```

## Step 8: Check Run Status

Poll the test run status to monitor progress and get results.

<CodeGroup>
  ```python Python theme={null}
  import time

  # Poll for test completion
  while True:
      status_response = requests.get(
          f"{BASE_URL}/v1/runs/{run_id}",
          headers={"x-api-key": API_KEY},
          params={"app_id": APP_ID}
      )
      run_status = status_response.json()
      
      print(f"Status: {run_status['status']}")
      
      if run_status["status"] in ["passed", "failed", "cancelled"]:
          print(f"Tests completed!")
          print(f"Total tests: {run_status['test_count']}")
          print(f"Statuses: {run_status.get('statuses', {})}")
          break
      
      time.sleep(10)  # Wait 10 seconds before next check
  ```

  ```javascript JavaScript theme={null}
  // Poll for test completion
  async function pollRunStatus() {
    while (true) {
      const statusResponse = await fetch(
        `${BASE_URL}/v1/runs/${runId}?app_id=${APP_ID}`,
        {
          headers: {
            "x-api-key": API_KEY,
          },
        }
      );

      const runStatus = await statusResponse.json();
      console.log(`Status: ${runStatus.status}`);

      if (["passed", "failed", "cancelled"].includes(runStatus.status)) {
        console.log("Tests completed!");
        console.log(`Total tests: ${runStatus.test_count}`);
        console.log(`Statuses:`, runStatus.statuses);
        break;
      }

      // Wait 10 seconds before next check
      await new Promise((resolve) => setTimeout(resolve, 10000));
    }
  }

  await pollRunStatus();
  ```

  ```bash cURL theme={null}
  # Check status (run this periodically)
  curl -X GET "https://api.noqa.ai/v1/runs/789e0123-e89b-12d3-a456-426614174999?app_id=550e8400-e29b-41d4-a716-446655440000" \
    -H "x-api-key: your-api-key"
  ```
</CodeGroup>

**Response (completed run):**

```json theme={null}
{
  "id": "789e0123-e89b-12d3-a456-426614174999",
  "created_at": "2024-12-31T10:35:00Z",
  "status": "passed",
  "test_count": 1,
  "statuses": {
    "passed": 1,
    "failed": 0
  },
  "build": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "version": "1.0.0 (5)",
    "name": "CI Build #123"
  },
  "device": {
    "model": "iPhone 15 Pro",
    "os_version": "17.6.1",
    "platform": "ios",
    "locale": "en_US"
  },
  "tests": [
    {
      "case_id": "123e4567-e89b-12d3-a456-426614174000",
      "case_name": "Login with valid credentials",
      "status": "passed"
    }
  ]
}
```

## Run Status Values

The test run can have the following statuses:

* `queued` - Test is waiting to start
* `running` - Test is currently executing
* `passed` - All tests passed successfully
* `failed` - One or more tests failed
* `cancelled` - Test run was cancelled

## Selecting Multiple Test Cases

You can run multiple test cases by providing multiple IDs or using tags:

<CodeGroup>
  ```python Python theme={null}
  # Run multiple specific tests
  run_response = requests.post(
      f"{BASE_URL}/v1/runs/",
      headers={"x-api-key": API_KEY},
      json={
          "app_id": APP_ID,
          "build_id": build_id,
          "cases": {
              "ids": [
                  "123e4567-e89b-12d3-a456-426614174000",
                  "456e7890-e89b-12d3-a456-426614174111"
              ]
          },
          "device": {
              "model": device["model"],
              "os_version": device["os_version"],
              "platform": device["platform"]
          }
      }
  )

  # Or run all tests with specific tags
  run_response = requests.post(
      f"{BASE_URL}/v1/runs/",
      headers={"x-api-key": API_KEY},
      json={
          "app_id": APP_ID,
          "build_id": build_id,
          "cases": {
              "tags": ["smoke", "regression"]
          },
          "device": {
              "model": device["model"],
              "os_version": device["os_version"],
              "platform": device["platform"]
          }
      }
  )
  ```

  ```javascript JavaScript theme={null}
  // Run multiple specific tests
  const runResponse = await fetch(`${BASE_URL}/v1/runs/`, {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      app_id: APP_ID,
      build_id: buildId,
      cases: {
        ids: [
          "123e4567-e89b-12d3-a456-426614174000",
          "456e7890-e89b-12d3-a456-426614174111",
        ],
      },
      device: {
        model: device.model,
        os_version: device.os_version,
        platform: device.platform,
      },
    }),
  });

  // Or run all tests with specific tags
  const tagRunResponse = await fetch(`${BASE_URL}/v1/runs/`, {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      app_id: APP_ID,
      build_id: buildId,
      cases: {
        tags: ["smoke", "regression"],
      },
      device: {
        model: device.model,
        os_version: device.os_version,
        platform: device.platform,
      },
    }),
  });
  ```
</CodeGroup>

## CI/CD Integration Examples

**`scripts/run_noqa_tests.py`:**

```python theme={null}
#!/usr/bin/env python3
import os
import sys
import time
import json
import requests
from pathlib import Path

# Configuration from environment
API_KEY = os.environ.get("NOQA_API_KEY")
APP_ID = os.environ.get("APP_ID")  # Optional - will fetch if not provided
APP_NAME = os.environ.get("APP_NAME")  # Optional - app name to search for
BUILD_PATH = os.environ.get("BUILD_PATH", "build/MyApp.ipa")  # or .apk for Android
PLATFORM = os.environ.get("PLATFORM", "ios")  # "ios" or "android"
BASE_URL = "https://api.noqa.ai"

def log(message):
    print(f"[noqa] {message}")

def get_app_id():
    """Get app ID from environment or fetch from API"""
    if APP_ID:
        log(f"Using APP_ID from environment: {APP_ID}")
        return APP_ID
    
    log("Fetching apps list...")
    response = requests.get(
        f"{BASE_URL}/v1/apps/",
        headers={"x-api-key": API_KEY}
    )
    response.raise_for_status()
    apps = response.json()
    
    if not apps:
        raise ValueError("No apps found in workspace")
    
    # If APP_NAME is provided, find by name
    if APP_NAME:
        app = next((a for a in apps if a["name"] == APP_NAME), None)
        if not app:
            raise ValueError(f"App with name '{APP_NAME}' not found")
        log(f"Found app by name: {app['name']} ({app['id']})")
    else:
        # Use first app
        app = apps[0]
        log(f"Using first app: {app['name']} ({app['id']})")
    
    return app["id"]

def get_presigned_url():
    """Get presigned URL for build upload"""
    log("Requesting presigned upload URL...")
    filename = Path(BUILD_PATH).name
    response = requests.post(
        f"{BASE_URL}/v1/builds/presigned-url",
        headers={"x-api-key": API_KEY},
        json={
            "app_id": APP_ID,
            "filename": filename
        }
    )
    response.raise_for_status()
    return response.json()

def upload_build(upload_url, build_path):
    """Upload build file (IPA or APK) to presigned URL"""
    log(f"Uploading build from {build_path}...")
    file_size = Path(build_path).stat().st_size
    log(f"File size: {file_size / 1024 / 1024:.2f} MB")
    
    with open(build_path, "rb") as f:
        response = requests.put(
            upload_url,
            data=f,
            headers={"Content-Type": "application/octet-stream"}
        )
    response.raise_for_status()
    log("Build uploaded successfully")

def create_build(object_key):
    """Create build record"""
    log("Creating build record...")
    response = requests.post(
        f"{BASE_URL}/v1/builds/",
        headers={"x-api-key": API_KEY},
        json={
            "app_id": APP_ID,
            "key": object_key,
            "name": f"CI Build {os.environ.get('GITHUB_RUN_NUMBER', 'local')}"
        }
    )
    response.raise_for_status()
    build_data = response.json()
    log(f"Build created: {build_data['id']} (v{build_data['version']})")
    return build_data

def get_test_cases():
    """Get available test cases"""
    log("Fetching test cases...")
    response = requests.get(
        f"{BASE_URL}/v1/cases/",
        headers={"x-api-key": API_KEY},
        params={"app_id": APP_ID, "tags": "smoke"}  # Filter by smoke tests
    )
    response.raise_for_status()
    cases = response.json()
    log(f"Found {len(cases)} smoke test cases")
    return cases

def get_devices():
    """Get available devices"""
    log(f"Fetching available {PLATFORM.upper()} devices...")
    response = requests.get(
        f"{BASE_URL}/v1/devices/",
        headers={"x-api-key": API_KEY},
        params={"platform": PLATFORM}
    )
    response.raise_for_status()
    devices = response.json()
    log(f"Found {len(devices)} available {PLATFORM.upper()} devices")
    return devices

def create_run(build_id, case_ids, device):
    """Create test run"""
    log("Creating test run...")
    response = requests.post(
        f"{BASE_URL}/v1/runs/",
        headers={"x-api-key": API_KEY},
        json={
            "app_id": APP_ID,
            "build_id": build_id,
            "cases": {"ids": case_ids},
            "device": {
                "model": device["model"],
                "os_version": device["os_version"],
                "platform": device["platform"],
                "locale": "en_US"
            }
        }
    )
    response.raise_for_status()
    run_data = response.json()
    log(f"Test run created: {run_data['id']}")
    return run_data

def poll_run_status(run_id, timeout=1800):
    """Poll test run status until completion"""
    log(f"Monitoring test run {run_id}...")
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        response = requests.get(
            f"{BASE_URL}/v1/runs/{run_id}",
            headers={"x-api-key": API_KEY},
            params={"app_id": APP_ID}
        )
        response.raise_for_status()
        run_status = response.json()
        
        status = run_status["status"]
        log(f"Status: {status}")
        
        if status in ["passed", "failed", "cancelled"]:
            return run_status
        
        time.sleep(15)  # Poll every 15 seconds
    
    raise TimeoutError(f"Test run did not complete within {timeout} seconds")

def main():
    try:
        # Step 1: Get app ID
        global APP_ID
        APP_ID = get_app_id()
        
        # Step 2: Get presigned URL
        presigned_data = get_presigned_url()
        
        # Step 3: Upload build
        upload_build(presigned_data["upload_url"], BUILD_PATH)
        
        # Step 4: Create build record
        build_data = create_build(presigned_data["key"])
        
        # Step 5: Get test cases
        cases = get_test_cases()
        if not cases:
            log("No test cases found with 'smoke' tag")
            return 1
        case_ids = [case["id"] for case in cases]
        
        # Step 6: Get devices
        devices = get_devices()
        if not devices:
            log(f"No {PLATFORM.upper()} devices available")
            return 1
        device = devices[0]  # Use first available device
        log(f"Using device: {device['model']} ({device['platform'].upper()} {device['os_version']})")
        
        # Step 7: Create and start test run
        run_data = create_run(build_data["id"], case_ids, device)
        
        # Step 8: Wait for completion
        final_status = poll_run_status(run_data["id"])
        
        # Save results
        with open("test-results.json", "w") as f:
            json.dump(final_status, f, indent=2)
        
        # Print summary
        log("=" * 50)
        log(f"Test run completed: {final_status['status'].upper()}")
        log(f"Total tests: {final_status['test_count']}")
        log(f"Results: {final_status.get('statuses', {})}")
        log("=" * 50)
        
        # Exit with appropriate code
        if final_status["status"] == "passed":
            log("All tests passed! ✓")
            return 0
        else:
            log("Some tests failed! ✗")
            return 1
            
    except Exception as e:
        log(f"Error: {e}")
        return 1

if __name__ == "__main__":
    sys.exit(main())
```
