kasy-cli 1.31.5 → 1.31.7

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/bin/kasy.js CHANGED
@@ -602,7 +602,22 @@ function buildProgram(language) {
602
602
  ],
603
603
  });
604
604
  if (sub === 'configure') await runCodemagicConfigure('.', { language });
605
- else if (sub === 'release') await runCodemagicRelease('.', { language });
605
+ else if (sub === 'release') {
606
+ const platform = await ui.select({
607
+ message: t('cli.command.codemagic.release.platformPick'),
608
+ options: [
609
+ { value: 'both', label: t('cli.command.codemagic.release.platform.both') },
610
+ { value: 'ios', label: t('cli.command.codemagic.release.platform.ios') },
611
+ { value: 'android', label: t('cli.command.codemagic.release.platform.android') },
612
+ ],
613
+ });
614
+ if (typeof platform === 'string') {
615
+ const relOpts = { language };
616
+ if (platform === 'ios') relOpts.ios = true;
617
+ else if (platform === 'android') relOpts.android = true;
618
+ await runCodemagicRelease('.', relOpts);
619
+ }
620
+ }
606
621
  else if (sub === 'status') {
607
622
  const buildId = await ui.text({
608
623
  message: 'Build ID',
@@ -625,9 +640,11 @@ function buildProgram(language) {
625
640
  codemagicCmd
626
641
  .command('release')
627
642
  .argument('[directory]', 'Project folder (default: current directory)', '.')
643
+ .option('--ios', t('cli.command.codemagic.release.iosOption'))
644
+ .option('--android', t('cli.command.codemagic.release.androidOption'))
628
645
  .description(t('cli.command.codemagic.release.description'))
629
- .action(async (directory) => {
630
- await runCodemagicRelease(directory, { language });
646
+ .action(async (directory, opts) => {
647
+ await runCodemagicRelease(directory, { language, ios: opts.ios, android: opts.android });
631
648
  }),
632
649
  t
633
650
  );
@@ -121,15 +121,17 @@ Atalho: `make release-ios`
121
121
 
122
122
  ---
123
123
 
124
- ### `kasy codemagic` — Release iOS (nuvem)
124
+ ### `kasy codemagic` — Release iOS/Android (nuvem)
125
125
 
126
- Dispara build iOS no Codemagic (ideal sem Mac).
126
+ Dispara build **iOS e Android** no Codemagic (ideal sem Mac), levando as chaves do `.env` no disparo.
127
127
 
128
128
  ```bash
129
- kasy add ci # cria codemagic.yaml (se necessário)
130
- kasy codemagic configure # token API + app/workflow ID
131
- kasy codemagic release # inicia build na nuvem
132
- kasy codemagic status <id> # status do build
129
+ kasy add ci # entrega o codemagic.yaml (se necessário)
130
+ kasy codemagic configure # token API valida e lista seus apps → branch
131
+ kasy codemagic release # iOS + Android na nuvem
132
+ kasy codemagic release --ios # iOS
133
+ kasy codemagic release --android # só Android
134
+ kasy codemagic status <id> # status do build
133
135
  ```
134
136
 
135
137
  Documentação: `docs/codemagic-release.md` (idioma da CLI)
@@ -11,18 +11,39 @@ const {
11
11
  codemagicReleaseDocPath,
12
12
  writeCodemagicEnv,
13
13
  validateCodemagicSetup,
14
+ loadProjectEnv,
15
+ listApps,
16
+ workflowIdFor,
17
+ resolveReleaseVars,
14
18
  triggerBuild,
15
19
  getBuildStatus,
16
20
  URL_CODEMAGIC_APPS,
17
21
  URL_CODEMAGIC_API,
18
22
  } = require('../utils/codemagic-release');
19
23
 
24
+ // Human label per platform (proper nouns — same across locales).
25
+ const PLATFORM_LABEL = { ios: 'iOS', android: 'Android' };
26
+
20
27
  async function assertProject(projectDir, t) {
21
28
  if (!(await isKasyFlutterProject(projectDir))) {
22
29
  throw new Error(t('codemagic.error.notProject'));
23
30
  }
24
31
  }
25
32
 
33
+ /**
34
+ * Decide which platforms to release. Explicit flags win; with no flag we build
35
+ * both. Returns an ordered list like ['ios', 'android'].
36
+ */
37
+ function platformsFromOptions(options) {
38
+ const wantIos = !!options.ios;
39
+ const wantAndroid = !!options.android;
40
+ if (!wantIos && !wantAndroid) return ['ios', 'android'];
41
+ const platforms = [];
42
+ if (wantIos) platforms.push('ios');
43
+ if (wantAndroid) platforms.push('android');
44
+ return platforms;
45
+ }
46
+
26
47
  async function runConfigure(directory, options = {}) {
27
48
  const lang = options.language || detectDefaultLanguage();
28
49
  const t = createTranslator(lang);
@@ -35,39 +56,62 @@ async function runConfigure(directory, options = {}) {
35
56
  ui.log.message(kleur.dim(`${t('codemagic.configure.doc')}: ${codemagicReleaseDocPath(lang)}`));
36
57
  ui.log.warn(t('codemagic.configure.checklist'));
37
58
 
38
- ui.log.info(t('codemagic.configure.openingLinks'));
39
- openUrl(URL_CODEMAGIC_API);
40
- openUrl(URL_CODEMAGIC_APPS);
41
-
42
59
  const cancel = () => { ui.cancel(t('codemagic.configure.cancelled')); process.exit(0); };
43
60
  const required = (v) => (v && String(v).trim().length > 0 ? undefined : t('codemagic.configure.q.required'));
44
61
 
45
- const token = await ui.password({
62
+ // 1. API token open the settings page so the user can copy it.
63
+ ui.log.info(t('codemagic.configure.openingToken'));
64
+ openUrl(URL_CODEMAGIC_API);
65
+ const tokenRaw = await ui.password({
46
66
  message: t('codemagic.configure.q.token'),
47
67
  validate: required,
48
68
  onCancel: cancel,
49
69
  });
50
- const appId = await ui.text({
51
- message: t('codemagic.configure.q.appId'),
52
- validate: required,
53
- onCancel: cancel,
54
- });
55
- const workflowId = await ui.text({
56
- message: t('codemagic.configure.q.workflowId'),
57
- initialValue: 'ios-workflow',
58
- onCancel: cancel,
59
- });
60
- const branch = await ui.text({
70
+ const token = String(tokenRaw).trim();
71
+
72
+ // 2. Validate the token by listing the user's apps.
73
+ const spinner = ui.spinner();
74
+ spinner.start(t('codemagic.configure.validating'));
75
+ let apps;
76
+ try {
77
+ apps = await listApps(token);
78
+ spinner.stop(t('codemagic.configure.validated'));
79
+ } catch (err) {
80
+ spinner.stop(`${t('codemagic.configure.tokenInvalid')} (${err.message})`, 2);
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ // 3. Pick the app (or guide the user to connect a repo first).
86
+ let appId;
87
+ if (!apps.length) {
88
+ ui.log.warn(t('codemagic.configure.noApps'));
89
+ openUrl(URL_CODEMAGIC_APPS);
90
+ const manual = await ui.text({
91
+ message: t('codemagic.configure.q.appId'),
92
+ validate: required,
93
+ onCancel: cancel,
94
+ });
95
+ appId = String(manual).trim();
96
+ } else {
97
+ appId = await ui.select({
98
+ message: t('codemagic.configure.pickApp'),
99
+ options: apps.map((a) => ({ value: a.id, label: a.name, hint: a.id })),
100
+ });
101
+ if (typeof appId === 'symbol') cancel();
102
+ }
103
+
104
+ // 4. Branch that triggers the build.
105
+ const branchRaw = await ui.text({
61
106
  message: t('codemagic.configure.q.branch'),
62
107
  initialValue: 'main',
63
108
  onCancel: cancel,
64
109
  });
65
110
 
66
111
  await writeCodemagicEnv(projectDir, {
67
- token: String(token).trim(),
68
- appId: String(appId).trim(),
69
- workflowId: String(workflowId).trim() || 'ios-workflow',
70
- branch: String(branch).trim() || 'main',
112
+ token,
113
+ appId,
114
+ branch: String(branchRaw).trim() || 'main',
71
115
  });
72
116
 
73
117
  ui.log.success(t('codemagic.configure.success'));
@@ -87,44 +131,64 @@ async function runRelease(directory, options = {}) {
87
131
  if (validation.issues.includes('missing_yaml')) {
88
132
  ui.log.message(kleur.dim(t('codemagic.error.addCi')));
89
133
  }
90
- if (validation.issues.includes('missing_env')) {
134
+ if (validation.issues.includes('missing_env') || validation.issues.includes('missing_token') || validation.issues.includes('missing_app_id')) {
91
135
  ui.log.message(kleur.dim(t('codemagic.error.runConfigure')));
92
136
  }
93
137
  process.exitCode = 1;
94
138
  return;
95
139
  }
96
140
 
97
- const googleScheme = await validateGoogleIosUrlScheme(projectDir);
98
- if (!googleScheme.ok) {
99
- printCompactHeader(t);
100
- ui.log.error(t('codemagic.error.googleUrlScheme'));
101
- ui.log.message(kleur.dim(googleScheme.error));
102
- ui.log.info(t('ios.error.googleUrlSchemeHint'));
103
- process.exitCode = 1;
104
- return;
141
+ const platforms = platformsFromOptions(options);
142
+
143
+ // iOS builds need the Google Sign-In URL scheme to be in sync.
144
+ if (platforms.includes('ios')) {
145
+ const googleScheme = await validateGoogleIosUrlScheme(projectDir);
146
+ if (!googleScheme.ok) {
147
+ printCompactHeader(t);
148
+ ui.log.error(t('codemagic.error.googleUrlScheme'));
149
+ ui.log.message(kleur.dim(googleScheme.error));
150
+ ui.log.info(t('ios.error.googleUrlSchemeHint'));
151
+ process.exitCode = 1;
152
+ return;
153
+ }
105
154
  }
106
155
 
107
156
  printCompactHeader(t);
108
157
  ui.intro(t('codemagic.release.title'));
109
158
 
110
- const spinner = ui.spinner();
111
- spinner.start(t('codemagic.release.spin'));
112
- try {
113
- const result = await triggerBuild(projectDir, validation.env);
114
- spinner.stop(t('codemagic.release.spinDone'));
115
- const buildId = result.buildId || result._id;
116
- if (buildId) {
117
- ui.log.success(t('codemagic.release.triggered'));
118
- ui.log.info(`Build ID: ${kleur.cyan(buildId)}`);
119
- ui.log.message(kleur.dim(`https://codemagic.io/builds/${buildId}`));
120
- ui.outro('');
121
- } else {
122
- ui.outro(t('codemagic.release.triggered'));
159
+ // Read the project's keys and carry them with the build.
160
+ const projectEnv = await loadProjectEnv(projectDir);
161
+ const variables = resolveReleaseVars(projectEnv);
162
+
163
+ if (platforms.includes('ios') && !variables.RC_IOS_PROD_KEY) {
164
+ ui.log.warn(t('codemagic.release.noIosKey'));
165
+ }
166
+ if (platforms.includes('android') && !variables.RC_ANDROID_PROD_KEY) {
167
+ ui.log.warn(t('codemagic.release.noAndroidKey'));
168
+ }
169
+
170
+ let anyFailed = false;
171
+ for (const platform of platforms) {
172
+ const label = PLATFORM_LABEL[platform];
173
+ const workflowId = workflowIdFor(validation.env, platform);
174
+ const spinner = ui.spinner();
175
+ spinner.start(t('codemagic.release.spinPlatform', { platform: label }));
176
+ try {
177
+ const result = await triggerBuild(validation.env, { workflowId, variables });
178
+ const buildId = result.buildId || result._id;
179
+ spinner.stop(t('codemagic.release.triggeredPlatform', { platform: label }));
180
+ if (buildId) {
181
+ ui.log.info(`Build ID: ${kleur.cyan(buildId)}`);
182
+ ui.log.message(kleur.dim(`https://codemagic.io/app/${validation.env.CODEMAGIC_APP_ID}/build/${buildId}`));
183
+ }
184
+ } catch (err) {
185
+ anyFailed = true;
186
+ spinner.stop(`${label}: ${err.message}`, 2);
123
187
  }
124
- } catch (err) {
125
- spinner.stop(err.message, 2);
126
- process.exitCode = 1;
127
188
  }
189
+
190
+ if (anyFailed) process.exitCode = 1;
191
+ ui.outro('');
128
192
  }
129
193
 
130
194
  async function runStatus(buildId, directory, options = {}) {
@@ -168,4 +232,5 @@ module.exports = {
168
232
  runConfigure,
169
233
  runRelease,
170
234
  runStatus,
235
+ platformsFromOptions,
171
236
  };
@@ -1733,6 +1733,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1733
1733
  // generation step once choices are made. The success card at the end shows
1734
1734
  // the full summary. Cancel any time with Ctrl+C.
1735
1735
 
1736
+ // Windows: disable native assets before the generator runs pub get / builds,
1737
+ // so a username with a space (C:\Users\John Silva) doesn't break the
1738
+ // objective_c hook compile. No-op elsewhere, idempotent, best-effort.
1739
+ require('../utils/env-tools').disableNativeAssetsWindows();
1740
+
1736
1741
  // ── Generate ────────────────────────────────────────────────────────────
1737
1742
  // Quick: single rolling line that mutates message. Advanced: stack each step.
1738
1743
  const stepper = ui.makeQuickStepper({ color: paintLime });
@@ -7,9 +7,7 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
7
7
  const { printCompactHeader } = require('../utils/brand');
8
8
  const { readBundleId, readPackageName } = require('../utils/mobile-identity');
9
9
  const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
10
- const { augmentedEnv } = require('../utils/env-tools');
11
-
12
- const isWindows = process.platform === 'win32';
10
+ const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
13
11
 
14
12
  function runCmd(cmd, args) {
15
13
  const res = spawnSync(cmd, args, { encoding: 'utf8' });
@@ -21,11 +19,9 @@ function runCmd(cmd, args) {
21
19
  }
22
20
 
23
21
  async function listFlutterDevices(projectDir) {
24
- const res = spawnSync('flutter', ['devices', '--machine'], {
22
+ const res = spawnSyncFlutter(['devices', '--machine'], {
25
23
  cwd: projectDir,
26
24
  encoding: 'utf8',
27
- env: augmentedEnv(),
28
- shell: isWindows,
29
25
  });
30
26
  if (res.status !== 0) return [];
31
27
  try {
@@ -268,6 +264,10 @@ async function runReset(directory, options = {}) {
268
264
  throw new Error(t('reset.error.notFlutterProject'));
269
265
  }
270
266
 
267
+ // Windows: disable native assets before the reinstall rebuild so a username
268
+ // with a space doesn't break the objective_c hook compile. No-op elsewhere.
269
+ disableNativeAssetsWindows();
270
+
271
271
  printCompactHeader(t);
272
272
  ui.intro(t('reset.title'));
273
273
 
@@ -1,25 +1,20 @@
1
1
  const path = require('node:path');
2
- const { spawnSync } = require('node:child_process');
3
2
  const fs = require('fs-extra');
4
3
  const kleur = require('kleur');
5
4
  const ui = require('../utils/ui');
6
5
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
7
6
  const { printCompactHeader, paintLime } = require('../utils/brand');
8
7
  const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
9
- const { augmentedEnv } = require('../utils/env-tools');
10
-
11
- const isWindows = process.platform === 'win32';
8
+ const { spawnSyncFlutter, disableNativeAssetsWindows } = require('../utils/env-tools');
12
9
 
13
10
  function listFlutterDevices(projectDir) {
14
- // Same as `kasy run`'s launcher: expose a freshly-installed Flutter SDK via
15
- // augmentedEnv() (the terminal's PATH may not have it yet), and use a shell on
16
- // Windows so the flutter.bat is found. Without this, a machine that just ran
17
- // `kasy new` reports "Flutter not found" here even though Flutter is installed.
18
- const res = spawnSync('flutter', ['devices', '--machine'], {
11
+ // Shared flutter spawner: exposes a freshly-installed SDK on PATH (the terminal
12
+ // may not have it yet right after `kasy new`) and handles the Windows flutter.bat
13
+ // without tripping Node's shell+args deprecation warning. Without it, a machine
14
+ // that just ran `kasy new` reports "Flutter not found" here.
15
+ const res = spawnSyncFlutter(['devices', '--machine'], {
19
16
  cwd: projectDir,
20
17
  encoding: 'utf8',
21
- env: augmentedEnv(),
22
- shell: isWindows,
23
18
  });
24
19
  if (res.status !== 0) return [];
25
20
  try {
@@ -254,6 +249,11 @@ async function runRun(directory, options = {}) {
254
249
  throw new Error(t('run.error.notFlutterProject'));
255
250
  }
256
251
 
252
+ // Windows: turn off native assets before building so a username with a space
253
+ // (C:\Users\John Silva) doesn't break the objective_c hook compile. No-op on
254
+ // macOS/Linux, idempotent, and best-effort — see disableNativeAssetsWindows.
255
+ disableNativeAssetsWindows();
256
+
257
257
  // Resolve device flag. If none of the platform shortcuts or -d is set,
258
258
  // ask the user when more than one device is available — `flutter run`
259
259
  // bails out with "More than one device connected" otherwise.
@@ -27,12 +27,14 @@ const ALWAYS_EXCLUDE = new Set([
27
27
  '.cursor',
28
28
  '.idea',
29
29
  '.claude', // AI assistant instructions — not for the client
30
- // CI/CD configs delivered only when the user selects the 'ci' module
31
- // (these files live in features/ci/ and are applied as a feature patch)
30
+ // GitHub/GitLab CI pipelines are not shipped to generated projects yet.
32
31
  '.github',
33
32
  '.gitlab',
34
33
  '.gitlab-ci.yml',
35
- 'codemagic.yaml',
34
+ // NOTE: codemagic.yaml is intentionally NOT excluded here. It ships in the
35
+ // base template and generate.js removes it unless the 'ci' module is
36
+ // selected (see the "Phase B" gating). This is what lets `kasy codemagic`
37
+ // work in a generated project.
36
38
  ]);
37
39
 
38
40
  // File basenames that should NEVER be copied, regardless of directory depth
@@ -236,6 +236,13 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
236
236
  // Remove directories of modules that were NOT selected
237
237
  await removeModuleDirs(targetDir, modules);
238
238
 
239
+ // codemagic.yaml ships in the base template (so `kasy codemagic` works out
240
+ // of the box). It belongs to the 'ci' module — strip it when ci was not
241
+ // selected, mirroring how the other optional modules are gated above.
242
+ if (!modules.includes('ci')) {
243
+ await fs.remove(path.join(targetDir, 'codemagic.yaml'));
244
+ }
245
+
239
246
  // Write no-op analytics_api.dart if analytics not selected
240
247
  if (!modules.includes('analytics')) {
241
248
  await writeNoOpAnalyticsApi(targetDir, packageName);
@@ -8,18 +8,22 @@ const CODEMAGIC_ENV_REL = path.join('.kasy', 'codemagic.env');
8
8
  const CODEMAGIC_API = 'https://api.codemagic.io';
9
9
 
10
10
  const URL_CODEMAGIC_APPS = 'https://codemagic.io/apps';
11
- const URL_CODEMAGIC_API = 'https://codemagic.io/settings/integrations';
11
+ // Codemagic API token lives in the account settings → Integrations page.
12
+ const URL_CODEMAGIC_API = 'https://codemagic.io/settings';
13
+
14
+ // Default workflow ids defined in the codemagic.yaml shipped by the `ci` feature.
15
+ const DEFAULT_IOS_WORKFLOW = 'ios-workflow';
16
+ const DEFAULT_ANDROID_WORKFLOW = 'android-workflow';
12
17
 
13
18
  function codemagicReleaseDocPath() {
14
19
  return 'docs/codemagic-release.md';
15
20
  }
16
21
 
17
- async function loadCodemagicEnv(projectDir) {
18
- const envPath = path.join(projectDir, CODEMAGIC_ENV_REL);
19
- if (!(await fs.pathExists(envPath))) {
20
- return null;
21
- }
22
- const content = await fs.readFile(envPath, 'utf8');
22
+ /**
23
+ * Parse a simple KEY=VALUE env file into an object. Ignores blanks/comments.
24
+ * Values keep everything after the first '=' (so URLs with ':' survive).
25
+ */
26
+ function parseEnvContent(content) {
23
27
  const env = {};
24
28
  for (const line of content.split('\n')) {
25
29
  const trimmed = line.trim();
@@ -31,12 +35,32 @@ async function loadCodemagicEnv(projectDir) {
31
35
  return env;
32
36
  }
33
37
 
34
- async function writeCodemagicEnv(projectDir, { token, appId, workflowId, branch }) {
38
+ async function loadCodemagicEnv(projectDir) {
39
+ const envPath = path.join(projectDir, CODEMAGIC_ENV_REL);
40
+ if (!(await fs.pathExists(envPath))) {
41
+ return null;
42
+ }
43
+ return parseEnvContent(await fs.readFile(envPath, 'utf8'));
44
+ }
45
+
46
+ /**
47
+ * Read the project's runtime `.env` (the app's config). Returns {} when absent.
48
+ */
49
+ async function loadProjectEnv(projectDir) {
50
+ const envPath = path.join(projectDir, '.env');
51
+ if (!(await fs.pathExists(envPath))) {
52
+ return {};
53
+ }
54
+ return parseEnvContent(await fs.readFile(envPath, 'utf8'));
55
+ }
56
+
57
+ async function writeCodemagicEnv(projectDir, { token, appId, iosWorkflowId, androidWorkflowId, branch }) {
35
58
  const lines = [
36
59
  '# Generated by kasy codemagic configure — do not commit',
37
60
  `CODEMAGIC_API_TOKEN=${token}`,
38
61
  `CODEMAGIC_APP_ID=${appId}`,
39
- `CODEMAGIC_WORKFLOW_ID=${workflowId || 'ios-workflow'}`,
62
+ `CODEMAGIC_IOS_WORKFLOW_ID=${iosWorkflowId || DEFAULT_IOS_WORKFLOW}`,
63
+ `CODEMAGIC_ANDROID_WORKFLOW_ID=${androidWorkflowId || DEFAULT_ANDROID_WORKFLOW}`,
40
64
  `CODEMAGIC_BRANCH=${branch || 'main'}`,
41
65
  '',
42
66
  ];
@@ -83,12 +107,92 @@ function httpsJson(method, urlPath, token, body) {
83
107
  });
84
108
  }
85
109
 
86
- async function triggerBuild(projectDir, env) {
110
+ /**
111
+ * List the Codemagic applications the token can access.
112
+ * Returns [{ id, name }] so the configure wizard can let the user pick one
113
+ * instead of pasting a raw Mongo id.
114
+ */
115
+ async function listApps(token) {
116
+ const result = await httpsJson('GET', '/apps', token);
117
+ const apps = Array.isArray(result) ? result : (result.applications || []);
118
+ return apps
119
+ .map((a) => ({ id: a._id || a.id, name: a.appName || a.name || a.repository?.name || '(unnamed app)' }))
120
+ .filter((a) => a.id);
121
+ }
122
+
123
+ /**
124
+ * Resolve which workflow id to trigger for a platform, honoring custom ids
125
+ * saved at configure time and falling back to the shipped defaults. The legacy
126
+ * single CODEMAGIC_WORKFLOW_ID (older configs) is treated as the iOS workflow.
127
+ */
128
+ function workflowIdFor(env, platform) {
129
+ if (platform === 'android') {
130
+ return env.CODEMAGIC_ANDROID_WORKFLOW_ID || DEFAULT_ANDROID_WORKFLOW;
131
+ }
132
+ return env.CODEMAGIC_IOS_WORKFLOW_ID || env.CODEMAGIC_WORKFLOW_ID || DEFAULT_IOS_WORKFLOW;
133
+ }
134
+
135
+ /**
136
+ * Build the environment variables to inject at trigger time, read from the
137
+ * project's `.env`. These travel with the build so the cloud has the SAME
138
+ * production keys you use locally — the user does not have to fill the
139
+ * Codemagic variable groups by hand.
140
+ *
141
+ * RevenueCat SDK keys (appl_/goog_/test_) are public keys, safe to send this
142
+ * way. Real secrets (keystore, App Store key, service account) stay in the
143
+ * dashboard and are NOT sent here.
144
+ */
145
+ function resolveReleaseVars(projectEnv) {
146
+ const vars = { ENV: 'prod' };
147
+
148
+ const passthrough = [
149
+ 'BACKEND_URL',
150
+ 'RC_TEST_KEY',
151
+ 'RC_IOS_PROD_KEY',
152
+ 'RC_ANDROID_PROD_KEY',
153
+ 'MIXPANEL_TOKEN',
154
+ 'SENTRY_DSN',
155
+ ];
156
+ for (const key of passthrough) {
157
+ const value = projectEnv[key];
158
+ if (value && value.trim()) vars[key] = value.trim();
159
+ }
160
+
161
+ // LLM chat endpoint may be named either way across template versions; the app
162
+ // reads AI_CHAT_ENDPOINT.
163
+ const endpoint = projectEnv.AI_CHAT_ENDPOINT || projectEnv.LLM_CHAT_ENDPOINT;
164
+ if (endpoint && endpoint.trim()) vars.AI_CHAT_ENDPOINT = endpoint.trim();
165
+
166
+ // Legacy single-key projects (pre test/prod split): promote a non-test key
167
+ // into the prod slot so release builds still get a real key.
168
+ if (!vars.RC_IOS_PROD_KEY && projectEnv.RC_IOS_API_KEY && !projectEnv.RC_IOS_API_KEY.startsWith('test_')) {
169
+ vars.RC_IOS_PROD_KEY = projectEnv.RC_IOS_API_KEY.trim();
170
+ }
171
+ if (!vars.RC_ANDROID_PROD_KEY && projectEnv.RC_ANDROID_API_KEY && !projectEnv.RC_ANDROID_API_KEY.startsWith('test_')) {
172
+ vars.RC_ANDROID_PROD_KEY = projectEnv.RC_ANDROID_API_KEY.trim();
173
+ }
174
+
175
+ // Numeric App Store id → APP_ID, used by the iOS workflow for build numbering.
176
+ if (projectEnv.APP_STORE_ID && projectEnv.APP_STORE_ID.trim()) {
177
+ vars.APP_ID = projectEnv.APP_STORE_ID.trim();
178
+ }
179
+
180
+ return vars;
181
+ }
182
+
183
+ /**
184
+ * Trigger one build. `variables` is injected via the build's environment so the
185
+ * cloud build carries the project's production keys.
186
+ */
187
+ async function triggerBuild(env, { workflowId, branch, variables }) {
87
188
  const body = {
88
189
  appId: env.CODEMAGIC_APP_ID,
89
- workflowId: env.CODEMAGIC_WORKFLOW_ID,
90
- branch: env.CODEMAGIC_BRANCH || 'main',
190
+ workflowId,
191
+ branch: branch || env.CODEMAGIC_BRANCH || 'main',
91
192
  };
193
+ if (variables && Object.keys(variables).length > 0) {
194
+ body.environment = { variables };
195
+ }
92
196
  return httpsJson('POST', '/builds', env.CODEMAGIC_API_TOKEN, body);
93
197
  }
94
198
 
@@ -109,7 +213,6 @@ async function validateCodemagicSetup(projectDir) {
109
213
  }
110
214
  if (!env.CODEMAGIC_API_TOKEN) issues.push('missing_token');
111
215
  if (!env.CODEMAGIC_APP_ID) issues.push('missing_app_id');
112
- if (!env.CODEMAGIC_WORKFLOW_ID) issues.push('missing_workflow');
113
216
  return { ok: issues.length === 0, issues, env };
114
217
  }
115
218
 
@@ -118,9 +221,15 @@ module.exports = {
118
221
  CODEMAGIC_API,
119
222
  URL_CODEMAGIC_APPS,
120
223
  URL_CODEMAGIC_API,
224
+ DEFAULT_IOS_WORKFLOW,
225
+ DEFAULT_ANDROID_WORKFLOW,
121
226
  codemagicReleaseDocPath,
122
227
  loadCodemagicEnv,
228
+ loadProjectEnv,
123
229
  writeCodemagicEnv,
230
+ listApps,
231
+ workflowIdFor,
232
+ resolveReleaseVars,
124
233
  triggerBuild,
125
234
  getBuildStatus,
126
235
  validateCodemagicSetup,