@daemux/store-automator 0.10.7 → 0.10.9

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.
package/src/prompt.mjs CHANGED
@@ -1,30 +1,79 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import { promptAppIdentity } from './prompts/app-identity.mjs';
3
+ import { promptCredentials } from './prompts/credentials.mjs';
4
+ import { promptStoreSettings, promptWebSettings } from './prompts/store-settings.mjs';
5
+ import { askQuestion } from './guide.mjs';
2
6
 
3
7
  function isInteractive() {
4
8
  return Boolean(process.stdin.isTTY);
5
9
  }
6
10
 
7
- function ask(rl, question) {
8
- return new Promise((resolve) => {
9
- rl.question(question, (answer) => {
10
- resolve(answer.trim());
11
- });
12
- });
11
+ function isNonInteractive() {
12
+ return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
13
13
  }
14
14
 
15
- function allTokensProvided(cliTokens) {
15
+ function allPromptsProvided(cliFlags) {
16
16
  return (
17
- cliTokens.stitchApiKey !== undefined &&
18
- cliTokens.cloudflareToken !== undefined &&
19
- cliTokens.cloudflareAccountId !== undefined
17
+ cliFlags.bundleId !== undefined &&
18
+ cliFlags.stitchApiKey !== undefined &&
19
+ cliFlags.cloudflareToken !== undefined &&
20
+ cliFlags.cloudflareAccountId !== undefined
20
21
  );
21
22
  }
22
23
 
23
- function allPromptsProvided(cliTokens) {
24
- return (
25
- cliTokens.bundleId !== undefined &&
26
- allTokensProvided(cliTokens)
24
+ async function promptMcpTokens(rl, cliFlags, currentConfig) {
25
+ const result = {
26
+ stitchApiKey: cliFlags.stitchApiKey ?? '',
27
+ cloudflareToken: cliFlags.cloudflareToken ?? '',
28
+ cloudflareAccountId: cliFlags.cloudflareAccountId ?? '',
29
+ };
30
+
31
+ const allProvided = (
32
+ cliFlags.stitchApiKey !== undefined &&
33
+ cliFlags.cloudflareToken !== undefined &&
34
+ cliFlags.cloudflareAccountId !== undefined
27
35
  );
36
+
37
+ if (allProvided) return result;
38
+
39
+ console.log('Press Enter to skip any token you do not have yet.');
40
+ console.log('');
41
+
42
+ if (cliFlags.stitchApiKey === undefined) {
43
+ result.stitchApiKey = await askQuestion(rl, 'Stitch MCP API Key', currentConfig.stitchApiKey || '');
44
+ }
45
+
46
+ if (cliFlags.cloudflareToken === undefined) {
47
+ result.cloudflareToken = await askQuestion(rl, 'Cloudflare API Token', currentConfig.cloudflareToken || '');
48
+ }
49
+
50
+ if (result.cloudflareToken && cliFlags.cloudflareAccountId === undefined) {
51
+ result.cloudflareAccountId = await askQuestion(
52
+ rl, 'Cloudflare Account ID', currentConfig.cloudflareAccountId || ''
53
+ );
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ export async function promptAll(rl, cliFlags, currentConfig, projectDir) {
60
+ console.log('');
61
+ console.log('\x1b[1m\x1b[36m=== Store Automator — Interactive Setup ===\x1b[0m');
62
+
63
+ const identity = await promptAppIdentity(rl, cliFlags, currentConfig);
64
+
65
+ const credentials = await promptCredentials(rl, cliFlags, currentConfig, projectDir);
66
+
67
+ const storeSettings = await promptStoreSettings(rl, cliFlags, currentConfig);
68
+
69
+ const webSettings = await promptWebSettings(rl, cliFlags, currentConfig);
70
+
71
+ console.log('');
72
+ console.log('\x1b[1m\x1b[36m=== MCP Tokens (Optional) ===\x1b[0m');
73
+ console.log('');
74
+ const mcpTokens = await promptMcpTokens(rl, cliFlags, currentConfig);
75
+
76
+ return { ...identity, ...credentials, ...storeSettings, ...webSettings, ...mcpTokens };
28
77
  }
29
78
 
30
79
  export async function promptForTokens(cliTokens = {}) {
@@ -40,61 +89,23 @@ export async function promptForTokens(cliTokens = {}) {
40
89
  return result;
41
90
  }
42
91
 
43
- if (!isInteractive()) {
92
+ if (!isInteractive() || isNonInteractive()) {
44
93
  console.log('Non-interactive terminal detected, skipping prompts.');
45
94
  console.log('Run "npx store-automator" manually to configure.');
46
95
  return result;
47
96
  }
48
97
 
49
- const rl = createInterface({
50
- input: process.stdin,
51
- output: process.stdout,
52
- });
53
-
54
- console.log('');
98
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
55
99
 
56
100
  try {
57
- if (cliTokens.bundleId === undefined) {
58
- console.log('App Configuration');
59
- console.log('');
60
- result.bundleId = await ask(
61
- rl,
62
- 'Bundle ID / Package Name (e.g., com.company.app): '
63
- );
64
- console.log('');
65
- }
66
-
67
- if (allTokensProvided(cliTokens)) {
68
- console.log('All MCP tokens provided via CLI flags.');
69
- return result;
70
- }
71
-
72
- console.log('MCP Server Configuration');
73
- console.log('Press Enter to skip any token you do not have yet.');
74
- console.log('');
75
-
76
- if (cliTokens.stitchApiKey === undefined) {
77
- result.stitchApiKey = await ask(
78
- rl,
79
- 'Stitch MCP API Key (STITCH_API_KEY value): '
80
- );
81
- }
82
-
83
- if (cliTokens.cloudflareToken === undefined) {
84
- result.cloudflareToken = await ask(
85
- rl,
86
- 'Cloudflare API Token: '
87
- );
88
- }
89
-
90
- if (result.cloudflareToken && cliTokens.cloudflareAccountId === undefined) {
91
- result.cloudflareAccountId = await ask(
92
- rl,
93
- 'Cloudflare Account ID: '
94
- );
95
- }
96
-
97
- return result;
101
+ const all = await promptAll(rl, cliTokens, {}, process.cwd());
102
+ return {
103
+ ...all,
104
+ bundleId: all.bundleId || result.bundleId,
105
+ stitchApiKey: all.stitchApiKey || result.stitchApiKey,
106
+ cloudflareToken: all.cloudflareToken || result.cloudflareToken,
107
+ cloudflareAccountId: all.cloudflareAccountId || result.cloudflareAccountId,
108
+ };
98
109
  } finally {
99
110
  rl.close();
100
111
  }
@@ -0,0 +1,66 @@
1
+ import { askQuestion, isPlaceholder } from '../guide.mjs';
2
+
3
+ const FIELDS = [
4
+ {
5
+ key: 'appName',
6
+ label: 'App Name (display name)',
7
+ configKey: 'app.name',
8
+ flag: 'appName',
9
+ },
10
+ {
11
+ key: 'bundleId',
12
+ label: 'Bundle ID (e.g. com.company.app)',
13
+ configKey: 'bundle_id',
14
+ flag: 'bundleId',
15
+ },
16
+ {
17
+ key: 'packageName',
18
+ label: 'Android Package Name',
19
+ configKey: 'android.package_name',
20
+ flag: 'packageName',
21
+ },
22
+ {
23
+ key: 'sku',
24
+ label: 'App Store Connect SKU',
25
+ configKey: 'ios.sku',
26
+ flag: 'sku',
27
+ },
28
+ {
29
+ key: 'appleId',
30
+ label: 'Apple Developer Account Email',
31
+ configKey: 'ios.apple_id',
32
+ flag: 'appleId',
33
+ },
34
+ ];
35
+
36
+ function getDefault(key, cliFlags, currentConfig) {
37
+ if (cliFlags[key] !== undefined) return cliFlags[key];
38
+ const configVal = currentConfig[key];
39
+ if (!isPlaceholder(configVal)) return configVal;
40
+ return '';
41
+ }
42
+
43
+ export async function promptAppIdentity(rl, cliFlags, currentConfig) {
44
+ console.log('');
45
+ console.log('\x1b[1m\x1b[36m=== App Identity ===\x1b[0m');
46
+ console.log('');
47
+
48
+ const result = {};
49
+
50
+ for (const field of FIELDS) {
51
+ const flagVal = cliFlags[field.flag];
52
+ if (flagVal !== undefined) {
53
+ result[field.key] = flagVal;
54
+ continue;
55
+ }
56
+
57
+ const defaultVal = getDefault(field.key, cliFlags, currentConfig);
58
+ result[field.key] = await askQuestion(rl, field.label, defaultVal);
59
+ }
60
+
61
+ if (!result.packageName && result.bundleId) {
62
+ result.packageName = result.bundleId;
63
+ }
64
+
65
+ return result;
66
+ }
@@ -0,0 +1,122 @@
1
+ import { join } from 'node:path';
2
+ import { runGuide, askQuestion, isPlaceholder } from '../guide.mjs';
3
+
4
+ function getDefault(key, cliFlags, currentConfig) {
5
+ if (cliFlags[key] !== undefined) return cliFlags[key];
6
+ const configVal = currentConfig[key];
7
+ if (!isPlaceholder(configVal)) return configVal;
8
+ return '';
9
+ }
10
+
11
+ async function promptAppStoreApiKey(rl, cliFlags, currentConfig, projectDir) {
12
+ await runGuide(rl, {
13
+ title: 'Create App Store Connect API Key',
14
+ steps: [
15
+ 'Log in to App Store Connect (appstoreconnect.apple.com)',
16
+ 'Go to Users and Access -> Integrations -> Team Keys',
17
+ 'Click + to generate a new key with "App Manager" role',
18
+ 'Download the .p8 file',
19
+ `Save to creds/AuthKey.p8 in your project`,
20
+ ],
21
+ verifyPath: join(projectDir, 'creds', 'AuthKey.p8'),
22
+ verifyDescription: 'App Store Connect API Key (.p8)',
23
+ confirmQuestion: 'Have you created and saved the API key?',
24
+ });
25
+
26
+ const keyId = await askQuestion(
27
+ rl,
28
+ 'App Store Connect Key ID',
29
+ getDefault('keyId', cliFlags, currentConfig)
30
+ );
31
+
32
+ const issuerId = await askQuestion(
33
+ rl,
34
+ 'App Store Connect Issuer ID',
35
+ getDefault('issuerId', cliFlags, currentConfig)
36
+ );
37
+
38
+ return { keyId, issuerId };
39
+ }
40
+
41
+ async function promptPlayServiceAccount(rl, projectDir) {
42
+ await runGuide(rl, {
43
+ title: 'Create Google Play Service Account',
44
+ steps: [
45
+ 'Go to Google Play Console -> Setup -> API access',
46
+ 'Create a service account or link existing one',
47
+ 'Grant "Release manager" permission to the service account',
48
+ 'Download the JSON key file',
49
+ 'Save to creds/play-service-account.json in your project',
50
+ ],
51
+ verifyPath: join(projectDir, 'creds', 'play-service-account.json'),
52
+ verifyDescription: 'Google Play Service Account JSON',
53
+ confirmQuestion: 'Have you set up the service account?',
54
+ });
55
+ }
56
+
57
+ async function promptAndroidKeystore(rl, cliFlags, currentConfig) {
58
+ await runGuide(rl, {
59
+ title: 'Set up Android Keystore',
60
+ steps: [
61
+ 'If you don\'t have a keystore, run:',
62
+ ' keytool -genkey -v -keystore creds/upload-keystore.jks \\',
63
+ ' -keyalg RSA -keysize 2048 -validity 10000',
64
+ 'Keep the keystore file safe — you cannot replace it once uploaded to Google Play',
65
+ ],
66
+ confirmQuestion: 'Have you created or located your Android keystore?',
67
+ });
68
+
69
+ const keystorePassword = await askQuestion(
70
+ rl,
71
+ 'Keystore password',
72
+ getDefault('keystorePassword', cliFlags, currentConfig)
73
+ );
74
+
75
+ return keystorePassword;
76
+ }
77
+
78
+ async function promptMatchSigning(rl, cliFlags, currentConfig) {
79
+ await runGuide(rl, {
80
+ title: 'Set up Match Code Signing',
81
+ steps: [
82
+ 'Create a PRIVATE Git repository for certificates (e.g. github.com/yourorg/certificates)',
83
+ 'Generate an SSH deploy key: ssh-keygen -t ed25519 -f creds/match_deploy_key',
84
+ 'Add the public key as a deploy key to the certificates repo (with write access)',
85
+ ],
86
+ confirmQuestion: 'Have you set up the certificates repository and deploy key?',
87
+ });
88
+
89
+ const matchDeployKeyPath = await askQuestion(
90
+ rl,
91
+ 'Path to Match deploy key',
92
+ getDefault('matchDeployKeyPath', cliFlags, currentConfig) || 'creds/match_deploy_key'
93
+ );
94
+
95
+ const matchGitUrl = await askQuestion(
96
+ rl,
97
+ 'Match certificates Git URL (SSH)',
98
+ getDefault('matchGitUrl', cliFlags, currentConfig)
99
+ );
100
+
101
+ return { matchDeployKeyPath, matchGitUrl };
102
+ }
103
+
104
+ export async function promptCredentials(rl, cliFlags, currentConfig, projectDir) {
105
+ console.log('');
106
+ console.log('\x1b[1m\x1b[36m=== Credentials Setup ===\x1b[0m');
107
+ console.log('');
108
+
109
+ const { keyId, issuerId } = await promptAppStoreApiKey(
110
+ rl, cliFlags, currentConfig, projectDir
111
+ );
112
+
113
+ await promptPlayServiceAccount(rl, projectDir);
114
+
115
+ const keystorePassword = await promptAndroidKeystore(rl, cliFlags, currentConfig);
116
+
117
+ const { matchDeployKeyPath, matchGitUrl } = await promptMatchSigning(
118
+ rl, cliFlags, currentConfig
119
+ );
120
+
121
+ return { keyId, issuerId, keystorePassword, matchDeployKeyPath, matchGitUrl };
122
+ }
@@ -0,0 +1,185 @@
1
+ import { runGuide, askQuestion, isPlaceholder } from '../guide.mjs';
2
+
3
+ function getDefault(key, cliFlags, currentConfig) {
4
+ if (cliFlags[key] !== undefined) return cliFlags[key];
5
+ const configVal = currentConfig[key];
6
+ if (!isPlaceholder(configVal)) return configVal;
7
+ return '';
8
+ }
9
+
10
+ function getDefaultWithFallback(key, cliFlags, currentConfig, fallback) {
11
+ const val = getDefault(key, cliFlags, currentConfig);
12
+ return val !== '' ? val : fallback;
13
+ }
14
+
15
+ async function promptIosSettings(rl, cliFlags, currentConfig) {
16
+ await runGuide(rl, {
17
+ title: 'Create App in App Store Connect',
18
+ steps: [
19
+ 'Go to App Store Connect -> My Apps -> +',
20
+ 'Select your Bundle ID',
21
+ 'Fill in app name and primary language',
22
+ 'Save the app',
23
+ ],
24
+ confirmQuestion: 'Have you created the app in App Store Connect?',
25
+ });
26
+
27
+ const primaryCategory = await askQuestion(
28
+ rl,
29
+ 'iOS Primary Category (e.g. GAMES, UTILITIES)',
30
+ getDefault('primaryCategory', cliFlags, currentConfig)
31
+ );
32
+
33
+ const secondaryCategory = await askQuestion(
34
+ rl,
35
+ 'iOS Secondary Category (optional)',
36
+ getDefault('secondaryCategory', cliFlags, currentConfig)
37
+ );
38
+
39
+ const priceTier = await askQuestion(
40
+ rl,
41
+ 'iOS Price Tier',
42
+ getDefaultWithFallback('priceTier', cliFlags, currentConfig, '0')
43
+ );
44
+
45
+ const submitForReview = await askQuestion(
46
+ rl,
47
+ 'Auto-submit for review? (true/false)',
48
+ getDefaultWithFallback('submitForReview', cliFlags, currentConfig, 'true')
49
+ );
50
+
51
+ const automaticRelease = await askQuestion(
52
+ rl,
53
+ 'Automatic release after approval? (true/false)',
54
+ getDefaultWithFallback('automaticRelease', cliFlags, currentConfig, 'true')
55
+ );
56
+
57
+ return {
58
+ primaryCategory,
59
+ secondaryCategory,
60
+ priceTier: Number(priceTier) || 0,
61
+ submitForReview: submitForReview === 'true',
62
+ automaticRelease: automaticRelease === 'true',
63
+ };
64
+ }
65
+
66
+ async function promptAndroidSettings(rl, cliFlags, currentConfig) {
67
+ await runGuide(rl, {
68
+ title: 'Create App in Google Play Console',
69
+ steps: [
70
+ 'Go to Google Play Console -> All apps -> Create app',
71
+ 'Enter app name and default language',
72
+ 'Select app type (App) and free/paid',
73
+ 'Complete the declarations and create',
74
+ ],
75
+ confirmQuestion: 'Have you created the app in Google Play Console?',
76
+ });
77
+
78
+ const track = await askQuestion(
79
+ rl,
80
+ 'Android release track (internal/alpha/beta/production)',
81
+ getDefaultWithFallback('track', cliFlags, currentConfig, 'internal')
82
+ );
83
+
84
+ const rolloutFraction = await askQuestion(
85
+ rl,
86
+ 'Rollout fraction (0.0 - 1.0)',
87
+ getDefaultWithFallback('rolloutFraction', cliFlags, currentConfig, '1.0')
88
+ );
89
+
90
+ const inAppUpdatePriority = await askQuestion(
91
+ rl,
92
+ 'In-app update priority (0-5)',
93
+ getDefaultWithFallback('inAppUpdatePriority', cliFlags, currentConfig, '3')
94
+ );
95
+
96
+ return {
97
+ track,
98
+ rolloutFraction: Number(rolloutFraction) || 1.0,
99
+ inAppUpdatePriority: Number(inAppUpdatePriority) || 3,
100
+ };
101
+ }
102
+
103
+ async function promptMetadataSettings(rl, cliFlags, currentConfig) {
104
+ const languages = await askQuestion(
105
+ rl,
106
+ 'Metadata languages (comma-separated, e.g. en-US,de-DE)',
107
+ getDefaultWithFallback('languages', cliFlags, currentConfig, 'en-US')
108
+ );
109
+
110
+ return { languages: languages.split(',').map((l) => l.trim()).filter(Boolean) };
111
+ }
112
+
113
+ export async function promptStoreSettings(rl, cliFlags, currentConfig) {
114
+ console.log('');
115
+ console.log('\x1b[1m\x1b[36m=== Store Settings ===\x1b[0m');
116
+ console.log('');
117
+
118
+ const ios = await promptIosSettings(rl, cliFlags, currentConfig);
119
+ const android = await promptAndroidSettings(rl, cliFlags, currentConfig);
120
+ const metadata = await promptMetadataSettings(rl, cliFlags, currentConfig);
121
+
122
+ return { ...ios, ...android, ...metadata };
123
+ }
124
+
125
+ export async function promptWebSettings(rl, cliFlags, currentConfig) {
126
+ console.log('');
127
+ console.log('\x1b[1m\x1b[36m=== Web Settings ===\x1b[0m');
128
+ console.log('');
129
+
130
+ const fields = [
131
+ { key: 'domain', label: 'Domain (e.g. example.com)' },
132
+ { key: 'cfProjectName', label: 'Cloudflare Pages project name' },
133
+ { key: 'tagline', label: 'App tagline' },
134
+ { key: 'primaryColor', label: 'Primary color (hex, e.g. #4A90D9)' },
135
+ { key: 'secondaryColor', label: 'Secondary color (hex)' },
136
+ { key: 'companyName', label: 'Company name' },
137
+ { key: 'contactEmail', label: 'Contact email' },
138
+ { key: 'supportEmail', label: 'Support email' },
139
+ { key: 'jurisdiction', label: 'Legal jurisdiction (e.g. Delaware, USA)' },
140
+ ];
141
+
142
+ const result = {};
143
+ for (const field of fields) {
144
+ result[field.key] = await askQuestion(
145
+ rl,
146
+ field.label,
147
+ getDefault(field.key, cliFlags, currentConfig)
148
+ );
149
+ }
150
+ return result;
151
+ }
152
+
153
+ export async function runPostInstallGuides(rl) {
154
+ await runGuide(rl, {
155
+ title: 'Create Private GitHub Repository',
156
+ steps: [
157
+ 'Go to github.com/new',
158
+ 'Create a PRIVATE repository',
159
+ 'Push your project to the repository',
160
+ ],
161
+ confirmQuestion: 'Have you created the GitHub repository?',
162
+ });
163
+
164
+ await runGuide(rl, {
165
+ title: 'Set GitHub Actions Secrets',
166
+ steps: [
167
+ 'Go to your repo -> Settings -> Secrets -> Actions',
168
+ 'Add secret MATCH_PASSWORD (your match encryption password)',
169
+ 'Add secret KEYSTORE_PASSWORD (your Android keystore password)',
170
+ 'Upload creds/ files as secrets if using CI',
171
+ ],
172
+ confirmQuestion: 'Have you configured GitHub Actions secrets?',
173
+ });
174
+
175
+ await runGuide(rl, {
176
+ title: 'Create Firebase Project (optional)',
177
+ steps: [
178
+ 'Go to console.firebase.google.com',
179
+ 'Create a new project or select existing',
180
+ 'Add your iOS and Android apps',
181
+ 'Download google-services.json and GoogleService-Info.plist',
182
+ ],
183
+ confirmQuestion: 'Have you set up Firebase? (skip if not needed)',
184
+ });
185
+ }
package/src/templates.mjs CHANGED
@@ -46,13 +46,18 @@ function copyIfMissing(srcPath, destPath, label, isDirectory) {
46
46
  }
47
47
  }
48
48
 
49
- export function installClaudeMd(targetPath, packageDir) {
49
+ export function installClaudeMd(targetPath, packageDir, appName) {
50
50
  const template = join(packageDir, 'templates', 'CLAUDE.md.template');
51
51
  if (!existsSync(template)) return;
52
52
  const action = existsSync(targetPath) ? 'Updating' : 'Installing';
53
53
  console.log(`${action} CLAUDE.md...`);
54
54
  ensureDir(join(targetPath, '..'));
55
- copyFileSync(template, targetPath);
55
+
56
+ let content = readFileSync(template, 'utf8');
57
+ if (appName) {
58
+ content = content.replace(/\{APP_NAME\}/g, appName);
59
+ }
60
+ writeFileSync(targetPath, content, 'utf8');
56
61
  }
57
62
 
58
63
  export function installCiTemplates(projectDir, packageDir) {
@@ -91,6 +91,7 @@ platform :ios do
91
91
  skip_binary_upload: true,
92
92
  skip_metadata: false,
93
93
  skip_screenshots: false,
94
+ overwrite_screenshots: true,
94
95
  run_precheck_before_submit: false,
95
96
  submit_for_review: false
96
97
  )
@@ -92,6 +92,22 @@ jobs:
92
92
  with:
93
93
  channel: stable
94
94
 
95
+ - name: Cache Flutter
96
+ uses: actions/cache@v4
97
+ with:
98
+ path: /Users/runner/.pub-cache
99
+ key: pub-cache-${{ hashFiles('app/pubspec.lock') }}
100
+ restore-keys: |
101
+ pub-cache-
102
+
103
+ - name: Cache CocoaPods
104
+ uses: actions/cache@v4
105
+ with:
106
+ path: app/ios/Pods
107
+ key: pods-${{ hashFiles('app/ios/Podfile.lock') }}
108
+ restore-keys: |
109
+ pods-
110
+
95
111
  - name: Install yq
96
112
  run: brew install yq
97
113
 
@@ -86,6 +86,29 @@ if [ "$READY" != "true" ]; then
86
86
  echo " 4. Upload at least one AAB manually for the first release"
87
87
  echo " 5. Grant the service account access to the app"
88
88
  echo ""
89
+ echo "::warning title=Google Play Not Ready::App setup incomplete. See job summary for missing steps."
90
+ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
91
+ cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY
92
+ ## :warning: Google Play Not Ready for Automation
93
+
94
+ The app **$PACKAGE_NAME** is not yet configured for automated publishing.
95
+
96
+ ### Missing steps:
97
+ $MISSING_STEPS
98
+
99
+ ### How to fix:
100
+
101
+ 1. Go to [Google Play Console](https://play.google.com/console)
102
+ 2. Ensure the app (\`$PACKAGE_NAME\`) has been manually created
103
+ 3. Complete the **Store listing** (description, graphics, screenshots)
104
+ 4. Complete the **Content rating** questionnaire
105
+ 5. Complete **Pricing & distribution** settings
106
+ 6. Upload at least one AAB manually for the first release
107
+ 7. Grant the service account access to the app under **Users & permissions**
108
+
109
+ > Once all steps are completed, re-run this workflow and Google Play readiness will pass.
110
+ SUMMARY
111
+ fi
89
112
  echo "Google Play is NOT ready. CI cannot proceed."
90
113
  exit 1
91
114
  else
@@ -15,12 +15,15 @@ if [ "${GOOGLE_PLAY_READY:-false}" != "true" ]; then
15
15
 
16
16
  AAB_DIR="$APP_ROOT/build/app/outputs/bundle/release"
17
17
  AAB_FILE=$(find "$AAB_DIR" -name "*.aab" -type f 2>/dev/null | head -1)
18
+ AAB_INFO=""
18
19
  if [ -n "$AAB_FILE" ]; then
20
+ AAB_INFO="Built AAB: $AAB_FILE ($(du -h "$AAB_FILE" | cut -f1))"
19
21
  echo ""
20
- echo "Built AAB: $AAB_FILE ($(du -h "$AAB_FILE" | cut -f1))"
22
+ echo "$AAB_INFO"
21
23
  else
24
+ AAB_INFO="No AAB found. Run build.sh first."
22
25
  echo ""
23
- echo "No AAB found. Run build.sh first."
26
+ echo "$AAB_INFO"
24
27
  fi
25
28
 
26
29
  echo ""
@@ -32,6 +35,28 @@ if [ "${GOOGLE_PLAY_READY:-false}" != "true" ]; then
32
35
  echo " 5. Complete the release form and roll out"
33
36
  echo ""
34
37
  echo "After the first manual upload, subsequent releases will be automated."
38
+ echo "::warning title=Manual AAB Upload Required::First release must be uploaded manually. See job summary for instructions."
39
+ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
40
+ cat >> "$GITHUB_STEP_SUMMARY" << SUMMARY
41
+ ## :warning: Manual AAB Upload Required - First Release
42
+
43
+ Google Play is not ready for automated binary uploads. This is expected for the first release.
44
+
45
+ **$AAB_INFO**
46
+
47
+ ### How to upload manually:
48
+
49
+ 1. Go to [Google Play Console](https://play.google.com/console)
50
+ 2. Select your app (\`$PACKAGE_NAME\`)
51
+ 3. Navigate to **Release > Testing > Internal testing** (or your target track)
52
+ 4. Click **Create new release**
53
+ 5. Upload the AAB file built by CI
54
+ 6. Fill in release notes
55
+ 7. Complete the release form and click **Start rollout**
56
+
57
+ > After the first manual upload, all subsequent binary uploads will be fully automated by CI.
58
+ SUMMARY
59
+ fi
35
60
  exit 1
36
61
  fi
37
62