@daemux/store-automator 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/bin/cli.mjs +38 -14
  2. package/package.json +1 -1
  3. package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
  4. package/src/ci-config.mjs +21 -0
  5. package/src/install-paths.mjs +93 -0
  6. package/src/install.mjs +33 -78
  7. package/src/templates.mjs +71 -1
  8. package/templates/Matchfile.template +8 -0
  9. package/templates/ci.config.yaml.template +3 -0
  10. package/templates/fastlane/android/Fastfile.template +2 -2
  11. package/templates/github/workflows/android-release.yml +72 -0
  12. package/templates/github/workflows/ios-release.yml +62 -0
  13. package/templates/scripts/ci/android/build.sh +50 -0
  14. package/templates/scripts/ci/android/check-readiness.sh +99 -0
  15. package/templates/scripts/ci/android/manage-version.sh +133 -0
  16. package/templates/scripts/ci/android/setup-keystore.sh +80 -0
  17. package/templates/scripts/ci/android/sync-iap.sh +63 -0
  18. package/templates/scripts/ci/android/update-data-safety.sh +65 -0
  19. package/templates/scripts/ci/android/upload-binary.sh +62 -0
  20. package/templates/scripts/ci/android/upload-metadata.sh +59 -0
  21. package/templates/scripts/ci/common/check-changed.sh +74 -0
  22. package/templates/scripts/ci/common/flutter-setup.sh +22 -0
  23. package/templates/scripts/ci/common/install-fastlane.sh +39 -0
  24. package/templates/scripts/ci/common/link-fastlane.sh +56 -0
  25. package/templates/scripts/ci/common/read-config.sh +71 -0
  26. package/templates/scripts/ci/ios/build.sh +52 -0
  27. package/templates/scripts/ci/ios/manage-version.sh +64 -0
  28. package/templates/scripts/ci/ios/set-build-number.sh +95 -0
  29. package/templates/scripts/ci/ios/setup-signing.sh +242 -0
  30. package/templates/scripts/ci/ios/sync-iap.sh +91 -0
  31. package/templates/scripts/ci/ios/upload-binary.sh +44 -0
  32. package/templates/scripts/ci/ios/upload-metadata.sh +92 -0
  33. package/templates/scripts/update_data_safety.py +220 -0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Runs Flutter pub get and verifies the SDK is available.
