Section 12/171 menit

12. TestFlight dan App Store Connect Integration

12. TestFlight dan App Store Connect Integration

Build Number Management

Xcode Cloud otomatis generate CI_BUILD_NUMBER yang increment dengan setiap build. Gunakan ini sebagai CFBundleVersion:

swift
#!/bin/zsh
# ci_scripts/ci_pre_xcodebuild.sh

if [[ "$CI_XCODEBUILD_ACTION" == "archive" ]]; then
    # Set build number dari CI_BUILD_NUMBER (auto-increment)
    agvtool new-version -all "$CI_BUILD_NUMBER"
    
    echo "Build number set to: $CI_BUILD_NUMBER"
fi

Release Notes Otomatis

Generate release notes dari git log untuk TestFlight:

swift
#!/bin/zsh
# ci_scripts/ci_post_xcodebuild.sh

if [[ "$CI_XCODEBUILD_ACTION" == "archive" ]]; then
    # Ambil commit messages sejak tag terakhir
    LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
    
    if [[ -n "$LAST_TAG" ]]; then
        COMMITS=$(git log "$LAST_TAG..HEAD" --pretty=format:"• %s" --no-merges | head -20)
    else
        COMMITS=$(git log --pretty=format:"• %s" --no-merges -10)
    fi
    
    # TestFlight release notes per locale
    mkdir -p "$CI_PRIMARY_REPOSITORY_PATH/release_notes"
    
    cat > "$CI_PRIMARY_REPOSITORY_PATH/release_notes/en-US.txt" << EOF
Build #${CI_BUILD_NUMBER} — Branch: ${CI_BRANCH}

Changes:
${COMMITS}
EOF
    
    echo "Release notes generated"
fi

App Store Connect API untuk Automasi

Untuk automasi lanjutan, gunakan App Store Connect API melalui Python atau Ruby di ci_scripts:

swift
#!/usr/bin/env python3
# ci_scripts/upload_metadata.py
# Jalankan dari ci_post_xcodebuild.sh

import os
import json
import urllib.request
import urllib.parse
import jwt
import time

# App Store Connect API credentials dari environment variables
KEY_ID = os.environ.get("ASC_KEY_ID")
ISSUER_ID = os.environ.get("ASC_ISSUER_ID")
PRIVATE_KEY = os.environ.get("ASC_PRIVATE_KEY", "").replace("\\n", "\n")

def generate_token():
    header = {"alg": "ES256", "kid": KEY_ID, "typ": "JWT"}
    payload = {
        "iss": ISSUER_ID,
        "iat": int(time.time()),
        "exp": int(time.time()) + 1200,
        "aud": "appstoreconnect-v1"
    }
    return jwt.encode(payload, PRIVATE_KEY, algorithm="ES256", headers=header)

def get_latest_build(app_id: str, version: str):
    token = generate_token()
    url = f"https://api.appstoreconnect.apple.com/v1/builds?filter[app]={app_id}&filter[preReleaseVersion.version]={version}&limit=1"
    
    req = urllib.request.Request(url)
    req.add_header("Authorization", f"Bearer {token}")
    req.add_header("Content-Type", "application/json")
    
    with urllib.request.urlopen(req) as response:
        return json.loads(response.read())

if __name__ == "__main__":
    app_id = os.environ.get("APP_STORE_CONNECT_APP_ID")
    version = os.environ.get("APP_VERSION", "1.0.0")
    
    builds = get_latest_build(app_id, version)
    print(f"Latest build: {json.dumps(builds, indent=2)}")