@daemux/store-automator 0.7.1 → 0.9.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 (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/bin/cli.mjs +38 -14
  3. package/package.json +1 -1
  4. package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
  5. package/plugins/store-automator/agents/appstore-meta-creator.md +13 -8
  6. package/plugins/store-automator/agents/architect.md +42 -1
  7. package/plugins/store-automator/agents/devops.md +26 -27
  8. package/scripts/check_google_play.py +1 -1
  9. package/scripts/manage_version_ios.py +1 -1
  10. package/src/ci-config.mjs +21 -0
  11. package/src/install-paths.mjs +92 -0
  12. package/src/install.mjs +33 -78
  13. package/src/templates.mjs +71 -1
  14. package/templates/CLAUDE.md.template +70 -16
  15. package/templates/Matchfile.template +8 -0
  16. package/templates/ci.config.yaml.template +3 -0
  17. package/templates/codemagic.template.yaml +131 -96
  18. package/templates/fastlane/android/Fastfile.template +23 -5
  19. package/templates/fastlane/ios/Fastfile.template +4 -3
  20. package/templates/github/workflows/android-release.yml +141 -0
  21. package/templates/github/workflows/codemagic-trigger.yml +15 -5
  22. package/templates/github/workflows/ios-release.yml +119 -0
  23. package/templates/scripts/check_google_play.py +1 -1
  24. package/templates/scripts/ci/android/build.sh +50 -0
  25. package/templates/scripts/ci/android/check-readiness.sh +93 -0
  26. package/templates/scripts/ci/android/manage-version.sh +134 -0
  27. package/templates/scripts/ci/android/setup-keystore.sh +80 -0
  28. package/templates/scripts/ci/android/sync-iap.sh +63 -0
  29. package/templates/scripts/ci/android/update-data-safety.sh +65 -0
  30. package/templates/scripts/ci/android/upload-binary.sh +62 -0
  31. package/templates/scripts/ci/android/upload-metadata.sh +77 -0
  32. package/templates/scripts/ci/common/check-changed.sh +74 -0
  33. package/templates/scripts/ci/common/flutter-setup.sh +22 -0
  34. package/templates/scripts/ci/common/install-fastlane.sh +39 -0
  35. package/templates/scripts/ci/common/link-fastlane.sh +56 -0
  36. package/templates/scripts/ci/common/read-config.sh +74 -0
  37. package/templates/scripts/ci/ios/build.sh +91 -0
  38. package/templates/scripts/ci/ios/manage-version.sh +64 -0
  39. package/templates/scripts/ci/ios/set-build-number.sh +92 -0
  40. package/templates/scripts/ci/ios/setup-signing.sh +283 -0
  41. package/templates/scripts/ci/ios/sync-iap.sh +80 -0
  42. package/templates/scripts/ci/ios/upload-binary.sh +43 -0
  43. package/templates/scripts/ci/ios/upload-metadata.sh +82 -0
  44. package/templates/scripts/create_app_record.py +1 -1
  45. package/templates/scripts/manage_version_ios.py +1 -1
  46. package/templates/scripts/update_data_safety.py +220 -0
@@ -4,6 +4,11 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
 
7
+ # Cancel in-progress runs when a newer build is triggered
8
+ concurrency:
9
+ group: codemagic-trigger-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
7
12
  jobs:
8
13
  trigger:
9
14
  runs-on: ubuntu-latest
@@ -52,11 +57,12 @@ jobs:
52
57
  APP_ID: ${{ steps.config.outputs.app_id }}
53
58
  BRANCH: ${{ github.ref_name }}
54
59
  run: |
55
- echo "${{ steps.config.outputs.workflows }}" | while IFS= read -r workflow; do
60
+ FAILED=0
61
+ while IFS= read -r workflow; do
56
62
  [ -z "$workflow" ] && continue
57
63
  if ! echo "$workflow" | grep -Eq '^[a-zA-Z0-9_-]+$'; then
58
- echo "ERROR: Invalid workflow name: $workflow"
59
- continue
64
+ echo "ERROR: Invalid workflow name: $workflow" >&2
65
+ exit 1
60
66
  fi
61
67
  echo "Triggering: $workflow"
62
68
  curl --max-time 30 --retry 2 -sf -X POST https://api.codemagic.io/builds \
@@ -64,5 +70,9 @@ jobs:
64
70
  -H "x-auth-token: $CM_TOKEN" \
65
71
  -d "{\"appId\": \"$APP_ID\", \"workflowId\": \"$workflow\", \"branch\": \"$BRANCH\"}" \
66
72
  && echo " -> success" \
67
- || echo " -> failed"
68
- done
73
+ || { echo " -> FAILED"; FAILED=1; }
74
+ done <<< "${{ steps.config.outputs.workflows }}"
75
+ if [ "$FAILED" -ne 0 ]; then
76
+ echo "ERROR: One or more Codemagic workflow triggers failed" >&2
77
+ exit 1
78
+ fi
@@ -0,0 +1,119 @@
1
+ name: iOS Release
2
+ # POST-MIGRATION CLEANUP (after GitHub Actions workflows are verified in production):
3
+ # - Remove .github/workflows/codemagic-trigger.yml
4
+ # - Remove any Codemagic-specific configuration files
5
+ # - Update README/docs to reference GitHub Actions instead of Codemagic
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ workflow_dispatch:
10
+
11
+ # Cancel in-progress runs when a newer build is triggered
12
+ concurrency:
13
+ group: ios-release-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ metadata:
18
+ name: Upload iOS Metadata
19
+ runs-on: macos-latest
20
+ timeout-minutes: 30
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0
26
+
27
+ - name: Setup Ruby
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: '3.3'
31
+
32
+ - name: Cache .ci-state
33
+ uses: actions/cache@v4
34
+ with:
35
+ path: .ci-state
36
+ key: ci-state-ios-metadata-${{ hashFiles('fastlane/metadata/**', 'fastlane/screenshots/ios/**') }}
37
+ restore-keys: ci-state-ios-metadata-
38
+
39
+ - name: Install yq
40
+ run: brew install yq
41
+
42
+ - name: Link Fastlane
43
+ run: scripts/ci/common/link-fastlane.sh ios
44
+
45
+ - name: Install Fastlane
46
+ run: scripts/ci/common/install-fastlane.sh ios
47
+
48
+ - name: Upload Metadata & Screenshots
49
+ run: scripts/ci/ios/upload-metadata.sh
50
+
51
+ - name: Sync IAP
52
+ run: scripts/ci/ios/sync-iap.sh
53
+
54
+ - name: Save .ci-state
55
+ if: success()
56
+ uses: actions/cache/save@v4
57
+ with:
58
+ path: .ci-state
59
+ key: ci-state-ios-metadata-${{ hashFiles('fastlane/metadata/**', 'fastlane/screenshots/ios/**') }}
60
+
61
+ build:
62
+ name: Build & Upload iOS
63
+ needs: [metadata]
64
+ runs-on: macos-latest
65
+ timeout-minutes: 60
66
+ env:
67
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
68
+ steps:
69
+ - name: Checkout
70
+ uses: actions/checkout@v4
71
+ with:
72
+ fetch-depth: 0
73
+
74
+ - name: Setup Ruby
75
+ uses: ruby/setup-ruby@v1
76
+ with:
77
+ ruby-version: '3.3'
78
+
79
+ - name: Setup Flutter
80
+ uses: subosito/flutter-action@v2
81
+ with:
82
+ channel: stable
83
+
84
+ - name: Install yq
85
+ run: brew install yq
86
+
87
+ - name: Link Fastlane
88
+ run: scripts/ci/common/link-fastlane.sh ios
89
+
90
+ - name: Install Fastlane
91
+ run: scripts/ci/common/install-fastlane.sh ios
92
+
93
+ - name: Setup Signing (Match)
94
+ run: scripts/ci/ios/setup-signing.sh
95
+
96
+ - name: Install Python dependencies
97
+ run: pip3 install --break-system-packages PyJWT
98
+
99
+ - name: Manage Version
100
+ run: scripts/ci/ios/manage-version.sh
101
+
102
+ - name: Set Build Number
103
+ run: scripts/ci/ios/set-build-number.sh
104
+
105
+ - name: Flutter Packages
106
+ run: scripts/ci/common/flutter-setup.sh
107
+
108
+ - name: Build IPA
109
+ run: scripts/ci/ios/build.sh
110
+
111
+ - name: Upload Artifacts
112
+ if: always()
113
+ uses: actions/upload-artifact@v4
114
+ with:
115
+ name: ios-ipa
116
+ path: app/build/ios/ipa/*.ipa
117
+
118
+ - name: Upload to App Store
119
+ run: scripts/ci/ios/upload-binary.sh
@@ -25,7 +25,7 @@ except ImportError:
25
25
  print("Installing dependencies...", file=sys.stderr)
26
26
  import subprocess
27
27
  subprocess.check_call(
28
- [sys.executable, "-m", "pip", "install", "PyJWT", "cryptography", "requests"],
28
+ [sys.executable, "-m", "pip", "install", "--break-system-packages", "PyJWT", "cryptography", "requests"],
29
29
  stdout=subprocess.DEVNULL,
30
30
  )
31
31
  import jwt
@@ -0,0 +1,50 @@
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 "${CM_KEYSTORE_PATH:-}" ]; then
9
+ echo "ERROR: CM_KEYSTORE_PATH not set. Run setup-keystore.sh first." >&2
10
+ exit 1
11
+ fi
12
+ if [ ! -f "$CM_KEYSTORE_PATH" ]; then
13
+ echo "ERROR: Keystore not found at $CM_KEYSTORE_PATH" >&2
14
+ exit 1
15
+ fi
16
+ if ! command -v flutter &>/dev/null; then
17
+ echo "ERROR: Flutter SDK not found in PATH" >&2
18
+ exit 1
19
+ fi
20
+
21
+ echo "Building Android AAB..."
22
+ echo " APP_ROOT: $APP_ROOT"
23
+ echo " CM_KEYSTORE_PATH: $CM_KEYSTORE_PATH"
24
+ echo " CM_KEY_ALIAS: ${CM_KEY_ALIAS:-not set}"
25
+
26
+ # --- Build AAB ---
27
+ cd "$APP_ROOT"
28
+ flutter build appbundle --release
29
+
30
+ # --- Locate AAB ---
31
+ AAB_DIR="$APP_ROOT/build/app/outputs/bundle/release"
32
+ AAB_FILE=$(find "$AAB_DIR" -name "*.aab" -type f 2>/dev/null | head -1)
33
+
34
+ if [ -z "${AAB_FILE:-}" ]; then
35
+ echo "ERROR: No .aab file found in $AAB_DIR" >&2
36
+ ls -la "$AAB_DIR" >&2 2>/dev/null || true
37
+ exit 1
38
+ fi
39
+
40
+ echo "AAB built: $AAB_FILE ($(du -h "$AAB_FILE" | cut -f1))"
41
+
42
+ # --- Export ---
43
+ export AAB_PATH="$AAB_FILE"
44
+
45
+ if [ -n "${GITHUB_ENV:-}" ]; then
46
+ echo "AAB_PATH=$AAB_FILE" >> "$GITHUB_ENV"
47
+ echo "Exported AAB_PATH to GITHUB_ENV"
48
+ fi
49
+
50
+ echo "Android build complete: $AAB_PATH"
@@ -0,0 +1,93 @@
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
+ # --- Helpers ---
8
+
9
+ # Export GOOGLE_PLAY_READY and persist to $GITHUB_ENV when in CI.
10
+ set_play_ready() {
11
+ local value="$1"
12
+ export GOOGLE_PLAY_READY="$value"
13
+ if [ -n "${GITHUB_ENV:-}" ]; then
14
+ echo "GOOGLE_PLAY_READY=$value" >> "$GITHUB_ENV"
15
+ fi
16
+ }
17
+
18
+ # Log an error, mark not-ready, and hard-fail.
19
+ fail_not_ready() {
20
+ echo "ERROR: $1" >&2
21
+ set_play_ready "false"
22
+ exit 1
23
+ }
24
+
25
+ # --- Validate required config ---
26
+ if [ -z "${GOOGLE_SA_JSON_PATH:-}" ]; then
27
+ fail_not_ready "GOOGLE_SA_JSON_PATH not set in ci.config.yaml"
28
+ fi
29
+
30
+ SA_FULL_PATH="$PROJECT_ROOT/$GOOGLE_SA_JSON_PATH"
31
+ if [ ! -f "$SA_FULL_PATH" ]; then
32
+ fail_not_ready "Service account JSON not found at $SA_FULL_PATH"
33
+ fi
34
+
35
+ if [ -z "${PACKAGE_NAME:-}" ]; then
36
+ fail_not_ready "PACKAGE_NAME not set in ci.config.yaml"
37
+ fi
38
+
39
+ # --- Set env vars for the Python script ---
40
+ export SA_JSON="$SA_FULL_PATH"
41
+ export PACKAGE_NAME
42
+
43
+ # --- Run the existing Python script ---
44
+ echo "Checking Google Play readiness for $PACKAGE_NAME..."
45
+ READINESS_JSON=$(python3 "$PROJECT_ROOT/scripts/check_google_play.py")
46
+
47
+ if [ -z "$READINESS_JSON" ]; then
48
+ fail_not_ready "check_google_play.py returned empty output"
49
+ fi
50
+
51
+ echo "Readiness info: $READINESS_JSON"
52
+
53
+ # --- Parse JSON output (single invocation for both fields) ---
54
+ # Expected format: {"ready": true/false, "missing_steps": [...]}
55
+ PARSED=$(echo "$READINESS_JSON" | python3 -c "
56
+ import sys, json
57
+ data = json.load(sys.stdin)
58
+ ready = str(data.get('ready', False)).lower()
59
+ steps = data.get('missing_steps', [])
60
+ print(ready)
61
+ for s in steps:
62
+ print(f' - {s}')
63
+ ")
64
+
65
+ # First line is the ready status; remaining lines are missing steps.
66
+ READY=$(echo "$PARSED" | head -n 1)
67
+ MISSING_STEPS=$(echo "$PARSED" | tail -n +2)
68
+
69
+ # --- Export result ---
70
+ set_play_ready "$READY"
71
+
72
+ # --- Print guidance if not ready ---
73
+ if [ "$READY" != "true" ]; then
74
+ echo ""
75
+ echo "============================================="
76
+ echo " Google Play is NOT ready for automation"
77
+ echo "============================================="
78
+ echo ""
79
+ echo "Missing steps:"
80
+ echo "$MISSING_STEPS"
81
+ echo ""
82
+ echo "To fix:"
83
+ echo " 1. Go to Google Play Console: https://play.google.com/console"
84
+ echo " 2. Ensure the app ($PACKAGE_NAME) has been manually created"
85
+ echo " 3. Complete the store listing, content rating, and pricing"
86
+ echo " 4. Upload at least one AAB manually for the first release"
87
+ echo " 5. Grant the service account access to the app"
88
+ echo ""
89
+ echo "Google Play is NOT ready. CI cannot proceed."
90
+ exit 1
91
+ else
92
+ echo "Google Play is ready for automated publishing"
93
+ fi
@@ -0,0 +1,134 @@
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
+ PUBSPEC="$APP_ROOT/pubspec.yaml"
8
+
9
+ # --- Step 1: Get iOS version (for cross-platform consistency) ---
10
+
11
+ echo "Getting iOS version for cross-platform consistency..."
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
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
20
+ export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
21
+ export APP_STORE_CONNECT_PRIVATE_KEY
22
+ APP_STORE_CONNECT_PRIVATE_KEY=$(cat "$P8_FULL_PATH")
23
+
24
+ VERSION_JSON=$(python3 "$PROJECT_ROOT/scripts/manage_version_ios.py")
25
+ if [ -z "$VERSION_JSON" ]; then
26
+ echo "ERROR: manage_version_ios.py returned empty output" >&2
27
+ exit 1
28
+ fi
29
+
30
+ APP_VERSION=$(echo "$VERSION_JSON" \
31
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])")
32
+ echo "iOS version: $APP_VERSION"
33
+
34
+ # --- Step 2: Get latest Android version code ---
35
+
36
+ echo "Getting latest Android version code..."
37
+
38
+ LATEST_CODE="0"
39
+
40
+ if [ "${GOOGLE_PLAY_READY:-false}" != "true" ]; then
41
+ echo "Google Play not ready. Reading version code from pubspec.yaml or default."
42
+ if [ -f "$PUBSPEC" ]; then
43
+ CURRENT_VERSION=$(grep '^version:' "$PUBSPEC" | head -1 | sed 's/version: //')
44
+ if [[ "$CURRENT_VERSION" == *"+"* ]]; then
45
+ LATEST_CODE="${CURRENT_VERSION#*+}"
46
+ echo "Current version code from pubspec.yaml: $LATEST_CODE"
47
+ fi
48
+ fi
49
+ elif [ ! -f "$PROJECT_ROOT/$GOOGLE_SA_JSON_PATH" ]; then
50
+ echo "ERROR: Service account JSON not found at $PROJECT_ROOT/$GOOGLE_SA_JSON_PATH" >&2
51
+ exit 1
52
+ else
53
+ SA_FULL_PATH="$PROJECT_ROOT/$GOOGLE_SA_JSON_PATH"
54
+ cd "$APP_ROOT/android"
55
+
56
+ if ! bundle exec fastlane --version >/dev/null 2>&1; then
57
+ "$SCRIPT_DIR/../common/install-fastlane.sh" android
58
+ fi
59
+
60
+ export PACKAGE_NAME
61
+ export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$SA_FULL_PATH"
62
+ export TRACK="${TRACK:-internal}"
63
+
64
+ # Primary: Fastlane action
65
+ LATEST_CODE=$(bundle exec fastlane run google_play_track_version_codes \
66
+ package_name:"$PACKAGE_NAME" \
67
+ json_key:"$SA_FULL_PATH" \
68
+ track:"$TRACK" 2>&1 \
69
+ | grep -oE '[0-9]+' | sort -n | tail -1)
70
+
71
+ # Fallback: direct Ruby Supply::Client API
72
+ if [ -z "$LATEST_CODE" ] || [ "$LATEST_CODE" = "0" ]; then
73
+ LATEST_CODE=$(bundle exec ruby -e "
74
+ require 'supply'
75
+ require 'supply/client'
76
+
77
+ Supply.config = FastlaneCore::Configuration.create(
78
+ Supply::Options.available_options,
79
+ {
80
+ package_name: ENV['PACKAGE_NAME'],
81
+ json_key: ENV['GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH'],
82
+ track: ENV.fetch('TRACK', 'internal')
83
+ }
84
+ )
85
+
86
+ client = Supply::Client.make_from_config
87
+ all_codes = []
88
+ ['internal', 'alpha', 'beta', 'production'].each do |track|
89
+ begin
90
+ track_codes = client.track_version_codes(track)
91
+ all_codes.concat(track_codes) if track_codes
92
+ rescue => e
93
+ # Track may not exist yet
94
+ end
95
+ end
96
+ puts all_codes.max || 0
97
+ ")
98
+ fi
99
+
100
+ if [ -z "$LATEST_CODE" ]; then
101
+ echo "ERROR: Failed to fetch version codes from Google Play" >&2
102
+ exit 1
103
+ fi
104
+
105
+ echo "Latest version code from Google Play: $LATEST_CODE"
106
+ fi
107
+
108
+ # --- Step 3: Increment and write ---
109
+
110
+ BUILD_NUMBER=$((LATEST_CODE + 1))
111
+ ANDROID_VERSION_CODE="$BUILD_NUMBER"
112
+
113
+ echo "New Android version code: $BUILD_NUMBER"
114
+
115
+ if [ ! -f "$PUBSPEC" ]; then
116
+ echo "ERROR: pubspec.yaml not found at $PUBSPEC" >&2
117
+ exit 1
118
+ fi
119
+
120
+ sed -i '' "s/^version: .*/version: ${APP_VERSION}+${BUILD_NUMBER}/" "$PUBSPEC"
121
+ echo "Updated pubspec.yaml: $(grep '^version:' "$PUBSPEC" | head -1)"
122
+
123
+ # --- Step 4: Export ---
124
+
125
+ export APP_VERSION BUILD_NUMBER ANDROID_VERSION_CODE
126
+
127
+ if [ -n "${GITHUB_ENV:-}" ]; then
128
+ echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_ENV"
129
+ echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
130
+ echo "ANDROID_VERSION_CODE=$ANDROID_VERSION_CODE" >> "$GITHUB_ENV"
131
+ echo "Exported version vars to GITHUB_ENV"
132
+ fi
133
+
134
+ echo "Android version management complete: ${APP_VERSION}+${BUILD_NUMBER}"
@@ -0,0 +1,80 @@
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
+ # --- Helper: write key=value to $GITHUB_ENV when running in CI ---
8
+ persist_to_github_env() {
9
+ if [ -n "${GITHUB_ENV:-}" ]; then
10
+ for pair in "$@"; do
11
+ echo "$pair" >> "$GITHUB_ENV"
12
+ done
13
+ fi
14
+ }
15
+
16
+ # --- Require keystore password (needed for generation AND validation) ---
17
+ if [ -z "${KEYSTORE_PASSWORD:-}" ]; then
18
+ echo "ERROR: KEYSTORE_PASSWORD not set in ci.config.yaml (credentials.keystore_password)" >&2
19
+ exit 1
20
+ fi
21
+
22
+ ANDROID_DIR="$APP_ROOT/android"
23
+ KEYSTORE_TARGET="$ANDROID_DIR/upload.keystore"
24
+ CREDS_KEYSTORE="$PROJECT_ROOT/creds/android_upload.keystore"
25
+
26
+ # --- Locate or create keystore ---
27
+ if [ -f "$KEYSTORE_TARGET" ]; then
28
+ echo "Keystore found at $KEYSTORE_TARGET"
29
+ elif [ -f "$CREDS_KEYSTORE" ]; then
30
+ echo "Keystore found in creds/, copying to $KEYSTORE_TARGET"
31
+ cp "$CREDS_KEYSTORE" "$KEYSTORE_TARGET"
32
+ else
33
+ echo "No keystore found. Generating a new upload keystore..."
34
+
35
+ keytool -genkey -v \
36
+ -keystore "$KEYSTORE_TARGET" \
37
+ -keyalg RSA \
38
+ -keysize 2048 \
39
+ -validity 10000 \
40
+ -alias upload \
41
+ -storepass "$KEYSTORE_PASSWORD" \
42
+ -keypass "$KEYSTORE_PASSWORD" \
43
+ -dname "CN=Upload Key, O=${APP_NAME:-App}"
44
+
45
+ echo "New keystore generated at $KEYSTORE_TARGET"
46
+
47
+ # Backup to creds/ for local development persistence.
48
+ # In CI the workspace is ephemeral, so the keystore should already exist
49
+ # in creds/ (committed or restored from a secret/artifact).
50
+ mkdir -p "$PROJECT_ROOT/creds"
51
+ cp "$KEYSTORE_TARGET" "$CREDS_KEYSTORE"
52
+ echo "Backed up keystore to $CREDS_KEYSTORE"
53
+ fi
54
+
55
+ # --- Resolve absolute path ---
56
+ KEYSTORE_ABS_PATH="$(cd "$(dirname "$KEYSTORE_TARGET")" && pwd)/$(basename "$KEYSTORE_TARGET")"
57
+
58
+ # --- Validate keystore ---
59
+ echo "Validating keystore..."
60
+ if ! keytool -list -keystore "$KEYSTORE_ABS_PATH" -storepass "$KEYSTORE_PASSWORD" -alias upload >/dev/null 2>&1; then
61
+ echo "ERROR: Keystore validation failed. Check password and alias." >&2
62
+ exit 1
63
+ fi
64
+ echo "Keystore validated successfully"
65
+
66
+ # --- Set Gradle signing env vars (matches build.gradle.kts) ---
67
+ export CM_KEYSTORE_PATH="$KEYSTORE_ABS_PATH"
68
+ export CM_KEYSTORE_PASSWORD="$KEYSTORE_PASSWORD"
69
+ export CM_KEY_ALIAS="upload"
70
+ export CM_KEY_PASSWORD="$KEYSTORE_PASSWORD"
71
+
72
+ persist_to_github_env \
73
+ "CM_KEYSTORE_PATH=$CM_KEYSTORE_PATH" \
74
+ "CM_KEYSTORE_PASSWORD=$CM_KEYSTORE_PASSWORD" \
75
+ "CM_KEY_ALIAS=$CM_KEY_ALIAS" \
76
+ "CM_KEY_PASSWORD=$CM_KEY_PASSWORD"
77
+
78
+ echo "Android keystore setup complete"
79
+ echo " CM_KEYSTORE_PATH=$CM_KEYSTORE_PATH"
80
+ echo " CM_KEY_ALIAS=$CM_KEY_ALIAS"
@@ -0,0 +1,63 @@
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
+ # --- Check Google Play readiness ---
8
+ if [ "${GOOGLE_PLAY_READY:-false}" != "true" ]; then
9
+ echo "ERROR: Google Play not ready. Cannot sync IAPs." >&2
10
+ exit 1
11
+ fi
12
+
13
+ # --- Check if IAP config exists ---
14
+ IAP_CONFIG="$PROJECT_ROOT/fastlane/iap_config.json"
15
+ if [ ! -f "$IAP_CONFIG" ]; then
16
+ echo "No iap_config.json found at $IAP_CONFIG. Skipping IAP sync."
17
+ exit 0
18
+ fi
19
+
20
+ # --- Check if IAP plugin is available ---
21
+ cd "$APP_ROOT/android"
22
+ if ! bundle exec gem list fastlane-plugin-iap --installed; then
23
+ echo "ERROR: fastlane-plugin-iap not installed." >&2
24
+ echo "To enable: add it to app/android/Pluginfile and run 'bundle install'." >&2
25
+ exit 1
26
+ fi
27
+
28
+ # --- Hash-based change detection ---
29
+ STATE_DIR="$PROJECT_ROOT/.ci-state"
30
+ mkdir -p "$STATE_DIR"
31
+
32
+ HASH=$(shasum -a 256 "$IAP_CONFIG" | cut -d' ' -f1)
33
+ STATE_FILE="$STATE_DIR/android-iap-hash"
34
+
35
+ if [ -f "$STATE_FILE" ]; then
36
+ STORED_HASH=$(cat "$STATE_FILE")
37
+ if [ "$HASH" = "$STORED_HASH" ]; then
38
+ echo "No changes in iap_config.json (hash: ${HASH:0:12}...). Skipping."
39
+ exit 0
40
+ fi
41
+ fi
42
+
43
+ echo "Changes detected in IAP config (hash: ${HASH:0:12}...)"
44
+
45
+ # --- Resolve service account path ---
46
+ SA_FULL_PATH="$PROJECT_ROOT/$GOOGLE_SA_JSON_PATH"
47
+ if [ ! -f "$SA_FULL_PATH" ]; then
48
+ echo "ERROR: Service account JSON not found at $SA_FULL_PATH" >&2
49
+ exit 1
50
+ fi
51
+
52
+ # --- Sync IAP via Fastlane ---
53
+ echo "Syncing Android IAP configuration..."
54
+
55
+ PACKAGE_NAME="$PACKAGE_NAME" \
56
+ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$SA_FULL_PATH" \
57
+ bundle exec fastlane sync_google_iap
58
+
59
+ echo "Android IAP sync complete"
60
+
61
+ # --- Update hash on success ---
62
+ echo "$HASH" > "$STATE_FILE"
63
+ echo "Updated state hash: ${HASH:0:12}..."
@@ -0,0 +1,65 @@
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
+ # --- Check Google Play readiness ---
8
+ if [ "${GOOGLE_PLAY_READY:-false}" != "true" ]; then
9
+ echo "ERROR: Google Play not ready. Cannot update data safety." >&2
10
+ exit 1
11
+ fi
12
+
13
+ # --- Check if data safety CSV exists ---
14
+ DATA_SAFETY_CSV="$PROJECT_ROOT/fastlane/data_safety.csv"
15
+ if [ ! -f "$DATA_SAFETY_CSV" ]; then
16
+ echo "No data_safety.csv found at $DATA_SAFETY_CSV. Skipping."
17
+ exit 0
18
+ fi
19
+
20
+ # --- Hash-based change detection ---
21
+ STATE_DIR="$PROJECT_ROOT/.ci-state"
22
+ mkdir -p "$STATE_DIR"
23
+
24
+ HASH=$(shasum -a 256 "$DATA_SAFETY_CSV" | cut -d' ' -f1)
25
+ STATE_FILE="$STATE_DIR/android-data-safety-hash"
26
+
27
+ if [ -f "$STATE_FILE" ]; then
28
+ STORED_HASH=$(cat "$STATE_FILE")
29
+ if [ "$HASH" = "$STORED_HASH" ]; then
30
+ echo "No changes in data_safety.csv (hash: ${HASH:0:12}...). Skipping."
31
+ exit 0
32
+ fi
33
+ fi
34
+
35
+ echo "Changes detected in data safety config (hash: ${HASH:0:12}...)"
36
+
37
+ # --- Link fastlane directories ---
38
+ "$SCRIPT_DIR/../common/link-fastlane.sh" android
39
+
40
+ # --- Ensure fastlane is installed ---
41
+ cd "$APP_ROOT/android"
42
+ if ! bundle exec fastlane --version >/dev/null 2>&1; then
43
+ "$SCRIPT_DIR/../common/install-fastlane.sh" android
44
+ fi
45
+
46
+ # --- Resolve service account path ---
47
+ SA_FULL_PATH="$PROJECT_ROOT/$GOOGLE_SA_JSON_PATH"
48
+ if [ ! -f "$SA_FULL_PATH" ]; then
49
+ echo "ERROR: Service account JSON not found at $SA_FULL_PATH" >&2
50
+ exit 1
51
+ fi
52
+
53
+ # --- Update data safety via Fastlane ---
54
+ echo "Updating Android data safety..."
55
+
56
+ PACKAGE_NAME="$PACKAGE_NAME" \
57
+ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$SA_FULL_PATH" \
58
+ DATA_SAFETY_CSV_PATH="$DATA_SAFETY_CSV" \
59
+ bundle exec fastlane update_data_safety
60
+
61
+ echo "Android data safety update complete"
62
+
63
+ # --- Update hash on success ---
64
+ echo "$HASH" > "$STATE_FILE"
65
+ echo "Updated state hash: ${HASH:0:12}..."