@daemux/store-automator 0.6.0 → 0.7.1
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/.claude-plugin/marketplace.json +2 -2
- package/README.md +6 -2
- package/bin/cli.mjs +9 -3
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/plugins/store-automator/agents/devops.md +8 -8
- package/src/ci-config.mjs +27 -4
- package/src/codemagic-api.mjs +4 -2
- package/src/codemagic-setup.mjs +30 -3
- package/src/install.mjs +16 -5
- package/src/mcp-setup.mjs +11 -2
- package/src/prompt.mjs +41 -8
- package/templates/ci.config.yaml.template +1 -0
- package/templates/codemagic.template.yaml +329 -111
- package/templates/fastlane/android/Fastfile.template +55 -7
- package/templates/fastlane/android/Pluginfile.template +4 -1
- package/templates/fastlane/ios/Fastfile.template +72 -17
- package/templates/fastlane/ios/Pluginfile.template +4 -1
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.7.1"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.7.1",
|
|
16
16
|
"keywords": ["flutter", "app-store", "google-play", "fastlane", "codemagic"]
|
|
17
17
|
}
|
|
18
18
|
]
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ npm install @daemux/store-automator
|
|
|
27
27
|
|
|
28
28
|
The postinstall script will:
|
|
29
29
|
|
|
30
|
-
1. Prompt for MCP server tokens (Stitch, Cloudflare, Codemagic)
|
|
30
|
+
1. Prompt for bundle ID and MCP server tokens (Stitch, Cloudflare, Codemagic)
|
|
31
31
|
2. Configure `.mcp.json` with MCP servers (Playwright, mobile-mcp, Stitch, Cloudflare, Codemagic)
|
|
32
32
|
3. Install the plugin marketplace and register agents
|
|
33
33
|
4. Copy `CLAUDE.md` template to `.claude/CLAUDE.md`
|
|
@@ -122,13 +122,14 @@ For CI/CD environments or scripted setups, pass tokens as CLI flags to skip inte
|
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
npx @daemux/store-automator \
|
|
125
|
+
--bundle-id=com.company.app \
|
|
125
126
|
--codemagic-token=YOUR_CM_TOKEN \
|
|
126
127
|
--stitch-key=YOUR_STITCH_KEY \
|
|
127
128
|
--cloudflare-token=YOUR_CF_TOKEN \
|
|
128
129
|
--cloudflare-account-id=YOUR_CF_ACCOUNT_ID
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
Any tokens provided via flags will skip the corresponding interactive prompt. If all four tokens are provided, the entire interactive session is skipped.
|
|
132
|
+
Any tokens provided via flags will skip the corresponding interactive prompt. If all four tokens are provided, the entire interactive session is skipped. The bundle ID, if provided, is automatically written to `bundle_id` and `package_name` in `ci.config.yaml`.
|
|
132
133
|
|
|
133
134
|
## CLI Options
|
|
134
135
|
|
|
@@ -142,6 +143,9 @@ Options:
|
|
|
142
143
|
-v, --version Show version number
|
|
143
144
|
-h, --help Show help
|
|
144
145
|
|
|
146
|
+
App Configuration:
|
|
147
|
+
--bundle-id=ID Bundle ID / Package Name (e.g., com.company.app)
|
|
148
|
+
|
|
145
149
|
MCP Token Flags (skip interactive prompts):
|
|
146
150
|
--codemagic-token=TOKEN Codemagic API token
|
|
147
151
|
--stitch-key=KEY Stitch MCP API key
|
package/bin/cli.mjs
CHANGED
|
@@ -27,11 +27,13 @@ function flagValue(arg, prefix) {
|
|
|
27
27
|
return arg.startsWith(prefix) ? arg.slice(prefix.length) : undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const valueFlags = {
|
|
31
31
|
'--codemagic-token=': 'codemagicToken',
|
|
32
|
+
'--codemagic-team-id=': 'codemagicTeamId',
|
|
32
33
|
'--stitch-key=': 'stitchApiKey',
|
|
33
34
|
'--cloudflare-token=': 'cloudflareToken',
|
|
34
35
|
'--cloudflare-account-id=': 'cloudflareAccountId',
|
|
36
|
+
'--bundle-id=': 'bundleId',
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
for (const arg of args) {
|
|
@@ -41,7 +43,7 @@ for (const arg of args) {
|
|
|
41
43
|
if ((v = flagValue(arg, '--workflow=')) !== undefined) { cmWorkflowId = v; continue; }
|
|
42
44
|
|
|
43
45
|
let matched = false;
|
|
44
|
-
for (const [prefix, key] of Object.entries(
|
|
46
|
+
for (const [prefix, key] of Object.entries(valueFlags)) {
|
|
45
47
|
if ((v = flagValue(arg, prefix)) !== undefined) {
|
|
46
48
|
cliTokens[key] = v;
|
|
47
49
|
matched = true;
|
|
@@ -85,8 +87,12 @@ Options:
|
|
|
85
87
|
-v, --version Show version number
|
|
86
88
|
-h, --help Show help
|
|
87
89
|
|
|
90
|
+
App Configuration:
|
|
91
|
+
--bundle-id=ID Bundle ID / Package Name (e.g., com.company.app)
|
|
92
|
+
|
|
88
93
|
MCP Token Flags (skip interactive prompts):
|
|
89
94
|
--codemagic-token=TOKEN Codemagic API token
|
|
95
|
+
--codemagic-team-id=ID Codemagic Team ID (from Teams page)
|
|
90
96
|
--stitch-key=KEY Stitch MCP API key
|
|
91
97
|
--cloudflare-token=TOKEN Cloudflare API token
|
|
92
98
|
--cloudflare-account-id=ID Cloudflare account ID
|
|
@@ -113,7 +119,7 @@ Examples:
|
|
|
113
119
|
npx @daemux/store-automator --github-setup Configure GitHub Actions
|
|
114
120
|
|
|
115
121
|
Non-interactive install (CI/CD):
|
|
116
|
-
npx @daemux/store-automator --codemagic-token=TOKEN --stitch-key=KEY
|
|
122
|
+
npx @daemux/store-automator --bundle-id=com.company.app --codemagic-token=TOKEN --codemagic-team-id=ID --stitch-key=KEY
|
|
117
123
|
npx @daemux/store-automator --cloudflare-token=TOKEN --cloudflare-account-id=ID`);
|
|
118
124
|
process.exit(0);
|
|
119
125
|
break; // eslint: no-fallthrough
|
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ The Codemagic pipeline runs two parallel workflows on push to `main`:
|
|
|
44
44
|
|
|
45
45
|
### Token Configuration
|
|
46
46
|
|
|
47
|
-
The Codemagic API token
|
|
47
|
+
The Codemagic API token and Team ID are auto-configured via the codemagic MCP server in `.mcp.json` (set during install). You do not need to resolve the token manually -- all MCP tool calls authenticate automatically. The `CODEMAGIC_TEAM_ID` env var enables default team resolution for team-scoped tools (`list_builds`, `get_team`, `list_team_members`, `create_variable_group`, `setup_asc_credentials`, `setup_code_signing`). If the codemagic MCP server is missing from `.mcp.json`, instruct the user to re-run `npx @daemux/store-automator` or add `--codemagic-token=TOKEN --codemagic-team-id=ID` to configure it.
|
|
48
48
|
|
|
49
49
|
### Triggering Builds
|
|
50
50
|
|
|
@@ -76,20 +76,20 @@ Build states: `queued` -> `preparing` -> `building` -> `testing` -> `publishing`
|
|
|
76
76
|
| `start_build` | Start a new build |
|
|
77
77
|
| `get_build` | Get details of a specific build |
|
|
78
78
|
| `cancel_build` | Cancel a running build |
|
|
79
|
-
| `list_builds` | List builds for a team (V3 API) |
|
|
79
|
+
| `list_builds` | List builds for a team (V3 API, uses default team if omitted) |
|
|
80
80
|
| `get_artifact_url` | Get the download URL for a build artifact |
|
|
81
81
|
| `create_public_artifact_url` | Create a time-limited public URL for an artifact |
|
|
82
82
|
| `list_caches` | List build caches for an application |
|
|
83
83
|
| `delete_caches` | Delete build caches for an application |
|
|
84
|
-
| `setup_asc_credentials` | Create variable group with
|
|
85
|
-
| `setup_code_signing` | Create variable group with iOS
|
|
84
|
+
| `setup_asc_credentials` | Create variable group with ASC credentials (uses default team if omitted) |
|
|
85
|
+
| `setup_code_signing` | Create variable group with iOS signing (uses default team if omitted) |
|
|
86
86
|
| `get_user` | Get the current authenticated user info |
|
|
87
87
|
| `list_teams` | List all teams the user belongs to |
|
|
88
|
-
| `get_team` | Get details of a specific team |
|
|
89
|
-
| `list_team_members` | List members of a
|
|
88
|
+
| `get_team` | Get details of a specific team (uses default team if omitted) |
|
|
89
|
+
| `list_team_members` | List members of a team (uses default team if omitted) |
|
|
90
90
|
| `list_variable_groups` | List variable groups for a team or application |
|
|
91
91
|
| `get_variable_group` | Get details of a specific variable group |
|
|
92
|
-
| `create_variable_group` | Create a new variable group |
|
|
92
|
+
| `create_variable_group` | Create a new variable group (uses default team if omitted) |
|
|
93
93
|
| `update_variable_group` | Update a variable group name or security setting |
|
|
94
94
|
| `delete_variable_group` | Delete a variable group |
|
|
95
95
|
| `list_variables` | List variables in a variable group |
|
|
@@ -366,7 +366,7 @@ Analyze Firestore usage patterns and optimize queries, indexes, and security rul
|
|
|
366
366
|
|
|
367
367
|
| File | Purpose |
|
|
368
368
|
|------|---------|
|
|
369
|
-
| `ci.config.yaml` | Single source of truth for all CI/CD config |
|
|
369
|
+
| `ci.config.yaml` | Single source of truth for all CI/CD config (includes team_id, app_id) |
|
|
370
370
|
| `codemagic.yaml` | Generated from template -- do not edit directly |
|
|
371
371
|
| `templates/codemagic.template.yaml` | Codemagic workflow template |
|
|
372
372
|
| `scripts/generate.sh` | Generates codemagic.yaml from ci.config.yaml |
|
package/src/ci-config.mjs
CHANGED
|
@@ -2,17 +2,24 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
const CI_CONFIG_FILE = 'ci.config.yaml';
|
|
5
|
-
const
|
|
5
|
+
const FIELD_PATTERNS = {
|
|
6
|
+
app_id: /^(\s*app_id:\s*)"[^"]*"/m,
|
|
7
|
+
team_id: /^(\s*team_id:\s*)"[^"]*"/m,
|
|
8
|
+
bundle_id: /^(\s*bundle_id:\s*)"[^"]*"/m,
|
|
9
|
+
package_name: /^(\s*package_name:\s*)"[^"]*"/m,
|
|
10
|
+
};
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
function writeCiField(projectDir, field, value) {
|
|
8
13
|
const configPath = join(projectDir, CI_CONFIG_FILE);
|
|
9
14
|
if (!existsSync(configPath)) return false;
|
|
10
15
|
|
|
11
16
|
try {
|
|
17
|
+
const pattern = FIELD_PATTERNS[field];
|
|
12
18
|
const content = readFileSync(configPath, 'utf8');
|
|
13
|
-
if (!
|
|
19
|
+
if (!pattern.test(content)) return false;
|
|
14
20
|
|
|
15
|
-
const
|
|
21
|
+
const safeValue = value.replace(/\$/g, '$$$$');
|
|
22
|
+
const updated = content.replace(pattern, `$1"${safeValue}"`);
|
|
16
23
|
if (updated === content) return false;
|
|
17
24
|
|
|
18
25
|
writeFileSync(configPath, updated, 'utf8');
|
|
@@ -21,3 +28,19 @@ export function writeCiAppId(projectDir, appId) {
|
|
|
21
28
|
return false;
|
|
22
29
|
}
|
|
23
30
|
}
|
|
31
|
+
|
|
32
|
+
export function writeCiAppId(projectDir, appId) {
|
|
33
|
+
return writeCiField(projectDir, 'app_id', appId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeCiTeamId(projectDir, teamId) {
|
|
37
|
+
return writeCiField(projectDir, 'team_id', teamId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function writeCiBundleId(projectDir, bundleId) {
|
|
41
|
+
return writeCiField(projectDir, 'bundle_id', bundleId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeCiPackageName(projectDir, packageName) {
|
|
45
|
+
return writeCiField(projectDir, 'package_name', packageName);
|
|
46
|
+
}
|
package/src/codemagic-api.mjs
CHANGED
|
@@ -50,8 +50,10 @@ export async function findAppByRepo(token, repoUrl) {
|
|
|
50
50
|
}) || null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export async function addApp(token, repoUrl) {
|
|
54
|
-
|
|
53
|
+
export async function addApp(token, repoUrl, teamId) {
|
|
54
|
+
const body = { repositoryUrl: repoUrl };
|
|
55
|
+
if (teamId) body.teamId = teamId;
|
|
56
|
+
return cmFetch(token, 'POST', '/apps', body);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export async function startBuild(token, appId, workflowId, branch) {
|
package/src/codemagic-setup.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { findAppByRepo, addApp, startBuild, getBuildStatus, normalizeRepoUrl } from './codemagic-api.mjs';
|
|
2
2
|
import { exec, resolveToken } from './utils.mjs';
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { writeCiAppId, writeCiTeamId } from './ci-config.mjs';
|
|
7
|
+
import { updateMcpAppId, updateMcpTeamId } from './mcp-setup.mjs';
|
|
6
8
|
|
|
7
9
|
const POLL_INTERVAL_MS = 30_000;
|
|
8
10
|
const POLL_TIMEOUT_MS = 15 * 60 * 1000;
|
|
@@ -94,6 +96,24 @@ async function pollBuildStatus(token, buildId) {
|
|
|
94
96
|
return 'timeout';
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
function resolveCliTeamId() {
|
|
100
|
+
const prefix = '--codemagic-team-id=';
|
|
101
|
+
for (const arg of process.argv) {
|
|
102
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readCiTeamId() {
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(join(process.cwd(), 'ci.config.yaml'), 'utf8');
|
|
110
|
+
const match = content.match(/^\s*team_id:\s*"([^"]+)"/m);
|
|
111
|
+
return match ? match[1] : undefined;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
export async function runCodemagicSetup(options) {
|
|
98
118
|
const {
|
|
99
119
|
tokenArg = '',
|
|
@@ -105,8 +125,10 @@ export async function runCodemagicSetup(options) {
|
|
|
105
125
|
|
|
106
126
|
const token = resolveToken(tokenArg);
|
|
107
127
|
const repoUrl = resolveRepoUrl();
|
|
128
|
+
const teamId = resolveCliTeamId() || readCiTeamId();
|
|
108
129
|
|
|
109
130
|
console.log(`Repository: ${repoUrl}`);
|
|
131
|
+
if (teamId) console.log(`Team ID: ${teamId}`);
|
|
110
132
|
console.log('Checking Codemagic for existing app...');
|
|
111
133
|
|
|
112
134
|
let app = await findAppByRepo(token, repoUrl);
|
|
@@ -115,7 +137,7 @@ export async function runCodemagicSetup(options) {
|
|
|
115
137
|
console.log(`App already registered: ${app.appName || app._id}`);
|
|
116
138
|
} else {
|
|
117
139
|
console.log('App not found. Adding to Codemagic...');
|
|
118
|
-
app = await addApp(token, repoUrl);
|
|
140
|
+
app = await addApp(token, repoUrl, teamId);
|
|
119
141
|
console.log(`App added: ${app.appName || app._id}`);
|
|
120
142
|
|
|
121
143
|
console.log('Setting up GitHub webhook...');
|
|
@@ -126,6 +148,11 @@ export async function runCodemagicSetup(options) {
|
|
|
126
148
|
const appIdWritten = writeCiAppId(process.cwd(), appId);
|
|
127
149
|
updateMcpAppId(process.cwd(), appId);
|
|
128
150
|
|
|
151
|
+
if (teamId) {
|
|
152
|
+
writeCiTeamId(process.cwd(), teamId);
|
|
153
|
+
updateMcpTeamId(process.cwd(), teamId);
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
if (!trigger) {
|
|
130
157
|
console.log('\nSetup complete. Use --trigger to start a build.\n');
|
|
131
158
|
if (appIdWritten) {
|
package/src/install.mjs
CHANGED
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
} from './utils.mjs';
|
|
10
10
|
import { injectEnvVars, injectStatusLine } from './settings.mjs';
|
|
11
11
|
import { promptForTokens } from './prompt.mjs';
|
|
12
|
-
import { getMcpServers, writeMcpJson, updateMcpAppId } from './mcp-setup.mjs';
|
|
12
|
+
import { getMcpServers, writeMcpJson, updateMcpAppId, updateMcpTeamId } from './mcp-setup.mjs';
|
|
13
13
|
import { installClaudeMd, installCiTemplates, installFirebaseTemplates } from './templates.mjs';
|
|
14
14
|
import { findAppByRepo, addApp, normalizeRepoUrl } from './codemagic-api.mjs';
|
|
15
|
-
import { writeCiAppId } from './ci-config.mjs';
|
|
15
|
+
import { writeCiAppId, writeCiTeamId, writeCiBundleId, writeCiPackageName } from './ci-config.mjs';
|
|
16
16
|
|
|
17
17
|
function checkClaudeCli() {
|
|
18
18
|
const result = exec('command -v claude') || exec('which claude');
|
|
@@ -112,7 +112,7 @@ function setupGitHubActions(codemagicToken) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
async function setupCodemagicApp(projectDir, codemagicToken) {
|
|
115
|
+
async function setupCodemagicApp(projectDir, codemagicToken, codemagicTeamId) {
|
|
116
116
|
if (!codemagicToken) return;
|
|
117
117
|
|
|
118
118
|
let repoUrl;
|
|
@@ -130,7 +130,7 @@ async function setupCodemagicApp(projectDir, codemagicToken) {
|
|
|
130
130
|
try {
|
|
131
131
|
let app = await findAppByRepo(codemagicToken, repoUrl);
|
|
132
132
|
if (!app) {
|
|
133
|
-
app = await addApp(codemagicToken, repoUrl);
|
|
133
|
+
app = await addApp(codemagicToken, repoUrl, codemagicTeamId);
|
|
134
134
|
console.log(`Codemagic app created: ${app.appName || app._id}`);
|
|
135
135
|
} else {
|
|
136
136
|
console.log(`Codemagic app found: ${app.appName || app._id}`);
|
|
@@ -142,6 +142,11 @@ async function setupCodemagicApp(projectDir, codemagicToken) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
updateMcpAppId(projectDir, app._id);
|
|
145
|
+
|
|
146
|
+
if (codemagicTeamId) {
|
|
147
|
+
writeCiTeamId(projectDir, codemagicTeamId);
|
|
148
|
+
updateMcpTeamId(projectDir, codemagicTeamId);
|
|
149
|
+
}
|
|
145
150
|
} catch (err) {
|
|
146
151
|
console.log(`Codemagic auto-setup skipped: ${err.message || err}`);
|
|
147
152
|
}
|
|
@@ -178,7 +183,13 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
|
|
|
178
183
|
installCiTemplates(projectDir, packageDir);
|
|
179
184
|
installFirebaseTemplates(projectDir, packageDir);
|
|
180
185
|
|
|
181
|
-
|
|
186
|
+
if (tokens.bundleId) {
|
|
187
|
+
const written = writeCiBundleId(projectDir, tokens.bundleId);
|
|
188
|
+
if (written) console.log(`Bundle ID set in ci.config.yaml: ${tokens.bundleId}`);
|
|
189
|
+
writeCiPackageName(projectDir, tokens.bundleId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await setupCodemagicApp(projectDir, tokens.codemagicToken, tokens.codemagicTeamId);
|
|
182
193
|
|
|
183
194
|
const scopeLabel = scope === 'user' ? 'global' : 'project';
|
|
184
195
|
console.log(`Configuring ${scopeLabel} settings...`);
|
package/src/mcp-setup.mjs
CHANGED
|
@@ -34,6 +34,7 @@ export function getMcpServers(tokens) {
|
|
|
34
34
|
|
|
35
35
|
if (tokens.codemagicToken) {
|
|
36
36
|
const codemagicEnv = { CODEMAGIC_API_TOKEN: tokens.codemagicToken };
|
|
37
|
+
if (tokens.codemagicTeamId) codemagicEnv.CODEMAGIC_TEAM_ID = tokens.codemagicTeamId;
|
|
37
38
|
if (tokens.codemagicAppId) codemagicEnv.CODEMAGIC_APP_ID = tokens.codemagicAppId;
|
|
38
39
|
servers.codemagic = {
|
|
39
40
|
command: 'npx',
|
|
@@ -77,14 +78,14 @@ export function writeMcpJson(projectDir, servers) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
function updateMcpEnvVar(projectDir, envKey, value) {
|
|
81
82
|
const mcpPath = join(projectDir, '.mcp.json');
|
|
82
83
|
if (!existsSync(mcpPath)) return false;
|
|
83
84
|
|
|
84
85
|
try {
|
|
85
86
|
const data = readJson(mcpPath);
|
|
86
87
|
if (!data.mcpServers?.codemagic?.env) return false;
|
|
87
|
-
data.mcpServers.codemagic.env
|
|
88
|
+
data.mcpServers.codemagic.env[envKey] = value;
|
|
88
89
|
writeJson(mcpPath, data);
|
|
89
90
|
return true;
|
|
90
91
|
} catch {
|
|
@@ -92,6 +93,14 @@ export function updateMcpAppId(projectDir, appId) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
export function updateMcpAppId(projectDir, appId) {
|
|
97
|
+
return updateMcpEnvVar(projectDir, 'CODEMAGIC_APP_ID', appId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function updateMcpTeamId(projectDir, teamId) {
|
|
101
|
+
return updateMcpEnvVar(projectDir, 'CODEMAGIC_TEAM_ID', teamId);
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
export function removeMcpServers(projectDir) {
|
|
96
105
|
const mcpPath = join(projectDir, '.mcp.json');
|
|
97
106
|
if (!existsSync(mcpPath)) return;
|
package/src/prompt.mjs
CHANGED
|
@@ -17,26 +17,36 @@ function allTokensProvided(cliTokens) {
|
|
|
17
17
|
cliTokens.stitchApiKey !== undefined &&
|
|
18
18
|
cliTokens.cloudflareToken !== undefined &&
|
|
19
19
|
cliTokens.cloudflareAccountId !== undefined &&
|
|
20
|
-
cliTokens.codemagicToken !== undefined
|
|
20
|
+
cliTokens.codemagicToken !== undefined &&
|
|
21
|
+
cliTokens.codemagicTeamId !== undefined
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function allPromptsProvided(cliTokens) {
|
|
26
|
+
return (
|
|
27
|
+
cliTokens.bundleId !== undefined &&
|
|
28
|
+
allTokensProvided(cliTokens)
|
|
21
29
|
);
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export async function promptForTokens(cliTokens = {}) {
|
|
25
33
|
const result = {
|
|
34
|
+
bundleId: cliTokens.bundleId ?? '',
|
|
26
35
|
stitchApiKey: cliTokens.stitchApiKey ?? '',
|
|
27
36
|
cloudflareToken: cliTokens.cloudflareToken ?? '',
|
|
28
37
|
cloudflareAccountId: cliTokens.cloudflareAccountId ?? '',
|
|
29
38
|
codemagicToken: cliTokens.codemagicToken ?? '',
|
|
39
|
+
codemagicTeamId: cliTokens.codemagicTeamId ?? '',
|
|
30
40
|
};
|
|
31
41
|
|
|
32
|
-
if (
|
|
33
|
-
console.log('All
|
|
42
|
+
if (allPromptsProvided(cliTokens)) {
|
|
43
|
+
console.log('All configuration provided via CLI flags, skipping prompts.');
|
|
34
44
|
return result;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
if (!isInteractive()) {
|
|
38
|
-
console.log('Non-interactive terminal detected, skipping
|
|
39
|
-
console.log('Run "npx store-automator" manually to configure
|
|
48
|
+
console.log('Non-interactive terminal detected, skipping prompts.');
|
|
49
|
+
console.log('Run "npx store-automator" manually to configure.');
|
|
40
50
|
return result;
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -45,12 +55,28 @@ export async function promptForTokens(cliTokens = {}) {
|
|
|
45
55
|
output: process.stdout,
|
|
46
56
|
});
|
|
47
57
|
|
|
48
|
-
console.log('');
|
|
49
|
-
console.log('MCP Server Configuration');
|
|
50
|
-
console.log('Press Enter to skip any token you do not have yet.');
|
|
51
58
|
console.log('');
|
|
52
59
|
|
|
53
60
|
try {
|
|
61
|
+
if (cliTokens.bundleId === undefined) {
|
|
62
|
+
console.log('App Configuration');
|
|
63
|
+
console.log('');
|
|
64
|
+
result.bundleId = await ask(
|
|
65
|
+
rl,
|
|
66
|
+
'Bundle ID / Package Name (e.g., com.company.app): '
|
|
67
|
+
);
|
|
68
|
+
console.log('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (allTokensProvided(cliTokens)) {
|
|
72
|
+
console.log('All MCP tokens provided via CLI flags.');
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('MCP Server Configuration');
|
|
77
|
+
console.log('Press Enter to skip any token you do not have yet.');
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
54
80
|
if (cliTokens.stitchApiKey === undefined) {
|
|
55
81
|
result.stitchApiKey = await ask(
|
|
56
82
|
rl,
|
|
@@ -79,6 +105,13 @@ export async function promptForTokens(cliTokens = {}) {
|
|
|
79
105
|
);
|
|
80
106
|
}
|
|
81
107
|
|
|
108
|
+
if (result.codemagicToken && cliTokens.codemagicTeamId === undefined) {
|
|
109
|
+
result.codemagicTeamId = await ask(
|
|
110
|
+
rl,
|
|
111
|
+
'Codemagic Team ID (optional, from Teams page): '
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
return result;
|
|
83
116
|
} finally {
|
|
84
117
|
rl.close();
|
|
@@ -59,6 +59,7 @@ web:
|
|
|
59
59
|
# Find app_id in your Codemagic dashboard URL: codemagic.io/app/{app_id}
|
|
60
60
|
# API token is stored in .mcp.json (codemagic MCP server, set during install)
|
|
61
61
|
codemagic:
|
|
62
|
+
team_id: "" # Team ID from Codemagic Teams page
|
|
62
63
|
app_id: ""
|
|
63
64
|
workflows:
|
|
64
65
|
- ios-release
|
|
@@ -19,14 +19,12 @@ workflows:
|
|
|
19
19
|
PRICE_TIER: "${PRICE_TIER}"
|
|
20
20
|
SUBMIT_FOR_REVIEW: "${SUBMIT_FOR_REVIEW}"
|
|
21
21
|
AUTOMATIC_RELEASE: "${AUTOMATIC_RELEASE}"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
bundle_identifier: "${BUNDLE_ID}"
|
|
22
|
+
FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS: "1"
|
|
23
|
+
APP_ROOT: "${APP_ROOT}"
|
|
25
24
|
cache:
|
|
26
25
|
cache_paths:
|
|
27
|
-
- $HOME/.codemagic_keys
|
|
28
26
|
- $HOME/.gem
|
|
29
|
-
- ios/vendor/bundle
|
|
27
|
+
- ${APP_ROOT}/ios/vendor/bundle
|
|
30
28
|
triggering:
|
|
31
29
|
events:
|
|
32
30
|
- push
|
|
@@ -34,15 +32,20 @@ workflows:
|
|
|
34
32
|
- pattern: main
|
|
35
33
|
include: true
|
|
36
34
|
scripts:
|
|
35
|
+
- name: Link Fastlane directories
|
|
36
|
+
script: |
|
|
37
|
+
ln -sfn "$CM_BUILD_DIR/fastlane/ios" "$CM_BUILD_DIR/$APP_ROOT/ios/fastlane"
|
|
38
|
+
ln -sfn "$CM_BUILD_DIR/fastlane" "$CM_BUILD_DIR/$APP_ROOT/fastlane"
|
|
39
|
+
|
|
37
40
|
- name: Ensure CERTIFICATE_PRIVATE_KEY
|
|
38
41
|
script: |
|
|
39
|
-
KEY_FILE="$
|
|
40
|
-
mkdir -p "$HOME/.codemagic_keys"
|
|
42
|
+
KEY_FILE="$CM_BUILD_DIR/creds/ios_dist_private_key"
|
|
41
43
|
if [ -f "$KEY_FILE" ]; then
|
|
42
|
-
echo "
|
|
44
|
+
echo "Using CERTIFICATE_PRIVATE_KEY from repo creds/"
|
|
43
45
|
else
|
|
44
|
-
echo "
|
|
45
|
-
ssh-keygen -t rsa -b 2048 -m PEM -f
|
|
46
|
+
echo "ERROR: creds/ios_dist_private_key not found in repo."
|
|
47
|
+
echo "Generate it with: ssh-keygen -t rsa -b 2048 -m PEM -f creds/ios_dist_private_key -q -N ''"
|
|
48
|
+
exit 1
|
|
46
49
|
fi
|
|
47
50
|
echo "CERTIFICATE_PRIVATE_KEY<<DELIMITER" >> $CM_ENV
|
|
48
51
|
cat "$KEY_FILE" >> $CM_ENV
|
|
@@ -57,17 +60,141 @@ workflows:
|
|
|
57
60
|
cat "$CM_BUILD_DIR/$P8_KEY_PATH" >> $CM_ENV
|
|
58
61
|
echo "" >> $CM_ENV
|
|
59
62
|
echo "KEYDELIMITER" >> $CM_ENV
|
|
63
|
+
# Write P8 key to temp file once for all Fastlane steps
|
|
64
|
+
P8_TMP="/tmp/fastlane_api_key.p8"
|
|
65
|
+
cat "$CM_BUILD_DIR/$P8_KEY_PATH" > "$P8_TMP"
|
|
66
|
+
echo "FASTLANE_API_KEY_PATH=$P8_TMP" >> $CM_ENV
|
|
67
|
+
|
|
68
|
+
- name: Install Fastlane
|
|
69
|
+
script: |
|
|
70
|
+
cd $CM_BUILD_DIR/$APP_ROOT/ios
|
|
71
|
+
gem install bundler
|
|
72
|
+
bundle install
|
|
73
|
+
|
|
74
|
+
- name: Ensure app record exists
|
|
75
|
+
script: |
|
|
76
|
+
echo "=== Checking if $BUNDLE_ID exists in App Store Connect ==="
|
|
77
|
+
|
|
78
|
+
# First try: list apps filtering by bundle ID
|
|
79
|
+
echo "--- Running: app-store-connect apps list --bundle-id-identifier $BUNDLE_ID ---"
|
|
80
|
+
APP_JSON=$(app-store-connect apps list \
|
|
81
|
+
--bundle-id-identifier "$BUNDLE_ID" \
|
|
82
|
+
--strict-match-identifier \
|
|
83
|
+
--json 2>/tmp/asc_apps_err.log || true)
|
|
84
|
+
|
|
85
|
+
echo "Apps list stderr:"
|
|
86
|
+
cat /tmp/asc_apps_err.log 2>/dev/null || true
|
|
87
|
+
echo "Apps list stdout (first 500 chars):"
|
|
88
|
+
echo "$APP_JSON" | head -c 500
|
|
89
|
+
|
|
90
|
+
EXISTING=$(echo "$APP_JSON" | python3 -c "import sys,json; data=json.load(sys.stdin); apps=data if isinstance(data,list) else []; print('yes' if len(apps)>0 else 'no')" 2>/dev/null || echo "no")
|
|
91
|
+
|
|
92
|
+
echo "App exists result: $EXISTING"
|
|
93
|
+
|
|
94
|
+
if [ "$EXISTING" = "yes" ]; then
|
|
95
|
+
echo "App record found for $BUNDLE_ID"
|
|
96
|
+
else
|
|
97
|
+
echo ""
|
|
98
|
+
echo "=========================================="
|
|
99
|
+
echo "BUILD STOPPED - APP RECORD NOT DETECTED"
|
|
100
|
+
echo "=========================================="
|
|
101
|
+
echo ""
|
|
102
|
+
echo "The app-store-connect CLI could not find $BUNDLE_ID."
|
|
103
|
+
echo "If you already created the app in App Store Connect,"
|
|
104
|
+
echo "this may be an API propagation delay or permission issue."
|
|
105
|
+
echo ""
|
|
106
|
+
echo "Retrying with verbose output..."
|
|
107
|
+
app-store-connect apps list --json 2>&1 | python3 -c "import sys,json;data=json.load(sys.stdin);apps=data if isinstance(data,list) else [];print(f'Total apps: {len(apps)}');[print(f' App: {a.get(\"attributes\",{}).get(\"name\",\"?\")} | Bundle: {a.get(\"attributes\",{}).get(\"bundleId\",\"?\")} | ID: {a.get(\"id\",\"?\")}') for a in apps[:10]]" 2>&1 || true
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
- name: Upload iOS metadata and screenshots
|
|
112
|
+
script: |
|
|
113
|
+
cd $CM_BUILD_DIR/$APP_ROOT/ios
|
|
114
|
+
bundle exec fastlane upload_metadata_ios
|
|
115
|
+
|
|
116
|
+
- name: Sync IAP and subscriptions
|
|
117
|
+
script: |
|
|
118
|
+
FORCE_FLAG=""
|
|
119
|
+
if [ ! -f "$CM_BUILD_DIR/.codemagic/ios_iap_synced" ]; then
|
|
120
|
+
FORCE_FLAG="--force"
|
|
121
|
+
echo "First IAP sync detected - forcing upload"
|
|
122
|
+
fi
|
|
123
|
+
if ./scripts/check_changed.sh $FORCE_FLAG fastlane/iap_config.json; then
|
|
124
|
+
cd $CM_BUILD_DIR/$APP_ROOT/ios
|
|
125
|
+
bundle exec fastlane sync_iap
|
|
126
|
+
# Create marker after successful sync
|
|
127
|
+
if [ -n "$FORCE_FLAG" ]; then
|
|
128
|
+
mkdir -p "$CM_BUILD_DIR/.codemagic"
|
|
129
|
+
touch "$CM_BUILD_DIR/.codemagic/ios_iap_synced"
|
|
130
|
+
cd "$CM_BUILD_DIR"
|
|
131
|
+
git add .codemagic/ios_iap_synced
|
|
132
|
+
git commit -m "chore: mark iOS IAP sync complete [skip ci]" || true
|
|
133
|
+
git push origin HEAD || true
|
|
134
|
+
echo "iOS IAP sync marker committed"
|
|
135
|
+
fi
|
|
136
|
+
else
|
|
137
|
+
echo "IAP config unchanged - skipping"
|
|
138
|
+
fi
|
|
60
139
|
|
|
61
140
|
- name: Set up iOS code signing
|
|
62
141
|
script: |
|
|
63
|
-
app-store-connect fetch-signing-files "$BUNDLE_ID" \
|
|
64
|
-
--type IOS_APP_STORE --create
|
|
65
142
|
keychain initialize
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
143
|
+
|
|
144
|
+
# Attempt to fetch or create signing files.
|
|
145
|
+
if app-store-connect fetch-signing-files "$BUNDLE_ID" \
|
|
146
|
+
--type IOS_APP_STORE \
|
|
147
|
+
--certificate-key=@env:CERTIFICATE_PRIVATE_KEY \
|
|
148
|
+
--create 2>/tmp/signing_err.log; then
|
|
149
|
+
echo "Signing files fetched successfully"
|
|
150
|
+
else
|
|
151
|
+
cat /tmp/signing_err.log
|
|
152
|
+
echo ""
|
|
153
|
+
echo "Signing failed. Deleting ALL distribution certs..."
|
|
154
|
+
|
|
155
|
+
for CERT_TYPE in IOS_DISTRIBUTION DISTRIBUTION; do
|
|
156
|
+
CERT_IDS=$(app-store-connect certificates list \
|
|
157
|
+
--type "$CERT_TYPE" --json 2>/dev/null \
|
|
158
|
+
| python3 -c "
|
|
159
|
+
import sys, json
|
|
160
|
+
certs = json.load(sys.stdin)
|
|
161
|
+
for c in certs:
|
|
162
|
+
print(c.get('id', ''))
|
|
163
|
+
" 2>/dev/null || true)
|
|
164
|
+
|
|
165
|
+
for CID in $CERT_IDS; do
|
|
166
|
+
if [ -n "$CID" ]; then
|
|
167
|
+
echo "Deleting $CERT_TYPE cert: $CID"
|
|
168
|
+
app-store-connect certificates delete "$CID" || true
|
|
169
|
+
fi
|
|
170
|
+
done
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
echo "Waiting 15s for Apple API propagation..."
|
|
174
|
+
sleep 15
|
|
175
|
+
|
|
176
|
+
# Verify certs are gone
|
|
177
|
+
REMAINING=$(app-store-connect certificates list \
|
|
178
|
+
--type DISTRIBUTION --json 2>/dev/null \
|
|
179
|
+
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
180
|
+
echo "Remaining DISTRIBUTION certs: $REMAINING"
|
|
181
|
+
|
|
182
|
+
echo "Retrying fetch-signing-files..."
|
|
183
|
+
app-store-connect fetch-signing-files "$BUNDLE_ID" \
|
|
184
|
+
--type IOS_APP_STORE \
|
|
185
|
+
--certificate-key=@env:CERTIFICATE_PRIVATE_KEY \
|
|
186
|
+
--create
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Verify signing artifacts exist
|
|
190
|
+
P12_COUNT=$(ls /Users/builder/Library/MobileDevice/Certificates/*.p12 2>/dev/null | wc -l)
|
|
191
|
+
if [ "$P12_COUNT" -eq 0 ]; then
|
|
192
|
+
echo "ERROR: No .p12 certificates found after signing setup"
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
69
196
|
keychain add-certificates
|
|
70
|
-
xcode-project use-profiles
|
|
197
|
+
xcode-project use-profiles --project $APP_ROOT/ios/Runner.xcodeproj
|
|
71
198
|
|
|
72
199
|
- name: Manage iOS version
|
|
73
200
|
script: |
|
|
@@ -84,50 +211,29 @@ workflows:
|
|
|
84
211
|
- name: Set Flutter version
|
|
85
212
|
script: |
|
|
86
213
|
BUILD_NUMBER=$(($(app-store-connect get-latest-app-store-build-number "$BUNDLE_ID" 2>/dev/null || echo "0") + 1))
|
|
87
|
-
if [ -z "$APP_VERSION" ]
|
|
214
|
+
if [ -z "$APP_VERSION" ]; then
|
|
88
215
|
APP_VERSION="1.0.0"
|
|
89
216
|
fi
|
|
90
|
-
|
|
217
|
+
# Normalize to semver (1.0 -> 1.0.0, 1 -> 1.0.0)
|
|
218
|
+
PARTS=$(echo "$APP_VERSION" | tr '.' '\n' | wc -l | tr -d ' ')
|
|
219
|
+
if [ "$PARTS" -eq 1 ]; then APP_VERSION="${APP_VERSION}.0.0"; fi
|
|
220
|
+
if [ "$PARTS" -eq 2 ]; then APP_VERSION="${APP_VERSION}.0"; fi
|
|
221
|
+
sed -i '' "s/^version:.*/version: ${APP_VERSION}+${BUILD_NUMBER}/" $APP_ROOT/pubspec.yaml
|
|
91
222
|
echo "Building: $APP_VERSION+$BUILD_NUMBER"
|
|
92
223
|
|
|
93
224
|
- name: Flutter packages
|
|
94
|
-
script: flutter pub get
|
|
225
|
+
script: cd $CM_BUILD_DIR/$APP_ROOT && flutter pub get
|
|
95
226
|
|
|
96
227
|
- name: Build iOS
|
|
97
|
-
script: flutter build ipa --release --export-options-plist=/
|
|
98
|
-
|
|
99
|
-
- name: Install Fastlane
|
|
100
|
-
script: |
|
|
101
|
-
cd ios
|
|
102
|
-
gem install bundler
|
|
103
|
-
bundle install
|
|
104
|
-
|
|
105
|
-
- name: Create app record (idempotent)
|
|
106
|
-
script: |
|
|
107
|
-
cd ios
|
|
108
|
-
bundle exec fastlane produce create \
|
|
109
|
-
-u "$APPLE_ID" \
|
|
110
|
-
-a "$BUNDLE_ID" \
|
|
111
|
-
--app_name "$APP_NAME" \
|
|
112
|
-
--sku "$SKU" \
|
|
113
|
-
|| true
|
|
114
|
-
|
|
115
|
-
- name: Deploy to App Store
|
|
116
|
-
script: |
|
|
117
|
-
cd ios
|
|
118
|
-
bundle exec fastlane deploy_ios
|
|
228
|
+
script: cd $CM_BUILD_DIR/$APP_ROOT && flutter build ipa --release --export-options-plist=/Users/builder/export_options.plist
|
|
119
229
|
|
|
120
|
-
- name:
|
|
230
|
+
- name: Upload IPA to App Store
|
|
121
231
|
script: |
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
bundle exec fastlane sync_iap
|
|
125
|
-
else
|
|
126
|
-
echo "IAP config unchanged - skipping"
|
|
127
|
-
fi
|
|
232
|
+
cd $CM_BUILD_DIR/$APP_ROOT/ios
|
|
233
|
+
bundle exec fastlane upload_binary_ios
|
|
128
234
|
|
|
129
235
|
artifacts:
|
|
130
|
-
- build/ios/ipa/*.ipa
|
|
236
|
+
- ${APP_ROOT}/build/ios/ipa/*.ipa
|
|
131
237
|
- /tmp/xcodebuild_logs/*.log
|
|
132
238
|
|
|
133
239
|
android-release:
|
|
@@ -136,6 +242,9 @@ workflows:
|
|
|
136
242
|
instance_type: mac_mini_m4
|
|
137
243
|
environment:
|
|
138
244
|
flutter: stable
|
|
245
|
+
groups:
|
|
246
|
+
- google_play_credentials
|
|
247
|
+
- android_keystore
|
|
139
248
|
vars:
|
|
140
249
|
PACKAGE_NAME: "${PACKAGE_NAME}"
|
|
141
250
|
APP_NAME: "${APP_NAME}"
|
|
@@ -148,11 +257,12 @@ workflows:
|
|
|
148
257
|
APPLE_KEY_ID: "${APPLE_KEY_ID}"
|
|
149
258
|
APPLE_ISSUER_ID: "${APPLE_ISSUER_ID}"
|
|
150
259
|
P8_KEY_PATH: "${P8_KEY_PATH}"
|
|
260
|
+
APP_ROOT: "${APP_ROOT}"
|
|
151
261
|
cache:
|
|
152
262
|
cache_paths:
|
|
153
263
|
- $HOME/.gem
|
|
154
264
|
- $HOME/.gradle/caches
|
|
155
|
-
- android/vendor/bundle
|
|
265
|
+
- ${APP_ROOT}/android/vendor/bundle
|
|
156
266
|
triggering:
|
|
157
267
|
events:
|
|
158
268
|
- push
|
|
@@ -160,11 +270,21 @@ workflows:
|
|
|
160
270
|
- pattern: main
|
|
161
271
|
include: true
|
|
162
272
|
scripts:
|
|
273
|
+
- name: Link Fastlane directories
|
|
274
|
+
script: |
|
|
275
|
+
ln -sfn "$CM_BUILD_DIR/fastlane/android" "$CM_BUILD_DIR/$APP_ROOT/android/fastlane"
|
|
276
|
+
ln -sfn "$CM_BUILD_DIR/fastlane" "$CM_BUILD_DIR/$APP_ROOT/fastlane"
|
|
277
|
+
|
|
163
278
|
- name: Ensure Android upload keystore
|
|
164
279
|
script: |
|
|
165
|
-
KEYSTORE_PATH="$CM_BUILD_DIR/android/upload.keystore"
|
|
280
|
+
KEYSTORE_PATH="$CM_BUILD_DIR/$APP_ROOT/android/upload.keystore"
|
|
281
|
+
CREDS_PATH="$CM_BUILD_DIR/creds/android_upload.keystore"
|
|
282
|
+
GENERATED=false
|
|
166
283
|
if [ -f "$KEYSTORE_PATH" ]; then
|
|
167
284
|
echo "Using existing upload keystore from repo"
|
|
285
|
+
elif [ -f "$CREDS_PATH" ]; then
|
|
286
|
+
echo "Restoring upload keystore from creds/"
|
|
287
|
+
cp "$CREDS_PATH" "$KEYSTORE_PATH"
|
|
168
288
|
else
|
|
169
289
|
echo "Generating new upload keystore..."
|
|
170
290
|
keytool -genkey -v \
|
|
@@ -177,8 +297,18 @@ workflows:
|
|
|
177
297
|
-storepass "$KEYSTORE_PASSWORD" \
|
|
178
298
|
-keypass "$KEYSTORE_PASSWORD" \
|
|
179
299
|
-dname "CN=Upload Key, O=Developer, C=US"
|
|
300
|
+
GENERATED=true
|
|
301
|
+
fi
|
|
302
|
+
# Always ensure creds/ backup exists
|
|
303
|
+
mkdir -p "$CM_BUILD_DIR/creds"
|
|
304
|
+
if [ ! -f "$CREDS_PATH" ] || ! cmp -s "$KEYSTORE_PATH" "$CREDS_PATH"; then
|
|
305
|
+
cp "$KEYSTORE_PATH" "$CREDS_PATH"
|
|
306
|
+
echo "Keystore backed up to creds/android_upload.keystore"
|
|
307
|
+
fi
|
|
308
|
+
# Commit keystore files if newly generated
|
|
309
|
+
if [ "$GENERATED" = "true" ]; then
|
|
180
310
|
cd "$CM_BUILD_DIR"
|
|
181
|
-
git add android/upload.keystore
|
|
311
|
+
git add --force $APP_ROOT/android/upload.keystore creds/android_upload.keystore
|
|
182
312
|
git commit -m "chore: add Android upload keystore [skip ci]"
|
|
183
313
|
git push origin HEAD
|
|
184
314
|
echo "Upload keystore generated and committed to repo"
|
|
@@ -194,27 +324,124 @@ workflows:
|
|
|
194
324
|
pip3 install PyJWT cryptography requests
|
|
195
325
|
export SA_JSON="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
196
326
|
export PACKAGE_NAME="$PACKAGE_NAME"
|
|
197
|
-
RESULT=$(python3 scripts/check_google_play.py)
|
|
198
|
-
READY=$(echo "$RESULT" | python3 -c "import sys,json; print(str(json.load(sys.stdin)
|
|
327
|
+
RESULT=$(python3 scripts/check_google_play.py 2>/dev/null || echo '{"ready":false}')
|
|
328
|
+
READY=$(echo "$RESULT" | python3 -c "import sys,json; print(str(json.load(sys.stdin).get('ready',False)).lower())" 2>/dev/null || echo "false")
|
|
199
329
|
if [ "$READY" != "true" ]; then
|
|
200
330
|
echo "GOOGLE_PLAY_READY=false" >> $CM_ENV
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
331
|
+
printf '%s\n' \
|
|
332
|
+
"# Google Play Setup - Manual Steps Required" \
|
|
333
|
+
"" \
|
|
334
|
+
"Your Android AAB is available in the build artifacts above." \
|
|
335
|
+
"" \
|
|
336
|
+
"## Steps to complete" \
|
|
337
|
+
"" \
|
|
338
|
+
"1. **Go to Google Play Console** - https://play.google.com/console" \
|
|
339
|
+
"2. **Create your app** (if not already created) with the correct package name" \
|
|
340
|
+
"3. **Upload the AAB** from build artifacts to an internal testing track" \
|
|
341
|
+
"4. **Complete the Store Listing** - Add title, descriptions, screenshots, and app icon" \
|
|
342
|
+
"5. **Complete the Content Rating** questionnaire" \
|
|
343
|
+
"6. **Set up Pricing and Distribution**" \
|
|
344
|
+
"7. **Complete the Data Safety** form" \
|
|
345
|
+
"8. **Review and roll out** the internal testing release" \
|
|
346
|
+
"" \
|
|
347
|
+
"## After completing all steps" \
|
|
348
|
+
"Just git push again - Codemagic will publish automatically on subsequent builds." \
|
|
349
|
+
> "$CM_BUILD_DIR/HOW_TO_GOOGLE_PLAY.md"
|
|
212
350
|
echo "Google Play not ready - see HOW_TO_GOOGLE_PLAY.md in artifacts"
|
|
213
351
|
else
|
|
214
352
|
echo "GOOGLE_PLAY_READY=true" >> $CM_ENV
|
|
353
|
+
# Detect first store sync: if marker file doesn't exist, force IAP/data safety upload
|
|
354
|
+
if [ ! -f "$CM_BUILD_DIR/.codemagic/android_store_synced" ]; then
|
|
355
|
+
echo "FIRST_STORE_SYNC=true" >> $CM_ENV
|
|
356
|
+
echo "First store sync detected - IAP and data safety will be force-uploaded"
|
|
357
|
+
else
|
|
358
|
+
echo "FIRST_STORE_SYNC=false" >> $CM_ENV
|
|
359
|
+
echo "Marker found - using change detection for IAP/data safety"
|
|
360
|
+
fi
|
|
215
361
|
echo "Google Play ready for automated publishing"
|
|
216
362
|
fi
|
|
217
363
|
|
|
364
|
+
- name: Install Fastlane
|
|
365
|
+
script: |
|
|
366
|
+
cd $CM_BUILD_DIR/$APP_ROOT/android
|
|
367
|
+
gem install bundler
|
|
368
|
+
bundle install
|
|
369
|
+
|
|
370
|
+
- name: Upload Android metadata and screenshots
|
|
371
|
+
script: |
|
|
372
|
+
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
373
|
+
echo "WARNING: Google Play not ready - skipping metadata upload."
|
|
374
|
+
echo "First AAB must be uploaded manually before metadata can sync."
|
|
375
|
+
exit 0
|
|
376
|
+
fi
|
|
377
|
+
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
378
|
+
cd $CM_BUILD_DIR/$APP_ROOT/android
|
|
379
|
+
if bundle exec fastlane upload_metadata_android; then
|
|
380
|
+
echo "Metadata uploaded successfully"
|
|
381
|
+
else
|
|
382
|
+
echo "WARNING: Metadata upload failed (app may still be in draft state)."
|
|
383
|
+
echo "Metadata will sync on next build after app leaves draft."
|
|
384
|
+
echo "Continuing build..."
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
- name: Sync subscriptions and IAP
|
|
388
|
+
script: |
|
|
389
|
+
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
390
|
+
echo "WARNING: Google Play not ready - skipping IAP sync."
|
|
391
|
+
exit 0
|
|
392
|
+
fi
|
|
393
|
+
FORCE_FLAG=""
|
|
394
|
+
if [ "${FIRST_STORE_SYNC:-false}" = "true" ]; then
|
|
395
|
+
FORCE_FLAG="--force"
|
|
396
|
+
fi
|
|
397
|
+
if ./scripts/check_changed.sh $FORCE_FLAG fastlane/iap_config.json; then
|
|
398
|
+
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
399
|
+
cd $CM_BUILD_DIR/$APP_ROOT/android
|
|
400
|
+
if bundle exec fastlane sync_google_iap 2>&1; then
|
|
401
|
+
echo "IAP sync completed successfully"
|
|
402
|
+
else
|
|
403
|
+
echo "WARNING: IAP sync failed (fastlane-plugin-iap may not be available yet)."
|
|
404
|
+
echo "IAP products can be configured manually in Google Play Console."
|
|
405
|
+
echo "Continuing build..."
|
|
406
|
+
fi
|
|
407
|
+
else
|
|
408
|
+
echo "IAP config unchanged - skipping"
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
- name: Update data safety form
|
|
412
|
+
script: |
|
|
413
|
+
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
414
|
+
echo "WARNING: Google Play not ready - skipping data safety update."
|
|
415
|
+
exit 0
|
|
416
|
+
fi
|
|
417
|
+
FORCE_FLAG=""
|
|
418
|
+
if [ "${FIRST_STORE_SYNC:-false}" = "true" ]; then
|
|
419
|
+
FORCE_FLAG="--force"
|
|
420
|
+
fi
|
|
421
|
+
if ./scripts/check_changed.sh $FORCE_FLAG fastlane/data_safety.csv; then
|
|
422
|
+
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
423
|
+
cd $CM_BUILD_DIR/$APP_ROOT/android
|
|
424
|
+
if bundle exec fastlane update_data_safety 2>&1; then
|
|
425
|
+
echo "Data safety form updated successfully"
|
|
426
|
+
else
|
|
427
|
+
echo "WARNING: Data safety update failed."
|
|
428
|
+
echo "Data safety can be configured manually in Google Play Console."
|
|
429
|
+
echo "Continuing build..."
|
|
430
|
+
fi
|
|
431
|
+
else
|
|
432
|
+
echo "Data safety unchanged - skipping"
|
|
433
|
+
fi
|
|
434
|
+
# After successful sync, create marker so future builds use change detection
|
|
435
|
+
if [ "${FIRST_STORE_SYNC:-false}" = "true" ]; then
|
|
436
|
+
mkdir -p "$CM_BUILD_DIR/.codemagic"
|
|
437
|
+
touch "$CM_BUILD_DIR/.codemagic/android_store_synced"
|
|
438
|
+
cd "$CM_BUILD_DIR"
|
|
439
|
+
git add .codemagic/android_store_synced
|
|
440
|
+
git commit -m "chore: mark Android store sync complete [skip ci]" || true
|
|
441
|
+
git push origin HEAD || true
|
|
442
|
+
echo "Store sync marker committed"
|
|
443
|
+
fi
|
|
444
|
+
|
|
218
445
|
- name: Manage Android version
|
|
219
446
|
script: |
|
|
220
447
|
# Read iOS version for consistency (if iOS workflow ran)
|
|
@@ -231,68 +458,59 @@ workflows:
|
|
|
231
458
|
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
232
459
|
LATEST_BUILD=0
|
|
233
460
|
else
|
|
234
|
-
|
|
461
|
+
export GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS="$(cat $CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH)"
|
|
462
|
+
LATEST_BUILD_OUTPUT=$(google-play get-latest-build-number \
|
|
235
463
|
--package-name "$PACKAGE_NAME" \
|
|
236
|
-
--tracks=production,beta,alpha,internal 2>/dev/null ||
|
|
464
|
+
--tracks=production,beta,alpha,internal 2>/dev/null || true)
|
|
465
|
+
# Extract just the number from output
|
|
466
|
+
LATEST_BUILD=$(echo "$LATEST_BUILD_OUTPUT" | grep -oE '^[0-9]+$' | tail -1)
|
|
467
|
+
if [ -z "$LATEST_BUILD" ]; then
|
|
468
|
+
echo "WARNING: Could not get latest build number from Google Play."
|
|
469
|
+
echo "Raw output: $LATEST_BUILD_OUTPUT"
|
|
470
|
+
echo "Falling back to build number 1"
|
|
471
|
+
LATEST_BUILD=1
|
|
472
|
+
fi
|
|
473
|
+
echo "Latest build number from Google Play: $LATEST_BUILD"
|
|
237
474
|
fi
|
|
238
475
|
NEW_BUILD=$(($LATEST_BUILD + 1))
|
|
239
|
-
if [ -z "$APP_VERSION" ]
|
|
476
|
+
if [ -z "$APP_VERSION" ]; then
|
|
240
477
|
APP_VERSION="1.0.0"
|
|
241
478
|
fi
|
|
242
|
-
|
|
479
|
+
# Normalize to semver (1.0 -> 1.0.0, 1 -> 1.0.0)
|
|
480
|
+
PARTS=$(echo "$APP_VERSION" | tr '.' '\n' | wc -l | tr -d ' ')
|
|
481
|
+
if [ "$PARTS" -eq 1 ]; then APP_VERSION="${APP_VERSION}.0.0"; fi
|
|
482
|
+
if [ "$PARTS" -eq 2 ]; then APP_VERSION="${APP_VERSION}.0"; fi
|
|
483
|
+
sed -i '' "s/^version:.*/version: ${APP_VERSION}+${NEW_BUILD}/" $APP_ROOT/pubspec.yaml
|
|
243
484
|
echo "ANDROID_VERSION_CODE=$NEW_BUILD" >> $CM_ENV
|
|
244
485
|
echo "Android versionCode: $NEW_BUILD, versionName: $APP_VERSION"
|
|
245
486
|
|
|
246
487
|
- name: Flutter packages
|
|
247
|
-
script: flutter pub get
|
|
488
|
+
script: cd $CM_BUILD_DIR/$APP_ROOT && flutter pub get
|
|
248
489
|
|
|
249
490
|
- name: Build Android
|
|
250
|
-
script: flutter build appbundle --release
|
|
491
|
+
script: cd $CM_BUILD_DIR/$APP_ROOT && flutter build appbundle --release
|
|
251
492
|
|
|
252
|
-
- name:
|
|
253
|
-
script: |
|
|
254
|
-
cd android
|
|
255
|
-
gem install bundler
|
|
256
|
-
bundle install
|
|
257
|
-
|
|
258
|
-
- name: Deploy to Google Play
|
|
493
|
+
- name: Upload AAB to Google Play
|
|
259
494
|
script: |
|
|
260
495
|
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
261
|
-
echo "
|
|
262
|
-
|
|
496
|
+
echo "============================================"
|
|
497
|
+
echo "FIRST RUN: AAB built but NOT uploaded."
|
|
498
|
+
echo "============================================"
|
|
499
|
+
echo ""
|
|
500
|
+
echo "Download the AAB from build artifacts and"
|
|
501
|
+
echo "upload it manually to Google Play Console."
|
|
502
|
+
echo "See HOW_TO_GOOGLE_PLAY.md in artifacts for"
|
|
503
|
+
echo "step-by-step instructions."
|
|
504
|
+
echo ""
|
|
505
|
+
echo "After completing manual setup, push again"
|
|
506
|
+
echo "for fully automated publishing."
|
|
507
|
+
echo "============================================"
|
|
508
|
+
exit 1
|
|
263
509
|
fi
|
|
264
510
|
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
265
|
-
cd android
|
|
266
|
-
bundle exec fastlane
|
|
267
|
-
|
|
268
|
-
- name: Sync subscriptions and IAP
|
|
269
|
-
script: |
|
|
270
|
-
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
271
|
-
echo "Skipping - Google Play setup incomplete"
|
|
272
|
-
exit 0
|
|
273
|
-
fi
|
|
274
|
-
if ./scripts/check_changed.sh fastlane/iap_config.json; then
|
|
275
|
-
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
276
|
-
cd android
|
|
277
|
-
bundle exec fastlane sync_google_iap
|
|
278
|
-
else
|
|
279
|
-
echo "IAP config unchanged - skipping"
|
|
280
|
-
fi
|
|
281
|
-
|
|
282
|
-
- name: Update data safety form
|
|
283
|
-
script: |
|
|
284
|
-
if [ "$GOOGLE_PLAY_READY" != "true" ]; then
|
|
285
|
-
echo "Skipping - Google Play setup incomplete"
|
|
286
|
-
exit 0
|
|
287
|
-
fi
|
|
288
|
-
if ./scripts/check_changed.sh fastlane/data_safety.csv; then
|
|
289
|
-
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
|
|
290
|
-
cd android
|
|
291
|
-
bundle exec fastlane update_data_safety
|
|
292
|
-
else
|
|
293
|
-
echo "Data safety unchanged - skipping"
|
|
294
|
-
fi
|
|
511
|
+
cd $CM_BUILD_DIR/$APP_ROOT/android
|
|
512
|
+
bundle exec fastlane upload_binary_android
|
|
295
513
|
|
|
296
514
|
artifacts:
|
|
297
|
-
- build/app/outputs/**/*.aab
|
|
515
|
+
- ${APP_ROOT}/build/app/outputs/**/*.aab
|
|
298
516
|
- $CM_BUILD_DIR/HOW_TO_GOOGLE_PLAY.md
|
|
@@ -5,26 +5,74 @@ default_platform(:android)
|
|
|
5
5
|
ROOT_DIR = ENV.fetch("CM_BUILD_DIR", File.expand_path("../..", __FILE__))
|
|
6
6
|
APP_ROOT = ENV.fetch("APP_ROOT", "app")
|
|
7
7
|
|
|
8
|
+
AAB_PATH = "#{ROOT_DIR}/#{APP_ROOT}/build/app/outputs/bundle/release/app-release.aab"
|
|
9
|
+
|
|
8
10
|
def metadata_changed?(path)
|
|
9
11
|
!sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
|
|
10
12
|
rescue StandardError
|
|
11
13
|
true
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
# Shared upload options reused across Android lanes.
|
|
17
|
+
def base_play_store_options
|
|
18
|
+
{
|
|
19
|
+
package_name: ENV["PACKAGE_NAME"],
|
|
20
|
+
track: ENV.fetch("TRACK", "internal"),
|
|
21
|
+
json_key: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"]
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rollout_options
|
|
26
|
+
{
|
|
27
|
+
rollout: ENV.fetch("ROLLOUT_FRACTION", "").empty? ? nil : ENV["ROLLOUT_FRACTION"],
|
|
28
|
+
in_app_update_priority: ENV.fetch("IN_APP_UPDATE_PRIORITY", "3").to_i
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
14
32
|
platform :android do
|
|
15
33
|
lane :deploy_android do
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
status = ENV.fetch("RELEASE_STATUS", "draft")
|
|
35
|
+
opts = base_play_store_options.merge(
|
|
36
|
+
aab: AAB_PATH,
|
|
37
|
+
release_status: status,
|
|
20
38
|
skip_upload_metadata: !metadata_changed?("fastlane/metadata/android/"),
|
|
21
39
|
skip_upload_screenshots: !metadata_changed?("fastlane/screenshots/android/"),
|
|
22
40
|
skip_upload_images: !metadata_changed?("fastlane/screenshots/android/"),
|
|
23
41
|
skip_upload_changelogs: false,
|
|
24
|
-
metadata_path: "#{ROOT_DIR}/fastlane/metadata/android"
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
metadata_path: "#{ROOT_DIR}/fastlane/metadata/android"
|
|
43
|
+
)
|
|
44
|
+
opts.merge!(rollout_options) unless status == "draft"
|
|
45
|
+
upload_to_play_store(opts)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
lane :upload_metadata_android do
|
|
49
|
+
upload_to_play_store(
|
|
50
|
+
base_play_store_options.merge(
|
|
51
|
+
skip_upload_aab: true,
|
|
52
|
+
skip_upload_apk: true,
|
|
53
|
+
skip_upload_metadata: false,
|
|
54
|
+
skip_upload_screenshots: false,
|
|
55
|
+
skip_upload_images: false,
|
|
56
|
+
skip_upload_changelogs: true,
|
|
57
|
+
release_status: "draft",
|
|
58
|
+
metadata_path: "#{ROOT_DIR}/fastlane/metadata/android"
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
lane :upload_binary_android do
|
|
64
|
+
status = ENV.fetch("RELEASE_STATUS", "draft")
|
|
65
|
+
opts = base_play_store_options.merge(
|
|
66
|
+
aab: AAB_PATH,
|
|
67
|
+
release_status: status,
|
|
68
|
+
skip_upload_metadata: true,
|
|
69
|
+
skip_upload_screenshots: true,
|
|
70
|
+
skip_upload_images: true,
|
|
71
|
+
skip_upload_changelogs: true
|
|
27
72
|
)
|
|
73
|
+
# Rollout percentage is only valid for non-draft releases
|
|
74
|
+
opts.merge!(rollout_options) unless status == "draft"
|
|
75
|
+
upload_to_play_store(opts)
|
|
28
76
|
end
|
|
29
77
|
|
|
30
78
|
lane :sync_google_iap do
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# fastlane-plugin-iap is not yet published.
|
|
2
|
+
# IAP sync is handled gracefully in codemagic.yaml (non-blocking).
|
|
3
|
+
# Uncomment when the plugin is available:
|
|
4
|
+
# gem "fastlane-plugin-iap", git: "https://github.com/daemux/fastlane-plugin-iap"
|
|
@@ -5,6 +5,20 @@ default_platform(:ios)
|
|
|
5
5
|
ROOT_DIR = ENV.fetch("CM_BUILD_DIR", File.expand_path("../..", __FILE__))
|
|
6
6
|
APP_ROOT = ENV.fetch("APP_ROOT", "app")
|
|
7
7
|
|
|
8
|
+
# Monkey-patch: Fastlane deliver crashes with "No data" when fetching
|
|
9
|
+
# app_store_review_detail on the first version (fastlane/fastlane#20538).
|
|
10
|
+
# Wrap the method to rescue the RuntimeError gracefully.
|
|
11
|
+
# Require deliver explicitly so the constant is available at parse time.
|
|
12
|
+
require "deliver"
|
|
13
|
+
::Deliver::UploadMetadata.prepend(Module.new do
|
|
14
|
+
def review_attachment_file(version)
|
|
15
|
+
super
|
|
16
|
+
rescue RuntimeError => e
|
|
17
|
+
raise unless e.message.include?("No data")
|
|
18
|
+
Fastlane::UI.important("Skipping review attachment: #{e.message} (first version)")
|
|
19
|
+
end
|
|
20
|
+
end)
|
|
21
|
+
|
|
8
22
|
def metadata_changed?(path)
|
|
9
23
|
!sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
|
|
10
24
|
rescue StandardError
|
|
@@ -30,26 +44,67 @@ def asc_api_key
|
|
|
30
44
|
end
|
|
31
45
|
end
|
|
32
46
|
|
|
47
|
+
# Shared deliver options reused across iOS lanes.
|
|
48
|
+
def base_deliver_options
|
|
49
|
+
{
|
|
50
|
+
api_key: asc_api_key,
|
|
51
|
+
app_identifier: ENV["BUNDLE_ID"],
|
|
52
|
+
force: true
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def metadata_deliver_options
|
|
57
|
+
base_deliver_options.merge(
|
|
58
|
+
metadata_path: "#{ROOT_DIR}/fastlane/metadata",
|
|
59
|
+
screenshots_path: "#{ROOT_DIR}/fastlane/screenshots/ios",
|
|
60
|
+
sync_screenshots: true,
|
|
61
|
+
primary_category: ENV.fetch("PRIMARY_CATEGORY", "UTILITIES"),
|
|
62
|
+
secondary_category: ENV.fetch("SECONDARY_CATEGORY", "PRODUCTIVITY")
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def submission_options
|
|
67
|
+
{
|
|
68
|
+
submit_for_review: ENV.fetch("SUBMIT_FOR_REVIEW", "true") == "true",
|
|
69
|
+
automatic_release: ENV.fetch("AUTOMATIC_RELEASE", "true") == "true"
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
33
73
|
platform :ios do
|
|
34
74
|
lane :deploy_ios do
|
|
35
75
|
deliver(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
metadata_deliver_options.merge(submission_options).merge(
|
|
77
|
+
ipa: Dir.glob("#{ROOT_DIR}/#{APP_ROOT}/build/ios/ipa/*.ipa").first,
|
|
78
|
+
app_rating_config_path: "#{ROOT_DIR}/fastlane/app_rating_config.json",
|
|
79
|
+
skip_metadata: !metadata_changed?("fastlane/metadata/ios/"),
|
|
80
|
+
skip_screenshots: !metadata_changed?("fastlane/screenshots/ios/"),
|
|
81
|
+
run_precheck_before_submit: true,
|
|
82
|
+
precheck_include_in_app_purchases: false
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
lane :upload_metadata_ios do
|
|
88
|
+
deliver(
|
|
89
|
+
metadata_deliver_options.merge(
|
|
90
|
+
skip_binary_upload: true,
|
|
91
|
+
skip_metadata: false,
|
|
92
|
+
skip_screenshots: false,
|
|
93
|
+
run_precheck_before_submit: false,
|
|
94
|
+
submit_for_review: false
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
lane :upload_binary_ios do
|
|
100
|
+
deliver(
|
|
101
|
+
base_deliver_options.merge(submission_options).merge(
|
|
102
|
+
ipa: Dir.glob("#{ROOT_DIR}/#{APP_ROOT}/build/ios/ipa/*.ipa").first,
|
|
103
|
+
skip_metadata: true,
|
|
104
|
+
skip_screenshots: true,
|
|
105
|
+
run_precheck_before_submit: true,
|
|
106
|
+
precheck_include_in_app_purchases: false
|
|
107
|
+
)
|
|
53
108
|
)
|
|
54
109
|
end
|
|
55
110
|
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# fastlane-plugin-iap is not yet published.
|
|
2
|
+
# IAP sync is handled gracefully in codemagic.yaml (non-blocking).
|
|
3
|
+
# Uncomment when the plugin is available:
|
|
4
|
+
# gem "fastlane-plugin-iap", git: "https://github.com/daemux/fastlane-plugin-iap"
|