@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
package/bin/cli.mjs CHANGED
@@ -34,6 +34,8 @@ const valueFlags = {
34
34
  '--cloudflare-token=': 'cloudflareToken',
35
35
  '--cloudflare-account-id=': 'cloudflareAccountId',
36
36
  '--bundle-id=': 'bundleId',
37
+ '--match-deploy-key=': 'matchDeployKey',
38
+ '--match-git-url=': 'matchGitUrl',
37
39
  };
38
40
 
39
41
  for (const arg of args) {
@@ -42,15 +44,11 @@ for (const arg of args) {
42
44
  if ((v = flagValue(arg, '--branch=')) !== undefined) { cmBranch = v; continue; }
43
45
  if ((v = flagValue(arg, '--workflow=')) !== undefined) { cmWorkflowId = v; continue; }
44
46
 
45
- let matched = false;
46
- for (const [prefix, key] of Object.entries(valueFlags)) {
47
- if ((v = flagValue(arg, prefix)) !== undefined) {
48
- cliTokens[key] = v;
49
- matched = true;
50
- break;
51
- }
47
+ const valueFlagEntry = Object.entries(valueFlags).find(([prefix]) => arg.startsWith(prefix));
48
+ if (valueFlagEntry) {
49
+ cliTokens[valueFlagEntry[1]] = arg.slice(valueFlagEntry[0].length);
50
+ continue;
52
51
  }
53
- if (matched) continue;
54
52
 
55
53
  switch (arg) {
56
54
  case '-g':
@@ -70,6 +68,9 @@ for (const arg of args) {
70
68
  case '--github-setup':
71
69
  action = 'github-setup';
72
70
  break;
71
+ case '--github-actions':
72
+ cliTokens.githubActions = true;
73
+ break;
73
74
  case '--trigger':
74
75
  cmTrigger = true;
75
76
  break;
@@ -105,34 +106,57 @@ Codemagic:
105
106
  --trigger Trigger build after setup
106
107
  --wait Wait for build completion (implies --trigger)
107
108
 
109
+ GitHub Actions CI mode:
110
+ --github-actions Use GitHub Actions instead of Codemagic (skip MCP setup)
111
+ --match-deploy-key=PATH Path to Match deploy key file (required with --github-actions)
112
+ --match-git-url=URL Git URL for Match certificates repo (required with --github-actions)
113
+
108
114
  GitHub Actions (auto-configured during install if gh CLI available):
109
115
  --github-setup Set CM_API_TOKEN secret for GitHub Actions
110
116
  --token=TOKEN API token (or set CM_API_TOKEN env var)
111
117
 
112
118
  Examples:
113
- npx @daemux/store-automator Install for project
119
+ npx @daemux/store-automator Install for project (Codemagic)
114
120
  npx @daemux/store-automator -g Install globally
115
121
  npx @daemux/store-automator -u Uninstall from project
116
122
  npx @daemux/store-automator -g -u Uninstall globally
117
123
  npx @daemux/store-automator --codemagic-setup Register with Codemagic
118
124
  npx @daemux/store-automator --codemagic-setup --trigger --wait Trigger and wait
119
- npx @daemux/store-automator --github-setup Configure GitHub Actions
125
+ npx @daemux/store-automator --github-setup Configure GitHub Actions secret
126
+
127
+ GitHub Actions install:
128
+ npx @daemux/store-automator --github-actions --bundle-id=ID --match-deploy-key=PATH --match-git-url=URL
120
129
 
121
- Non-interactive install (CI/CD):
122
- npx @daemux/store-automator --bundle-id=com.company.app --codemagic-token=TOKEN --codemagic-team-id=ID --stitch-key=KEY
130
+ Non-interactive install (Codemagic):
131
+ npx @daemux/store-automator --bundle-id=ID --codemagic-token=TOKEN --stitch-key=KEY
123
132
  npx @daemux/store-automator --cloudflare-token=TOKEN --cloudflare-account-id=ID`);
124
133
  process.exit(0);
125
- break; // eslint: no-fallthrough
126
134
  case '-v':
127
135
  case '--version':
128
136
  console.log(pkg.version);
129
137
  process.exit(0);
130
- break; // eslint: no-fallthrough
131
138
  }
132
139
  }
133
140
 
134
141
  if (cmWait) cmTrigger = true;
135
142
 
143
+ if (cliTokens.githubActions) {
144
+ const missing = [];
145
+ if (!cliTokens.matchDeployKey) missing.push('--match-deploy-key');
146
+ if (!cliTokens.matchGitUrl) missing.push('--match-git-url');
147
+ if (!cliTokens.bundleId) missing.push('--bundle-id');
148
+ if (missing.length > 0) {
149
+ console.error(`Error: --github-actions requires: ${missing.join(', ')}`);
150
+ console.error('');
151
+ console.error('Example:');
152
+ console.error(' npx @daemux/store-automator --github-actions \\');
153
+ console.error(' --bundle-id=com.company.app \\');
154
+ console.error(' --match-deploy-key=creds/match_deploy_key \\');
155
+ console.error(' --match-git-url=git@github.com:org/certs.git');
156
+ process.exit(1);
157
+ }
158
+ }
159
+
136
160
  notifier.notify();
137
161
 
138
162
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Full App Store & Google Play automation for Flutter apps with Claude Code agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-automator",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
package/src/ci-config.mjs CHANGED
@@ -7,6 +7,8 @@ const FIELD_PATTERNS = {
7
7
  team_id: /^(\s*team_id:\s*)"[^"]*"/m,
8
8
  bundle_id: /^(\s*bundle_id:\s*)"[^"]*"/m,
9
9
  package_name: /^(\s*package_name:\s*)"[^"]*"/m,
10
+ deploy_key_path: /^(\s*deploy_key_path:\s*)"[^"]*"/m,
11
+ git_url: /^(\s*git_url:\s*)"[^"]*"/m,
10
12
  };
11
13
 
12
14
  function writeCiField(projectDir, field, value) {
@@ -44,3 +46,22 @@ export function writeCiBundleId(projectDir, bundleId) {
44
46
  export function writeCiPackageName(projectDir, packageName) {
45
47
  return writeCiField(projectDir, 'package_name', packageName);
46
48
  }
49
+
50
+ export function writeMatchConfig(projectDir, { deployKeyPath, gitUrl }) {
51
+ const wrote1 = writeCiField(projectDir, 'deploy_key_path', deployKeyPath);
52
+ const wrote2 = writeCiField(projectDir, 'git_url', gitUrl);
53
+ return wrote1 || wrote2;
54
+ }
55
+
56
+ export function readFlutterRoot(projectDir) {
57
+ const configPath = join(projectDir, CI_CONFIG_FILE);
58
+ if (!existsSync(configPath)) return '.';
59
+
60
+ try {
61
+ const content = readFileSync(configPath, 'utf8');
62
+ const match = content.match(/^flutter_root:\s*"([^"]*)"/m);
63
+ return match ? match[1] : '.';
64
+ } catch {
65
+ return '.';
66
+ }
67
+ }
@@ -0,0 +1,93 @@
1
+ import { execSync, execFileSync } from 'node:child_process';
2
+ import { installGitHubActionsTemplates, installMatchfile } from './templates.mjs';
3
+ import { findAppByRepo, addApp, normalizeRepoUrl } from './codemagic-api.mjs';
4
+ import {
5
+ writeCiAppId, writeCiTeamId, writeMatchConfig, readFlutterRoot,
6
+ } from './ci-config.mjs';
7
+ import { updateMcpAppId, updateMcpTeamId } from './mcp-setup.mjs';
8
+
9
+ function setupGitHubActionsSecret(codemagicToken) {
10
+ if (!codemagicToken) return false;
11
+
12
+ try {
13
+ execFileSync('which', ['gh'], { encoding: 'utf8', stdio: 'pipe' });
14
+ const authStatus = execFileSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: 'pipe' });
15
+ if (authStatus.includes('not logged')) return false;
16
+
17
+ execFileSync('gh', ['secret', 'set', 'CM_API_TOKEN', '--body', codemagicToken], {
18
+ encoding: 'utf8',
19
+ stdio: 'pipe',
20
+ });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ async function setupCodemagicApp(projectDir, codemagicToken, codemagicTeamId) {
28
+ if (!codemagicToken) return;
29
+
30
+ let repoUrl;
31
+ try {
32
+ const raw = execSync('git remote get-url origin', {
33
+ encoding: 'utf8',
34
+ stdio: ['pipe', 'pipe', 'pipe'],
35
+ }).trim();
36
+ if (!raw) return;
37
+ repoUrl = normalizeRepoUrl(raw);
38
+ } catch {
39
+ return;
40
+ }
41
+
42
+ try {
43
+ let app = await findAppByRepo(codemagicToken, repoUrl);
44
+ if (!app) {
45
+ app = await addApp(codemagicToken, repoUrl, codemagicTeamId);
46
+ console.log(`Codemagic app created: ${app.appName || app._id}`);
47
+ } else {
48
+ console.log(`Codemagic app found: ${app.appName || app._id}`);
49
+ }
50
+
51
+ const written = writeCiAppId(projectDir, app._id);
52
+ if (written) {
53
+ console.log(`Codemagic app_id written to ci.config.yaml`);
54
+ }
55
+
56
+ updateMcpAppId(projectDir, app._id);
57
+
58
+ if (codemagicTeamId) {
59
+ writeCiTeamId(projectDir, codemagicTeamId);
60
+ updateMcpTeamId(projectDir, codemagicTeamId);
61
+ }
62
+ } catch (err) {
63
+ console.log(`Codemagic auto-setup skipped: ${err.message || err}`);
64
+ }
65
+ }
66
+
67
+ export function installGitHubActionsPath(projectDir, packageDir, cliTokens) {
68
+ console.log('Configuring GitHub Actions mode...');
69
+ installGitHubActionsTemplates(projectDir, packageDir);
70
+
71
+ const flutterRoot = readFlutterRoot(projectDir);
72
+ installMatchfile(projectDir, packageDir, flutterRoot, {
73
+ matchGitUrl: cliTokens.matchGitUrl,
74
+ bundleId: cliTokens.bundleId,
75
+ });
76
+
77
+ const wrote = writeMatchConfig(projectDir, {
78
+ deployKeyPath: cliTokens.matchDeployKey,
79
+ gitUrl: cliTokens.matchGitUrl,
80
+ });
81
+ if (wrote) console.log('Match credentials written to ci.config.yaml');
82
+ }
83
+
84
+ export async function installCodemagicPath(projectDir, tokens) {
85
+ await setupCodemagicApp(projectDir, tokens.codemagicToken, tokens.codemagicTeamId);
86
+
87
+ const ghConfigured = setupGitHubActionsSecret(tokens.codemagicToken);
88
+ if (ghConfigured) {
89
+ console.log('GitHub Actions: CM_API_TOKEN secret configured.');
90
+ } else if (tokens.codemagicToken) {
91
+ console.log('GitHub Actions: secret not set (gh CLI unavailable or not authenticated).');
92
+ }
93
+ }
package/src/install.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync, cpSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { execSync, execFileSync } from 'node:child_process';
4
+ import { execSync } from 'node:child_process';
5
5
  import {
6
6
  MARKETPLACE_DIR, KNOWN_MP_PATH, CACHE_DIR,
7
7
  MARKETPLACE_NAME, PLUGIN_REF,
@@ -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, updateMcpTeamId } from './mcp-setup.mjs';
12
+ import { getMcpServers, writeMcpJson } from './mcp-setup.mjs';
13
13
  import { installClaudeMd, installCiTemplates, installFirebaseTemplates } from './templates.mjs';
14
- import { findAppByRepo, addApp, normalizeRepoUrl } from './codemagic-api.mjs';
15
- import { writeCiAppId, writeCiTeamId, writeCiBundleId, writeCiPackageName } from './ci-config.mjs';
14
+ import { writeCiBundleId, writeCiPackageName } from './ci-config.mjs';
15
+ import { installGitHubActionsPath, installCodemagicPath } from './install-paths.mjs';
16
16
 
17
17
  function checkClaudeCli() {
18
18
  const result = exec('command -v claude') || exec('which claude');
@@ -94,61 +94,19 @@ function printSummary(scope, oldVersion, newVersion) {
94
94
  }
95
95
  }
96
96
 
97
- function setupGitHubActions(codemagicToken) {
98
- if (!codemagicToken) return false;
99
-
100
- try {
101
- execFileSync('which', ['gh'], { encoding: 'utf8', stdio: 'pipe' });
102
- const authStatus = execFileSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: 'pipe' });
103
- if (authStatus.includes('not logged')) return false;
104
-
105
- execFileSync('gh', ['secret', 'set', 'CM_API_TOKEN', '--body', codemagicToken], {
106
- encoding: 'utf8',
107
- stdio: 'pipe',
108
- });
109
- return true;
110
- } catch {
111
- return false;
112
- }
113
- }
114
-
115
- async function setupCodemagicApp(projectDir, codemagicToken, codemagicTeamId) {
116
- if (!codemagicToken) return;
117
-
118
- let repoUrl;
119
- try {
120
- const raw = execSync('git remote get-url origin', {
121
- encoding: 'utf8',
122
- stdio: ['pipe', 'pipe', 'pipe'],
123
- }).trim();
124
- if (!raw) return;
125
- repoUrl = normalizeRepoUrl(raw);
126
- } catch {
127
- return;
128
- }
129
-
130
- try {
131
- let app = await findAppByRepo(codemagicToken, repoUrl);
132
- if (!app) {
133
- app = await addApp(codemagicToken, repoUrl, codemagicTeamId);
134
- console.log(`Codemagic app created: ${app.appName || app._id}`);
135
- } else {
136
- console.log(`Codemagic app found: ${app.appName || app._id}`);
137
- }
138
-
139
- const written = writeCiAppId(projectDir, app._id);
140
- if (written) {
141
- console.log(`Codemagic app_id written to ci.config.yaml`);
142
- }
143
-
144
- updateMcpAppId(projectDir, app._id);
145
-
146
- if (codemagicTeamId) {
147
- writeCiTeamId(projectDir, codemagicTeamId);
148
- updateMcpTeamId(projectDir, codemagicTeamId);
149
- }
150
- } catch (err) {
151
- console.log(`Codemagic auto-setup skipped: ${err.message || err}`);
97
+ function printNextSteps(isGitHubActions) {
98
+ console.log('');
99
+ console.log('Next steps:');
100
+ if (isGitHubActions) {
101
+ console.log(' 1. Fill ci.config.yaml with credentials');
102
+ console.log(' 2. Add creds/AuthKey.p8 and creds/play-service-account.json');
103
+ console.log(' 3. Set MATCH_PASSWORD secret in GitHub repository settings');
104
+ console.log(' 4. Start Claude Code');
105
+ } else {
106
+ console.log(' 1. Fill ci.config.yaml (codemagic.app_id is auto-configured if token was provided)');
107
+ console.log(' 2. Add creds/AuthKey.p8 and creds/play-service-account.json');
108
+ console.log(' 3. Start Claude Code');
109
+ console.log(' Note: For auto-trigger, install gh CLI and run "gh auth login"');
152
110
  }
153
111
  }
154
112
 
@@ -157,11 +115,18 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
157
115
 
158
116
  console.log('Installing/updating Daemux Store Automator...');
159
117
 
160
- const tokens = await promptForTokens(cliTokens);
118
+ const isGitHubActions = Boolean(cliTokens.githubActions);
119
+
120
+ const tokens = isGitHubActions
121
+ ? { bundleId: cliTokens.bundleId ?? '' }
122
+ : await promptForTokens(cliTokens);
161
123
 
162
124
  const projectDir = process.cwd();
163
- const servers = getMcpServers(tokens);
164
- writeMcpJson(projectDir, servers);
125
+
126
+ if (!isGitHubActions) {
127
+ const servers = getMcpServers(tokens);
128
+ writeMcpJson(projectDir, servers);
129
+ }
165
130
 
166
131
  const oldVersion = readMarketplaceVersion();
167
132
  const packageDir = getPackageDir();
@@ -189,7 +154,11 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
189
154
  writeCiPackageName(projectDir, tokens.bundleId);
190
155
  }
191
156
 
192
- await setupCodemagicApp(projectDir, tokens.codemagicToken, tokens.codemagicTeamId);
157
+ if (isGitHubActions) {
158
+ installGitHubActionsPath(projectDir, packageDir, cliTokens);
159
+ } else {
160
+ await installCodemagicPath(projectDir, tokens);
161
+ }
193
162
 
194
163
  const scopeLabel = scope === 'user' ? 'global' : 'project';
195
164
  console.log(`Configuring ${scopeLabel} settings...`);
@@ -197,20 +166,6 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
197
166
  injectEnvVars(settingsPath);
198
167
  injectStatusLine(settingsPath);
199
168
 
200
- const ghConfigured = setupGitHubActions(tokens.codemagicToken);
201
- if (ghConfigured) {
202
- console.log('GitHub Actions: CM_API_TOKEN secret configured.');
203
- } else if (tokens.codemagicToken) {
204
- console.log('GitHub Actions: secret not set (gh CLI unavailable or not authenticated).');
205
- }
206
-
207
169
  printSummary(scope, oldVersion, newVersion);
208
- console.log('');
209
- console.log('Next steps:');
210
- console.log(' 1. Fill ci.config.yaml (codemagic.app_id is auto-configured if token was provided)');
211
- console.log(' 2. Add creds/AuthKey.p8 and creds/play-service-account.json');
212
- console.log(' 3. Start Claude Code');
213
- if (!ghConfigured) {
214
- console.log(' Note: For auto-trigger, install gh CLI and run "gh auth login"');
215
- }
170
+ printNextSteps(isGitHubActions);
216
171
  }
package/src/templates.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, cpSync, copyFileSync } from 'node:fs';
1
+ import { existsSync, cpSync, copyFileSync, chmodSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ensureDir } from './utils.mjs';
4
4
 
@@ -22,6 +22,17 @@ const FIREBASE_COPIES = [
22
22
  ['firebase/.firebaserc.template', 'backend/.firebaserc'],
23
23
  ];
24
24
 
25
+ const GH_ACTIONS_WORKFLOWS = [
26
+ ['github/workflows/ios-release.yml', '.github/workflows/ios-release.yml'],
27
+ ['github/workflows/android-release.yml', '.github/workflows/android-release.yml'],
28
+ ];
29
+
30
+ const GH_ACTIONS_SCRIPT_DIRS = [
31
+ 'scripts/ci/common',
32
+ 'scripts/ci/android',
33
+ 'scripts/ci/ios',
34
+ ];
35
+
25
36
  function copyIfMissing(srcPath, destPath, label, isDirectory) {
26
37
  if (!existsSync(srcPath)) return;
27
38
  if (existsSync(destPath)) {
@@ -81,3 +92,62 @@ export function installFirebaseTemplates(projectDir, packageDir) {
81
92
  copyIfMissing(join(templateDir, src), join(projectDir, dest), dest, false);
82
93
  }
83
94
  }
95
+
96
+ function copyOrUpdate(srcPath, destPath, label) {
97
+ if (!existsSync(srcPath)) return;
98
+ ensureDir(join(destPath, '..'));
99
+ if (existsSync(destPath)) {
100
+ console.log(` ${label} exists, overwriting.`);
101
+ }
102
+ copyFileSync(srcPath, destPath);
103
+ }
104
+
105
+ function chmodShFiles(dirPath) {
106
+ if (!existsSync(dirPath)) return;
107
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
108
+ const fullPath = join(dirPath, entry.name);
109
+ if (entry.isDirectory()) {
110
+ chmodShFiles(fullPath);
111
+ } else if (entry.name.endsWith('.sh')) {
112
+ chmodSync(fullPath, 0o755);
113
+ }
114
+ }
115
+ }
116
+
117
+ export function installGitHubActionsTemplates(projectDir, packageDir) {
118
+ console.log('Installing GitHub Actions templates...');
119
+ const templateDir = join(packageDir, 'templates');
120
+
121
+ for (const [src, dest] of GH_ACTIONS_WORKFLOWS) {
122
+ copyOrUpdate(join(templateDir, src), join(projectDir, dest), dest);
123
+ }
124
+
125
+ for (const scriptDir of GH_ACTIONS_SCRIPT_DIRS) {
126
+ const scriptsSrc = join(templateDir, scriptDir);
127
+ const scriptsDest = join(projectDir, scriptDir);
128
+ if (existsSync(scriptsSrc)) {
129
+ ensureDir(scriptsDest);
130
+ cpSync(scriptsSrc, scriptsDest, { recursive: true });
131
+ chmodShFiles(scriptsDest);
132
+ console.log(` ${scriptDir}/ copied.`);
133
+ }
134
+ }
135
+ }
136
+
137
+ export function installMatchfile(projectDir, packageDir, flutterRoot, { matchGitUrl, bundleId }) {
138
+ console.log('Installing Matchfile...');
139
+ const src = join(packageDir, 'templates', 'Matchfile.template');
140
+ if (!existsSync(src)) return;
141
+
142
+ const destDir = join(projectDir, flutterRoot, 'ios', 'fastlane');
143
+ ensureDir(destDir);
144
+ const dest = join(destDir, 'Matchfile');
145
+
146
+ copyOrUpdate(src, dest, 'Matchfile');
147
+
148
+ const content = readFileSync(dest, 'utf8');
149
+ const updated = content
150
+ .replace('{{MATCH_GIT_URL}}', matchGitUrl)
151
+ .replace('{{BUNDLE_ID}}', bundleId);
152
+ writeFileSync(dest, updated, 'utf8');
153
+ }
@@ -0,0 +1,8 @@
1
+ git_url(ENV["MATCH_GIT_URL"] || "{{MATCH_GIT_URL}}")
2
+ storage_mode("git")
3
+ type("appstore")
4
+ app_identifier(ENV["BUNDLE_ID"] || "{{BUNDLE_ID}}")
5
+ git_branch("main")
6
+
7
+ # Set to true after initial cert creation for faster CI runs
8
+ # readonly(true)
@@ -12,6 +12,9 @@ credentials:
12
12
  service_account_json_path: creds/play-service-account.json
13
13
  android:
14
14
  keystore_password: "REPLACE_WITH_PASSWORD"
15
+ match:
16
+ deploy_key_path: ""
17
+ git_url: ""
15
18
 
16
19
  # === APP IDENTITY ===
17
20
  app:
@@ -24,7 +24,7 @@ end
24
24
 
25
25
  def rollout_options
26
26
  {
27
- rollout: ENV.fetch("ROLLOUT_FRACTION", "").empty? ? nil : ENV["ROLLOUT_FRACTION"],
27
+ rollout: ENV.fetch("ROLLOUT_FRACTION", "").empty? ? nil : ENV["ROLLOUT_FRACTION"].to_f,
28
28
  in_app_update_priority: ENV.fetch("IN_APP_UPDATE_PRIORITY", "3").to_i
29
29
  }
30
30
  end
@@ -86,6 +86,6 @@ platform :android do
86
86
  lane :update_data_safety do
87
87
  script = "#{ROOT_DIR}/scripts/update_data_safety.py"
88
88
  csv = "#{ROOT_DIR}/fastlane/data_safety.csv"
89
- sh("python3", script) if File.exist?(csv)
89
+ sh("python3", script, csv) if File.exist?(csv)
90
90
  end
91
91
  end
@@ -0,0 +1,72 @@
1
+ name: Android Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ skip_build:
7
+ description: 'Skip build (metadata only)'
8
+ required: false
9
+ default: 'false'
10
+ type: boolean
11
+
12
+ env:
13
+ FLUTTER_VERSION: '3.x'
14
+
15
+ jobs:
16
+ android-release:
17
+ runs-on: macos-latest-xlarge
18
+ timeout-minutes: 60
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Restore CI state cache
23
+ uses: actions/cache@v4
24
+ with:
25
+ path: .ci-state
26
+ key: ci-state-android-${{ github.ref_name }}
27
+ restore-keys: ci-state-android-
28
+
29
+ - name: Setup Flutter
30
+ uses: subosito/flutter-action@v2
31
+ with:
32
+ flutter-version: ${{ env.FLUTTER_VERSION }}
33
+
34
+ - name: Setup Java
35
+ uses: actions/setup-java@v4
36
+ with:
37
+ distribution: 'temurin'
38
+ java-version: '17'
39
+
40
+ - name: Upload metadata
41
+ run: scripts/ci/android/upload-metadata.sh
42
+
43
+ - name: Setup keystore
44
+ if: inputs.skip_build != 'true'
45
+ run: source scripts/ci/android/setup-keystore.sh
46
+
47
+ - name: Check Google Play readiness
48
+ if: inputs.skip_build != 'true'
49
+ run: scripts/ci/android/check-readiness.sh
50
+
51
+ - name: Manage Android version
52
+ if: inputs.skip_build != 'true'
53
+ run: source scripts/ci/android/manage-version.sh
54
+
55
+ - name: Flutter pub get
56
+ if: inputs.skip_build != 'true'
57
+ run: scripts/ci/common/flutter-setup.sh
58
+
59
+ - name: Build Android
60
+ if: inputs.skip_build != 'true'
61
+ run: scripts/ci/android/build.sh
62
+
63
+ - name: Upload binary
64
+ if: inputs.skip_build != 'true'
65
+ run: scripts/ci/android/upload-binary.sh
66
+
67
+ - name: Save CI state cache
68
+ if: always()
69
+ uses: actions/cache/save@v4
70
+ with:
71
+ path: .ci-state
72
+ key: ci-state-android-${{ github.ref_name }}-${{ github.run_id }}
@@ -0,0 +1,62 @@
1
+ name: iOS Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ skip_build:
7
+ description: 'Skip build (metadata/screenshots only)'
8
+ required: false
9
+ default: 'false'
10
+ type: boolean
11
+
12
+ env:
13
+ FLUTTER_VERSION: '3.x'
14
+
15
+ jobs:
16
+ ios-release:
17
+ runs-on: macos-latest-xlarge
18
+ timeout-minutes: 60
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Restore CI state cache
23
+ uses: actions/cache@v4
24
+ with:
25
+ path: .ci-state
26
+ key: ci-state-ios-${{ github.ref_name }}
27
+ restore-keys: ci-state-ios-
28
+
29
+ - name: Setup Flutter
30
+ uses: subosito/flutter-action@v2
31
+ with:
32
+ flutter-version: ${{ env.FLUTTER_VERSION }}
33
+
34
+ # Note: No separate "Load CI config" step needed here.
35
+ # Each script sources read-config.sh internally.
36
+
37
+ - name: Setup iOS signing (Fastlane Match)
38
+ env:
39
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
40
+ run: scripts/ci/ios/setup-signing.sh
41
+
42
+ - name: Upload metadata & screenshots
43
+ run: scripts/ci/ios/upload-metadata.sh
44
+
45
+ - name: Sync IAP
46
+ run: scripts/ci/ios/sync-iap.sh
47
+
48
+ # Build & upload binary steps (added when build scripts are created)
49
+ # - name: Build iOS
50
+ # if: inputs.skip_build != 'true'
51
+ # run: scripts/ci/ios/build.sh
52
+
53
+ # - name: Upload binary
54
+ # if: inputs.skip_build != 'true'
55
+ # run: scripts/ci/ios/upload-binary.sh
56
+
57
+ - name: Save CI state cache
58
+ if: always()
59
+ uses: actions/cache/save@v4
60
+ with:
61
+ path: .ci-state
62
+ key: ci-state-ios-${{ github.ref_name }}-${{ github.run_id }}