@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,91 @@
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 IAP Sync ==="
9
+
10
+ # ── Step 1: Check if IAP config file exists ──
11
+ IAP_CONFIG="$PROJECT_ROOT/fastlane/iap_config.json"
12
+
13
+ if [ ! -f "$IAP_CONFIG" ]; then
14
+ echo "No IAP config found at $IAP_CONFIG. Skipping IAP sync."
15
+ exit 0
16
+ fi
17
+
18
+ # ── Step 2: Install Fastlane (needed to check plugin availability) ──
19
+ "$COMMON_DIR/install-fastlane.sh" ios
20
+
21
+ # ── Step 3: Check if IAP plugin is available ──
22
+ cd "$APP_ROOT/ios"
23
+
24
+ if ! bundle exec gem list fastlane-plugin-iap --installed >/dev/null 2>&1; then
25
+ echo "WARNING: fastlane-plugin-iap not installed (plugin not yet published)."
26
+ echo "Skipping IAP sync. This is expected until the IAP plugin is released."
27
+ echo "To install when available: add 'fastlane-plugin-iap' to $APP_ROOT/ios/Gemfile"
28
+ exit 0
29
+ fi
30
+
31
+ echo "fastlane-plugin-iap is installed. Proceeding with sync."
32
+
33
+ # ── Step 4: Hash-based change detection ──
34
+ CURRENT_HASH=$(shasum -a 256 "$IAP_CONFIG" | cut -d' ' -f1)
35
+
36
+ STATE_DIR="$PROJECT_ROOT/.ci-state"
37
+ mkdir -p "$STATE_DIR"
38
+ STATE_FILE="$STATE_DIR/ios-iap-hash"
39
+
40
+ if [ -f "$STATE_FILE" ]; then
41
+ STORED_HASH=$(cat "$STATE_FILE")
42
+ if [ "$CURRENT_HASH" = "$STORED_HASH" ]; then
43
+ echo "IAP config unchanged (hash: ${CURRENT_HASH:0:12}...). Skipping sync."
44
+ exit 0
45
+ fi
46
+ echo "IAP config changed (old: ${STORED_HASH:0:12}..., new: ${CURRENT_HASH:0:12}...)"
47
+ else
48
+ echo "No cached hash found. First run — will sync IAPs."
49
+ fi
50
+
51
+ # ── Step 5: Setup Fastlane symlink and build dir ──
52
+ export CM_BUILD_DIR="$PROJECT_ROOT"
53
+ "$COMMON_DIR/link-fastlane.sh" ios
54
+
55
+ # ── Step 6: Set up App Store Connect API key ──
56
+ P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
57
+ if [ ! -f "$P8_FULL_PATH" ]; then
58
+ echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
59
+ exit 1
60
+ fi
61
+
62
+ export APP_STORE_CONNECT_API_KEY_KEY_ID="$APPLE_KEY_ID"
63
+ export APP_STORE_CONNECT_API_KEY_ISSUER_ID="$APPLE_ISSUER_ID"
64
+ export APP_STORE_CONNECT_API_KEY_KEY="$(cat "$P8_FULL_PATH")"
65
+ export APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64="false"
66
+
67
+ echo "ASC API key configured (Key ID: $APPLE_KEY_ID)"
68
+
69
+ # ── Step 7: Run IAP sync ──
70
+ echo "Syncing IAPs to App Store Connect..."
71
+
72
+ cd "$APP_ROOT/ios"
73
+
74
+ set +e
75
+ FASTLANE_API_KEY_PATH="$P8_FULL_PATH" \
76
+ BUNDLE_ID="$BUNDLE_ID" \
77
+ bundle exec fastlane sync_iap
78
+ SYNC_EXIT=$?
79
+ set -e
80
+
81
+ # ── Step 8: Update hash on success ──
82
+ if [ $SYNC_EXIT -eq 0 ]; then
83
+ echo "$CURRENT_HASH" > "$STATE_FILE"
84
+ echo "IAP sync successful. Hash cached: ${CURRENT_HASH:0:12}..."
85
+ else
86
+ echo "ERROR: IAP sync failed (exit code: $SYNC_EXIT)" >&2
87
+ echo "Hash NOT cached — next run will retry."
88
+ exit $SYNC_EXIT
89
+ fi
90
+
91
+ echo "=== iOS IAP Sync Complete ==="
@@ -0,0 +1,44 @@
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 "$BUNDLE_ID" ]; then
9
+ echo "ERROR: BUNDLE_ID not set in ci.config.yaml" >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [ -z "$P8_KEY_PATH" ] || [ -z "$APPLE_KEY_ID" ] || [ -z "$APPLE_ISSUER_ID" ]; then
14
+ echo "ERROR: Apple credentials not configured in ci.config.yaml" >&2
15
+ exit 1
16
+ fi
17
+
18
+ P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
19
+ if [ ! -f "$P8_FULL_PATH" ]; then
20
+ echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
21
+ exit 1
22
+ fi
23
+
24
+ # --- Setup Fastlane ---
25
+ export CM_BUILD_DIR="$PROJECT_ROOT"
26
+ "$SCRIPT_DIR/../common/link-fastlane.sh" ios
27
+ "$SCRIPT_DIR/../common/install-fastlane.sh" ios
28
+
29
+ # --- Set up Fastlane environment ---
30
+ export FASTLANE_API_KEY_PATH="$P8_FULL_PATH"
31
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
32
+ export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
33
+ export APP_STORE_CONNECT_PRIVATE_KEY
34
+ APP_STORE_CONNECT_PRIVATE_KEY=$(cat "$P8_FULL_PATH")
35
+
36
+ echo "ASC API key configured (Key ID: $APPLE_KEY_ID)"
37
+
38
+ # --- Upload via Fastlane ---
39
+ echo "Uploading IPA to App Store Connect..."
40
+ cd "$APP_ROOT/ios"
41
+
42
+ bundle exec fastlane upload_binary_ios
43
+
44
+ echo "iOS binary upload complete"
@@ -0,0 +1,92 @@
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 Metadata & Screenshots Upload ==="
9
+
10
+ # --- Check preconditions ---
11
+ METADATA_DIR="$PROJECT_ROOT/fastlane/metadata"
12
+ SCREENSHOTS_DIR="$PROJECT_ROOT/fastlane/screenshots/ios"
13
+
14
+ if [ ! -d "$METADATA_DIR" ] && [ ! -d "$SCREENSHOTS_DIR" ]; then
15
+ echo "No metadata or screenshots directories found. Skipping upload."
16
+ exit 0
17
+ fi
18
+
19
+ HASH_DIRS=()
20
+ for dir in "$METADATA_DIR" "$SCREENSHOTS_DIR"; do
21
+ [ -d "$dir" ] && HASH_DIRS+=("$dir") && echo " Found: $dir"
22
+ done
23
+
24
+ # --- Hash-based change detection ---
25
+ HASH=$(
26
+ find "${HASH_DIRS[@]}" -type f ! -name '.DS_Store' -print0 \
27
+ | LC_ALL=C sort -z \
28
+ | xargs -0 shasum -a 256 2>/dev/null \
29
+ | shasum -a 256 \
30
+ | cut -d' ' -f1
31
+ )
32
+
33
+ STATE_DIR="$PROJECT_ROOT/.ci-state"
34
+ mkdir -p "$STATE_DIR"
35
+ STATE_FILE="$STATE_DIR/ios-metadata-hash"
36
+
37
+ if [ -f "$STATE_FILE" ]; then
38
+ STORED_HASH=$(cat "$STATE_FILE")
39
+ if [ "$HASH" = "$STORED_HASH" ]; then
40
+ echo "Metadata and screenshots unchanged (hash: ${HASH:0:12}...). Skipping upload."
41
+ exit 0
42
+ fi
43
+ echo "Changes detected (old: ${STORED_HASH:0:12}..., new: ${HASH:0:12}...)"
44
+ else
45
+ echo "No cached hash found. First run — will upload metadata."
46
+ fi
47
+
48
+ # --- Validate Apple credentials ---
49
+ if [ -z "$P8_KEY_PATH" ] || [ -z "$APPLE_KEY_ID" ] || [ -z "$APPLE_ISSUER_ID" ]; then
50
+ echo "ERROR: Apple credentials not configured in ci.config.yaml" >&2
51
+ exit 1
52
+ fi
53
+
54
+ P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
55
+ if [ ! -f "$P8_FULL_PATH" ]; then
56
+ echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
57
+ exit 1
58
+ fi
59
+
60
+ # --- Set up Fastlane environment ---
61
+ export FASTLANE_API_KEY_PATH="$P8_FULL_PATH"
62
+ export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
63
+ export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
64
+ export CM_BUILD_DIR="$PROJECT_ROOT"
65
+ export FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS=1
66
+
67
+ echo "ASC API key configured (Key ID: $APPLE_KEY_ID)"
68
+
69
+ # --- Link and install Fastlane ---
70
+ "$COMMON_DIR/link-fastlane.sh" ios
71
+ "$COMMON_DIR/install-fastlane.sh" ios
72
+
73
+ # --- Run upload ---
74
+ echo "Uploading iOS metadata and screenshots..."
75
+ cd "$APP_ROOT/ios"
76
+
77
+ set +e
78
+ bundle exec fastlane upload_metadata_ios
79
+ UPLOAD_EXIT=$?
80
+ set -e
81
+
82
+ # --- Handle result ---
83
+ if [ $UPLOAD_EXIT -eq 0 ]; then
84
+ echo "$HASH" > "$STATE_FILE"
85
+ echo "iOS metadata uploaded successfully. Hash cached: ${HASH:0:12}..."
86
+ else
87
+ echo "ERROR: iOS metadata upload failed (exit code: $UPLOAD_EXIT)" >&2
88
+ echo "Hash NOT cached — next run will retry."
89
+ exit $UPLOAD_EXIT
90
+ fi
91
+
92
+ echo "=== iOS Metadata & Screenshots Upload Complete ==="
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Update Google Play data safety section from a CSV file.
4
+
5
+ Uses the Google Play Android Developer API v3 via the
6
+ google-api-python-client library with service account authentication.
7
+
8
+ IMPORTANT: The Google Play Developer API has LIMITED support for
9
+ programmatic data safety form updates. The API supports editing the
10
+ data safety form only through the App Content API (appEdits).
11
+ If the API does not support the operation, this script logs the
12
+ limitation and exits gracefully.
13
+
14
+ CSV format:
15
+ question_id,response
16
+ DATA_COLLECTED_PERSONAL_INFO,true
17
+ DATA_SHARED_PERSONAL_INFO,false
18
+ ...
19
+
20
+ Environment variables:
21
+ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH - path to service account JSON
22
+ PACKAGE_NAME - Android package name (e.g., com.firstclass.gigachat)
23
+ """
24
+
25
+ import csv
26
+ import json
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+
31
+
32
+ def ensure_dependencies():
33
+ """Install required Python packages if not already present."""
34
+ import subprocess
35
+ subprocess.run(
36
+ ["pip3", "install", "google-api-python-client", "google-auth"],
37
+ stdout=subprocess.DEVNULL,
38
+ stderr=subprocess.DEVNULL,
39
+ )
40
+
41
+
42
+ def load_service_account_credentials(sa_json_path: str):
43
+ """Load Google service account credentials."""
44
+ try:
45
+ from google.oauth2 import service_account
46
+ from googleapiclient.discovery import build
47
+
48
+ credentials = service_account.Credentials.from_service_account_file(
49
+ sa_json_path,
50
+ scopes=["https://www.googleapis.com/auth/androidpublisher"],
51
+ )
52
+ service = build("androidpublisher", "v3", credentials=credentials)
53
+ return service
54
+ except ImportError:
55
+ print(
56
+ "ERROR: google-api-python-client or google-auth not installed.",
57
+ file=sys.stderr,
58
+ )
59
+ print("Install with: pip3 install google-api-python-client google-auth", file=sys.stderr)
60
+ sys.exit(1)
61
+ except Exception as e:
62
+ print(f"ERROR: Failed to load service account: {e}", file=sys.stderr)
63
+ sys.exit(1)
64
+
65
+
66
+ def parse_data_safety_csv(csv_path: str) -> dict:
67
+ """Parse the data safety CSV file into a dictionary."""
68
+ data = {}
69
+ with open(csv_path, "r", encoding="utf-8") as f:
70
+ reader = csv.DictReader(f)
71
+ for row in reader:
72
+ question_id = row.get("question_id", "").strip()
73
+ response = row.get("response", "").strip()
74
+ if question_id:
75
+ data[question_id] = response
76
+ return data
77
+
78
+
79
+ def _cleanup_edit_with_warning(service, package_name: str, edit_id: str, warning: str):
80
+ """Print a warning and delete an unused edit."""
81
+ print(f"WARNING: {warning}", file=sys.stderr)
82
+ print("Data safety forms must be updated manually via Google Play Console.", file=sys.stderr)
83
+ service.edits().delete(packageName=package_name, editId=edit_id).execute()
84
+
85
+
86
+ def _apply_data_safety_edit(service, package_name: str, edit_id: str, data_safety_responses: dict) -> bool:
87
+ """Apply data safety responses within an existing edit. Returns True on success."""
88
+ try:
89
+ current = (
90
+ service.edits()
91
+ .dataSafety()
92
+ .get(packageName=package_name, editId=edit_id)
93
+ .execute()
94
+ )
95
+ print(f"Current data safety state retrieved: {json.dumps(current, indent=2)}")
96
+ update_body = current.copy()
97
+ for question_id, response in data_safety_responses.items():
98
+ update_body[question_id] = response
99
+
100
+ service.edits().dataSafety().update(
101
+ packageName=package_name,
102
+ editId=edit_id,
103
+ body=update_body,
104
+ ).execute()
105
+ print("Data safety form updated")
106
+
107
+ service.edits().commit(
108
+ packageName=package_name, editId=edit_id
109
+ ).execute()
110
+ print(f"Edit {edit_id} committed successfully")
111
+ return True
112
+
113
+ except AttributeError:
114
+ _cleanup_edit_with_warning(
115
+ service, package_name, edit_id,
116
+ "The dataSafety API endpoint is not available in the current google-api-python-client version.",
117
+ )
118
+ return False
119
+
120
+ except Exception as e:
121
+ error_str = str(e)
122
+ if "404" in error_str or "not found" in error_str.lower():
123
+ _cleanup_edit_with_warning(
124
+ service, package_name, edit_id,
125
+ "The dataSafety endpoint returned 404. This API may not be available for this app yet.",
126
+ )
127
+ return False
128
+ raise
129
+
130
+
131
+ def update_data_safety(
132
+ service, package_name: str, data_safety_responses: dict
133
+ ) -> bool:
134
+ """
135
+ Attempt to update the data safety form via the Google Play API.
136
+
137
+ Creates an edit, delegates the data safety update to _apply_data_safety_edit,
138
+ and handles top-level failures.
139
+
140
+ Returns True on success, False if the API does not support the operation.
141
+ """
142
+ try:
143
+ edit_request = service.edits().insert(packageName=package_name, body={})
144
+ edit_response = edit_request.execute()
145
+ edit_id = edit_response["id"]
146
+ print(f"Created edit: {edit_id}")
147
+
148
+ return _apply_data_safety_edit(service, package_name, edit_id, data_safety_responses)
149
+
150
+ except Exception as e:
151
+ print(f"ERROR: Failed to update data safety: {e}", file=sys.stderr)
152
+ return False
153
+
154
+
155
+ def validate_inputs():
156
+ """Validate environment variables and CLI arguments. Returns (sa_json_path, package_name, csv_path)."""
157
+ sa_json_path = os.environ.get("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH", "")
158
+ package_name = os.environ.get("PACKAGE_NAME", "")
159
+
160
+ if not sa_json_path:
161
+ print("ERROR: GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH not set", file=sys.stderr)
162
+ sys.exit(1)
163
+
164
+ if not package_name:
165
+ print("ERROR: PACKAGE_NAME not set", file=sys.stderr)
166
+ sys.exit(1)
167
+
168
+ if len(sys.argv) < 2:
169
+ print(f"Usage: {sys.argv[0]} <data_safety.csv>", file=sys.stderr)
170
+ sys.exit(1)
171
+
172
+ csv_path = sys.argv[1]
173
+ if not Path(csv_path).exists():
174
+ print(f"ERROR: CSV file not found: {csv_path}", file=sys.stderr)
175
+ sys.exit(1)
176
+
177
+ if not Path(sa_json_path).exists():
178
+ print(f"ERROR: Service account JSON not found: {sa_json_path}", file=sys.stderr)
179
+ sys.exit(1)
180
+
181
+ return sa_json_path, package_name, csv_path
182
+
183
+
184
+ def main():
185
+ # --- Ensure dependencies are installed ---
186
+ ensure_dependencies()
187
+
188
+ # --- Validate inputs ---
189
+ sa_json_path, package_name, csv_path = validate_inputs()
190
+
191
+ # --- Parse CSV ---
192
+ data_safety_responses = parse_data_safety_csv(csv_path)
193
+ print(f"Parsed {len(data_safety_responses)} data safety responses from {csv_path}")
194
+
195
+ if not data_safety_responses:
196
+ print("WARNING: No data safety responses found in CSV. Nothing to update.")
197
+ sys.exit(0)
198
+
199
+ # --- Connect to Google Play API ---
200
+ service = load_service_account_credentials(sa_json_path)
201
+
202
+ # --- Attempt update ---
203
+ success = update_data_safety(service, package_name, data_safety_responses)
204
+
205
+ if success:
206
+ print("Data safety update completed successfully")
207
+ result = {"status": "updated", "responses_count": len(data_safety_responses)}
208
+ else:
209
+ print("Data safety update was not applied (API limitation)")
210
+ print("Please update the data safety form manually at:")
211
+ print(f" https://play.google.com/console/developers/app/{package_name}/app-content/data-safety")
212
+ result = {"status": "manual_required", "responses_count": len(data_safety_responses)}
213
+
214
+ print(json.dumps(result))
215
+ # Exit 0 even on API limitation -- this is graceful degradation
216
+ sys.exit(0)
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()