5
+ # Usage: scripts/ci/common/flutter-setup.sh
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "$SCRIPT_DIR/read-config.sh"
9
+
10
+ if ! command -v flutter &>/dev/null; then
11
+ echo "ERROR: Flutter SDK not found in PATH" >&2
12
+ exit 1
13
+ fi
14
+
15
+ echo "Flutter version:"
16
+ flutter --version
17
+
18
+ echo "Running flutter pub get in $APP_ROOT..."
19
+ cd "$APP_ROOT"
20
+ flutter pub get
21
+
22
+ echo "Flutter setup complete"
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Installs Fastlane and its dependencies for the given platform.
5
+ # Usage: scripts/ci/common/install-fastlane.sh <ios|android>
6
+
7
+ PLATFORM="${1:?Usage: install-fastlane.sh <ios|android>}"
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ source "$SCRIPT_DIR/read-config.sh"
11
+
12
+ PLATFORM_DIR="$APP_ROOT/$PLATFORM"
13
+
14
+ if [ ! -d "$PLATFORM_DIR" ]; then
15
+ echo "ERROR: Platform directory not found: $PLATFORM_DIR" >&2
16
+ exit 1
17
+ fi
18
+
19
+ GEMFILE="$PLATFORM_DIR/Gemfile"
20
+ if [ ! -f "$GEMFILE" ]; then
21
+ echo "ERROR: Gemfile not found at $GEMFILE" >&2
22
+ exit 1
23
+ fi
24
+
25
+ echo "Installing Fastlane for $PLATFORM..."
26
+ cd "$PLATFORM_DIR"
27
+
28
+ if ! command -v bundle &>/dev/null; then
29
+ echo "Installing bundler..."
30
+ gem install bundler --no-document --user-install
31
+ fi
32
+
33
+ export BUNDLE_PATH="${BUNDLE_PATH:-vendor/bundle}"
34
+ bundle install --jobs 4 --retry 3
35
+
36
+ echo "Verifying Fastlane installation..."
37
+ bundle exec fastlane --version
38
+
39
+ echo "Fastlane installed successfully for $PLATFORM"
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Creates symlinks so that `cd app/ios && bundle exec fastlane` (or android)
5
+ # can find the Fastfile. Fastlane config lives at project root under fastlane/ios/
6
+ # and fastlane/android/. This script links them into the platform directories.
7
+ #
8
+ # Usage: scripts/ci/common/link-fastlane.sh <ios|android>
9
+
10
+ PLATFORM="${1:?Usage: link-fastlane.sh <ios|android>}"
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "$SCRIPT_DIR/read-config.sh"
14
+
15
+ create_symlink() {
16
+ local target="$1"
17
+ local link="$2"
18
+
19
+ if [ -e "$link" ] && [ ! -L "$link" ]; then
20
+ echo "WARNING: $link exists and is not a symlink. Skipping." >&2
21
+ return 1
22
+ fi
23
+
24
+ ln -sfn "$target" "$link"
25
+ echo "Linked: $link -> $target"
26
+
27
+ if [ ! -e "$link" ]; then
28
+ echo "ERROR: Symlink target does not exist: $target" >&2
29
+ return 1
30
+ fi
31
+ }
32
+
33
+ echo "Linking Fastlane directories for $PLATFORM..."
34
+
35
+ FASTLANE_ROOT="$PROJECT_ROOT/fastlane"
36
+ PLATFORM_DIR="$APP_ROOT/$PLATFORM"
37
+
38
+ if [ ! -d "$FASTLANE_ROOT/$PLATFORM" ]; then
39
+ echo "ERROR: Fastlane config not found at $FASTLANE_ROOT/$PLATFORM" >&2
40
+ exit 1
41
+ fi
42
+
43
+ if [ ! -d "$PLATFORM_DIR" ]; then
44
+ echo "ERROR: Platform directory not found at $PLATFORM_DIR" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # Create symlink: app/ios/fastlane -> ../../fastlane/ios
49
+ # Create symlink: app/android/fastlane -> ../../fastlane/android
50
+ RELATIVE_TARGET="../../fastlane/$PLATFORM"
51
+ LINK_PATH="$PLATFORM_DIR/fastlane"
52
+
53
+ create_symlink "$RELATIVE_TARGET" "$LINK_PATH"
54
+
55
+ echo "Fastlane linking complete for $PLATFORM"
56
+ ls -la "$LINK_PATH"
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Reads values from ci.config.yaml using yq and exports them as environment variables.
5
+ # Usage: source scripts/ci/common/read-config.sh
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
9
+ export PROJECT_ROOT
10
+
11
+ CONFIG="$PROJECT_ROOT/ci.config.yaml"
12
+
13
+ if [ ! -f "$CONFIG" ]; then
14
+ echo "ERROR: ci.config.yaml not found at $CONFIG" >&2
15
+ exit 1
16
+ fi
17
+
18
+ if ! command -v yq &>/dev/null; then
19
+ echo "ERROR: yq is not installed. Install with: brew install yq" >&2
20
+ exit 1
21
+ fi
22
+
23
+ # App root
24
+ export FLUTTER_ROOT=$(yq '.flutter_root // "."' "$CONFIG")
25
+ export APP_ROOT="$PROJECT_ROOT/$FLUTTER_ROOT"
26
+
27
+ # App identity
28
+ export BUNDLE_ID=$(yq '.app.bundle_id // ""' "$CONFIG")
29
+ export PACKAGE_NAME=$(yq '.app.package_name // ""' "$CONFIG")
30
+ export APP_NAME=$(yq '.app.name // ""' "$CONFIG")
31
+ export SKU=$(yq '.app.sku // ""' "$CONFIG")
32
+ export APPLE_ID=$(yq '.app.apple_id // ""' "$CONFIG")
33
+
34
+ # Credentials - Apple
35
+ export P8_KEY_PATH=$(yq '.credentials.apple.p8_key_path // ""' "$CONFIG")
36
+ export APPLE_KEY_ID=$(yq '.credentials.apple.key_id // ""' "$CONFIG")
37
+ export APPLE_ISSUER_ID=$(yq '.credentials.apple.issuer_id // ""' "$CONFIG")
38
+
39
+ # Credentials - Google
40
+ export GOOGLE_SA_JSON_PATH=$(yq '.credentials.google.service_account_json_path // ""' "$CONFIG")
41
+
42
+ # Credentials - Android signing
43
+ export KEYSTORE_PASSWORD=$(yq '.credentials.android.keystore_password // ""' "$CONFIG")
44
+
45
+ # Credentials - Match (iOS code signing)
46
+ export MATCH_GIT_URL=$(yq '.credentials.match.git_url // ""' "$CONFIG")
47
+ export MATCH_DEPLOY_KEY_PATH=$(yq '.credentials.match.deploy_key_path // ""' "$CONFIG")
48
+
49
+ # iOS App Store settings
50
+ export PRIMARY_CATEGORY=$(yq '.ios.primary_category // ""' "$CONFIG")
51
+ export SECONDARY_CATEGORY=$(yq '.ios.secondary_category // ""' "$CONFIG")
52
+ export PRICE_TIER=$(yq '.ios.price_tier // ""' "$CONFIG")
53
+ export SUBMIT_FOR_REVIEW=$(yq '.ios.submit_for_review // "false"' "$CONFIG")
54
+ export AUTOMATIC_RELEASE=$(yq '.ios.automatic_release // "false"' "$CONFIG")
55
+
56
+ # Android Play Store settings
57
+ export TRACK=$(yq '.android.track // "internal"' "$CONFIG")
58
+ export ROLLOUT_FRACTION=$(yq '.android.rollout_fraction // ""' "$CONFIG")
59
+ export IN_APP_UPDATE_PRIORITY=$(yq '.android.in_app_update_priority // "0"' "$CONFIG")
60
+
61
+ # Codemagic CI/CD
62
+ export CODEMAGIC_APP_ID=$(yq '.codemagic.app_id // ""' "$CONFIG")
63
+ export CODEMAGIC_TEAM_ID=$(yq '.codemagic.team_id // ""' "$CONFIG")
64
+
65
+ # Web
66
+ export WEB_DOMAIN=$(yq '.web.domain // ""' "$CONFIG")
67
+
68
+ echo "Config loaded from $CONFIG"
69
+ echo " APP_ROOT=$APP_ROOT"
70
+ echo " BUNDLE_ID=$BUNDLE_ID"
71
+ echo " PACKAGE_NAME=$PACKAGE_NAME"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/../common/read-config.sh"
6
+
7
+ # --- Validate prerequisites ---
8
+ if [ -z "${EXPORT_OPTIONS_PLIST:-}" ]; then
9
+ echo "ERROR: EXPORT_OPTIONS_PLIST not set. Run setup-signing.sh first (TASK_3)." >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [ ! -f "$EXPORT_OPTIONS_PLIST" ]; then
14
+ echo "ERROR: Export options plist not found at $EXPORT_OPTIONS_PLIST" >&2
15
+ exit 1
16
+ fi
17
+
18
+ if ! command -v flutter &>/dev/null; then
19
+ echo "ERROR: Flutter SDK not found in PATH" >&2
20
+ exit 1
21
+ fi
22
+
23
+ # --- Build IPA ---
24
+ echo "Building IPA..."
25
+ echo " APP_ROOT: $APP_ROOT"
26
+ echo " EXPORT_OPTIONS_PLIST: $EXPORT_OPTIONS_PLIST"
27
+
28
+ cd "$APP_ROOT"
29
+ flutter build ipa \
30
+ --release \
31
+ --export-options-plist="$EXPORT_OPTIONS_PLIST"
32
+
33
+ # --- Verify IPA exists ---
34
+ IPA_DIR="$APP_ROOT/build/ios/ipa"
35
+ IPA_FILE=$(find "$IPA_DIR" -name "*.ipa" -type f 2>/dev/null | head -1)
36
+ if [ -z "$IPA_FILE" ]; then
37
+ echo "ERROR: No .ipa file found in $IPA_DIR" >&2
38
+ exit 1
39
+ fi
40
+
41
+ echo "IPA built successfully: $IPA_FILE"
42
+ echo "IPA size: $(du -h "$IPA_FILE" | cut -f1)"
43
+
44
+ # --- Export ---
45
+ export IPA_PATH="$IPA_FILE"
46
+
47
+ if [ -n "${GITHUB_ENV:-}" ]; then
48
+ echo "IPA_PATH=$IPA_FILE" >> "$GITHUB_ENV"
49
+ echo "Exported IPA_PATH to GITHUB_ENV"
50
+ fi
51
+
52
+ echo "iOS build complete: $IPA_PATH"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/../common/read-config.sh"
6
+
7
+ # --- Validate required config ---
8
+ if [ -z "$APPLE_KEY_ID" ] || [ -z "$APPLE_ISSUER_ID" ] || [ -z "$P8_KEY_PATH" ]; then
9
+ echo "ERROR: Missing Apple credentials in ci.config.yaml (APPLE_KEY_ID, APPLE_ISSUER_ID, or P8_KEY_PATH)" >&2
10
+ exit 1
11
+ fi
12
+
13
+ P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
14
+ if [ ! -f "$P8_FULL_PATH" ]; then
15
+ echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
16
+ exit 1
17
+ fi
18
+
19
+ # --- Set ASC API env vars for manage_version_ios.py ---
20
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
21
+ export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
22
+ export APP_STORE_CONNECT_PRIVATE_KEY
23
+ APP_STORE_CONNECT_PRIVATE_KEY=$(cat "$P8_FULL_PATH")
24
+
25
+ # --- Export BUNDLE_ID for manage_version_ios.py ---
26
+ export BUNDLE_ID
27
+
28
+ # --- Run the existing Python script ---
29
+ echo "Running manage_version_ios.py..."
30
+ VERSION_JSON=$(python3 "$PROJECT_ROOT/scripts/manage_version_ios.py")
31
+
32
+ if [ -z "$VERSION_JSON" ]; then
33
+ echo "ERROR: manage_version_ios.py returned empty output" >&2
34
+ exit 1
35
+ fi
36
+
37
+ echo "Version info: $VERSION_JSON"
38
+
39
+ # --- Parse JSON output ---
40
+ # Expected format: {"version": "X.Y.Z", "version_id": "...", "state": "..."}
41
+ read -r APP_VERSION APP_VERSION_ID APP_STATUS < <(
42
+ echo "$VERSION_JSON" | python3 -c "
43
+ import sys, json
44
+ d = json.load(sys.stdin)
45
+ print(d['version'], d.get('version_id',''), d.get('state',''))
46
+ "
47
+ )
48
+
49
+ echo "Parsed version: $APP_VERSION (id: $APP_VERSION_ID, status: $APP_STATUS)"
50
+
51
+ # --- Export to environment ---
52
+ export APP_VERSION
53
+ export APP_VERSION_ID
54
+ export APP_STATUS
55
+
56
+ # Write to $GITHUB_ENV for inter-step communication in CI
57
+ if [ -n "${GITHUB_ENV:-}" ]; then
58
+ echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_ENV"
59
+ echo "APP_VERSION_ID=$APP_VERSION_ID" >> "$GITHUB_ENV"
60
+ echo "APP_STATUS=$APP_STATUS" >> "$GITHUB_ENV"
61
+ echo "Exported to GITHUB_ENV"
62
+ fi
63
+
64
+ echo "iOS version management complete: $APP_VERSION"
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/../common/read-config.sh"
6
+
7
+ # --- Validate prerequisites ---
8
+ if [ -z "${APP_VERSION:-}" ]; then
9
+ echo "ERROR: APP_VERSION not set. Run manage-version.sh first." >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [ -z "$BUNDLE_ID" ]; then
14
+ echo "ERROR: BUNDLE_ID not set in ci.config.yaml" >&2
15
+ exit 1
16
+ fi
17
+
18
+ # --- Ensure ASC API env vars are set ---
19
+ if [ -z "${APP_STORE_CONNECT_KEY_IDENTIFIER:-}" ]; then
20
+ P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
21
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
22
+ export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
23
+ export APP_STORE_CONNECT_PRIVATE_KEY
24
+ APP_STORE_CONNECT_PRIVATE_KEY=$(cat "$P8_FULL_PATH")
25
+ fi
26
+
27
+ # --- Ensure PyJWT is installed ---
28
+ pip3 install PyJWT >/dev/null 2>&1 || true
29
+
30
+ # --- Fetch latest build number from ASC ---
31
+ echo "Fetching latest build number from App Store Connect..."
32
+
33
+ LATEST_BUILD=$(python3 -c "
34
+ import os, json, time, jwt, urllib.request
35
+
36
+ key_id = os.environ['APP_STORE_CONNECT_KEY_IDENTIFIER']
37
+ issuer_id = os.environ['APP_STORE_CONNECT_ISSUER_ID']
38
+ private_key = os.environ['APP_STORE_CONNECT_PRIVATE_KEY']
39
+ bundle_id = os.environ.get('BUNDLE_ID', '')
40
+
41
+ # Generate JWT token
42
+ now = int(time.time())
43
+ payload = {'iss': issuer_id, 'iat': now, 'exp': now + 1200, 'aud': 'appstoreconnect-v1'}
44
+ token = jwt.encode(payload, private_key, algorithm='ES256', headers={'kid': key_id})
45
+ headers = {'Authorization': f'Bearer {token}'}
46
+
47
+ try:
48
+ # Resolve app ID from bundle ID
49
+ req = urllib.request.Request(
50
+ f'https://api.appstoreconnect.apple.com/v1/apps?filter[bundleId]={bundle_id}',
51
+ headers=headers)
52
+ with urllib.request.urlopen(req) as resp:
53
+ app_id = json.loads(resp.read())['data'][0]['id']
54
+
55
+ # Fetch latest build
56
+ req = urllib.request.Request(
57
+ f'https://api.appstoreconnect.apple.com/v1/builds?filter[app]={app_id}&sort=-version&limit=1',
58
+ headers=headers)
59
+ with urllib.request.urlopen(req) as resp:
60
+ builds = json.loads(resp.read())['data']
61
+
62
+ print(builds[0]['attributes']['version'] if builds else '0')
63
+ except Exception:
64
+ print('0')
65
+ " 2>/dev/null || echo "0")
66
+
67
+ echo "Latest build number from ASC: $LATEST_BUILD"
68
+
69
+ # --- Increment build number ---
70
+ BUILD_NUMBER=$((LATEST_BUILD + 1))
71
+ echo "New build number: $BUILD_NUMBER"
72
+
73
+ # --- Write to pubspec.yaml ---
74
+ PUBSPEC="$APP_ROOT/pubspec.yaml"
75
+ if [ ! -f "$PUBSPEC" ]; then
76
+ echo "ERROR: pubspec.yaml not found at $PUBSPEC" >&2
77
+ exit 1
78
+ fi
79
+
80
+ echo "Updating pubspec.yaml: version: ${APP_VERSION}+${BUILD_NUMBER}"
81
+ sed -i '' "s/^version: .*/version: ${APP_VERSION}+${BUILD_NUMBER}/" "$PUBSPEC"
82
+
83
+ # Verify the write
84
+ WRITTEN_VERSION=$(grep '^version:' "$PUBSPEC" | head -1)
85
+ echo "Written to pubspec.yaml: $WRITTEN_VERSION"
86
+
87
+ # --- Export ---
88
+ export BUILD_NUMBER
89
+
90
+ if [ -n "${GITHUB_ENV:-}" ]; then
91
+ echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
92
+ echo "Exported BUILD_NUMBER to GITHUB_ENV"
93
+ fi
94
+
95
+ echo "Build number set: $BUILD_NUMBER"
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ COMMON_DIR="$SCRIPT_DIR/../common"
6
+ source "$COMMON_DIR/read-config.sh"
7
+
8
+ echo "=== iOS Signing Setup (Fastlane Match) ==="
9
+
10
+ # --- Step 1: Validate config (read-config.sh already exported vars) ---
11
+ validate_config() {
12
+ local match_key="$PROJECT_ROOT/$MATCH_DEPLOY_KEY_PATH"
13
+ if [ ! -f "$match_key" ]; then
14
+ echo "ERROR: Match deploy key not found at $match_key" >&2
15
+ exit 1
16
+ fi
17
+
18
+ if [ -z "$MATCH_GIT_URL" ]; then
19
+ echo "ERROR: MATCH_GIT_URL not configured in ci.config.yaml" >&2
20
+ exit 1
21
+ fi
22
+
23
+ if [ -z "${MATCH_PASSWORD:-}" ]; then
24
+ echo "ERROR: MATCH_PASSWORD env var is required (encryption password for Match repo)" >&2
25
+ exit 1
26
+ fi
27
+
28
+ local p8_key="$PROJECT_ROOT/$P8_KEY_PATH"
29
+ if [ ! -f "$p8_key" ]; then
30
+ echo "ERROR: P8 key file not found at $p8_key" >&2
31
+ exit 1
32
+ fi
33
+ }
34
+
35
+ # --- Step 2: Set up SSH agent with deploy key ---
36
+ setup_ssh_agent() {
37
+ echo "Setting up SSH agent..."
38
+ local match_key="$PROJECT_ROOT/$MATCH_DEPLOY_KEY_PATH"
39
+
40
+ eval "$(ssh-agent -s)"
41
+ chmod 600 "$match_key"
42
+ ssh-add "$match_key"
43
+
44
+ mkdir -p ~/.ssh
45
+ ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
46
+
47
+ echo "SSH agent configured with deploy key"
48
+ }
49
+
50
+ # --- Step 3: Set up App Store Connect API key ---
51
+ setup_api_key() {
52
+ local p8_full_path="$PROJECT_ROOT/$P8_KEY_PATH"
53
+
54
+ # Env vars for Match and Fastlane built-in api_key resolution
55
+ export APP_STORE_CONNECT_API_KEY_KEY_ID="$APPLE_KEY_ID"
56
+ export APP_STORE_CONNECT_API_KEY_ISSUER_ID="$APPLE_ISSUER_ID"
57
+ export APP_STORE_CONNECT_API_KEY_KEY="$(cat "$p8_full_path")"
58
+ export APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64="false"
59
+
60
+ # Aliases for existing Fastfile helpers
61
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
62
+ cp "$p8_full_path" /tmp/AuthKey.p8
63
+ chmod 600 /tmp/AuthKey.p8
64
+ export FASTLANE_API_KEY_PATH="/tmp/AuthKey.p8"
65
+
66
+ echo "App Store Connect API key configured (Key ID: $APPLE_KEY_ID)"
67
+ }
68
+
69
+ # --- Step 4: Create temporary keychain ---
70
+ KEYCHAIN_NAME="fastlane_tmp"
71
+ KEYCHAIN_DB="$KEYCHAIN_NAME.keychain-db"
72
+ KEYCHAIN_PASSWORD=""
73
+
74
+ setup_keychain() {
75
+ KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
76
+
77
+ echo "Creating temporary keychain: $KEYCHAIN_DB"
78
+
79
+ # Remove existing keychain if present (idempotent)
80
+ security delete-keychain "$KEYCHAIN_DB" 2>/dev/null || true
81
+
82
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_DB"
83
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_DB"
84
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_DB"
85
+
86
+ # Add to search list while preserving existing keychains
87
+ local existing_keychains
88
+ existing_keychains=$(security list-keychains -d user | tr -d '"' | tr '\n' ' ')
89
+ security list-keychains -d user -s "$KEYCHAIN_DB" $existing_keychains
90
+ security default-keychain -s "$KEYCHAIN_DB"
91
+
92
+ echo "Keychain created and unlocked"
93
+ }
94
+
95
+ # --- Step 5: Install Fastlane if needed ---
96
+ install_fastlane() {
97
+ "$COMMON_DIR/install-fastlane.sh" ios
98
+ }
99
+
100
+ # --- Step 6: Run Fastlane Match ---
101
+ run_match() {
102
+ echo "Running Fastlane Match..."
103
+ cd "$APP_ROOT/ios"
104
+
105
+ MATCH_READONLY="${MATCH_READONLY:-false}"
106
+
107
+ bundle exec fastlane match appstore \
108
+ --git_url "$MATCH_GIT_URL" \
109
+ --app_identifier "$BUNDLE_ID" \
110
+ --keychain_name "$KEYCHAIN_DB" \
111
+ --keychain_password "$KEYCHAIN_PASSWORD" \
112
+ --readonly "$MATCH_READONLY"
113
+
114
+ echo "Match completed successfully"
115
+ }
116
+
117
+ # --- Step 7: Find provisioning profile UUID ---
118
+ PROFILE_UUID=""
119
+ TEAM_ID=""
120
+
121
+ find_profile_uuid() {
122
+ local profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
123
+
124
+ if [ ! -d "$profiles_dir" ]; then
125
+ echo "ERROR: Provisioning Profiles directory not found" >&2
126
+ exit 1
127
+ fi
128
+
129
+ for profile in "$profiles_dir"/*.mobileprovision; do
130
+ [ -f "$profile" ] || continue
131
+ local plist
132
+ plist=$(security cms -D -i "$profile" 2>/dev/null) || continue
133
+ local profile_app_id
134
+ profile_app_id=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" \
135
+ /dev/stdin <<< "$plist" 2>/dev/null || echo "")
136
+ if [[ "$profile_app_id" == *"$BUNDLE_ID" ]]; then
137
+ PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" \
138
+ /dev/stdin <<< "$plist" 2>/dev/null || echo "")
139
+ if [ -n "$PROFILE_UUID" ]; then
140
+ echo "Found provisioning profile: $PROFILE_UUID"
141
+ break
142
+ fi
143
+ fi
144
+ done
145
+
146
+ if [ -z "$PROFILE_UUID" ]; then
147
+ echo "ERROR: Could not find provisioning profile for $BUNDLE_ID" >&2
148
+ echo "Installed profiles:" >&2
149
+ ls -la "$profiles_dir/" 2>/dev/null || echo "(directory not found)" >&2
150
+ exit 1
151
+ fi
152
+ }
153
+
154
+ find_team_id() {
155
+ # Try extracting from signing identity
156
+ TEAM_ID=$(security find-identity -v -p codesigning "$KEYCHAIN_DB" \
157
+ | grep "Apple Distribution" \
158
+ | head -1 \
159
+ | sed 's/.*(\([A-Z0-9]*\))"/\1/' || echo "")
160
+
161
+ if [ -n "$TEAM_ID" ]; then
162
+ return
163
+ fi
164
+
165
+ # Fallback: extract from provisioning profile
166
+ local profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
167
+ TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print :TeamIdentifier:0" \
168
+ /dev/stdin <<< "$(security cms -D -i "$profiles_dir/$PROFILE_UUID.mobileprovision" 2>/dev/null)" \
169
+ 2>/dev/null || echo "")
170
+ }
171
+
172
+ # --- Step 8: Generate ExportOptions.plist ---
173
+ generate_export_options() {
174
+ echo "Generating ExportOptions.plist..."
175
+ local export_options="$PROJECT_ROOT/ios_export_options.plist"
176
+ local profile_name="match AppStore $BUNDLE_ID"
177
+
178
+ cat > "$export_options" <<PLIST
179
+ <?xml version="1.0" encoding="UTF-8"?>
180
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
181
+ <plist version="1.0">
182
+ <dict>
183
+ <key>method</key>
184
+ <string>app-store</string>
185
+ <key>signingStyle</key>
186
+ <string>manual</string>
187
+ <key>teamID</key>
188
+ <string>${TEAM_ID}</string>
189
+ <key>provisioningProfiles</key>
190
+ <dict>
191
+ <key>${BUNDLE_ID}</key>
192
+ <string>${profile_name}</string>
193
+ </dict>
194
+ <key>uploadBitcode</key>
195
+ <false/>
196
+ <key>uploadSymbols</key>
197
+ <true/>
198
+ </dict>
199
+ </plist>
200
+ PLIST
201
+
202
+ echo "ExportOptions.plist written to $export_options"
203
+ }
204
+
205
+ # --- Step 9: Export outputs ---
206
+ export_outputs() {
207
+ local export_options="$PROJECT_ROOT/ios_export_options.plist"
208
+ local -a outputs=(
209
+ "EXPORT_OPTIONS_PLIST=$export_options"
210
+ "KEYCHAIN_NAME=$KEYCHAIN_DB"
211
+ "PROFILE_UUID=$PROFILE_UUID"
212
+ "TEAM_ID=$TEAM_ID"
213
+ )
214
+
215
+ if [ -n "${GITHUB_ENV:-}" ]; then
216
+ printf '%s\n' "${outputs[@]}" >> "$GITHUB_ENV"
217
+ echo "Outputs written to GITHUB_ENV"
218
+ else
219
+ echo ""
220
+ echo "=== Local Mode Outputs ==="
221
+ printf '%s\n' "${outputs[@]}"
222
+ fi
223
+
224
+ echo ""
225
+ echo "=== Signing Verification ==="
226
+ echo "Installed identities:"
227
+ security find-identity -v -p codesigning "$KEYCHAIN_DB"
228
+ echo ""
229
+ echo "iOS signing setup complete"
230
+ }
231
+
232
+ # --- Main ---
233
+ validate_config
234
+ setup_ssh_agent
235
+ setup_api_key
236
+ setup_keychain
237
+ install_fastlane
238
+ run_match
239
+ find_profile_uuid
240
+ find_team_id
241
+ generate_export_options
242
+ export_outputs